Initial ui-kit: Button, дизайн-токены, сборка Vite, Storybook
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
*.log
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
"stories": [
|
||||||
|
"../src/**/*.mdx",
|
||||||
|
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@chromatic-com/storybook",
|
||||||
|
"@storybook/addon-vitest",
|
||||||
|
"@storybook/addon-a11y",
|
||||||
|
"@storybook/addon-docs",
|
||||||
|
"@storybook/addon-mcp"
|
||||||
|
],
|
||||||
|
"framework": "@storybook/react-vite"
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Preview } from '@storybook/react-vite'
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
a11y: {
|
||||||
|
// 'todo' - show a11y violations in the test UI only
|
||||||
|
// 'error' - fail CI on a11y violations
|
||||||
|
// 'off' - skip a11y checks entirely
|
||||||
|
test: 'todo'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
# @tnsu/ui-kit — память проекта
|
||||||
|
|
||||||
|
Единый пакет UI-компонентов для SPFx web part'ов ТНСУ + Storybook как витрина.
|
||||||
|
Этот файл — источник правды по решениям, плану и прогрессу. Обновлять по ходу работы.
|
||||||
|
|
||||||
|
## Зачем проект
|
||||||
|
|
||||||
|
У команды много SPFx web part'ов в **разных репозиториях** (request-portal, бывший
|
||||||
|
1c-ticket-form, pto-tracker и др.). В каждом дизайн разъезжается, компоненты копируются
|
||||||
|
и пишутся заново. Цель — вынести компоненты в один npm-пакет, подключать его в любую
|
||||||
|
web part через `npm i`, и держать единый вид через дизайн-токены.
|
||||||
|
|
||||||
|
Источник компонентов: `request-portal/src/webparts/requestPortal/shared/ui/` — там уже
|
||||||
|
~30 чистых компонентов на SCSS-модулях с barrel-экспортом (`ui/index.ts`). Это база.
|
||||||
|
|
||||||
|
## Ключевые архитектурные решения
|
||||||
|
|
||||||
|
- **Гибридный подход**: отдельный репозиторий `tnsu-ui-kit` (пакет + Storybook),
|
||||||
|
публикуется в GitLab Package Registry. Существующие web part'ы остаются в своих репах
|
||||||
|
и просто ставят пакет. НЕ монорепо (не хотим переселять существующие проекты).
|
||||||
|
- **Сборка — Vite (library mode)**, не tsup. Причина: компоненты на `.module.scss`,
|
||||||
|
Vite понимает SCSS-модули из коробки и он же движок Storybook → один инструмент.
|
||||||
|
- **React/Fluent/tslib — только `peerDependencies`** (React 17, @fluentui/react 8, tslib).
|
||||||
|
Защита от двух React в бандле («Invalid hook call»). Локально те же версии стоят как
|
||||||
|
devDependencies для сборки и Storybook.
|
||||||
|
- **Дизайн-токены** (`tokens.scss`: цвета, отступы, радиусы) — единый рычаг общего вида.
|
||||||
|
Компоненты тянут значения оттуда, а не хардкодят.
|
||||||
|
- **Скоуп имени `@tnsu`** — сверить с именем группы в GitLab на шаге публикации.
|
||||||
|
Пока имя ни на что не влияет, переименование = find-replace.
|
||||||
|
- **Что переносим**: только чистые UI-примитивы (Button, TextInput, FormCard, Select,
|
||||||
|
ItemsTable, PeopleField...). НЕ переносим бизнес-логику: `approval/`, `pnp/`,
|
||||||
|
`dictionary/`, сервисы SharePoint — они привязаны к спискам. Возможно позже отдельный
|
||||||
|
пакет `@tnsu/sp-core`.
|
||||||
|
- **Расширение через пропсы**, не форки. Если в старом проекте компонент выглядел иначе —
|
||||||
|
добавляем `variant`/`size` в ui-kit, а не плодим копии.
|
||||||
|
- **Версии инструментов синхронны с SPFx**: TypeScript пинить на `~5.8.0` (как в
|
||||||
|
request-portal). Без пина `npm i typescript` ставит TS 7 (нативный компилятор без
|
||||||
|
JS Compiler API) — vite-plugin-dts падает. То же правило для React/Fluent.
|
||||||
|
|
||||||
|
## План (маршрут)
|
||||||
|
|
||||||
|
1. [x] Скелет репо — папка, git, package.json, зависимости
|
||||||
|
2. [x] Сборка (Vite) — vite.config.ts, пакет собирается в dist/
|
||||||
|
3. [x] Первый компонент — Button перенесён, сборка чистая
|
||||||
|
4. [x] Дизайн-токены — src/tokens/_tokens.scss перенесён из request-portal
|
||||||
|
5. [x] Storybook — витрина работает (SB 10.4 + Vite), Button виден со всеми вариантами
|
||||||
|
6. [x] Публикация — @tnsu/ui-kit@0.0.1 опубликован в Gitea (это Gitea 1.26.4, НЕ GitLab!)
|
||||||
|
7. [~] Потребление проверено ПОЛНОСТЬЮ: request-portal собрался (heft webpack ок) +
|
||||||
|
кнопки из пакета отрисовались в SharePoint workbench со стилями (accent-золото,
|
||||||
|
secondary-рамки). CSS из пакета применяется в рантайме. Осталась публикация в registry.
|
||||||
|
|
||||||
|
ВАЖНО про потребление (проверено 2026-08-13):
|
||||||
|
- Тест через локальный тарбол (`npm pack` → `npm i ../tnsu-ui-kit/*.tgz`), НЕ npm link
|
||||||
|
(link тащит второй React → Invalid hook call).
|
||||||
|
- CSS подключается в SPFx через `import '@tnsu/ui-kit/styles.css'` в корневом компоненте
|
||||||
|
веб-парта (RequestPortal.tsx). SPFx-webpack (5.95) обрабатывает .css из node_modules — ОК.
|
||||||
|
- Заменён Button в SubmitActions.tsx (`../Button/Button` → `@tnsu/ui-kit`). Правки НЕ
|
||||||
|
коммитить, пока пакет не опубликован в registry (сейчас ссылается на file:-тарбол).
|
||||||
|
|
||||||
|
Известный tech-debt: Sass `@import` устарел (Dart Sass 3.0). Мигрировать токены и
|
||||||
|
модули на `@use`/`@forward` — отдельной задачей, сейчас работает с варнингом.
|
||||||
|
|
||||||
|
## Прогресс / журнал
|
||||||
|
|
||||||
|
- **Шаг 1 готов**: создан репо `C:\Users\gorodetskiy.timur\Documents\Projects\tnsu-ui-kit`,
|
||||||
|
`git init`, болванка package.json.
|
||||||
|
- **Шаг 2 в работе**: переписан package.json (@tnsu/ui-kit, exports, peerDeps, Vite-скрипты),
|
||||||
|
ставятся devDependencies, добавлен .gitignore.
|
||||||
|
|
||||||
|
## Как мигрировать старый проект (памятка на будущее)
|
||||||
|
|
||||||
|
1. `npm i @tnsu/ui-kit` — ничего не ломается, пакет просто лежит рядом.
|
||||||
|
2. Заменяем импорты по одному компоненту/форме: `../shared/ui` → `@tnsu/ui-kit`.
|
||||||
|
Пропсы те же, JSX не меняется. Старый и новый компонент могут сосуществовать.
|
||||||
|
3. Проверили форму → удалили старый компонент из `shared/ui`.
|
||||||
|
4. Повторяем, пока `shared/ui` не опустеет.
|
||||||
|
5. Перед миграцией проекта проверить: его версии React/Fluent совместимы с peerDeps ui-kit.
|
||||||
|
|
||||||
|
## Публикация (Gitea, НЕ GitLab!)
|
||||||
|
|
||||||
|
- Хостинг git — **Gitea 1.26.4** на http://git.tnsu.kz (не GitLab). Registry npm привязан
|
||||||
|
к ВЛАДЕЛЬЦУ, не к проекту. Никакого "project ID" не нужно.
|
||||||
|
- Адрес registry: `http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/`
|
||||||
|
- `.npmrc` в проекте (можно коммитить, НЕ секрет):
|
||||||
|
`@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/`
|
||||||
|
- Токен (СЕКРЕТ) — в `~/.npmrc` (C:\Users\gorodetskiy.timur\.npmrc), в git НЕ попадает:
|
||||||
|
`//git.tnsu.kz/api/packages/timur.gorodetskiy/npm/:_authToken=<TOKEN>`
|
||||||
|
- Токен создаётся в Gitea: аватар → Настройки → Приложения → права `package` = Чтение и запись.
|
||||||
|
- Публикация: `npm run build && npm publish`. Бамп версии в package.json перед каждой
|
||||||
|
публикацией (npm не даёт перезаписать уже опубликованную версию).
|
||||||
|
- Потребителю (request-portal и др.) нужен такой же `@tnsu:registry` в его `.npmrc` +
|
||||||
|
свой токен на чтение пакетов.
|
||||||
|
|
||||||
|
## Окружение
|
||||||
|
|
||||||
|
- Node: v22.x (как в SPFx: >=22.14 <23)
|
||||||
|
- React 17.0.1, @fluentui/react ^8.106.4, tslib 2.3.1
|
||||||
|
- GitLab: http://git.tnsu.kz/timur.gorodetskiy/tnsu-ui-kit.git
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# @tnsu/ui-kit
|
||||||
|
|
||||||
|
Общие UI-компоненты для SPFx web part'ов ТНСУ. Единый дизайн, один источник правды.
|
||||||
|
|
||||||
|
> Внутренняя память проекта (решения, план, прогресс) — в [PROJECT.md](./PROJECT.md).
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm i @tnsu/ui-kit
|
||||||
|
```
|
||||||
|
|
||||||
|
Пакет требует, чтобы в проекте уже были (peerDependencies):
|
||||||
|
`react@17`, `react-dom@17`, `@fluentui/react@8`, `tslib`.
|
||||||
|
У любого SPFx-решения этого поколения они есть.
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Button } from '@tnsu/ui-kit';
|
||||||
|
import '@tnsu/ui-kit/styles.css'; // один раз в точке входа web part
|
||||||
|
|
||||||
|
<Button variant="primary">Отправить</Button>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Компоненты
|
||||||
|
|
||||||
|
_Наполняется по мере переноса. Полная витрина со всеми состояниями — в Storybook._
|
||||||
|
|
||||||
|
| Компонент | Описание | Статус |
|
||||||
|
|-----------|----------|--------|
|
||||||
|
| Button | Кнопка, варианты primary/accent/secondary/ghost/danger | ⏳ переносится |
|
||||||
|
|
||||||
|
## Разработка
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm i # поставить зависимости
|
||||||
|
npm run build # собрать пакет в dist/
|
||||||
|
npm run dev # сборка в watch-режиме
|
||||||
|
# npm run storybook — витрина (добавим на шаге 5)
|
||||||
|
```
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.module.scss" {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
Generated
+6049
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "@tnsu/ui-kit",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "TNSU shared UI components for SPFx web parts",
|
||||||
|
"author": "Timur Gorodetskiy",
|
||||||
|
"license": "ISC",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "http://git.tnsu.kz/timur.gorodetskiy/tnsu-ui-kit.git"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"./styles.css": "./dist/style.css"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"**/*.css"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fluentui/react": "^8.106.4",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fluentui/react": "^8.125.7",
|
||||||
|
"@types/react": "^17.0.45",
|
||||||
|
"@types/react-dom": "^17.0.17",
|
||||||
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"sass": "^1.102.0",
|
||||||
|
"tslib": "^2.3.1",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"vite": "^8.2.1",
|
||||||
|
"vite-plugin-dts": "^5.0.3",
|
||||||
|
"storybook": "^10.5.7",
|
||||||
|
"@storybook/react-vite": "^10.5.7",
|
||||||
|
"@chromatic-com/storybook": "latest",
|
||||||
|
"@storybook/addon-vitest": "^10.5.7",
|
||||||
|
"@storybook/addon-a11y": "^10.5.7",
|
||||||
|
"@storybook/addon-docs": "^10.5.7",
|
||||||
|
"@storybook/addon-mcp": "latest",
|
||||||
|
"vitest": "latest",
|
||||||
|
"playwright": "latest",
|
||||||
|
"@vitest/browser-playwright": "latest",
|
||||||
|
"@vitest/coverage-v8": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
@import "../tokens/tokens";
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: $r-sm;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm {
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: $navy-800;
|
||||||
|
border-color: $navy-800;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $navy-700;
|
||||||
|
box-shadow: $sh-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent {
|
||||||
|
background: $grad-gold;
|
||||||
|
border-color: $gold-500;
|
||||||
|
color: $navy-950;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $sh-gold;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background: $white;
|
||||||
|
border-color: $gray-300;
|
||||||
|
color: $navy-800;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $gold-500;
|
||||||
|
color: $navy-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: $navy-700;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $navy-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
background: $white;
|
||||||
|
border-color: $gray-300;
|
||||||
|
color: $red-500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $red-500;
|
||||||
|
background: $red-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
|
// meta — описание компонента для Storybook: где показать в дереве слева,
|
||||||
|
// какой компонент и какими контролами крутить пропсы в панели справа.
|
||||||
|
const meta: Meta<typeof Button> = {
|
||||||
|
title: 'Components/Button',
|
||||||
|
component: Button,
|
||||||
|
tags: ['autodocs'], // автоматически сгенерит страницу-документацию
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['primary', 'accent', 'secondary', 'ghost', 'danger']
|
||||||
|
},
|
||||||
|
size: { control: 'radio', options: ['md', 'sm'] },
|
||||||
|
fullWidth: { control: 'boolean' },
|
||||||
|
disabled: { control: 'boolean' }
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
children: 'Кнопка',
|
||||||
|
variant: 'primary'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Button>;
|
||||||
|
|
||||||
|
// Каждый export — отдельная «история» (состояние компонента) в дереве Storybook.
|
||||||
|
export const Primary: Story = { args: { variant: 'primary' } };
|
||||||
|
export const Accent: Story = { args: { variant: 'accent' } };
|
||||||
|
export const Secondary: Story = { args: { variant: 'secondary' } };
|
||||||
|
export const Ghost: Story = { args: { variant: 'ghost' } };
|
||||||
|
export const Danger: Story = { args: { variant: 'danger', children: 'Удалить' } };
|
||||||
|
export const Small: Story = { args: { size: 'sm' } };
|
||||||
|
export const Disabled: Story = { args: { disabled: true } };
|
||||||
|
|
||||||
|
// render позволяет показать сразу несколько кнопок — удобно для обзора палитры.
|
||||||
|
export const AllVariants: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||||
|
<Button variant="primary">Primary</Button>
|
||||||
|
<Button variant="accent">Accent</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="ghost">Ghost</Button>
|
||||||
|
<Button variant="danger">Danger</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Button.module.scss';
|
||||||
|
|
||||||
|
type ButtonVariant = 'primary' | 'accent' | 'secondary' | 'ghost' | 'danger';
|
||||||
|
type ButtonSize = 'md' | 'sm';
|
||||||
|
|
||||||
|
interface IButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantClass: Record<ButtonVariant, string> = {
|
||||||
|
primary: styles.primary,
|
||||||
|
accent: styles.accent,
|
||||||
|
secondary: styles.secondary,
|
||||||
|
ghost: styles.ghost,
|
||||||
|
danger: styles.danger
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Button: React.FC<IButtonProps> = ({
|
||||||
|
variant = 'secondary',
|
||||||
|
size = 'md',
|
||||||
|
fullWidth = false,
|
||||||
|
type = 'button',
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const classes = [
|
||||||
|
styles.btn,
|
||||||
|
variantClass[variant],
|
||||||
|
size === 'sm' ? styles.sm : '',
|
||||||
|
fullWidth ? styles.full : '',
|
||||||
|
className
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// type по умолчанию 'button', чтобы кнопки внутри формы не сабмитили её случайно.
|
||||||
|
return (
|
||||||
|
<button type={type} className={classes} {...rest}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Button } from "./Button/Button";
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// ============================================
|
||||||
|
// Tansu Construction — Design Tokens (V5: Executive Premium)
|
||||||
|
// Партиал: только SCSS-переменные и миксины, БЕЗ вывода CSS.
|
||||||
|
// Импортируется в каждый *.module.scss:
|
||||||
|
// @import '<путь>/styles/design-tokens';
|
||||||
|
// Здесь намеренно нет блока :root — чтобы импорт в десятки
|
||||||
|
// модулей не дублировал CSS-переменные в бандле. Значения
|
||||||
|
// используем напрямую через SCSS-переменные ($navy-800 и т.д.).
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// ---- Navy (Primary) ----
|
||||||
|
$navy-950: #0A1B30;
|
||||||
|
$navy-900: #0F2440;
|
||||||
|
$navy-800: #1B3A5C;
|
||||||
|
$navy-700: #244B73;
|
||||||
|
$navy-600: #2D5C8A;
|
||||||
|
$navy-100: #E8EEF4;
|
||||||
|
$navy-50: #F4F7FA;
|
||||||
|
|
||||||
|
// ---- Gold (Accent) ----
|
||||||
|
$gold-600: #B8881A;
|
||||||
|
$gold-500: #D4A01E;
|
||||||
|
$gold-400: #E5B635;
|
||||||
|
$gold-300: #F0CC5E;
|
||||||
|
$gold-100: #FDF5E0;
|
||||||
|
$gold-50: #FEFBF0;
|
||||||
|
|
||||||
|
// ---- Gray (Neutral) ----
|
||||||
|
$gray-900: #111827;
|
||||||
|
$gray-700: #374151;
|
||||||
|
$gray-500: #6B7280;
|
||||||
|
$gray-400: #9CA3AF;
|
||||||
|
$gray-300: #D1D5DB;
|
||||||
|
$gray-200: #E5E7EB;
|
||||||
|
$gray-100: #F3F4F6;
|
||||||
|
$gray-50: #F9FAFB;
|
||||||
|
|
||||||
|
// ---- Semantic ----
|
||||||
|
$red-500: #DC2626;
|
||||||
|
$red-50: #FEF2F2;
|
||||||
|
$green-600: #15803D;
|
||||||
|
$green-50: #F0FDF4;
|
||||||
|
$blue-600: #2563EB;
|
||||||
|
$blue-50: #EFF6FF;
|
||||||
|
$amber-700: #854D0E;
|
||||||
|
|
||||||
|
$white: #fff;
|
||||||
|
|
||||||
|
// ---- Typography ----
|
||||||
|
// Заголовки страниц и секций — Lora (serif). Остальной текст — Inter (sans).
|
||||||
|
$font-serif: 'Lora', Georgia, serif;
|
||||||
|
$font-sans: 'Inter', -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
|
||||||
|
$fs-page-title: 24px; // Lora 600
|
||||||
|
$fs-form-title: 22px; // Lora 600
|
||||||
|
$fs-section-title: 15px; // Lora 600
|
||||||
|
$fs-body: 14px; // Inter 400
|
||||||
|
$fs-sm: 13px;
|
||||||
|
$fs-label: 12px; // Inter 500
|
||||||
|
$fs-caption: 11px;
|
||||||
|
$fs-overline: 10.5px; // uppercase, letter-spacing 1.4-1.8px
|
||||||
|
|
||||||
|
// ---- Radius ----
|
||||||
|
$r-sm: 6px;
|
||||||
|
$r-md: 10px;
|
||||||
|
$r-lg: 14px;
|
||||||
|
$r-full: 100px;
|
||||||
|
|
||||||
|
// ---- Shadows ----
|
||||||
|
$sh-1: 0 1px 2px rgba(15,36,64,.04), 0 1px 3px rgba(15,36,64,.06);
|
||||||
|
$sh-2: 0 4px 16px rgba(15,36,64,.07);
|
||||||
|
$sh-3: 0 12px 32px rgba(15,36,64,.10);
|
||||||
|
$sh-gold: 0 4px 20px rgba(212,160,30,.18);
|
||||||
|
$sh-modal: 0 24px 70px rgba(10,27,48,.25);
|
||||||
|
|
||||||
|
// ---- Gradients (фирменные) ----
|
||||||
|
$grad-header: linear-gradient(135deg, $navy-950 0%, $navy-800 85%);
|
||||||
|
$grad-gold: linear-gradient(135deg, $gold-400, $gold-500);
|
||||||
|
$grad-gold-v: linear-gradient(180deg, $gold-400, $gold-600);
|
||||||
|
$grad-hairline: linear-gradient(90deg, $gold-400, transparent 70%);
|
||||||
|
|
||||||
|
// ---- Spacing ----
|
||||||
|
$sp-1: 4px; $sp-2: 8px; $sp-3: 12px;
|
||||||
|
$sp-4: 16px; $sp-5: 20px; $sp-6: 24px;
|
||||||
|
$sp-7: 28px; $sp-8: 32px;
|
||||||
|
|
||||||
|
// ---- Готовые миксины ----
|
||||||
|
@mixin page-header {
|
||||||
|
background: $grad-header;
|
||||||
|
border-radius: $r-lg;
|
||||||
|
padding: 26px 30px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, $gold-500, transparent 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin card-hover-gold {
|
||||||
|
transition: .22s;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $sh-3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: $gold-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin btn-accent {
|
||||||
|
background: $grad-gold;
|
||||||
|
color: $navy-950;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid $gold-500;
|
||||||
|
&:hover { box-shadow: $sh-gold; transform: translateY(-1px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin input-focus-gold {
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
border-color: $gold-500;
|
||||||
|
box-shadow: 0 0 0 3px rgba(212,160,30,.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вид поля «только чтение» (режим согласующего / отправленная заявка):
|
||||||
|
// приглушённый фон, серый текст, без золотого фокуса — визуально «неактивно».
|
||||||
|
// $sel — псевдокласс состояния поля (:read-only для input/textarea, :disabled для select).
|
||||||
|
@mixin readonly-field($sel) {
|
||||||
|
&#{$sel},
|
||||||
|
&#{$sel}:focus {
|
||||||
|
background: $gray-50;
|
||||||
|
color: $gray-500;
|
||||||
|
border-color: $gray-200;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin hairline {
|
||||||
|
height: 1px;
|
||||||
|
background: $grad-hairline;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2018",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.stories.tsx"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/// <reference types="vitest/config" />
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import dts from "vite-plugin-dts";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
||||||
|
import { playwright } from '@vitest/browser-playwright';
|
||||||
|
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), dts({
|
||||||
|
include: ["src"],
|
||||||
|
// истории — только для витрины Storybook, в пакет их типы не кладём
|
||||||
|
exclude: ["src/**/*.stories.tsx"],
|
||||||
|
insertTypesEntry: true
|
||||||
|
})],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(import.meta.dirname, "src/index.ts"),
|
||||||
|
formats: ["es", "cjs"],
|
||||||
|
fileName: format => format === "es" ? "index.js" : "index.cjs",
|
||||||
|
cssFileName: "style"
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
// peer-зависимости НЕ бандлим — их принесёт web part
|
||||||
|
external: ["react", "react-dom", "react/jsx-runtime", "@fluentui/react", "tslib"]
|
||||||
|
},
|
||||||
|
cssCodeSplit: false,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
projects: [{
|
||||||
|
extends: true,
|
||||||
|
plugins: [
|
||||||
|
// The plugin will run tests for the stories defined in your Storybook config
|
||||||
|
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
||||||
|
storybookTest({
|
||||||
|
configDir: path.join(dirname, '.storybook')
|
||||||
|
})],
|
||||||
|
test: {
|
||||||
|
name: 'storybook',
|
||||||
|
browser: {
|
||||||
|
enabled: true,
|
||||||
|
headless: true,
|
||||||
|
provider: playwright({}),
|
||||||
|
instances: [{
|
||||||
|
browser: 'chromium'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@vitest/browser-playwright" />
|
||||||
Reference in New Issue
Block a user