diff --git a/.gitignore b/.gitignore index c28f682..09957d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ public/ +test/public/ .tmp/ *.generated.ts *.tgz diff --git a/README.md b/README.md new file mode 100644 index 0000000..98f15f3 --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# @gromlab/svg-sprites + +Генерация SVG-спрайтов из папок с иконками. TypeScript-типизация, SVG-трансформации, React-компонент и HTML-превью из коробки. + +## Установка + +```bash +npm install @gromlab/svg-sprites +``` + +## Быстрый старт + +Создайте файл `svg-sprites.config.ts` в корне проекта: + +```ts +import { defineConfig } from '@gromlab/svg-sprites' + +export default defineConfig({ + output: 'public', + publicPath: '/public', + react: 'src/shared/ui/svg-sprite', + + sprites: [ + { name: 'icons', input: 'src/assets/icons' }, + { name: 'logos', input: 'src/assets/logos' }, + ], +}) +``` + +Запустите генерацию: + +```bash +npx svg-sprites +``` + +Результат: + +``` +public/ + icons.sprite.svg + logos.sprite.svg + preview.html + +src/shared/ui/svg-sprite/ + svg-sprite.tsx + svg-sprite.module.css +``` + +## Использование компонента + +```tsx +import { SvgSprite } from './shared/ui/svg-sprite' + +// Иконка из первого спрайта (по умолчанию) + + +// Иконка из другого спрайта + + +// Обёртка в (удобно для inline-элементов) + +``` + +Компонент полностью типизирован — автодополнение работает для имён иконок и спрайтов. + +## Управление цветом + +При сборке цвета иконок заменяются на CSS-переменные. + +**Моно-иконка** (один цвет) — наследует `color` текста: + +```css +.button { color: red; } +``` + +Или точечно через CSS-переменную: + +```css +.button { --icon-color-1: #ff0000; } +``` + +**Мульти-иконка** (несколько цветов) — каждый цвет задаётся отдельной переменной: + +```css +.card { + --icon-color-1: #ff0000; + --icon-color-2: #00ff00; +} +``` + +## Конфигурация + +### `output` (обязательный) + +Папка для сгенерированных спрайтов. + +```ts +output: 'public/sprites' +``` + +### `sprites` (обязательный) + +Массив спрайтов для генерации. + +```ts +sprites: [ + { + name: 'icons', // имя спрайта → icons.sprite.svg + input: 'src/assets/icons', // папка с SVG-файлами + mode: 'stack', // 'stack' (по умолчанию) или 'symbol' + }, + { + name: 'flags', + input: [ // или массив конкретных файлов + 'src/components/button/arrow.svg', + 'src/components/modal/close.svg', + ], + }, +] +``` + +### `publicPath` + +Публичный URL-путь к спрайтам. Зашивается в React-компонент для формирования `href`. + +```ts +publicPath: '/img/sprites' +// → +``` + +### `react` + +Путь для генерации React-компонента. Имена файлов берутся из названия папки. + +```ts +react: 'src/shared/ui/svg-sprite' +// → svg-sprite.tsx + svg-sprite.module.css +``` + +Если не задан — компонент и типы не генерируются. + +### `preview` + +Генерация HTML-превью со всеми иконками. По умолчанию: `true`. + +```ts +preview: false +``` + +### `transform` + +Настройки трансформации SVG. Все опции включены по умолчанию. + +```ts +transform: { + removeSize: true, // удаляет width/height с + replaceColors: true, // заменяет цвета на CSS-переменные + addTransition: true, // добавляет transition к элементам с цветом +} +``` + +Например, для спрайта с фиксированными цветами: + +```ts +transform: { + replaceColors: false, +} +``` + +## Сгенерированные типы + +React-компонент включает TypeScript-типы для каждого спрайта: + +```ts +import type { + IconsIconName, // 'check' | 'arrow-left' | ... + LogosIconName, // 'github' | 'twitter' | ... + SpriteName, // 'icons' | 'logos' + SpriteMap, // { icons: IconsIconName, logos: LogosIconName } +} from './shared/ui/svg-sprite' +``` + +## Способы рендера + +| Способ | Управление цветом | Пример | +|--------|-------------------|--------| +| React / SVG `` | CSS-переменные, `color` | `` | +| CSS `mask-image` | `background-color` (монохром) | `.icon { mask: url(...); background-color: red; }` | +| `` | нет | `` | + +## Программный API + +```ts +import { generate, defineConfig } from '@gromlab/svg-sprites' + +const config = defineConfig({ + output: 'public', + sprites: [{ name: 'icons', input: 'assets/icons' }], +}) + +const results = await generate(config) +``` + +## Лицензия + +MIT diff --git a/package-lock.json b/package-lock.json index 8c47faf..f6d3c0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "citty": "^0.1.6", "colorette": "^2.0.20", + "jiti": "^2.6.1", "svg-sprite": "^2.0.4" }, "bin": { @@ -1302,15 +1302,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1438,6 +1429,7 @@ "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -1796,6 +1788,15 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", diff --git a/package.json b/package.json index f2aba82..4f3bfd9 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,11 @@ "dist" ], "scripts": { - "build": "tsup", + "build": "tsup && npm run build:preview", + "build:preview": "cd preview && npx vite build", "dev": "tsup --watch", "typecheck": "tsc --noEmit", - "sprite": "node dist/cli.js --input sprites --output public", + "sprite": "node dist/cli.js", "prepublishOnly": "npm run build" }, "keywords": [ @@ -35,8 +36,8 @@ "node": ">=18" }, "dependencies": { - "citty": "^0.1.6", "colorette": "^2.0.20", + "jiti": "^2.6.1", "svg-sprite": "^2.0.4" }, "devDependencies": { diff --git a/preview/.gitignore b/preview/.gitignore new file mode 100644 index 0000000..fb1b64a --- /dev/null +++ b/preview/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +public/dev-data.js + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/preview/.templates/business/{{name.kebabCase}}/index.ts b/preview/.templates/business/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..6c47526 --- /dev/null +++ b/preview/.templates/business/{{name.kebabCase}}/index.ts @@ -0,0 +1 @@ +export { {{name.pascalCase}}Business } from './{{name.kebabCase}}.business' diff --git a/preview/.templates/business/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css b/preview/.templates/business/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/.templates/business/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/.templates/business/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts b/preview/.templates/business/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..59561c1 --- /dev/null +++ b/preview/.templates/business/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts @@ -0,0 +1,11 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры бизнес-модуля {{name.pascalCase}}. + */ +export type {{name.pascalCase}}BusinessParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}BusinessProps = RootAttrs & {{name.pascalCase}}BusinessParams diff --git a/preview/.templates/business/{{name.kebabCase}}/{{name.kebabCase}}.business.tsx b/preview/.templates/business/{{name.kebabCase}}/{{name.kebabCase}}.business.tsx new file mode 100644 index 0000000..a29fefc --- /dev/null +++ b/preview/.templates/business/{{name.kebabCase}}/{{name.kebabCase}}.business.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { {{name.pascalCase}}BusinessProps } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * <Назначение бизнес-модуля {{name.pascalCase}} в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + */ +export const {{name.pascalCase}}Business = (props: {{name.pascalCase}}BusinessProps) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/.templates/infrastructure/{{name.kebabCase}}/index.ts b/preview/.templates/infrastructure/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..482460d --- /dev/null +++ b/preview/.templates/infrastructure/{{name.kebabCase}}/index.ts @@ -0,0 +1 @@ +export { {{name.pascalCase}}Infra } from './{{name.kebabCase}}.infra' diff --git a/preview/.templates/infrastructure/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css b/preview/.templates/infrastructure/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/.templates/infrastructure/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/.templates/infrastructure/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts b/preview/.templates/infrastructure/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..e6d1113 --- /dev/null +++ b/preview/.templates/infrastructure/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts @@ -0,0 +1,11 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры инфраструктурного модуля {{name.pascalCase}}. + */ +export type {{name.pascalCase}}InfraParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}InfraProps = RootAttrs & {{name.pascalCase}}InfraParams diff --git a/preview/.templates/infrastructure/{{name.kebabCase}}/{{name.kebabCase}}.infra.tsx b/preview/.templates/infrastructure/{{name.kebabCase}}/{{name.kebabCase}}.infra.tsx new file mode 100644 index 0000000..c7e02bc --- /dev/null +++ b/preview/.templates/infrastructure/{{name.kebabCase}}/{{name.kebabCase}}.infra.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { {{name.pascalCase}}InfraProps } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * <Назначение инфраструктурного модуля {{name.pascalCase}} в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + */ +export const {{name.pascalCase}}Infra = (props: {{name.pascalCase}}InfraProps) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/.templates/layout/{{name.kebabCase}}/index.ts b/preview/.templates/layout/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..dcafc99 --- /dev/null +++ b/preview/.templates/layout/{{name.kebabCase}}/index.ts @@ -0,0 +1 @@ +export { {{name.pascalCase}}Layout } from './{{name.kebabCase}}.layout' diff --git a/preview/.templates/layout/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css b/preview/.templates/layout/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/.templates/layout/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/.templates/layout/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts b/preview/.templates/layout/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..10f6669 --- /dev/null +++ b/preview/.templates/layout/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts @@ -0,0 +1,11 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры {{name.pascalCase}}Layout. + */ +export type {{name.pascalCase}}LayoutParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}LayoutProps = RootAttrs & {{name.pascalCase}}LayoutParams diff --git a/preview/.templates/layout/{{name.kebabCase}}/{{name.kebabCase}}.layout.tsx b/preview/.templates/layout/{{name.kebabCase}}/{{name.kebabCase}}.layout.tsx new file mode 100644 index 0000000..e9f49e1 --- /dev/null +++ b/preview/.templates/layout/{{name.kebabCase}}/{{name.kebabCase}}.layout.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { {{name.pascalCase}}LayoutProps } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * <Назначение layout {{name.pascalCase}} в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + */ +export const {{name.pascalCase}}Layout = (props: {{name.pascalCase}}LayoutProps) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/.templates/module/{{name.kebabCase}}/index.ts b/preview/.templates/module/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..3bd4b91 --- /dev/null +++ b/preview/.templates/module/{{name.kebabCase}}/index.ts @@ -0,0 +1 @@ +export { {{name.pascalCase}} } from './{{name.kebabCase}}' diff --git a/preview/.templates/module/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css b/preview/.templates/module/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/.templates/module/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/.templates/module/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts b/preview/.templates/module/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..5ba1c73 --- /dev/null +++ b/preview/.templates/module/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts @@ -0,0 +1,11 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры {{name.pascalCase}}. + */ +export type {{name.pascalCase}}Params = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params diff --git a/preview/.templates/module/{{name.kebabCase}}/{{name.kebabCase}}.tsx b/preview/.templates/module/{{name.kebabCase}}/{{name.kebabCase}}.tsx new file mode 100644 index 0000000..9a53d60 --- /dev/null +++ b/preview/.templates/module/{{name.kebabCase}}/{{name.kebabCase}}.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * <Назначение компонента {{name.pascalCase}} в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + */ +export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/.templates/screen/{{name.kebabCase}}/index.ts b/preview/.templates/screen/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..b49ccd4 --- /dev/null +++ b/preview/.templates/screen/{{name.kebabCase}}/index.ts @@ -0,0 +1 @@ +export { {{name.pascalCase}}Screen } from './{{name.kebabCase}}.screen' diff --git a/preview/.templates/screen/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css b/preview/.templates/screen/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/.templates/screen/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/.templates/screen/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts b/preview/.templates/screen/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..3907720 --- /dev/null +++ b/preview/.templates/screen/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts @@ -0,0 +1,11 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры экрана {{name.pascalCase}}. + */ +export type {{name.pascalCase}}ScreenParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}ScreenProps = RootAttrs & {{name.pascalCase}}ScreenParams diff --git a/preview/.templates/screen/{{name.kebabCase}}/{{name.kebabCase}}.screen.tsx b/preview/.templates/screen/{{name.kebabCase}}/{{name.kebabCase}}.screen.tsx new file mode 100644 index 0000000..8ec6d7e --- /dev/null +++ b/preview/.templates/screen/{{name.kebabCase}}/{{name.kebabCase}}.screen.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { {{name.pascalCase}}ScreenProps } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * <Назначение экрана {{name.pascalCase}} в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + */ +export const {{name.pascalCase}}Screen = (props: {{name.pascalCase}}ScreenProps) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/.templates/store/{{name.kebabCase}}/index.ts b/preview/.templates/store/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..8a56ac1 --- /dev/null +++ b/preview/.templates/store/{{name.kebabCase}}/index.ts @@ -0,0 +1,2 @@ +export { use{{name.pascalCase}}Store } from './{{name.kebabCase}}.store' +export type { {{name.pascalCase}}State } from './{{name.kebabCase}}.type' diff --git a/preview/.templates/store/{{name.kebabCase}}/{{name.kebabCase}}.store.ts b/preview/.templates/store/{{name.kebabCase}}/{{name.kebabCase}}.store.ts new file mode 100644 index 0000000..5c8acfd --- /dev/null +++ b/preview/.templates/store/{{name.kebabCase}}/{{name.kebabCase}}.store.ts @@ -0,0 +1,9 @@ +import { create } from 'zustand' +import type { {{name.pascalCase}}State } from './{{name.kebabCase}}.type' + +/** + * Стор {{name.pascalCase}}. + */ +export const use{{name.pascalCase}}Store = create<{{name.pascalCase}}State>()(() => ({ + +})) diff --git a/preview/.templates/store/{{name.kebabCase}}/{{name.kebabCase}}.type.ts b/preview/.templates/store/{{name.kebabCase}}/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..e895d00 --- /dev/null +++ b/preview/.templates/store/{{name.kebabCase}}/{{name.kebabCase}}.type.ts @@ -0,0 +1,6 @@ +/** + * Состояние {{name.pascalCase}}. + */ +export interface {{name.pascalCase}}State { + +} diff --git a/preview/.templates/ui/{{name.kebabCase}}/index.ts b/preview/.templates/ui/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..5b35acb --- /dev/null +++ b/preview/.templates/ui/{{name.kebabCase}}/index.ts @@ -0,0 +1 @@ +export { {{name.pascalCase}} } from './{{name.kebabCase}}.ui' diff --git a/preview/.templates/ui/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css b/preview/.templates/ui/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/.templates/ui/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/.templates/ui/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts b/preview/.templates/ui/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..5ba1c73 --- /dev/null +++ b/preview/.templates/ui/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts @@ -0,0 +1,11 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры {{name.pascalCase}}. + */ +export type {{name.pascalCase}}Params = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params diff --git a/preview/.templates/ui/{{name.kebabCase}}/{{name.kebabCase}}.ui.tsx b/preview/.templates/ui/{{name.kebabCase}}/{{name.kebabCase}}.ui.tsx new file mode 100644 index 0000000..9a53d60 --- /dev/null +++ b/preview/.templates/ui/{{name.kebabCase}}/{{name.kebabCase}}.ui.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * <Назначение компонента {{name.pascalCase}} в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + */ +export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/.templates/widget/{{name.kebabCase}}/index.ts b/preview/.templates/widget/{{name.kebabCase}}/index.ts new file mode 100644 index 0000000..573d446 --- /dev/null +++ b/preview/.templates/widget/{{name.kebabCase}}/index.ts @@ -0,0 +1 @@ +export { {{name.pascalCase}}Widget } from './{{name.kebabCase}}.widget' diff --git a/preview/.templates/widget/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css b/preview/.templates/widget/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/.templates/widget/{{name.kebabCase}}/styles/{{name.kebabCase}}.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/.templates/widget/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts b/preview/.templates/widget/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts new file mode 100644 index 0000000..9025222 --- /dev/null +++ b/preview/.templates/widget/{{name.kebabCase}}/types/{{name.kebabCase}}.type.ts @@ -0,0 +1,11 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры виджета {{name.pascalCase}}. + */ +export type {{name.pascalCase}}WidgetParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}WidgetProps = RootAttrs & {{name.pascalCase}}WidgetParams diff --git a/preview/.templates/widget/{{name.kebabCase}}/{{name.kebabCase}}.widget.tsx b/preview/.templates/widget/{{name.kebabCase}}/{{name.kebabCase}}.widget.tsx new file mode 100644 index 0000000..a4db24b --- /dev/null +++ b/preview/.templates/widget/{{name.kebabCase}}/{{name.kebabCase}}.widget.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { {{name.pascalCase}}WidgetProps } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * <Назначение виджета {{name.pascalCase}} в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + */ +export const {{name.pascalCase}}Widget = (props: {{name.pascalCase}}WidgetProps) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/AGENTS.md b/preview/AGENTS.md new file mode 100644 index 0000000..ba482ad --- /dev/null +++ b/preview/AGENTS.md @@ -0,0 +1,43 @@ +# AGENTS.md + +Это корневой диспетчер. Он определяет твою роль и отправляет тебя к твоему файлу инструкций. Дальше ты работаешь **строго** по нему. + +## Жёсткие правила + +1. Прочитай **только** файл своей роли из таблицы ниже. +2. Не читай файлы других ролей. Не читай `ai/` рекурсивно «для контекста». +3. Внутри файла роли есть свои обязательные разделы, прикладные разделы и триггеры — следуй его внутреннему протоколу, не додумывай свой. +4. Дополнительные файлы из `ai/` читай **только** когда на них явно ссылается твой файл роли или сработавший триггер. + +## Определение роли + +Роль определяется в таком порядке: + +1. Переменная окружения `AI_ROLE`. +2. Явное указание в первом сообщении пользователя («работай как developer», «ты reviewer» и т.п.). +3. Если ни того, ни другого нет — **остановись и спроси**. Не выбирай роль сам. + +## Карта ролей + +| Роль | Файл инструкций | Назначение | +|--------------|-------------------|-----------------------------------------------| +| `developer` | `ai/DEVELOP.md` | Написание и редактирование кода проекта | +| `reviewer` | `ai/REVIEW.md` | Код-ревью, проверка на соответствие стайлгайду | +| `architect` | `ai/ARCHITECT.md` | Проектирование модулей, слоёв, API | +| ... | ... | ... | + +> Оставь в таблице только те роли, которые реально существуют в `ai/`. + +## Протокол запуска + +1. Определи роль (см. выше). +2. Открой соответствующий файл из таблицы — это твой единственный источник истины. +3. Выполняй его внутренний протокол: сначала обязательные правила, затем прикладные разделы и триггеры по мере появления задач. +4. Если в ходе работы нужна инструкция, которой нет ни в твоём файле роли, ни в её триггерах — **не ищи её сам в других ролях**. Сообщи пользователю и спроси, как быть (переключить роль, дополнить инструкцию, и т.п.). + +## Что запрещено + +- Читать файлы других ролей даже выборочно. +- Сканировать `ai/` целиком или строить по ней собственную карту. +- Смешивать правила из разных ролей в одном ответе. +- Додумывать правила, которых нет в твоём файле роли. diff --git a/preview/ai/DEVELOP.md b/preview/ai/DEVELOP.md new file mode 100644 index 0000000..1a2850f --- /dev/null +++ b/preview/ai/DEVELOP.md @@ -0,0 +1,96 @@ +# Стайлгайд — Разработка + +Правила и стандарты разработки на Next.js и TypeScript. + +## Как работать + +1. **Изучи обязательные правила** (таблица ниже) — они действуют при любой задаче. +2. Найди задачу в таблицах триггеров → открой триггер. +3. Триггер укажет какие прикладные разделы прочитать и какие шаги выполнить. +4. Перед каждой подзадачей возвращайся к триггерам — проверяй, нет ли готового. +5. Если триггера нет — ищи прикладной раздел по области задачи. + +--- + +## Обязательные правила + +Прочитай эти разделы **до начала работы**. Соблюдай при написании любого кода. + +| Раздел | Файл | Что внутри | +|--------|------|------------| +| Структура проекта | applied/project-structure.md | Организация папок и файлов | +| Архитектура | basics/architecture.md | SLM Design: слои, модули, сегменты | +| Стиль кода | basics/code-style.md | Форматирование, импорты, отступы | +| Именование | basics/naming.md | Имена файлов, переменных, событий | +| Типизация | basics/typing.md | type vs interface, generic, any/unknown | +| Документирование | basics/documentation.md | JSDoc для функций, компонентов, типов | +| Технологии | basics/tech-stack.md | Допустимые библиотеки и зависимости | + +--- + +## Прикладные разделы + +Справочник по областям. Читай тот раздел, который относится к текущей задаче. + +| Область | Файл | Когда читать | +|---------|------|--------------| +| Компоненты | applied/components.md | Создание или редактирование React-компонентов | +| Стили | applied/styles.md | CSS Modules, PostCSS, переменные, медиа-запросы | +| Файлы роутинга | applied/page-level.md | page.tsx, layout.tsx, error.tsx, not-found.tsx | +| Шаблоны и генерация | applied/templates-generation.md | Генерация кода из шаблонов | +| Настройка VS Code | applied/vscode.md | Расширения, settings.json, сниппеты | +| SVG-спрайты | applied/svg-sprites.md | Работа с SVG-иконками и спрайтами | +| Хуки | applied/hooks.md | Создание и использование кастомных хуков *(в разработке)* | +| Сторы | applied/stores.md | Глобальное состояние, Zustand *(в разработке)* | +| API | applied/api.md | Запросы, клиенты, обработка ответов *(в разработке)* | +| Локализация | applied/localization.md | i18next, переводы *(в разработке)* | +| Изображения | applied/images-sprites.md | Подключение и оптимизация изображений *(в разработке)* | +| Шрифты | applied/fonts.md | Подключение и настройка шрифтов *(в разработке)* | +| Видео | applied/video.md | Встраивание видео *(в разработке)* | + +--- + +## Триггеры + +Пошаговые инструкции. Найди задачу → открой триггер → выполняй по шагам. + +### Создание + +| Задача | Триггер | Описание | +|--------|---------|----------| +| Создать компонент | triggers/develop/create-component.md | Переиспользуемый UI-элемент без бизнес-логики | +| Создать фичу | triggers/develop/create-feature.md | Самодостаточный блок с бизнес-логикой и UI | +| Создать виджет | triggers/develop/create-widget.md | Композиция нескольких фичей и сущностей | +| Создать сущность | triggers/develop/create-entity.md | Бизнес-объект с моделью данных и UI-представлением | +| Создать хук | triggers/develop/create-hook.md | Кастомный React-хук с переиспользуемой логикой | +| Создать стор | triggers/develop/create-store.md | Глобальное или модульное состояние через Zustand | +| Создать страницу | triggers/develop/create-page.md | Новый route в Next.js — экран + page.tsx | +| Создать layout | triggers/develop/create-layout.md | Общая обёртка layout.tsx для группы страниц | +| Создать проект | triggers/develop/create-project.md | Инициализация нового проекта из шаблона | +| Сгенерировать модуль | triggers/develop/generate-module.md | Создание модуля из шаблонов `.templates/` | + +### Стилизация и ресурсы + +| Задача | Триггер | Описание | +|--------|---------|----------| +| Стилизовать компонент | triggers/develop/style-component.md | Выбор подхода и написание CSS для компонента | +| Добавить иконку | triggers/develop/add-icon.md | SVG-иконка через спрайт-систему | +| Добавить изображение | triggers/develop/add-image.md | Растровое изображение (png, jpg, webp) | +| Добавить видео | triggers/develop/add-video.md | Встраивание видео на страницу | +| Подключить шрифт | triggers/develop/add-font.md | Подключение нового шрифта в проект | + +### Данные и состояние + +| Задача | Триггер | Описание | +|--------|---------|----------| +| Добавить API-запрос | triggers/develop/add-api-request.md | Клиентский запрос данных через SWR | +| Подключить стор | triggers/develop/connect-store.md | Подключение существующего стора к компоненту | +| Серверные данные (RSC) | triggers/develop/add-server-data.md | Получение данных в серверных компонентах | + +### Инфраструктура + +| Задача | Триггер | Описание | +|--------|---------|----------| +| Добавить перевод | triggers/develop/add-localization.md | Ключи перевода и подключение i18next | +| Добавить зависимость | triggers/develop/add-dependency.md | Подключение новой npm-библиотеки | +| Настроить VS Code | triggers/develop/setup-vscode.md | Расширения, настройки редактора, сниппеты | diff --git a/preview/ai/applied/api.md b/preview/ai/applied/api.md new file mode 100644 index 0000000..aa89beb --- /dev/null +++ b/preview/ai/applied/api.md @@ -0,0 +1,5 @@ +--- +scope: applied +keywords: [api, запрос, fetch, SWR, эндпоинт, REST, клиент] +when: "Работа с API: запросы, клиенты, обработка ответов" +--- diff --git a/preview/ai/applied/components.md b/preview/ai/applied/components.md new file mode 100644 index 0000000..e6cb766 --- /dev/null +++ b/preview/ai/applied/components.md @@ -0,0 +1,118 @@ +--- +title: Компоненты +scope: applied +keywords: [компонент, props, jsx, ui, clsx, cl, React, FC] +when: "Создание или редактирование React-компонентов: структура, пропсы, стили" +--- +# Компоненты + +Правила написания React-компонентов: файловая структура модуля, типизация пропсов, документирование и реализация. Раздел охватывает компоненты всех слоёв — от `shared/ui` до `screens`. + +Архитектурные слои и их назначение описаны в разделе [Архитектура](/basics/architecture). + + +## Правила организации + +1. Один компонент — один файл. +2. Компонент не содержит бизнес-логики — логика и сайд-эффекты выносятся в хуки или сторы. +3. Дочерние компоненты размещаются в сегменте `ui/` и подчиняются тем же правилам структуры. +4. Публичный API модуля — только `index.ts`. Прямые импорты внутренних файлов запрещены. + +## Базовая структура компонента + +Минимальный набор файлов: компонент, стили, типы и публичный экспорт. + +```text +container/ +├── styles/ +│ └── container.module.css +├── types/ +│ └── container.type.ts +├── container.ui.tsx +└── index.ts +``` + +## Именования + +- Имя корневого css класса всегда `.root` +- Тип пропсов именуется `{ComponentName}Props`. +- Тип пользовательских параметров именуется `{ComponentName}Params`. + +## Типизация + +Структура типов компонента показана в [примере](#пример). Ниже — обоснования ключевых решений. + +- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно. +- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра. +- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных. +- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/basics/typing). + +## Реализация + +- Пропсы деструктурируются в теле компонента, не в параметрах. +- Порядок: пользовательские → системные (`children`, `className`) → `...htmlAttr`. +- `className` объединяется с корневым классом через `cl()`: `cl(styles.root, className)`. +- `...htmlAttr` прокидывается на корневой элемент. + +## Пример + +`container/types/container.type.ts` + +```ts +import type { HTMLAttributes } from 'react' + +/** + * Параметры компонента Container. + */ +export type ContainerParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type ContainerProps = RootAttrs & ContainerParams +``` + +`container/styles/container.module.css` + +```css +.root { + max-width: var(--content-width); + margin: 0 auto; + padding: 0 var(--spacing-4); +} +``` + +`container/container.ui.tsx` + +```tsx +import cl from 'clsx' +import type { ContainerProps } from './types/container.type' +import styles from './styles/container.module.css' + +/** + * Контейнер с адаптивной максимальной шириной. + * + * Используется для: + * - обёртки контента страниц с ограничением ширины + * - центрирования блоков в лейауте + */ +export const Container = (props: ContainerProps) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} +``` + +`container/index.ts` + +```ts +export { Container } from './container.ui' +``` + +## Дочерние компоненты + +Если модулю нужны внутренние подкомпоненты — генерировать их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя. diff --git a/preview/ai/applied/fonts.md b/preview/ai/applied/fonts.md new file mode 100644 index 0000000..aa9baae --- /dev/null +++ b/preview/ai/applied/fonts.md @@ -0,0 +1,5 @@ +--- +scope: applied +keywords: [шрифт, font, next/font, подключение шрифта, woff] +when: "Подключение и настройка шрифтов" +--- diff --git a/preview/ai/applied/hooks.md b/preview/ai/applied/hooks.md new file mode 100644 index 0000000..52f342e --- /dev/null +++ b/preview/ai/applied/hooks.md @@ -0,0 +1,5 @@ +--- +scope: applied +keywords: [хук, hook, use, кастомный хук, useState, useEffect] +when: "Создание или использование кастомных хуков" +--- diff --git a/preview/ai/applied/images-sprites.md b/preview/ai/applied/images-sprites.md new file mode 100644 index 0000000..a5ffc0b --- /dev/null +++ b/preview/ai/applied/images-sprites.md @@ -0,0 +1,5 @@ +--- +scope: applied +keywords: [изображение, картинка, image, next/image, public, оптимизация] +when: "Работа с изображениями: подключение, оптимизация" +--- diff --git a/preview/ai/applied/localization.md b/preview/ai/applied/localization.md new file mode 100644 index 0000000..b03e5f0 --- /dev/null +++ b/preview/ai/applied/localization.md @@ -0,0 +1,5 @@ +--- +scope: applied +keywords: [i18n, локализация, перевод, язык, i18next, namespace] +when: "Локализация: добавление переводов, работа с i18next" +--- diff --git a/preview/ai/applied/page-level.md b/preview/ai/applied/page-level.md new file mode 100644 index 0000000..6b29e35 --- /dev/null +++ b/preview/ai/applied/page-level.md @@ -0,0 +1,68 @@ +--- +title: Файлы роутинга +scope: applied +keywords: [page.tsx, layout.tsx, error.tsx, not-found.tsx, loading.tsx, App Router, metadata] +when: "Работа с файлами роутинга Next.js App Router: page, layout, error, not-found" +--- +# Файлы роутинга + +Правила для специальных файлов App Router (`page.tsx`, `layout.tsx`, `error.tsx`, `not-found.tsx` и др.) — чем наш подход отличается от дефолтного. + +## Что нужно знать + +Страница в проекте — это два файла: экран в `src/screens/` (вся логика, стили, зависимости) и `page.tsx` в `src/app/` (точка входа для роутинга Next.js). Экран генерируется из шаблона, `page.tsx` создаётся вручную. + +## Организация + +- `page.tsx` — тонкий файл: только `metadata` и рендер экрана. Логика, стили и зависимости живут в экране, не в `page.tsx`. +- `error.tsx` и `not-found.tsx` делегируют разметку экранам по тому же принципу. +- `layout.tsx` — точка подключения провайдеров и глобальных стилей. Вёрстка layout-обёрток выносится в слой `layouts/`. +- Стили в файлах роутинга не используются — стилизация только внутри вызываемых компонентов. + +## Реализация + +- Каждый `page.tsx` экспортирует `metadata` с `title` — он подставляется в шаблон корневого layout (`%s | App`). +- Корневой `layout.tsx` задаёт `metadata` с `title.template`, `description`, `metadataBase` и OpenGraph-настройками. + +## Примеры + +`src/app/profile/[id]/page.tsx` + +```tsx +import type { Metadata } from 'next' +import { ProfileScreen } from '@/screens/profile' + +export const metadata: Metadata = { + title: 'Профиль', + description: 'Страница профиля пользователя', +} + +type ProfilePageProps = { + params: Promise<{ id: string }> +} + +export default async function ProfilePage({ params }: ProfilePageProps) { + const { id } = await params + + return +} +``` + +`src/app/error.tsx` + +```tsx +'use client' + +import { ErrorScreen } from '@/screens/error' + +type ErrorPageProps = { + error: Error & { digest?: string } + reset: () => void +} + +const ErrorPage = ({ error, reset }: ErrorPageProps) => { + return +} + +export default ErrorPage +``` diff --git a/preview/ai/applied/project-structure.md b/preview/ai/applied/project-structure.md new file mode 100644 index 0000000..48da125 --- /dev/null +++ b/preview/ai/applied/project-structure.md @@ -0,0 +1,101 @@ +--- +title: Структура проекта +scope: applied +keywords: [структура проекта, папки, src/app, src/shared, SLM Design, Next.js структура] +when: "Организация папок и файлов в Next.js проекте" +--- +# Структура проекта + +Раздел описывает расположение файлов и папок в проекте Next.js (App Router). + +## Корень репозитория + +```text +project-root/ +├── .templates/ # Шаблоны для генерации модулей +├── .vscode/ # Настройки и рекомендуемые расширения VS Code +├── public/ # Статика, доступная по прямому URL +├── src/ # Исходный код приложения +├── .env.example # Переменные окружения проекта (шаблон) +├── .env # Переменные окружения проекта (не коммитить) +├── .gitignore +├── AGENTS.md # Инструкции для AI-агентов +├── biome.json # Линтер и форматтер (вместо ESLint + Prettier) +├── next.config.ts # Конфигурация Next.js +├── package.json # Зависимости и скрипты +├── postcss.config.mjs # Конфигурация PostCSS +└── tsconfig.json # Конфигурация TypeScript +``` + +## Папка `public/` + +Хранит статические файлы, которые отдаются по прямому URL без обработки сборщиком: + +```text +public/ +└── og-image.png +``` + +Компоненты, стили и другой исходный код здесь не размещаются. + +## Папка `src/` + +```text +src/ +├── app/ # Роутинг Next.js, провайдеры, глобальные стили +├── layouts/ # Каркасы страниц (header, footer, sidebar) +├── screens/ # Контент конкретной страницы +├── widgets/ # Составные блоки интерфейса, не привязанные к домену +├── business/ # Бизнес-домены (auth, catalog, orders) +├── infrastructure/ # Техсервисы (theme, i18n, API-адаптеры) +├── ui/ # UI-кит без бизнес-логики (button, modal, toast) +└── shared/ # Общие ресурсы (утилиты, типы, стили) +``` + +Принципы организации слоёв описаны в разделе [Архитектура](../basics/architecture). + +### Папка `app/` + +Точка входа приложения. Совмещает инициализацию (провайдеры, глобальные стили) и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты). + +```text +src/app/ +├── providers/ # Провайдеры приложения +├── styles/ # Глобальные стили +├── layout.tsx # Корневой layout +└── page.tsx # Главная страница +``` + +## Папка `.templates/` + +Содержит шаблоны для генерации кода. Каждый подкаталог — шаблон отдельного типа модуля: + +```text +.templates/ +├── component/ # Шаблон компонента +├── screen/ # Шаблон экрана +├── layout/ # Шаблон layout +├── widget/ # Шаблон виджета +├── business/ # Шаблон бизнес-модуля +└── store/ # Шаблон стора +``` + +Подробнее о генерации описано в разделе [Шаблоны и генерация кода](./templates-generation). + +## Конфигурационные файлы + +| Файл | Назначение | +|---|---| +| `next.config.ts` | Настройки Next.js: редиректы, переменные окружения, webpack | +| `tsconfig.json` | Настройки TypeScript: пути, строгость, таргет | +| `biome.json` | Правила линтера и форматтера Biome | +| `postcss.config.mjs` | Подключение PostCSS-плагинов (CSS Modules, custom media) | +| `package.json` | Зависимости, версии, npm-скрипты | +| `AGENTS.md` | Инструкции для AI-агентов, работающих в проекте | + +## Переменные окружения + +- `.env` — переменные окружения проекта, запрещено коммитить +- `.env.example` — шаблон, коммитится в репозиторий + +Переменные с префиксом `NEXT_PUBLIC_` доступны в клиентском коде. Остальные доступны только на сервере. diff --git a/preview/ai/applied/stores.md b/preview/ai/applied/stores.md new file mode 100644 index 0000000..a3f3a92 --- /dev/null +++ b/preview/ai/applied/stores.md @@ -0,0 +1,5 @@ +--- +scope: applied +keywords: [стор, store, zustand, состояние, глобальное состояние] +when: "Работа с глобальным состоянием: создание стора, подписка" +--- diff --git a/preview/ai/applied/styles.md b/preview/ai/applied/styles.md new file mode 100644 index 0000000..065397d --- /dev/null +++ b/preview/ai/applied/styles.md @@ -0,0 +1,285 @@ +--- +title: Стили +scope: applied +keywords: [css, postcss, модули, css modules, токены, медиа-запросы, вложенность, класс] +when: "Стилизация: CSS Modules, PostCSS, переменные, медиа-запросы" +--- +# Стили + +Раздел описывает правила написания CSS: PostCSS Modules, вложенность, медиа-запросы, переменные, форматирование. + +## Общие правила + +- Только **PostCSS** и **CSS Modules** для кастомной стилизации. +- Подход **Mobile First** — стили пишутся от мобильных к десктопу. +- Именование классов — `camelCase` (`.root`, `.buttonNext`, `.itemTitle`). +- Модификаторы — отдельный класс с `_`, применяется через `&._modifier`. + +**Хорошо** +```css +.submitButton { + padding: 8px 16px; + + &._disabled { + opacity: 0.5; + } +} +``` + +**Плохо** +```css +/* Плохо: kebab-case и вложенный элемент вместо отдельного класса. */ +.submit-button { + padding: 8px 16px; + + &__icon { + margin-right: 8px; + } +} +``` + +## Вложенность + +- Вложенность селекторов запрещена. +- Исключения: + - Псевдоклассы: `&:hover`, `&:active`, `&:focus`, `&:disabled` и т.д. + - Псевдоэлементы: `&::before`, `&::after`. + - Медиа-запросы: `@media`. + - Модификаторы: `&._active`, `&._disabled`. +- Каждый вложенный блок отделяется пустой строкой от предыдущих свойств. + +**Хорошо** +```css +.card { + padding: 16px; + background-color: var(--color-bg); + + &:hover { + background-color: var(--color-bg-hover); + } + + &::after { + content: ''; + display: block; + } + + &._highlighted { + border-color: var(--color-primary); + } + + @media (--md) { + padding: 24px; + } +} + +.cardTitle { + font-size: 16px; + + @media (--md) { + font-size: 20px; + } +} +``` + +**Плохо** +```css +/* Плохо: вложенность селекторов, нет пустых строк между блоками. */ +.card { + padding: 16px; + .cardTitle { + font-size: 16px; + } + &:hover { + background-color: var(--color-bg-hover); + } +} +``` + +## Медиа-запросы + +- Только **Custom Media Queries**: `@media (--md) {}`. +- Запрещены произвольные breakpoints: `@media (min-width: 768px)`. +- `@media` пишется только **внутри** селектора. +- Запрещено писать `@media` на верхнем уровне с селекторами внутри. + +**Хорошо** +```css +.sidebar { + display: none; + + @media (--md) { + display: block; + } +} + +.sidebarTitle { + font-size: 14px; + + @media (--md) { + font-size: 18px; + } +} +``` + +**Плохо** +```css +/* Плохо: @media на верхнем уровне с селекторами внутри. */ +@media (--md) { + .sidebar { + display: block; + } + + .sidebarTitle { + font-size: 18px; + } +} + +/* Плохо: произвольный breakpoint вместо custom media. */ +.sidebar { + @media (min-width: 992px) { + display: block; + } +} +``` + +## CSS-переменные + +- Цвета (`--color-*`), отступы (`--space-*`), скругления (`--radius-*`) определяются в `app/styles/variables.css` через `:root`. +- Файл переменных подключается один раз в корневом layout/entry point — после этого переменные доступны глобально через каскад. +- Не дублировать магические значения в компонентах. + +**Хорошо** +```css +/* app/styles/variables.css */ +:root { + --color-primary: #3b82f6; + --color-bg: #ffffff; + --color-bg-hover: #f5f5f5; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --radius-1: 4px; + --radius-2: 8px; +} +``` + +```css +/* компонент */ +.card { + padding: var(--space-3); + border-radius: var(--radius-2); + background-color: var(--color-bg); +} +``` + +**Плохо** +```css +/* Плохо: магические значения вместо переменных. */ +.card { + padding: 12px; + border-radius: 8px; + background-color: #ffffff; +} +``` + +## Custom Media + +- Breakpoints определяются через Custom Media Queries в `app/styles/media.css`. +- Custom media подключаются глобально через конфиг PostCSS (плагин `postcss-custom-media`) — не импортировать в файлы стилей. + +```css +/* app/styles/media.css */ +@custom-media --sm (min-width: 36em); +@custom-media --md (min-width: 62em); +@custom-media --lg (min-width: 82em); +``` + +## Импорт стилей + +- Стили компонента импортируются только внутри своего компонента. +- Запрещено импортировать стили одного компонента в другой. +- Custom media не импортируются в файлы стилей — они подключаются глобально через конфиг PostCSS. + +## Форматирование + +- Пустая строка между селекторами верхнего уровня. +- Пустая строка перед каждым вложенным блоком (медиа, псевдокласс, модификатор). + +**Хорошо** +```css +.userBar { + display: none; + color: var(--color-text); + + @media (--md) { + display: flex; + } +} + +.userBarButton { + background-color: var(--color-bg); + + &:hover { + background-color: var(--color-bg-hover); + } + + &._active { + background-color: var(--color-primary); + } +} +``` + +**Плохо** +```css +/* Плохо: нет пустых строк между селекторами и вложенными блоками. */ +.userBar { + display: none; + color: var(--color-text); + @media (--md) { + display: flex; + } +} +.userBarButton { + background-color: var(--color-bg); + &:hover { + background-color: var(--color-bg-hover); + } + &._active { + background-color: var(--color-primary); + } +} +``` + +## Единицы измерения + +- `px` — основная единица измерения. +- Остальные (`em`, `rem`, `%`, `vh`/`vw`) — допускаются по необходимости дизайна. + +## Порядок CSS-свойств + +В стилях рекомендуется придерживаться логического порядка свойств: + +1. Позиционирование (`position`, `top`, `left`, `z-index`). +2. Блочная модель (`display`, `width`, `height`, `margin`, `padding`). +3. Оформление (`background`, `border`, `box-shadow`, `border-radius`). +4. Текст (`font`, `color`, `text-align`, `line-height`). +5. Прочее (`transition`, `animation`, `opacity`, `cursor`). + +## Комментарии + +- Желательно не писать комментарии в CSS. +- Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение. + +## Приоритет стилизации + +Основной UI-фреймворк проекта — **Mantine**. При стилизации компонентов придерживаться следующего приоритета: + +1. **Mantine-компоненты и их пропсы** — в первую очередь использовать встроенные возможности Mantine (пропсы, `classNames`, `styles`). +2. **Глобальные CSS-токены** (`--color-*`, `--space-*`, `--radius-*`) — для значений, которые не покрываются Mantine. +3. **PostCSS Modules** — когда Mantine не покрывает задачу и нужна кастомная стилизация. + +## Что запрещено + +- **Инлайн-стили** — использование атрибута `style` в компонентах строго запрещено. +- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены. +- **Глобальные стили** вне `app/styles/` запрещены. diff --git a/preview/ai/applied/svg-sprites.md b/preview/ai/applied/svg-sprites.md new file mode 100644 index 0000000..1d59ecd --- /dev/null +++ b/preview/ai/applied/svg-sprites.md @@ -0,0 +1,7 @@ +--- +title: SVG-спрайты +scope: applied +keywords: [svg, спрайт, иконка, icon, sprite] +when: "Работа с SVG-иконками и спрайтами" +--- +# SVG-спрайты diff --git a/preview/ai/applied/templates-generation.md b/preview/ai/applied/templates-generation.md new file mode 100644 index 0000000..60da81f --- /dev/null +++ b/preview/ai/applied/templates-generation.md @@ -0,0 +1,174 @@ +--- +title: Шаблоны и генерация кода +scope: applied +keywords: [шаблон, генерация, template, scaffold, plop, hygen, .templates] +when: "Генерация кода из шаблонов, создание новых шаблонов" +--- + +::: v-pre + +# Шаблоны и генерация кода + +Как работают шаблоны, как их создавать, синтаксис переменных и как генерировать код с помощью расширения VS Code и CLI. + +## Структура шаблонов + +Все шаблоны лежат в `.templates/` в корне проекта. Каждая папка — отдельный шаблон. + +```text +.templates/ +├── component/ # шаблон компонента +│ └── {{name.kebabCase}}/ +│ ├── styles/ +│ │ └── {{name.kebabCase}}.module.css +│ ├── types/ +│ │ └── {{name.kebabCase}}.type.ts +│ ├── {{name.kebabCase}}.tsx +│ └── index.ts +└── store/ # шаблон Zustand стора + └── {{name.kebabCase}}/ + ├── {{name.kebabCase}}.store.ts + ├── {{name.kebabCase}}.type.ts + └── index.ts +``` + +## Синтаксис шаблонов + +Переменные работают в именах файлов/папок и внутри файлов. Базовая переменная — `name`. + +```text +{{variable}} +``` + +Модификаторы меняют регистр и формат записи: + +```text +{{name.pascalCase}} → MyButton +{{name.camelCase}} → myButton +{{name.kebabCase}} → my-button +{{name.snakeCase}} → my_button +{{name.screamingSnakeCase}} → MY_BUTTON +``` + +## Как создать новый шаблон + +1. Создать папку в `.templates/` с именем шаблона (например `hook`). +2. Внутри разместить файлы и папки, используя `{{name}}` и модификаторы в именах и содержимом. +3. Шаблон сразу доступен и в расширении VS Code, и в CLI. + +Пример — создание шаблона для хука: + +```text +.templates/ +└── hook/ + └── {{name.kebabCase}}/ + ├── {{name.kebabCase}}.hook.ts + └── index.ts +``` + +```ts +// .templates/hook/{{name.kebabCase}}.hook.ts +export const {{name.camelCase}} = () => { + +} +``` + +```ts +// .templates/hook/index.ts +export { {{name.camelCase}} } from './{{name.kebabCase}}.hook' +``` + +## Примеры шаблонов + +### Шаблон компонента + +```ts +// .templates/component/index.ts +export { {{name.pascalCase}} } from './{{name.kebabCase}}' +``` + +```ts +// .templates/component/types/{{name.kebabCase}}.type.ts +import type { HTMLAttributes } from 'react' + +/** + * Параметры {{name.pascalCase}}. + */ +export type {{name.pascalCase}}Params = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params +``` + +```tsx +// .templates/component/{{name.kebabCase}}.tsx +import cl from 'clsx' +import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type' +import styles from './styles/{{name.kebabCase}}.module.css' + +/** + * {{name.pascalCase}}. + */ +export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => { + const { children, className, ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} +``` + +```css +/* .templates/component/styles/{{name.kebabCase}}.module.css */ +.root { + +} +``` + +## Генерация через VS Code + +Template File Generator | gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) — расширение для генерации файлов и папок из шаблонов через интерфейс редактора. + +1. ПКМ на целевой папке в проводнике VS Code. +2. **Generate from template** → выбрать шаблон. +3. Ввести имя (например `button`) — расширение подставит его во все переменные `{{name}}`. + +## Генерация через CLI + +[@gromlab/create](https://www.npmjs.com/package/@gromlab/create) — CLI для генерации из тех же шаблонов. Используется через npx, глобальная установка не требуется. + +```bash +npx @gromlab/create <шаблон> <имя> <путь> +``` + +| Команда | Что создаёт | +|---|---| +| `npx @gromlab/create component button src/ui` | Компонент | +| `npx @gromlab/create business auth src/business` | Бизнес-модуль | +| `npx @gromlab/create widget header src/widgets` | Виджет | +| `npx @gromlab/create layout admin src/layouts` | Layout | +| `npx @gromlab/create screen home src/screens` | Экран | +| `npx @gromlab/create store auth src/business/auth/stores` | Стор | + +::: + +## Какие модули генерируются из шаблонов + +| Модуль | Слой | Шаблон | +|---|---|---| +| Компонент | `ui/` | `component` | +| Бизнес-модуль | `business/` | `business` | +| Виджет | `widgets/` | `widget` | +| Layout | `layouts/` | `layout` | +| Экран | `screens/` | `screen` | +| Стор | `stores/` | `store` | + +## Когда создавать новый шаблон + +- Повторяющаяся структура появляется больше одного раза. +- Существующий шаблон не покрывает нужный тип модуля. + diff --git a/preview/ai/applied/video.md b/preview/ai/applied/video.md new file mode 100644 index 0000000..b8bd490 --- /dev/null +++ b/preview/ai/applied/video.md @@ -0,0 +1,5 @@ +--- +scope: applied +keywords: [видео, video, плеер, mp4] +when: "Встраивание и работа с видео" +--- diff --git a/preview/ai/applied/vscode.md b/preview/ai/applied/vscode.md new file mode 100644 index 0000000..5d313fe --- /dev/null +++ b/preview/ai/applied/vscode.md @@ -0,0 +1,89 @@ +--- +title: Настройка VS Code +scope: applied +keywords: [vscode, редактор, расширение, настройка, extension, .vscode] +when: "Настройка VS Code: расширения, settings.json, сниппеты" +--- +# Настройка VS Code + +Каждый проект содержит папку `.vscode/` с конфигурацией редактора. Это гарантирует, что все участники команды работают с одинаковыми настройками форматирования, линтинга и расширениями. + +## Структура `.vscode/` + +```text +.vscode/ +├── extensions.json # Рекомендуемые расширения +└── settings.json # Настройки редактора для проекта +``` + +Оба файла коммитятся в репозиторий. + +## Расширения + +Файл `.vscode/extensions.json` определяет список расширений, которые VS Code предложит установить при открытии проекта. + +```json +// .vscode/extensions.json +{ + "recommendations": [ + "biomejs.biome", + "MyTemplateGenerator.mytemplategenerator", + "csstools.postcss" + ] +} +``` + +| Расширение | Назначение | +|---|---| +| [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) | Линтинг и форматирование кода. Заменяет ESLint и Prettier | +| Template File Generator \| gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) | Генерация файлов и папок из шаблонов `.templates/` через контекстное меню | +| [PostCSS Language Support](https://marketplace.visualstudio.com/items?itemName=csstools.postcss) | Подсветка синтаксиса и автодополнение для PostCSS (`@custom-media`, `@nest` и др.) | + +### Зачем это нужно + +- Новый участник команды получает все нужные расширения одним кликом. +- Нет разночтений: все используют одинаковый форматтер и линтер. +- Расширения привязаны к проекту, а не к конкретному разработчику. + +## Настройки редактора + +Файл `.vscode/settings.json` переопределяет пользовательские настройки VS Code на уровне проекта. + +```json +// .vscode/settings.json +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "files.associations": { + "*.css": "postcss" + } +} +``` + +### Разбор настроек + +| Настройка | Значение | Что делает | +|---|---|---| +| `editor.defaultFormatter` | `biomejs.biome` | Biome используется как единственный форматтер для всех файлов | +| `editor.formatOnSave` | `true` | Код автоматически форматируется при каждом сохранении | +| `codeActionsOnSave.source.fixAll.biome` | `explicit` | Biome автоматически применяет безопасные исправления при сохранении | +| `codeActionsOnSave.source.organizeImports.biome` | `explicit` | Импорты сортируются и группируются автоматически при сохранении | +| `files.associations` | `"*.css": "postcss"` | Все CSS-файлы открываются с подсветкой PostCSS вместо стандартного CSS | + +### Зачем это нужно + +- **Единый стиль кода** -- форматирование происходит автоматически, невозможно закоммитить неформатированный код. +- **Автофикс при сохранении** -- распространённые ошибки линтинга исправляются без ручного вмешательства. +- **Сортировка импортов** -- импорты всегда в одном порядке, без конфликтов при мерже. +- **PostCSS-подсветка** -- кастомные at-правила (`@custom-media`, `@define-mixin`) подсвечиваются корректно, а не как ошибки. + +## Что не должно быть в `.vscode/` + +Не коммитятся файлы, специфичные для конкретного разработчика: + +- **Не коммитить**: отладочные конфигурации с локальными путями, персональные сниппеты, настройки тем оформления. +- **Коммитить**: только `extensions.json` и `settings.json` с общими для команды настройками. diff --git a/preview/ai/basics/architecture.md b/preview/ai/basics/architecture.md new file mode 100644 index 0000000..6a17d9d --- /dev/null +++ b/preview/ai/basics/architecture.md @@ -0,0 +1,665 @@ +--- +title: Архитектура +scope: basics +keywords: [SLM Design, слой, модуль, сегмент, архитектура, FSD, scoped layered module] +when: "Организация кода: слои, модули, зависимости между модулями" +--- + + +# SLM Design +Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. + +## Преимущества + +### Вертикальная организация домена + +Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. + +### Dependency Injection без фреймворков + +Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. + +### Разделение ответственности без перегрузки слоёв + +Сервисы приложения (`infrastructure/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. + +### Горизонтальная инкапсуляция + +Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. + +### Колокация по умолчанию + +Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. + +### Явное разделение каркаса и контента + +Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. + +### Масштабирование через группировку + +При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). + +## Происхождение + +SLM Design вырос на основе: + +- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей +- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое +- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию +- **Colocation Principle** — код живёт рядом с местом использования + +## Пример структуры проекта + +```text +src/ +├── app/ +│ +├── layouts/ +│ ├── main/ +│ └── dashboard/ +│ +├── screens/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── about/ +│ +├── widgets/ +│ ├── page-heading/ +│ ├── hero-section/ +│ └── promo-banner/ +│ +├── business/ +│ ├── auth/ +│ ├── catalog/ +│ ├── orders/ +│ └── chat/ +│ +├── infrastructure/ +│ ├── theme/ +│ ├── i18n/ +│ ├── backend-api/ +│ └── logger/ +│ +├── ui/ +│ ├── button/ +│ ├── input/ +│ ├── modal/ +│ ├── toast/ +│ └── dropdown/ +│ +└── shared/ + ├── lib/ + ├── types/ + └── styles/ +``` + +## Принципы + +- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. +- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. +- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. +- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. + + +## Слои + +Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. + +### Определение + +**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** + +### Группы слоёв + +Слои делятся на три группы: + +| Группа | Слои | Описание | +|--------|------|----------| +| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | +| Ядро | `business`, `infrastructure`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | +| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | + +### Направление зависимостей + +Любой импорт между модулями — только через публичный API. + +``` +app → [ layouts | screens ] → widgets → business → infrastructure → ui → shared +``` + +- `layouts` и `screens` — параллельные слои, не импортируют друг друга +- Модули одного слоя в группе «Композиция» изолированы друг от друга +- Модули одного слоя `infrastructure` и `ui` могут импортировать друг друга через публичный API +- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую +- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях + + +### Слой App + +Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen. + +В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. + +#### Требования + +- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация +- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов +- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует +- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы +- Никем не импортируется + +### Слой Layouts + +Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar). + +```text +src/layouts/ +├── main/ +├── dashboard/ +└── auth/ +``` + +#### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую + +### Слой Screens + +Контент конкретной страницы: собирает её из модулей нижних слоёв. + +```text +src/screens/ +├── home/ +├── products/ +├── product-detail/ +├── about/ +└── contacts/ +``` + +Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/screens/ +├── shop/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── cart/ +├── account/ +│ ├── profile/ +│ ├── settings/ +│ └── order-history/ +└── info/ + ├── about/ + ├── contacts/ + └── faq/ +``` + +#### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business` + +### Слой Widgets + +Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts. + +Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget. + +```text +src/widgets/ +├── page-heading/ +├── hero-section/ +├── onboarding-checklist/ +├── promo-banner/ +└── error-boundary/ +``` + +#### Требования + +- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/` +- Используется в нескольких screens или layouts + +### Слой Business + +Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами. + +Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`. Cross-domain зависимости по коду реализуются через фабрику. `import type` между доменами разрешён напрямую. + +Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки. + +```text +src/business/ +├── auth/ +├── catalog/ +├── orders/ +├── checkout/ +└── chat/ +``` + +Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/business/ +├── commerce/ +│ ├── catalog/ +│ ├── cart/ +│ ├── orders/ +│ └── checkout/ +└── communication/ + ├── chat/ + └── notifications/ +``` + +#### Требования + +- Один модуль = один бизнес-домен +- Циклические зависимости между доменами запрещены +- Импорт кода между доменами — через фабрику. `import type` — напрямую +- Доменные типы (`User`, `Product`) живут здесь, не в `shared/` + +### Слой Infrastructure + +Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. + +Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`. + +Отличие от `shared/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +```text +src/infrastructure/ +├── theme/ +├── i18n/ +├── backend-api/ +├── maps-api/ +├── logger/ +├── feature-flags/ +└── realtime/ +``` + +#### Требования + +- Один модуль = один техсервис +- Импортирует `infrastructure/`, `ui/`, `shared/` + +### Слой UI + +UI-кит без бизнес-логики: button, carousel, toast, modal. + +Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. + +Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. + +```text +src/ui/ +├── button/ +├── input/ +├── icon/ +├── carousel/ +├── modal/ +├── toast/ +├── dropdown/ +├── tabs/ +└── tooltip/ +``` + +Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. + +```text +src/ui/ +├── primitives/ +│ ├── button/ +│ ├── input/ +│ ├── icon/ +│ └── badge/ +└── composites/ + ├── carousel/ + ├── modal/ + ├── dropdown/ + ├── tabs/ + └── tooltip/ +``` + +#### Требования + +- Не содержит бизнес-логику +- Импортирует только `ui/` и `shared/` + +### Слой Shared + +Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. + +Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. + +Отличие от `infrastructure/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. + +```text +src/shared/ +├── lib/ +├── types/ +├── styles/ +└── sprites/ +``` + +#### Требования + +- Не имеет runtime-состояния + + +## Модули + +Раздел описывает модули SLM: что такое модуль, из чего он состоит и как взаимодействует с остальным кодом. + +### Определение + +**Модуль — универсальный строительный блок архитектуры. Живёт на слое и содержит всё необходимое для своей работы: компоненты, хуки, сторы, сервисы, типы, стили. Набор содержимого не фиксирован — включаются только нужные части.** + +### Модуль vs компонент + +**Компонент** — один `.tsx` файл. Не имеет своих сегментов, использует сегменты родительского модуля. Живёт в корне или `ui/` сегменте модуля. + +**Модуль** — папка, которая может содержать корневой компонент, сегменты (`hooks/`, `types/`, `styles/`, `ui/`, `parts/` и т.д.) и публичный API (`index.ts`). + +```text +auth/ +├── ui/ +│ ├── auth-guard.tsx +│ └── logout-button.tsx +├── parts/ +│ ├── login-form/ +│ ├── registration-form/ +│ └── restore-form/ +├── hooks/ +├── stores/ +├── types/ +├── auth.tsx # корневой компонент (опционален) +└── index.ts +``` + +### Структура + +Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль может состоять даже из одного `index.ts` с реэкспортом типов. + +```text +{module-name}/ +├── {module-name}.tsx # корневой компонент (опционален) +├── ui/ # компоненты модуля (только .tsx) +├── parts/ # вложенные модули (со своими сегментами) +├── hooks/ # хуки +├── stores/ # сторы состояния +├── services/ # внешние источники данных +├── mappers/ # трансформация данных между форматами +├── types/ # типы +├── styles/ # стили +├── lib/ # утилиты модуля +├── config/ # константы +└── index.ts # публичный API +``` + +Подробное описание каждого сегмента — в разделе [Сегменты](/reference/segments). + +### Публичный API + +Модуль экспортирует наружу только то, что нужно другим. Всё остальное — внутреннее. + +```ts +// business/auth/index.ts +export type { User, Session } from './types/user.types' +export { useAuth } from './hooks/use-auth.hook' +export { AuthGuard } from './ui/auth-guard' +``` + +Импорт в обход `index.ts` запрещён: + +```ts +// Плохо +import { validateToken } from '@/business/auth/lib/tokens' + +// Хорошо +import { useAuth } from '@/business/auth' +``` + +### Фабрика + +Если модуль зависит от кода другого бизнес-домена — он экспортирует фабрику. Фабрика декларирует необходимые зависимости и возвращает API модуля. Точка использования (screen, widget, layout) предоставляет зависимости при вызове. + +Модуль без cross-domain зависимостей экспортирует API напрямую. Типы всегда экспортируются напрямую — `import type` не является runtime-зависимостью. + +#### Модуль без зависимостей — прямой экспорт: + +```ts +// business/auth/index.ts +export { useAuth } from './hooks/use-auth' +export { useCurrentUser } from './hooks/use-current-user' +export type { User, Session } from './types' +``` + +#### Модуль с зависимостями — фабрика: + +```ts +// business/chat/types/deps.ts +import type { User } from '@/business/auth' + +export interface ChatDeps { + useCurrentUser: () => User | null +} +``` + +```ts +// business/chat/index.ts +import type { ChatDeps } from './types/deps' + +export function chatFactory(deps: ChatDeps) { + return { + useMessages: (roomId: string) => { + const user = deps.useCurrentUser() + // ... + }, + useSendMessage: (roomId: string) => { + const user = deps.useCurrentUser() + return (text: string) => { /* ... */ } + }, + useChatRooms: () => { + const user = deps.useCurrentUser() + // ... + }, + ChatBadge: ({ count }: { count: number }) => { /* ... */ }, + } +} + +export type { Message, ChatRoom } from './types' +export type { ChatDeps } from './types/deps' +``` + +#### Использование на странице: + +```tsx +// screens/support/support.tsx +import { useCurrentUser } from '@/business/auth' +import { chatFactory } from '@/business/chat' + +const chat = chatFactory({ useCurrentUser }) + +export function SupportScreen() { + const { useMessages, useSendMessage, ChatBadge } = chat + const messages = useMessages('support') + const sendMessage = useSendMessage('support') + + return ( +
+ + {messages.map(m => )} + +
+ ) +} +``` + +### Жизненный цикл + +Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. + +- Нужен на одной странице → `screens/{name}/parts/` +- Появился в 2+ местах → поднимается по природе: + - абстрактный UI → `ui/` + - блок с данными/логикой → `widgets/` + - представление бизнес-домена → `business/{area}/parts/` + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. + + +## Сегменты + +Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. + +### Определение + +**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** + +### Обзор + +| Сегмент | Содержимое | +|---------|------------| +| `ui/` | Компоненты модуля — только `.tsx` файлы | +| `parts/` | Вложенные модули со своими сегментами | +| `hooks/` | React-хуки | +| `stores/` | Сторы состояния | +| `services/` | Работа с внешними источниками данных | +| `mappers/` | Трансформация данных между форматами | +| `types/` | TypeScript-типы и интерфейсы | +| `styles/` | Стили | +| `lib/` | Утилиты и хелперы модуля | +| `config/` | Константы и конфигурация | + +### Сегмент ui/ + +Компоненты, принадлежащие модулю. Содержит только `.tsx` файлы — без своих сегментов, стилей, типов, хуков. Использует сегменты родительского модуля. + +```text +auth/ +├── ui/ +│ ├── auth-provider.tsx +│ ├── auth-guard.tsx +│ └── logout-button.tsx +├── types/ +├── hooks/ +└── index.ts +``` + +Если компоненту нужны собственные сегменты — это уже не `ui/`, а `parts/`. + +### Сегмент parts/ + +Вложенные модули со своими сегментами. Каждый элемент `parts/` — полноценный модуль: папка с компонентом, хуками, стилями, типами и т.д. + +```text +home/ +├── parts/ +│ ├── hero-section/ +│ │ ├── hero-section.tsx +│ │ ├── styles/ +│ │ └── parts/ +│ │ └── top-banner/ +│ │ └── top-banner.tsx +│ └── features-section/ +│ ├── features-section.tsx +│ └── hooks/ +├── home.screen.tsx +└── index.ts +``` + +Отличие от `ui/`: элемент `parts/` — модуль со своими сегментами. Элемент `ui/` — компонент, один `.tsx` файл. + +Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. + +Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. + +### Сегмент hooks/ + +React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. + +```text +hooks/ +├── use-auth.hook.ts +├── use-session.hook.ts +└── use-permissions.hook.ts +``` + +### Сегмент stores/ + +Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.). + +```text +stores/ +├── auth.store.ts +└── session.store.ts +``` + +### Сегмент services/ + +Работа с внешними источниками данных: API-вызовы, запросы, подписки. + +```text +services/ +├── auth.service.ts +└── token.service.ts +``` + +### Сегмент mappers/ + +Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel. + +```text +mappers/ +├── map-user.ts +├── map-product.ts +└── map-order-to-dto.ts +``` + +### Сегмент types/ + +TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. + +```text +types/ +├── user.type.ts +└── session.type.ts +``` + +### Сегмент styles/ + +Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). + +```text +styles/ +├── auth.module.css +└── login-form.module.css +``` + +### Сегмент lib/ + +Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. + +```text +lib/ +├── validate-email.ts +└── format-phone.ts +``` + +Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. + +### Сегмент config/ + +Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. + +```text +config/ +├── routes.ts +└── constants.ts +``` diff --git a/preview/ai/basics/code-style.md b/preview/ai/basics/code-style.md new file mode 100644 index 0000000..f933e18 --- /dev/null +++ b/preview/ai/basics/code-style.md @@ -0,0 +1,154 @@ +--- +title: Стиль кода +scope: basics +keywords: [форматирование, импорт, отступ, кавычки, early return, точка с запятой, линтер] +when: "Написание или ревью любого кода: форматирование, импорты, структура файла" +--- +# Стиль кода + +Раздел описывает единые правила оформления кода: отступы, переносы, кавычки, порядок импортов и базовую читаемость. + +## Отступы + +- 2 пробела (не табы). + +## Длина строк + +- Ориентироваться на 100 символов, но превышение допустимо, если строка читается легко. +- Переносить выражение на новые строки, когда строка становится плохо читаемой. +- Не переносить строку внутри строковых литералов без необходимости. + +**Хорошо** +```ts +const config = createRequestConfig( + endpoint, + { + headers: { + 'X-Request-Id': requestId, + 'X-User-Id': userId, + }, + params: { + page, + pageSize, + sort: 'createdAt', + }, + }, + timeoutMs, +); +``` + +**Плохо** +```ts +// Плохо: длинная строка с вложенными структурами плохо читается. +const config = createRequestConfig(endpoint, { headers: { 'X-Request-Id': requestId, 'X-User-Id': userId }, params: { page, pageSize, sort: 'createdAt' } }, timeoutMs); +``` + +## Кавычки + +- В JavaScript/TypeScript использовать одинарные кавычки. +- В JSX/TSX для атрибутов использовать двойные кавычки. +- Шаблонные строки использовать только при интерполяции или многострочном тексте. + +**Хорошо** +```ts +const label = 'Сохранить'; +const title = `Привет, ${name}`; +``` + +```tsx + +``` + +**Плохо** +```ts +// Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки. +const label = "Сохранить"; +const title = 'Привет, ' + name; +``` + +```tsx +// Плохо: одинарные кавычки в JSX-атрибутах. + +``` + +## Точки с запятой и запятые + +- Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным. +- В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна. + +## Импорты + +- В именованных импортах использовать пробелы внутри фигурных скобок. +- Типы импортировать через `import type`. +- `default` экспорт избегать, использовать именованные. `default` импорт допустим (например, стили CSS Modules, сторонние библиотеки). +- Избегать импорта всего модуля через `*`. + +**Хорошо** +```ts +import { MyComponent } from 'MyComponent'; +import type { User } from '../model/types'; +import styles from './styles/button.module.css'; +``` + +**Плохо** +```ts +// Плохо: отсутствие пробелов в именованном импорте. +import type {User} from '../model/types'; +// Плохо: default экспорт. +export default MyComponent; +``` + +## Ранние возвраты (early return) + +- Использовать ранние возвраты для упрощения чтения. +- Избегать `else` после `return`. + +**Хорошо** +```ts +const getName = (user?: { name: string }) => { + if (!user) { + return 'Гость'; + } + + return user.name; +}; +``` + +**Плохо** +```ts +// Плохо: лишний else после return усложняет чтение. +const getName = (user?: { name: string }) => { + if (user) { + return user.name; + } else { + return 'Гость'; + } +}; +``` + +## Форматирование объектов и массивов + +- В многострочных объектах каждое свойство на новой строке. +- В многострочных массивах каждый элемент на новой строке. +- Объекты и массивы можно писать в одну строку, если длина строки не превышает 100 символов. +- В однострочных объектах и массивах использовать пробелы после запятых. + +**Хорошо** +```ts +const roles = ['admin', 'editor', 'viewer']; +const options = { id: 1, name: 'User' }; + +const config = { + url: '/api/users', + method: 'GET', + params: { page: 1, pageSize: 20 }, +}; +``` + +**Плохо** +```ts +// Плохо: нет пробелов после запятых и объект слишком длинный для одной строки. +const roles = ['admin','editor','viewer']; +const options = { id: 1,name: 'User' }; +const config = { url: '/api/users', method: 'GET', params: { page: 1, pageSize: 20 } }; +``` diff --git a/preview/ai/basics/documentation.md b/preview/ai/basics/documentation.md new file mode 100644 index 0000000..96ac8a5 --- /dev/null +++ b/preview/ai/basics/documentation.md @@ -0,0 +1,136 @@ +--- +title: Документирование +scope: basics +keywords: [JSDoc, комментарий, документирование, описание функции, описание компонента] +when: "Документирование кода: JSDoc для функций, компонентов, типов" +--- +# Документирование + +Этот раздел описывает правила документирования кода: когда и как писать +комментарии к компонентам, функциям, типам и интерфейсам. + +## Общие правила + +- Документировать публичные функции, компоненты, типы, интерфейсы и enum. +- Не документировать очевидное — если название говорит само за себя, комментарий не нужен. +- Не документировать параметры, возвращаемые значения и типы пропсов — они видны из сигнатуры. +- Описание через пользу и назначение, а не через внутреннюю реализацию. +- Описание завершается точкой. + +## Функции + +Для документирования функций используется шаблон. Описание механики опционально — +добавляется когда логика нетривиальна. + +**Шаблон** +```ts +/** + * <Что делает функция в 1 строке>. + * + * <Опционально: описание сложной механики или важных нюансов>. + */ +``` + +**Хорошо** +```ts +/** + * Форматирует цену с символом валюты. + */ +export const formatPrice = (value: number): string => { ... } + +/** + * Рекурсивно собирает дерево категорий из плоского списка. + * + * Группирует элементы по parentId, начиная с корневых (parentId = null). + * Категории без родителя попадают в корень дерева. + */ +export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { ... } +``` + +**Плохо** +```ts +// Плохо: дублирует сигнатуру. +/** + * @param value - число + * @returns строка с ценой + */ +``` + +## Компоненты + +Компонент описывает своё **назначение** и **сценарии применения** — это помогает понять, когда и где его использовать, без необходимости читать реализацию. + +**Шаблон** +```ts +/** + * <Назначение компонента в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + * - <сценарий 3> + */ +``` + +**Хорошо** +```tsx +/** + * Контейнер с адаптивной максимальной шириной. + * + * Используется для: + * - обёртки контента страниц с ограничением ширины + * - центрирования блоков в лейауте + */ +export const Container = (props: ContainerProps) => { ... } +``` + +**Плохо** +```tsx +// Плохо: описывает реализацию, а не назначение. +/** + * Рендерит div с className и htmlAttr. + */ + +// Плохо: нет описания вообще. +export const Container = (props: ContainerProps) => { ... } +``` + +## Типы, интерфейсы, enum + +Документируются назначение сущности и каждое её поле. + +**Хорошо** +```ts +/** + * Фильтры списка задач. + */ +export enum TodoFilter { + /** Все задачи. */ + ALL = 'all', + /** Только активные. */ + ACTIVE = 'active', + /** Только завершённые. */ + COMPLETED = 'completed', +} + +/** + * Задача пользователя. + */ +export interface TodoItem { + /** Уникальный идентификатор задачи. */ + id: string; + /** Текст задачи. */ + text: string; + /** Статус выполнения. */ + completed: boolean; +} +``` + +**Плохо** +```ts +// Плохо: описывает очевидное. +export interface TodoItem { + /** id — это id */ + id: string; +} +``` diff --git a/preview/ai/basics/naming.md b/preview/ai/basics/naming.md new file mode 100644 index 0000000..1519712 --- /dev/null +++ b/preview/ai/basics/naming.md @@ -0,0 +1,149 @@ +--- +title: Именование +scope: basics +keywords: [camelCase, kebab-case, PascalCase, имя файла, имя переменной, имя компонента, имя хука] +when: "Создание файлов, переменных, компонентов, хуков — выбор имени" +--- +# Именование + +Этот раздел описывает соглашения об именовании в проекте. Единые правила делают код предсказуемым и упрощают навигацию по проекту. + +## Базовые правила + +| Что | Рекомендуется | +| ---------------- | ---------------------- | +| Папки | `kebab-case` | +| Файлы | `kebab-case` | +| Переменные | `camelCase` | +| Константы | `SCREAMING_SNAKE_CASE` | +| Классы | `PascalCase` | +| React-компоненты | `PascalCase` | +| Хуки | `useSomething` | +| CSS классы | `camelCase` | +| Ключи enum | `SCREAMING_SNAKE_CASE` | + + +## Именование файлов + +Суффикс обозначает роль или тип файла. Пишется в единственном числе. +Формат: `name..ts`. + +**Хуки** +- `use-name.hook.ts` — файл хука, функция именуется `useName` + +**Корневые компоненты модулей** +- `.business.tsx` — бизнес-модуль (`business/`) +- `.infra.tsx` — инфраструктурный модуль (`infrastructure/`) +- `.ui.tsx` — UI-компонент (`ui/`) +- `.screen.tsx` — экран (`screens/`) +- `.widget.tsx` — виджет (`widgets/`) +- `.layout.tsx` — layout (`layouts/`) + +**Логика** +- `.store.ts` — стор +- `.service.ts` — сервис + +**Типы и контракты** +- `.type.ts` — типы и интерфейсы +- `.interface.ts` — интерфейсы +- `.enum.ts` — enum +- `.dto.ts` — внешние DTO +- `.schema.ts` — схемы валидации +- `.constant.ts` — константы +- `.config.ts` — конфигурация + +**Утилиты** +- `.util.ts` — утилиты +- `.helper.ts` — вспомогательные функции +- `.lib.ts` — библиотечный код + +**Тесты** +- `.test.ts` — тесты +- `.mock.ts` — моки + +**Хорошо** +```text +business/ +└── auth-by-email/ + ├── ui/ + │ └── login-form.tsx + ├── hooks/ + │ └── use-auth.hook.ts + ├── stores/ + │ └── auth.store.ts + ├── types/ + │ └── auth.type.ts + ├── auth-by-email.business.tsx + └── index.ts +``` + +**Плохо** +```text +business/ +└── authByEmail/ + ├── LoginForm.tsx + ├── useAuth.ts + ├── authStore.ts + └── index.ts +``` + +## Булевы значения + +- Использовать префиксы `is`, `has`, `can`, `should`. + +**Хорошо** +```ts +const isReady = true; +const hasAccess = false; +const canSubmit = true; +const shouldRedirect = false; +``` + +**Плохо** +```ts +// Плохо: неясное булево значение без префикса. +const ready = true; +const access = false; +const submit = true; +``` + +## События и обработчики + +- Обработчики начинать с `handle`. +- События и колбэки начинать с `on`. + +**Хорошо** +```ts +const handleSubmit = () => { ... }; +const onSubmit = () => { ... }; +``` + +**Плохо** +```ts +// Плохо: неочевидное назначение имени. +const submitClick = () => { ... }; +``` + +## Коллекции + +- Для массивов использовать имена во множественном числе. +- Для словарей/мап — использовать суффиксы `ById`, `Map`, `Dict`. + +**Хорошо** +```ts +const users = []; +const usersById = {} as Record; +const userIds = ['u1', 'u2']; +const ordersMap = new Map(); +const featureFlagsDict = { beta: true, legacy: false } as Record; +``` + +**Плохо** +```ts +// Плохо: имя не отражает, что это коллекция. +const user = []; +// Плохо: словарь назван как массив. +const usersMap = []; +// Плохо: по имени непонятно, что это словарь. +const users = {} as Record; +``` diff --git a/preview/ai/basics/tech-stack.md b/preview/ai/basics/tech-stack.md new file mode 100644 index 0000000..14773db --- /dev/null +++ b/preview/ai/basics/tech-stack.md @@ -0,0 +1,43 @@ +--- +title: Технологии и библиотеки +scope: basics +keywords: [стек, React, TypeScript, Next.js, Mantine, библиотека, зависимость] +when: "Выбор библиотеки или технологии, проверка допустимости зависимости" +--- +# Технологии и библиотеки + +Этот раздел описывает базовый стек технологий и библиотек, принятый в проекте. + +## Что используем + +### Стек +- `React` / `TypeScript` — основной стек для UI и приложения. +- `Next.js` — для продуктовых сайтов. + +### Архитектура +- `SLM Design (Scoped Layered Module Design)` — модульная архитектура: слои, модули, направление зависимостей. Подробнее в разделе [Архитектура](/basics/architecture). + +### UI компоненты +- `Mantine UI` — базовые UI-компоненты. + +### Работа с данными (API) +- `@gromlab/api-codegen` — генерация API‑клиентов и типов. +- `SWR` — получение, кеширование, ревалидация, дедубликация. +- `SWR (useSWRSubscription)` — сокеты, реалтайм подписки. + +### Store +- `Zustand` — глобальное состояние. + +### Локализация +- `i18next (i18n)` — локализация всех пользовательских текстов. + +### Тестирование +- `Vitest` — тестирование. + +### Стили +- `PostCSS Modules` — изоляция стилей. +- `Mobile First` — подход к адаптивной верстке. +- `clsx` — конкатенация CSS‑классов. + +### Генерация +- `@gromlab/create` — шаблонизатор для создания слоёв и других файлов из шаблонов. diff --git a/preview/ai/basics/typing.md b/preview/ai/basics/typing.md new file mode 100644 index 0000000..297c8e3 --- /dev/null +++ b/preview/ai/basics/typing.md @@ -0,0 +1,58 @@ +--- +title: Типизация +scope: basics +keywords: [type, interface, generic, any, unknown, enum, типизация, пропсы] +when: "Типизация кода: выбор type vs interface, работа с generic, запрет any" +--- +# Типизация + +Этот раздел описывает правила типизации: как типизировать компоненты, функции и работу с `any`/`unknown`. + +## Общие правила + +- Указывать типы для параметров компонентов, возвращаемых значений и параметров функций. +- Предпочитать `type` для описания сущностей и `interface` для расширяемых контрактов. +- Избегать `any` и `unknown` без необходимости. +- Не использовать `ts-ignore`, кроме крайних случаев с явным комментарием причины. + +## Функции + +- Для публичных функций указывать возвращаемый тип. +- Не полагаться на неявный вывод для важных API. + +**Хорошо** +```ts +export const formatPrice = (value: number): string => { + return `${value} ₽`; +}; +``` + +**Плохо** +```ts +// Плохо: нет явного возвращаемого типа. +export const formatPrice = (value: number) => { + return `${value} ₽`; +}; +``` + +## Работа с any/unknown + +- `any` использовать только для временных заглушек. +- `unknown` сужать через проверки перед использованием. + +**Хорошо** +```ts +const parse = (value: unknown): string => { + if (typeof value === 'string') { + return value; + } + + return ''; +}; +``` + +**Плохо** +```ts +// Плохо: any отключает проверку типов. +const parse = (value: any) => value; +``` diff --git a/preview/ai/triggers/develop/add-api-request.md b/preview/ai/triggers/develop/add-api-request.md new file mode 100644 index 0000000..ab91dbd --- /dev/null +++ b/preview/ai/triggers/develop/add-api-request.md @@ -0,0 +1,35 @@ +--- +title: Добавить API-запрос +--- + +# Добавить API-запрос + +Инструкция по добавлению запроса к серверу: создание клиента, хука, обработка ответа. + +## Прочитай перед началом + +- applied/api.md — правила API-слоя: клиенты, эндпоинты, обработка ошибок +- basics/typing.md — типизация запросов и ответов + +## Шаги + +1. Определи подход: + - Клиентские данные → SWR / хук + - Серверные данные → серверный компонент (RSC) + +2. Опиши типы запроса и ответа. + +3. Создай или расширь API-клиент (→ applied/api.md). + +4. Создай хук для использования в компоненте (→ triggers/develop/create-hook.md). + +## Смежные триггеры + +- triggers/develop/create-hook.md — хук для запроса +- triggers/develop/create-component.md — компонент, использующий данные + +## Проверь себя + +- [ ] Типы запроса и ответа описаны +- [ ] Хук для использования в компоненте создан +- [ ] Обработка ошибок реализована diff --git a/preview/ai/triggers/develop/add-dependency.md b/preview/ai/triggers/develop/add-dependency.md new file mode 100644 index 0000000..ed7a7a1 --- /dev/null +++ b/preview/ai/triggers/develop/add-dependency.md @@ -0,0 +1,24 @@ +--- +title: Добавить зависимость +--- + +# Добавить зависимость + +Инструкция по добавлению новой npm-зависимости в проект. Проверь допустимость перед установкой. + +## Прочитай перед началом + +- basics/tech-stack.md — разрешённый стек, допустимые библиотеки + +## Шаги + +1. Проверь, что библиотека не дублирует уже используемую (→ basics/tech-stack.md). + +2. Проверь, что библиотека входит в разрешённый список или обоснуй необходимость. + +3. Установи как `dependency` или `devDependency` в зависимости от назначения. + +## Проверь себя + +- [ ] Библиотека не дублирует уже используемую +- [ ] Библиотека входит в разрешённый список (→ basics/tech-stack.md) diff --git a/preview/ai/triggers/develop/add-font.md b/preview/ai/triggers/develop/add-font.md new file mode 100644 index 0000000..abe29e6 --- /dev/null +++ b/preview/ai/triggers/develop/add-font.md @@ -0,0 +1,28 @@ +--- +title: Подключить шрифт +--- + +# Подключить шрифт + +Инструкция по подключению и настройке шрифта в проекте. + +## Прочитай перед началом + +- applied/fonts.md — правила подключения шрифтов: форматы, загрузка, CSS-переменные + +## Шаги + +1. Подготовь файлы шрифта (woff2). + +2. Подключи шрифт по правилам (→ applied/fonts.md). + +3. Зарегистрируй CSS-переменную для шрифта. + +## Смежные триггеры + +- triggers/develop/style-component.md — использование шрифта в стилях + +## Проверь себя + +- [ ] Файл шрифта в формате woff2 +- [ ] CSS-переменная для шрифта зарегистрирована diff --git a/preview/ai/triggers/develop/add-icon.md b/preview/ai/triggers/develop/add-icon.md new file mode 100644 index 0000000..f5dab10 --- /dev/null +++ b/preview/ai/triggers/develop/add-icon.md @@ -0,0 +1,29 @@ +--- +title: Добавить иконку +--- + +# Добавить иконку + +Инструкция по добавлению SVG-иконки в проект через спрайт-систему. + +## Прочитай перед началом + +- applied/svg-sprites.md — правила SVG-спрайтов: структура, именование, использование + +## Шаги + +1. Подготовь SVG-файл: убери лишние атрибуты, оптимизируй. + +2. Добавь SVG в спрайт по правилам (→ applied/svg-sprites.md). + +3. Используй иконку в компоненте через компонент-обёртку. + +## Смежные триггеры + +- triggers/develop/create-component.md — если нужен компонент-обёртка для иконки +- triggers/develop/style-component.md — стилизация иконки (размер, цвет) + +## Проверь себя + +- [ ] SVG оптимизирован — убраны лишние атрибуты +- [ ] Иконка добавлена в спрайт по правилам (→ applied/svg-sprites.md) diff --git a/preview/ai/triggers/develop/add-image.md b/preview/ai/triggers/develop/add-image.md new file mode 100644 index 0000000..cdefdc3 --- /dev/null +++ b/preview/ai/triggers/develop/add-image.md @@ -0,0 +1,30 @@ +--- +title: Добавить изображение +--- + +# Добавить изображение + +Инструкция по добавлению и использованию растровых изображений в проекте. + +## Прочитай перед началом + +- applied/images-sprites.md — правила работы с изображениями: оптимизация, форматы, подключение + +## Шаги + +1. Определи тип изображения: + - Статическое (логотип, декор) → `public/` + - Динамическое (контентное) → URL из API + +2. Оптимизируй изображение (формат, размер, сжатие). + +3. Подключи в компоненте по правилам (→ applied/images-sprites.md). + +## Смежные триггеры + +- triggers/develop/create-component.md — если нужен компонент-обёртка для изображения + +## Проверь себя + +- [ ] Изображение оптимизировано (формат, размер, сжатие) +- [ ] Подключено по правилам (→ applied/images-sprites.md) diff --git a/preview/ai/triggers/develop/add-localization.md b/preview/ai/triggers/develop/add-localization.md new file mode 100644 index 0000000..330641f --- /dev/null +++ b/preview/ai/triggers/develop/add-localization.md @@ -0,0 +1,28 @@ +--- +title: Добавить перевод +--- + +# Добавить перевод + +Инструкция по добавлению локализации: создание ключей перевода и подключение в компоненте. + +## Прочитай перед началом + +- applied/localization.md — правила локализации: namespace, ключи, форматирование + +## Шаги + +1. Определи namespace для переводов (→ applied/localization.md). + +2. Добавь ключи перевода в файлы локализации. + +3. Подключи переводы в компоненте (→ applied/localization.md). + +## Смежные триггеры + +- triggers/develop/create-component.md — если компонент ещё не создан + +## Проверь себя + +- [ ] Ключи перевода добавлены в файлы локализации +- [ ] Namespace определён (→ applied/localization.md) diff --git a/preview/ai/triggers/develop/add-server-data.md b/preview/ai/triggers/develop/add-server-data.md new file mode 100644 index 0000000..0857e39 --- /dev/null +++ b/preview/ai/triggers/develop/add-server-data.md @@ -0,0 +1,33 @@ +--- +title: Добавить серверные данные +--- + +# Добавить серверные данные + +Инструкция по получению данных в серверных компонентах (RSC) Next.js. + +## Прочитай перед началом + +- applied/page-level.md — серверные компоненты в App Router +- applied/api.md — API-клиенты + +## Шаги + +1. Определи где получать данные: + - В `page.tsx` / `layout.tsx` → серверный fetch + - В клиентском компоненте → SWR (→ triggers/develop/add-api-request.md) + +2. Создай или расширь серверный API-клиент. + +3. Получи данные в серверном компоненте и передай через пропсы. + +## Смежные триггеры + +- triggers/develop/add-api-request.md — клиентские запросы (SWR) +- triggers/develop/create-page.md — серверный fetch в page.tsx + +## Проверь себя + +- [ ] Определён тип: серверный fetch или клиентский SWR +- [ ] Типы запроса и ответа описаны +- [ ] Данные передаются через пропсы, не через глобальное состояние diff --git a/preview/ai/triggers/develop/add-video.md b/preview/ai/triggers/develop/add-video.md new file mode 100644 index 0000000..a319276 --- /dev/null +++ b/preview/ai/triggers/develop/add-video.md @@ -0,0 +1,27 @@ +--- +title: Добавить видео +--- + +# Добавить видео + +Инструкция по встраиванию видео в проект. + +## Прочитай перед началом + +- applied/video.md — правила работы с видео: форматы, плеер, оптимизация + +## Шаги + +1. Определи тип видео: + - Локальное → `public/` + - Внешнее (YouTube, Vimeo) → embed + +2. Подключи видео по правилам (→ applied/video.md). + +## Смежные триггеры + +- triggers/develop/create-component.md — если нужен компонент-обёртка для видео + +## Проверь себя + +- [ ] Видео подключено по правилам (→ applied/video.md) diff --git a/preview/ai/triggers/develop/connect-store.md b/preview/ai/triggers/develop/connect-store.md new file mode 100644 index 0000000..0457ef0 --- /dev/null +++ b/preview/ai/triggers/develop/connect-store.md @@ -0,0 +1,31 @@ +--- +title: Подключить стор к компоненту +--- + +# Подключить стор к компоненту + +Инструкция по подключению стора к React-компоненту. + +## Прочитай перед началом + +- applied/stores.md — правила сторов: подписка, селекторы + +## Шаги + +1. Определи нужен ли стор: + - Локальное состояние → `useState` / `useReducer` + - Глобальное состояние → стор + +2. Если стор не существует — создай его (→ triggers/develop/create-store.md). + +3. Подключи стор в компоненте через селектор (→ applied/stores.md). + +## Смежные триггеры + +- triggers/develop/create-store.md — создание нового стора +- triggers/develop/create-hook.md — хук-обёртка над стором + +## Проверь себя + +- [ ] Используется селектор, а не подписка на весь стор +- [ ] Выбор локальное/глобальное состояние обоснован diff --git a/preview/ai/triggers/develop/create-component.md b/preview/ai/triggers/develop/create-component.md new file mode 100644 index 0000000..013ce3f --- /dev/null +++ b/preview/ai/triggers/develop/create-component.md @@ -0,0 +1,38 @@ +--- +title: Создать компонент +--- + +# Создать компонент + +Инструкция по созданию React-компонента в проекте. Определи слой, сгенерируй из шаблона, реализуй по правилам. + +## Прочитай перед началом + +- applied/components.md — правила компонентов: структура файлов, пропсы, документирование +- basics/naming.md — именование файла и экспортов + +## Шаги + +1. Определи слой компонента по его назначению (→ basics/architecture.md): + - `ui/` — переиспользуемый UI без бизнес-логики + - `business/` — бизнес-домен с логикой и UI + - `widgets/` — составной блок, не привязанный к домену + - `screens/{name}/parts/` — локальный блок одной страницы + +2. Сгенерируй модуль из шаблона (→ triggers/develop/generate-module.md). + +3. Реализуй компонент по правилам (→ applied/components.md). + +4. Если нужны стили — см. triggers/develop/style-component.md. + +## Смежные триггеры + +- triggers/develop/style-component.md — стилизация компонента +- triggers/develop/add-icon.md — добавление иконки в компонент +- triggers/develop/generate-module.md — генерация из шаблона + +## Проверь себя + +- [ ] Компонент создан из шаблона, не вручную +- [ ] Файл и экспорт именованы по конвенции (→ basics/naming.md) +- [ ] Пропсы типизированы (→ basics/typing.md) diff --git a/preview/ai/triggers/develop/create-entity.md b/preview/ai/triggers/develop/create-entity.md new file mode 100644 index 0000000..36e5245 --- /dev/null +++ b/preview/ai/triggers/develop/create-entity.md @@ -0,0 +1,34 @@ +--- +title: Создать сущность +--- + +# Создать сущность + +Инструкция по созданию бизнес-модуля на слое `business/`. Сущность — бизнес-домен с UI-представлением и моделью данных. + +## Прочитай перед началом + +- basics/architecture.md — слои и зависимости +- applied/components.md — правила компонентов + +## Шаги + +1. Сгенерируй модуль из шаблона `business` (→ triggers/develop/generate-module.md). + +2. Определи модель данных — типы в `types/`. + +3. Реализуй UI-компонент сущности. + +4. Настрой публичный API — экспорт через `index.ts`. + +## Смежные триггеры + +- triggers/develop/create-component.md — UI-компонент сущности +- triggers/develop/create-store.md — стор для сущности +- triggers/develop/generate-module.md — генерация из шаблона + +## Проверь себя + +- [ ] Модуль создан из шаблона `business` +- [ ] Модель данных определена — типы в `types/` +- [ ] Публичный API настроен — экспорт через `index.ts` diff --git a/preview/ai/triggers/develop/create-feature.md b/preview/ai/triggers/develop/create-feature.md new file mode 100644 index 0000000..a8bdf7a --- /dev/null +++ b/preview/ai/triggers/develop/create-feature.md @@ -0,0 +1,37 @@ +--- +title: Создать фичу +--- + +# Создать фичу + +Инструкция по созданию бизнес-модуля на слое `business/`. Фича — самодостаточный блок с бизнес-логикой и UI. + +## Прочитай перед началом + +- basics/architecture.md — слои и зависимости +- applied/components.md — правила компонентов + +## Шаги + +1. Сгенерируй модуль из шаблона `business` (→ triggers/develop/generate-module.md). + +2. Реализуй компонент фичи (→ applied/components.md). + +3. Если нужен стор — создай в `stores/` (→ triggers/develop/create-store.md). + +4. Если нужны хуки — создай в `hooks/` (→ triggers/develop/create-hook.md). + +5. Настрой публичный API — экспорт через `index.ts`. + +## Смежные триггеры + +- triggers/develop/create-component.md — компонент внутри фичи +- triggers/develop/create-store.md — стор для фичи +- triggers/develop/create-hook.md — хук для фичи +- triggers/develop/generate-module.md — генерация из шаблона + +## Проверь себя + +- [ ] Модуль создан из шаблона `business` +- [ ] Публичный API настроен — экспорт через `index.ts` +- [ ] Cross-domain зависимости реализованы через фабрику (→ basics/architecture.md) diff --git a/preview/ai/triggers/develop/create-hook.md b/preview/ai/triggers/develop/create-hook.md new file mode 100644 index 0000000..37e0ec2 --- /dev/null +++ b/preview/ai/triggers/develop/create-hook.md @@ -0,0 +1,36 @@ +--- +title: Создать хук +--- + +# Создать хук + +Инструкция по созданию кастомного React-хука. Определи где он живёт, реализуй по правилам. + +## Прочитай перед началом + +- applied/hooks.md — правила хуков +- basics/naming.md — именование (префикс `use`) +- basics/typing.md — типизация параметров и возврата + +## Шаги + +1. Определи область хука: + - Утилитарный (не привязан к бизнес-логике) → `shared/hooks/` + - Привязан к фиче/сущности → `model/` внутри модуля + +2. Создай файл с именем `use-{name}.ts`. + +3. Реализуй хук по правилам (→ applied/hooks.md). + +4. Экспортируй через публичный API модуля. + +## Смежные триггеры + +- triggers/develop/create-component.md — если хук используется в новом компоненте +- triggers/develop/connect-store.md — если хук подключает стор + +## Проверь себя + +- [ ] Имя начинается с `use` (→ basics/naming.md) +- [ ] Параметры и возвращаемое значение типизированы +- [ ] Хук экспортирован через публичный API модуля diff --git a/preview/ai/triggers/develop/create-layout.md b/preview/ai/triggers/develop/create-layout.md new file mode 100644 index 0000000..660be9f --- /dev/null +++ b/preview/ai/triggers/develop/create-layout.md @@ -0,0 +1,34 @@ +--- +title: Создать layout +--- + +# Создать layout + +Инструкция по созданию layout.tsx в Next.js App Router. + +## Прочитай перед началом + +- applied/page-level.md — правила layout.tsx: провайдеры, metadata, вёрстка +- applied/project-structure.md — структура `src/app/` + +## Шаги + +1. Определи уровень layout: + - Корневой (`src/app/layout.tsx`) — провайдеры, глобальные стили, metadata + - Вложенный (`src/app/{route}/layout.tsx`) — layout для группы страниц + +2. Создай `layout.tsx` в нужном маршруте. + +3. Вёрстку layout-обёрток вынеси в слой `layouts/` (→ applied/page-level.md). + +4. Layout содержит только провайдеры и вызов layout-компонента — не вёрстку. + +## Смежные триггеры + +- triggers/develop/create-page.md — страницы внутри layout +- triggers/develop/create-component.md — layout-компонент в `layouts/` + +## Проверь себя + +- [ ] Вёрстка вынесена в layout-компонент в `layouts/` +- [ ] layout.tsx содержит только провайдеры и вызов layout-компонента diff --git a/preview/ai/triggers/develop/create-page.md b/preview/ai/triggers/develop/create-page.md new file mode 100644 index 0000000..a23eb94 --- /dev/null +++ b/preview/ai/triggers/develop/create-page.md @@ -0,0 +1,36 @@ +--- +title: Создать страницу +--- + +# Создать страницу + +Инструкция по добавлению нового route в Next.js проект. Страница — это экран + page.tsx. + +## Прочитай перед началом + +- applied/page-level.md — правила файлов роутинга: page.tsx, layout.tsx, metadata +- applied/project-structure.md — где располагаются файлы + +## Шаги + +1. Сгенерируй экран из шаблона `screen` в `src/screens/` (→ triggers/develop/generate-module.md). + +2. Заполни экран логикой и стилями. + +3. Создай `page.tsx` в нужном маршруте `src/app/`. + - page.tsx тонкий: только `metadata` и рендер экрана + - Никакой логики, стилей и хуков в page.tsx + +4. Добавь `metadata` с `title` (→ applied/page-level.md). + +## Смежные триггеры + +- triggers/develop/generate-module.md — генерация экрана из шаблона +- triggers/develop/create-layout.md — если нужен новый layout для маршрута +- triggers/develop/create-component.md — компоненты внутри экрана + +## Проверь себя + +- [ ] Экран создан из шаблона `screen` в `src/screens/` +- [ ] page.tsx тонкий — только metadata и рендер экрана +- [ ] metadata содержит title и description diff --git a/preview/ai/triggers/develop/create-project.md b/preview/ai/triggers/develop/create-project.md new file mode 100644 index 0000000..45adfa8 --- /dev/null +++ b/preview/ai/triggers/develop/create-project.md @@ -0,0 +1,52 @@ +--- +title: Создать проект +scope: applied +keywords: [создать проект, новый проект, tiged, шаблон проекта, init] +when: "Создание нового Next.js проекта из шаблона" +--- + +# Создать проект + +Инструкция по созданию нового Next.js проекта из готового шаблона. Проект готов к разработке сразу после установки зависимостей. + +## Прочитай перед началом + +- basics/getting-started.md — знакомство со стеком и особенностями проекта +- applied/project-structure.md — структура папок и файлов + +## Шаги + +1. Создай проект из шаблона: + + ```bash + npx tiged git@gromlab.ru:templates/nextjs.git my-app + cd my-app + npm install + ``` + +2. Ознакомься со структурой проекта (→ applied/project-structure.md). + +3. Настрой VS Code (→ triggers/develop/setup-vscode.md). + +## Что входит в шаблон + +- Next.js + TypeScript (App Router) +- Mantine UI + PostCSS Modules +- Biome (линтинг и форматирование) +- Zustand, SWR +- Структура SLM Design (`screens/`, `layouts/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`) +- Шаблоны генерации (`.templates/`) +- Конфигурация VS Code (`.vscode/`) +- CSS-токены (цвета, отступы, радиусы, медиа) +- Open Graph метаданные + +## Смежные триггеры + +- triggers/develop/setup-vscode.md — настройка редактора +- triggers/develop/create-page.md — добавление первой страницы + +## Проверь себя + +- [ ] Проект создан из шаблона через `npx tiged` +- [ ] Зависимости установлены +- [ ] VS Code настроен (→ triggers/develop/setup-vscode.md) diff --git a/preview/ai/triggers/develop/create-store.md b/preview/ai/triggers/develop/create-store.md new file mode 100644 index 0000000..8ca9d13 --- /dev/null +++ b/preview/ai/triggers/develop/create-store.md @@ -0,0 +1,36 @@ +--- +title: Создать стор +--- + +# Создать стор + +Инструкция по созданию стора для управления состоянием. Определи область, сгенерируй из шаблона. + +## Прочитай перед началом + +- applied/stores.md — правила сторов +- basics/naming.md — именование +- basics/typing.md — типизация состояния и экшенов + +## Шаги + +1. Определи область стора: + - Глобальный → `shared/model/` + - Привязан к фиче/сущности → `model/` внутри модуля + +2. Сгенерируй из шаблона `store` (→ triggers/develop/generate-module.md). + +3. Реализуй стор по правилам (→ applied/stores.md). + +4. Экспортируй через публичный API модуля. + +## Смежные триггеры + +- triggers/develop/connect-store.md — подключение стора к компоненту +- triggers/develop/generate-module.md — генерация из шаблона + +## Проверь себя + +- [ ] Стор создан из шаблона `store` +- [ ] Состояние и экшены типизированы +- [ ] Стор экспортирован через публичный API модуля diff --git a/preview/ai/triggers/develop/create-widget.md b/preview/ai/triggers/develop/create-widget.md new file mode 100644 index 0000000..f717580 --- /dev/null +++ b/preview/ai/triggers/develop/create-widget.md @@ -0,0 +1,32 @@ +--- +title: Создать виджет +--- + +# Создать виджет + +Инструкция по созданию модуля на слое `widgets/`. Виджет — композиция фичей и сущностей. + +## Прочитай перед началом + +- basics/architecture.md — слои и зависимости +- applied/components.md — правила компонентов + +## Шаги + +1. Сгенерируй модуль из шаблона `widget` (→ triggers/develop/generate-module.md). + +2. Скомпонуй виджет из существующих фичей и сущностей. + +3. Настрой публичный API — экспорт через `index.ts`. + +## Смежные триггеры + +- triggers/develop/create-feature.md — если нужна новая фича для виджета +- triggers/develop/create-component.md — UI-компоненты внутри виджета +- triggers/develop/generate-module.md — генерация из шаблона + +## Проверь себя + +- [ ] Виджет создан из шаблона `widget` +- [ ] Композиция из существующих фичей/сущностей, не дублирует логику +- [ ] Публичный API настроен — экспорт через `index.ts` diff --git a/preview/ai/triggers/develop/generate-module.md b/preview/ai/triggers/develop/generate-module.md new file mode 100644 index 0000000..8125ae4 --- /dev/null +++ b/preview/ai/triggers/develop/generate-module.md @@ -0,0 +1,36 @@ +--- +title: Сгенерировать модуль из шаблона +--- + +# Сгенерировать модуль из шаблона + +Инструкция по генерации модуля из шаблонов `.templates/`. Ручное создание файловой структуры запрещено. + +## Прочитай перед началом + +- applied/templates-generation.md — шаблоны, синтаксис, инструменты генерации + +## Шаги + +1. Определи тип модуля и шаблон (→ applied/templates-generation.md): + - Компонент → `component` + - Бизнес-модуль → `business` + - Виджет → `widget` + - Layout → `layout` + - Экран → `screen` + - Стор → `store` + +2. Запусти генерацию (→ applied/templates-generation.md). + +3. Если подходящего шаблона нет — сначала создай шаблон, затем генерируй. + +## Смежные триггеры + +- triggers/develop/create-component.md — после генерации компонента +- triggers/develop/create-feature.md — после генерации бизнес-модуля +- triggers/develop/create-store.md — после генерации стора + +## Проверь себя + +- [ ] Модуль создан из шаблона, не вручную +- [ ] Выбран правильный шаблон для типа модуля (→ applied/templates-generation.md) diff --git a/preview/ai/triggers/develop/setup-vscode.md b/preview/ai/triggers/develop/setup-vscode.md new file mode 100644 index 0000000..953a78e --- /dev/null +++ b/preview/ai/triggers/develop/setup-vscode.md @@ -0,0 +1,24 @@ +--- +title: Настроить VS Code +--- + +# Настроить VS Code + +Инструкция по настройке VS Code для работы с проектом. + +## Прочитай перед началом + +- applied/vscode.md — настройки, расширения, сниппеты + +## Шаги + +1. Установи рекомендованные расширения (→ applied/vscode.md). + +2. Проверь настройки `.vscode/settings.json`. + +3. Настрой сниппеты при необходимости. + +## Проверь себя + +- [ ] Рекомендованные расширения установлены +- [ ] Настройки `.vscode/settings.json` проверены diff --git a/preview/ai/triggers/develop/style-component.md b/preview/ai/triggers/develop/style-component.md new file mode 100644 index 0000000..175144f --- /dev/null +++ b/preview/ai/triggers/develop/style-component.md @@ -0,0 +1,35 @@ +--- +title: Стилизовать компонент +--- + +# Стилизовать компонент + +Инструкция по выбору подхода к стилизации и написанию стилей для компонента. + +## Прочитай перед началом + +- applied/styles.md — правила CSS: PostCSS Modules, токены, медиа-запросы + +## Шаги + +1. Определи подход (→ applied/styles.md): + - Mantine-компонент → используй пропсы Mantine, не пиши CSS + - CSS-токены достаточно → используй токены + - Нужна кастомная стилизация → PostCSS Modules + +2. Создай файл стилей `{component-name}.module.css` рядом с компонентом. + +3. Напиши стили по правилам (→ applied/styles.md). + +4. Подключи стили в компоненте через `cl()`. + +## Смежные триггеры + +- triggers/develop/create-component.md — если компонент ещё не создан +- triggers/develop/add-icon.md — если нужна иконка в компоненте + +## Проверь себя + +- [ ] Приоритет стилизации соблюдён: Mantine → токены → PostCSS Modules +- [ ] Нет инлайн-стилей и магических значений +- [ ] Файл стилей именован `{component-name}.module.css` diff --git a/preview/eslint.config.js b/preview/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/preview/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/preview/index.html b/preview/index.html new file mode 100644 index 0000000..ef0ad56 --- /dev/null +++ b/preview/index.html @@ -0,0 +1,13 @@ + + + + + + SVG Sprites Preview + + + +
+ + + diff --git a/preview/package-lock.json b/preview/package-lock.json new file mode 100644 index 0000000..b2e4a3a --- /dev/null +++ b/preview/package-lock.json @@ -0,0 +1,3103 @@ +{ + "name": "preview", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "preview", + "version": "0.0.0", + "dependencies": { + "clsx": "^2.1.1", + "react": "^19.2.5", + "react-colorful": "^5.6.1", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.9", + "vite-plugin-singlefile": "^2.3.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.126.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", + "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", + "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", + "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.341", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.341.tgz", + "integrity": "sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", + "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.126.0", + "@rolldown/pluginutils": "1.0.0-rc.16" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-x64": "1.0.0-rc.16", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", + "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", + "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.16", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-singlefile": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", + "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.59.0", + "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/preview/package.json b/preview/package.json new file mode 100644 index 0000000..c2c9834 --- /dev/null +++ b/preview/package.json @@ -0,0 +1,34 @@ +{ + "name": "preview", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev:prepare": "node scripts/generate-dev-data.js", + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "clsx": "^2.1.1", + "react": "^19.2.5", + "react-colorful": "^5.6.1", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.9", + "vite-plugin-singlefile": "^2.3.3" + } +} diff --git a/preview/scripts/generate-dev-data.js b/preview/scripts/generate-dev-data.js new file mode 100644 index 0000000..c6fea13 --- /dev/null +++ b/preview/scripts/generate-dev-data.js @@ -0,0 +1,163 @@ +/** + * Генерирует dev-данные для preview из реальных спрайтов основного пакета. + * + * Запуск: node scripts/generate-dev-data.js + * + * Результат: + * public/dev-data.js — window.__SPRITES_DATA__ с метаданными иконок + * public/dev-sprites.svg — инлайновые из всех спрайтов + */ +import fs from 'node:fs' +import path from 'node:path' + +const ROOT = path.resolve(import.meta.dirname, '../..') +const SPRITES_OUTPUT = path.join(ROOT, 'preview/public') +const PREVIEW_PUBLIC = path.join(import.meta.dirname, '../public') + +/** Извлекает id иконок из SVG-спрайта. */ +function extractIconIds(spritePath) { + const content = fs.readFileSync(spritePath, 'utf-8') + const ids = [] + const regex = /<(?:svg|symbol)\b[^>]*\bid="([^"]+)"/g + let match + while ((match = regex.exec(content)) !== null) { + ids.push(match[1]) + } + return ids.sort() +} + +/** Извлекает viewBox из SVG-фрагмента иконки. */ +function extractViewBox(svgFragment) { + const match = svgFragment.match(/viewBox="([^"]+)"/) + if (!match) return null + const parts = match[1].split(/\s+/).map(Number) + if (parts.length !== 4) return null + return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] } +} + +/** Извлекает CSS-переменные из SVG-фрагмента иконки. */ +function extractIconVars(svgFragment) { + const vars = new Map() + const regex = /var\((--icon-color-\d+),\s*([^)]+)\)/g + let match + while ((match = regex.exec(svgFragment)) !== null) { + if (!vars.has(match[1])) { + vars.set(match[1], match[2].trim()) + } + } + return [...vars.entries()].map(([varName, fallback]) => ({ + varName, + fallback, + hex: colorToHex(fallback), + isCurrentColor: fallback.toLowerCase() === 'currentcolor', + })) +} + +/** Извлекает фрагменты иконок из спрайта. */ +function extractIconFragments(spritePath) { + const content = fs.readFileSync(spritePath, 'utf-8') + const fragments = new Map() + const regex = /<(?:svg|symbol)\b[^>]*\bid="([^"]+)"[^>]*>[\s\S]*?<\/(?:svg|symbol)>/g + let match + while ((match = regex.exec(content)) !== null) { + fragments.set(match[1], match[0]) + } + return fragments +} + +/** Конвертирует CSS-цвет в hex. */ +function colorToHex(color) { + const named = { + red: '#ff0000', blue: '#0000ff', green: '#008000', white: '#ffffff', + black: '#000000', yellow: '#ffff00', cyan: '#00ffff', magenta: '#ff00ff', + orange: '#ffa500', purple: '#800080', pink: '#ffc0cb', gray: '#808080', + grey: '#808080', currentcolor: '#000000', + } + const lower = color.toLowerCase().trim() + if (lower.startsWith('#')) { + if (lower.length === 4) return `#${lower[1]}${lower[1]}${lower[2]}${lower[2]}${lower[3]}${lower[3]}` + return lower + } + return named[lower] || '#000000' +} + +/** Подготавливает спрайт для инлайна — вложенные . */ +function prepareInlineSprite(spritePath) { + let content = fs.readFileSync(spritePath, 'utf-8') + content = content.replace(/<\?xml[^?]*\?>\s*/g, '') + content = content.replace(/' : '' + } + depth++ + if (depth > 1) { + const cleanAttrs = attrs.replace(/\s*xmlns="[^"]*"/g, '') + return `` + } + return `` + }) + return content +} + +// --- Main --- +const spriteFiles = fs.readdirSync(SPRITES_OUTPUT).filter((entry) => { + return entry.endsWith('.sprite.svg') +}) + +const groups = [] +const inlineSprites = [] + +for (const fileName of spriteFiles) { + const spritePath = path.join(SPRITES_OUTPUT, fileName) + const name = fileName.replace('.sprite.svg', '') + + const fragments = extractIconFragments(spritePath) + const ids = extractIconIds(spritePath) + + const icons = ids.map((id) => { + const fragment = fragments.get(id) || '' + return { + id, + group: name, + mode: 'stack', + spriteFile: fileName, + viewBox: extractViewBox(fragment), + vars: extractIconVars(fragment), + } + }) + + groups.push({ name, mode: 'stack', spriteFile: fileName, icons }) + inlineSprites.push(prepareInlineSprite(spritePath)) +} + +// Write dev-data.js — данные + инлайн-спрайты через DOM injection +fs.mkdirSync(PREVIEW_PUBLIC, { recursive: true }) + +const svgContent = inlineSprites.join('\n').replace(/`/g, '\\`').replace(/\$/g, '\\$') + +const dataJs = [ + `window.__SPRITES_DATA__ = ${JSON.stringify({ groups }, null, 2)};`, + '', + '// Inject inline SVG sprites into DOM via DOMParser for correct SVG namespace', + '(function() {', + ` var svg = \`${svgContent}\`;`, + ' var parser = new DOMParser();', + ' var doc = parser.parseFromString("
" + svg + "
", "text/html");', + ' var nodes = doc.body.firstChild.childNodes;', + ' while (nodes.length > 0) {', + ' document.body.insertBefore(nodes[0], document.body.firstChild);', + ' }', + '})();', +].join('\n') + +fs.writeFileSync(path.join(PREVIEW_PUBLIC, 'dev-data.js'), dataJs) + +// Cleanup old separate file if exists +const oldSvg = path.join(PREVIEW_PUBLIC, 'dev-sprites.svg') +if (fs.existsSync(oldSvg)) fs.unlinkSync(oldSvg) + +console.log(`Generated dev data: ${groups.length} groups, ${groups.reduce((s, g) => s + g.icons.length, 0)} icons`) diff --git a/preview/src/App.tsx b/preview/src/App.tsx new file mode 100644 index 0000000..24877b2 --- /dev/null +++ b/preview/src/App.tsx @@ -0,0 +1,108 @@ +import { useState, useMemo } from 'react' +import { useTheme } from './infrastructure/theme' +import { Banner } from './ui/banner' +import { SearchInput } from './ui/search-input' +import { IconGrid } from './ui/icon-grid' +import { IconModal } from './ui/icon-modal' +import type { SpritesData, IconData } from './shared/types' +import styles from './app/styles/app.module.css' + +declare global { + interface Window { + __SPRITES_DATA__?: SpritesData + } +} + +const MOCK_DATA: SpritesData = { + groups: [], +} + +/** + * Корневой компонент превью SVG-спрайтов. + * + * Используется для: + * - отображения всех иконок из сгенерированных спрайтов + * - просмотра деталей иконки и кода использования + */ +export const App = () => { + const { toggle } = useTheme() + const data = window.__SPRITES_DATA__ ?? MOCK_DATA + const [searchQuery, setSearchQuery] = useState('') + const [selectedIconId, setSelectedIconId] = useState(null) + + const isFileProtocol = location.protocol === 'file:' + + const totalIcons = useMemo( + () => data.groups.reduce((sum, g) => sum + g.icons.length, 0), + [data.groups], + ) + + const selectedIcon = useMemo((): IconData | null => { + if (!selectedIconId) { + return null + } + + for (const group of data.groups) { + const found = group.icons.find((i) => i.id === selectedIconId) + if (found) { + return found + } + } + + return null + }, [selectedIconId, data.groups]) + + const handleCloseModal = (): void => { + setSelectedIconId(null) + } + + return ( + <> + {isFileProtocol && ( + + file:// — Preview opened from local file. External SVG + references won't work in code snippets. Use a local server for full + functionality. + + )} + +
+

SVG Sprites

+ {totalIcons} icons +
+ + +
+
+ + + + + + + + ) +} diff --git a/preview/src/app/styles/app.module.css b/preview/src/app/styles/app.module.css new file mode 100644 index 0000000..70c2570 --- /dev/null +++ b/preview/src/app/styles/app.module.css @@ -0,0 +1,64 @@ +.header { + display: flex; + align-items: center; + gap: var(--space-4); + flex-wrap: wrap; + margin-bottom: var(--space-6); +} + +.title { + font-size: 1.5rem; + font-weight: 700; +} + +.count { + font-size: 13px; + color: var(--color-muted); + font-weight: 400; +} + +.toolbar { + display: flex; + gap: var(--space-3); + margin-left: auto; + align-items: center; +} + +.themeButton { + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--radius-2); + background: var(--color-card-bg); + color: var(--color-fg); + cursor: pointer; + font-size: 14px; +} + +.themeButton:hover { + background: var(--color-card-hover); +} + +.footer { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-3); + margin-top: auto; + padding-top: var(--space-4); + border-top: 1px solid var(--color-border); + color: var(--color-muted); + font-size: 13px; +} + +.footerText { + font-weight: 500; +} + +.footerLink { + color: var(--color-accent); + text-decoration: none; +} + +.footerLink:hover { + text-decoration: underline; +} diff --git a/preview/src/app/styles/globals.css b/preview/src/app/styles/globals.css new file mode 100644 index 0000000..dac9728 --- /dev/null +++ b/preview/src/app/styles/globals.css @@ -0,0 +1,26 @@ +@import './variables.css'; + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--color-bg); + color: var(--color-fg); + padding: var(--space-6); + max-width: 1400px; + margin: 0 auto; + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +#root { + flex: 1; + display: flex; + flex-direction: column; +} diff --git a/preview/src/app/styles/variables.css b/preview/src/app/styles/variables.css new file mode 100644 index 0000000..d9adaa7 --- /dev/null +++ b/preview/src/app/styles/variables.css @@ -0,0 +1,60 @@ +:root { + --color-bg: #f0f0f3; + --color-fg: #1a1a1a; + --color-card-bg: #ffffff; + --color-card-hover: #eaeaed; + --color-border: #d8d8d8; + --color-accent: #3b82f6; + --color-muted: #888888; + --color-code-bg: #f5f5f5; + + --radius-1: 4px; + --radius-2: 8px; + --radius-3: 12px; + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + + --icon-size: 128px; + + --checkerboard: conic-gradient( + var(--color-checker-a) 25%, var(--color-checker-b) 0 50%, + var(--color-checker-a) 0 75%, var(--color-checker-b) 0 + ); + --checkerboard-size: 8px 8px; + --color-checker-a: #e9e9e9; + --color-checker-b: #ffffff; + + --font-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + --color-bg: #1a1a1a; + --color-fg: #e5e5e5; + --color-card-bg: #2a2a2a; + --color-card-hover: #333333; + --color-border: #404040; + --color-code-bg: #2a2a2a; + --color-checker-a: #333333; + --color-checker-b: #2a2a2a; + } +} + +:root[data-theme='dark'] { + --color-bg: #1a1a1a; + --color-fg: #e5e5e5; + --color-card-bg: #2a2a2a; + --color-card-hover: #333333; + --color-border: #404040; + --color-code-bg: #2a2a2a; + --color-checker-a: #333333; + --color-checker-b: #2a2a2a; +} + diff --git a/preview/src/infrastructure/theme/hooks/use-theme.hook.ts b/preview/src/infrastructure/theme/hooks/use-theme.hook.ts new file mode 100644 index 0000000..f4e95c7 --- /dev/null +++ b/preview/src/infrastructure/theme/hooks/use-theme.hook.ts @@ -0,0 +1,40 @@ +import { useState, useCallback, useEffect } from 'react' +import type { Theme } from '../types/theme.type' + +const getSystemTheme = (): Theme => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +/** + * Управляет темой приложения: авто-определение по системе + ручное переключение. + */ +export const useTheme = () => { + const [theme, setTheme] = useState(getSystemTheme) + + const toggle = useCallback(() => { + setTheme((prev) => { + const next: Theme = prev === 'dark' ? 'light' : 'dark' + document.documentElement.dataset.theme = next + return next + }) + }, []) + + useEffect(() => { + // Устанавливаем data-theme при инициализации, чтобы CSS-селекторы + // (включая подсветку кода) работали сразу, а не только после ручного переключения. + document.documentElement.dataset.theme = getSystemTheme() + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + + const handleChange = (): void => { + const next = getSystemTheme() + document.documentElement.dataset.theme = next + setTheme(next) + } + + mediaQuery.addEventListener('change', handleChange) + return () => mediaQuery.removeEventListener('change', handleChange) + }, []) + + return { theme, toggle } +} diff --git a/preview/src/infrastructure/theme/index.ts b/preview/src/infrastructure/theme/index.ts new file mode 100644 index 0000000..0f8c14f --- /dev/null +++ b/preview/src/infrastructure/theme/index.ts @@ -0,0 +1,2 @@ +export { useTheme } from './hooks/use-theme.hook' +export type { Theme } from './types/theme.type' diff --git a/preview/src/infrastructure/theme/styles/theme.module.css b/preview/src/infrastructure/theme/styles/theme.module.css new file mode 100644 index 0000000..c3a2af6 --- /dev/null +++ b/preview/src/infrastructure/theme/styles/theme.module.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/preview/src/infrastructure/theme/theme.infra.tsx b/preview/src/infrastructure/theme/theme.infra.tsx new file mode 100644 index 0000000..c98a234 --- /dev/null +++ b/preview/src/infrastructure/theme/theme.infra.tsx @@ -0,0 +1,4 @@ +/** + * Theme infrastructure module — реэкспортирует хук. + * Компонент не требуется, тема управляется через data-theme на documentElement. + */ diff --git a/preview/src/infrastructure/theme/types/theme.type.ts b/preview/src/infrastructure/theme/types/theme.type.ts new file mode 100644 index 0000000..7afba61 --- /dev/null +++ b/preview/src/infrastructure/theme/types/theme.type.ts @@ -0,0 +1,11 @@ +/** + * Тема приложения. + */ +export type Theme = 'light' | 'dark' + +/** + * Параметры Theme. + */ +export type ThemeParams = {} + +export type ThemeProps = ThemeParams diff --git a/preview/src/main.tsx b/preview/src/main.tsx new file mode 100644 index 0000000..17af05b --- /dev/null +++ b/preview/src/main.tsx @@ -0,0 +1,28 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './app/styles/globals.css' +import { App } from './App' + +function loadDevScript(): Promise { + return new Promise((resolve, reject) => { + const script = document.createElement('script') + script.src = '/dev-data.js' + script.onload = () => resolve() + script.onerror = reject + document.head.appendChild(script) + }) +} + +async function loadDevData(): Promise { + if (import.meta.env.DEV && !window.__SPRITES_DATA__) { + await loadDevScript() + } +} + +loadDevData().then(() => { + createRoot(document.getElementById('root')!).render( + + + , + ) +}) diff --git a/preview/src/shared/lib/rgb-to-hex.util.ts b/preview/src/shared/lib/rgb-to-hex.util.ts new file mode 100644 index 0000000..3204eb7 --- /dev/null +++ b/preview/src/shared/lib/rgb-to-hex.util.ts @@ -0,0 +1,16 @@ +/** + * Конвертирует CSS-цвет (rgb или hex) в hex-формат. + */ +export const rgbToHex = (color: string): string => { + if (color.startsWith('#')) { + return color + } + + const match = color.match(/\d+/g) + + if (match && match.length >= 3) { + return '#' + match.slice(0, 3).map((c) => parseInt(c).toString(16).padStart(2, '0')).join('') + } + + return '#000000' +} diff --git a/preview/src/shared/types/index.ts b/preview/src/shared/types/index.ts new file mode 100644 index 0000000..da40077 --- /dev/null +++ b/preview/src/shared/types/index.ts @@ -0,0 +1,6 @@ +export type { + IconVar, + IconData, + SpriteGroup, + SpritesData, +} from './sprites-data.type' diff --git a/preview/src/shared/types/sprites-data.type.ts b/preview/src/shared/types/sprites-data.type.ts new file mode 100644 index 0000000..3147dbe --- /dev/null +++ b/preview/src/shared/types/sprites-data.type.ts @@ -0,0 +1,63 @@ +/** + * CSS-переменная иконки. + */ +export type IconVar = { + /** Имя CSS-переменной. */ + varName: string + /** Fallback-значение. */ + fallback: string + /** HEX-значение для color picker. */ + hex: string + /** Является ли fallback значением currentColor. */ + isCurrentColor: boolean +} + +/** + * Размеры viewBox иконки. + */ +export type IconViewBox = { + x: number + y: number + width: number + height: number +} + +/** + * Данные одной иконки. + */ +export type IconData = { + /** Идентификатор иконки. */ + id: string + /** Имя группы (папки спрайта). */ + group: string + /** Режим спрайта. */ + mode: 'stack' | 'symbol' + /** Относительный путь к файлу спрайта. */ + spriteFile: string + /** Размеры viewBox иконки. */ + viewBox: IconViewBox | null + /** CSS-переменные иконки. */ + vars: IconVar[] +} + +/** + * Группа спрайтов. + */ +export type SpriteGroup = { + /** Имя группы. */ + name: string + /** Режим спрайта. */ + mode: 'stack' | 'symbol' + /** Относительный путь к файлу спрайта. */ + spriteFile: string + /** Иконки в группе. */ + icons: IconData[] +} + +/** + * Данные для рендера превью. + */ +export type SpritesData = { + /** Группы спрайтов. */ + groups: SpriteGroup[] +} diff --git a/preview/src/ui/banner/banner.ui.tsx b/preview/src/ui/banner/banner.ui.tsx new file mode 100644 index 0000000..b8b4634 --- /dev/null +++ b/preview/src/ui/banner/banner.ui.tsx @@ -0,0 +1,20 @@ +import cl from 'clsx' +import type { BannerProps } from './types/banner.type' +import styles from './styles/banner.module.css' + +/** + * Баннер-уведомление. + * + * Используется для: + * - предупреждений о file:// протоколе + * - информационных сообщений + */ +export const Banner = (props: BannerProps) => { + const { children, className, variant = 'warn', ...htmlAttr } = props + + return ( +
+ {children} +
+ ) +} diff --git a/preview/src/ui/banner/index.ts b/preview/src/ui/banner/index.ts new file mode 100644 index 0000000..4500a84 --- /dev/null +++ b/preview/src/ui/banner/index.ts @@ -0,0 +1 @@ +export { Banner } from './banner.ui' diff --git a/preview/src/ui/banner/styles/banner.module.css b/preview/src/ui/banner/styles/banner.module.css new file mode 100644 index 0000000..3e99433 --- /dev/null +++ b/preview/src/ui/banner/styles/banner.module.css @@ -0,0 +1,29 @@ +.root { + display: flex; + align-items: center; + gap: var(--space-2); + padding: 10px var(--space-4); + border-radius: var(--radius-2); + font-size: 13px; + margin-bottom: var(--space-5); +} + +._warn { + background: #fef3c7; + color: #92400e; + border: 1px solid #fde68a; +} + +:root[data-theme='dark'] ._warn { + background: #451a03; + color: #fde68a; + border-color: #92400e; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) ._warn { + background: #451a03; + color: #fde68a; + border-color: #92400e; + } +} diff --git a/preview/src/ui/banner/types/banner.type.ts b/preview/src/ui/banner/types/banner.type.ts new file mode 100644 index 0000000..0e86e7a --- /dev/null +++ b/preview/src/ui/banner/types/banner.type.ts @@ -0,0 +1,14 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры Banner. + */ +export type BannerParams = { + /** Вариант отображения. */ + variant?: 'warn' | 'info' +} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type BannerProps = RootAttrs & BannerParams diff --git a/preview/src/ui/code-block/code-block.ui.tsx b/preview/src/ui/code-block/code-block.ui.tsx new file mode 100644 index 0000000..27bb0e5 --- /dev/null +++ b/preview/src/ui/code-block/code-block.ui.tsx @@ -0,0 +1,33 @@ +import { useState, useMemo } from 'react' +import cl from 'clsx' +import { highlight } from './lib/highlight.lib' +import type { CodeBlockProps } from './types/code-block.type' +import styles from './styles/code-block.module.css' + +export const CodeBlock = (props: CodeBlockProps) => { + const { code, language = 'html', copyable = true, className, ...htmlAttr } = props + + const [isCopied, setIsCopied] = useState(false) + + const highlightedHtml = useMemo(() => highlight(code, language), [code, language]) + + const handleCopy = (): void => { + navigator.clipboard.writeText(code).then(() => { + setIsCopied(true) + setTimeout(() => setIsCopied(false), 1500) + }) + } + + return ( +
+
+        
+      
+ {copyable && ( + + )} +
+ ) +} diff --git a/preview/src/ui/code-block/index.ts b/preview/src/ui/code-block/index.ts new file mode 100644 index 0000000..91aec98 --- /dev/null +++ b/preview/src/ui/code-block/index.ts @@ -0,0 +1 @@ +export { CodeBlock } from './code-block.ui' diff --git a/preview/src/ui/code-block/lib/highlight.lib.ts b/preview/src/ui/code-block/lib/highlight.lib.ts new file mode 100644 index 0000000..73c2031 --- /dev/null +++ b/preview/src/ui/code-block/lib/highlight.lib.ts @@ -0,0 +1,68 @@ +const ESCAPE_RE = /[&<>"]/g +const ESCAPE_MAP: Record = { '&': '&', '<': '<', '>': '>', '"': '"' } + +const escape = (str: string): string => str.replace(ESCAPE_RE, (c) => ESCAPE_MAP[c]) + +type Token = { kind: string; match: string } +type Rule = [RegExp, string] + +const HTML_RULES: Rule[] = [ + [//g, 'comment'], + [/<\/?[\w-]+/g, 'tag'], + [/<\/?/g, 'punctuation'], + [/\/?>/g, 'punctuation'], + [/[\w-]+(?=\s*=)/g, 'attr'], + [/=/g, 'punctuation'], + [/"[^"]*"|'[^']*'/g, 'string'], +] + +const CSS_RULES: Rule[] = [ + [/\/\*[\s\S]*?\*\//g, 'comment'], + [/\.[\w-]+/g, 'selector'], + [/[\w-]+(?=\s*:)/g, 'property'], + [/[:;]/g, 'punctuation'], + [/\{|\}/g, 'punctuation'], + [/'[^']*'|"[^"]*"/g, 'string'], + [/#[\da-fA-F]{3,8}\b/g, 'color'], + [/\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|deg|s|ms)?/g, 'number'], + [/url\([^)]*\)/g, 'function'], +] + +const RULES: Record = { html: HTML_RULES, css: CSS_RULES, xml: HTML_RULES } + +const tokenize = (code: string, lang: string): Token[] => { + const rules = RULES[lang] ?? [] + const tokens: Token[] = [] + let pos = 0 + + while (pos < code.length) { + let matched = false + for (const [re, kind] of rules) { + re.lastIndex = pos + const m = re.exec(code) + if (m && m.index === pos) { + tokens.push({ kind, match: m[0] }) + pos += m[0].length + matched = true + break + } + } + if (!matched) { + const last = tokens[tokens.length - 1] + const ch = code[pos] + if (last && last.kind === 'plain') { + last.match += ch + } else { + tokens.push({ kind: 'plain', match: ch }) + } + pos++ + } + } + + return tokens +} + +export const highlight = (code: string, lang: string): string => + tokenize(code, lang) + .map((t) => (t.kind === 'plain' ? escape(t.match) : `${escape(t.match)}`)) + .join('') diff --git a/preview/src/ui/code-block/styles/code-block.module.css b/preview/src/ui/code-block/styles/code-block.module.css new file mode 100644 index 0000000..51b5f56 --- /dev/null +++ b/preview/src/ui/code-block/styles/code-block.module.css @@ -0,0 +1,85 @@ +.root { + position: relative; + border-radius: var(--radius-2); + overflow: hidden; + background: var(--color-code-bg); +} + +.pre { + margin: 0; + padding: var(--space-4); + overflow-x: auto; + font-size: 12px; + font-family: var(--font-mono); + line-height: 1.6; +} + +.code { + font-family: var(--font-mono); +} + +:global(:root[data-theme='light']) .code { + --hl-tag: #116329; + --hl-attr: #0550ae; + --hl-string: #0a3069; + --hl-comment: #8b949e; + --hl-punctuation: #6639ba; + --hl-selector: #6639ba; + --hl-property: #0550ae; + --hl-color: #0a3069; + --hl-number: #0550ae; + --hl-function: #0550ae; +} + +:global(:root[data-theme='dark']) .code { + --hl-tag: #7ee787; + --hl-attr: #79c0ff; + --hl-string: #a5d6ff; + --hl-comment: #8b949e; + --hl-punctuation: #d2a8ff; + --hl-selector: #d2a8ff; + --hl-property: #79c0ff; + --hl-color: #a5d6ff; + --hl-number: #79c0ff; + --hl-function: #d2a8ff; +} + +.code :global(.hl-tag), +.code :global(.hl-attr), +.code :global(.hl-string), +.code :global(.hl-comment), +.code :global(.hl-punctuation), +.code :global(.hl-selector), +.code :global(.hl-property), +.code :global(.hl-color), +.code :global(.hl-number), +.code :global(.hl-function) { + color: var(--hl-tag); +} + +.code :global(.hl-attr) { color: var(--hl-attr); } +.code :global(.hl-string) { color: var(--hl-string); } +.code :global(.hl-comment) { color: var(--hl-comment); } +.code :global(.hl-punctuation) { color: var(--hl-punctuation); } +.code :global(.hl-selector) { color: var(--hl-selector); } +.code :global(.hl-property) { color: var(--hl-property); } +.code :global(.hl-color) { color: var(--hl-color); } +.code :global(.hl-number) { color: var(--hl-number); } +.code :global(.hl-function) { color: var(--hl-function); } + +.copyButton { + position: absolute; + top: var(--space-2); + right: var(--space-2); + padding: var(--space-1) var(--space-2); + font-size: 11px; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-1); + color: var(--color-muted); + cursor: pointer; +} + +.copyButton:hover { + color: var(--color-fg); +} diff --git a/preview/src/ui/code-block/types/code-block.type.ts b/preview/src/ui/code-block/types/code-block.type.ts new file mode 100644 index 0000000..cbfcf41 --- /dev/null +++ b/preview/src/ui/code-block/types/code-block.type.ts @@ -0,0 +1,18 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры CodeBlock. + */ +export type CodeBlockParams = { + /** Код для отображения. */ + code: string + /** Язык подсветки синтаксиса. */ + language?: 'html' | 'css' | 'xml' + /** Показывать кнопку копирования. */ + copyable?: boolean +} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type CodeBlockProps = RootAttrs & CodeBlockParams diff --git a/preview/src/ui/color-picker/color-picker.ui.tsx b/preview/src/ui/color-picker/color-picker.ui.tsx new file mode 100644 index 0000000..7c6f411 --- /dev/null +++ b/preview/src/ui/color-picker/color-picker.ui.tsx @@ -0,0 +1,74 @@ +import { useState, useRef, useEffect, useCallback } from 'react' +import { HexColorPicker } from 'react-colorful' +import cl from 'clsx' +import type { ColorPickerProps } from './types/color-picker.type' +import styles from './styles/color-picker.module.css' + +/** + * Выбор цвета с попапом react-colorful и HEX-инпутом. + * + * Используется для: + * - настройки CSS-переменных цвета иконок + * - визуального подбора цвета в модалке + */ +export const ColorPicker = (props: ColorPickerProps) => { + const { value, onValueChange, label, className, ...htmlAttr } = props + + const [isOpen, setIsOpen] = useState(false) + const popoverRef = useRef(null) + const swatchRef = useRef(null) + + const handleToggle = useCallback(() => { + setIsOpen((prev) => !prev) + }, []) + + useEffect(() => { + if (!isOpen) return + + const handleClickOutside = (e: MouseEvent): void => { + const target = e.target as Node + + if ( + popoverRef.current && !popoverRef.current.contains(target) && + swatchRef.current && !swatchRef.current.contains(target) + ) { + setIsOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [isOpen]) + + const handleHexInput = (e: React.ChangeEvent): void => { + const hex = e.target.value + + if (/^#[0-9a-fA-F]{6}$/.test(hex)) { + onValueChange(hex) + } + } + + return ( +
+
+ ) +} diff --git a/preview/src/ui/color-picker/index.ts b/preview/src/ui/color-picker/index.ts new file mode 100644 index 0000000..ff2b65f --- /dev/null +++ b/preview/src/ui/color-picker/index.ts @@ -0,0 +1 @@ +export { ColorPicker } from './color-picker.ui' diff --git a/preview/src/ui/color-picker/styles/color-picker.module.css b/preview/src/ui/color-picker/styles/color-picker.module.css new file mode 100644 index 0000000..6d9682b --- /dev/null +++ b/preview/src/ui/color-picker/styles/color-picker.module.css @@ -0,0 +1,53 @@ +.root { + position: relative; + display: inline-flex; + align-items: center; + gap: var(--space-2); +} + +.swatch { + width: 24px; + height: 24px; + border-radius: var(--radius-1); + border: 1px solid var(--color-border); + cursor: pointer; + padding: 0; + background: none; +} + +.label { + font-size: 12px; + color: var(--color-muted); + font-family: var(--font-mono); +} + +.popover { + position: absolute; + top: calc(100% + var(--space-2)); + left: 0; + z-index: 200; + padding: var(--space-3); + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +.hexInput { + display: block; + width: 100%; + margin-top: var(--space-2); + padding: var(--space-1) var(--space-2); + font-size: 12px; + font-family: var(--font-mono); + color: var(--color-fg); + background: var(--color-card-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-1); + outline: none; + text-align: center; +} + +.hexInput:focus { + border-color: var(--color-accent); +} diff --git a/preview/src/ui/color-picker/types/color-picker.type.ts b/preview/src/ui/color-picker/types/color-picker.type.ts new file mode 100644 index 0000000..e65f1e6 --- /dev/null +++ b/preview/src/ui/color-picker/types/color-picker.type.ts @@ -0,0 +1,18 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры ColorPicker. + */ +export type ColorPickerParams = { + /** Текущий цвет в HEX. */ + value: string + /** Обработчик изменения цвета. */ + onValueChange: (color: string) => void + /** Подпись под picker-ом. */ + label?: string +} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = Omit, 'onChange'> + +export type ColorPickerProps = RootAttrs & ColorPickerParams diff --git a/preview/src/ui/icon-card/icon-card.ui.tsx b/preview/src/ui/icon-card/icon-card.ui.tsx new file mode 100644 index 0000000..01c86d0 --- /dev/null +++ b/preview/src/ui/icon-card/icon-card.ui.tsx @@ -0,0 +1,29 @@ +import cl from 'clsx' +import type { IconCardProps } from './types/icon-card.type' +import styles from './styles/icon-card.module.css' + +/** + * Карточка иконки в сетке превью. + * + * Используется для: + * - отображения иконки из спрайта с именем + * - открытия модалки деталей по клику + */ +export const IconCard = (props: IconCardProps) => { + const { iconId, onSelect, className, ...htmlAttr } = props + + const handleClick = (): void => { + onSelect?.(iconId) + } + + return ( +
+
+ + + +
+ {iconId} +
+ ) +} diff --git a/preview/src/ui/icon-card/index.ts b/preview/src/ui/icon-card/index.ts new file mode 100644 index 0000000..d0dce9d --- /dev/null +++ b/preview/src/ui/icon-card/index.ts @@ -0,0 +1 @@ +export { IconCard } from './icon-card.ui' diff --git a/preview/src/ui/icon-card/styles/icon-card.module.css b/preview/src/ui/icon-card/styles/icon-card.module.css new file mode 100644 index 0000000..f3625c3 --- /dev/null +++ b/preview/src/ui/icon-card/styles/icon-card.module.css @@ -0,0 +1,37 @@ +.root { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); + padding: var(--space-4) var(--space-2); + border-radius: var(--radius-2); + background: var(--color-card-bg); + cursor: pointer; + transition: background 0.15s; +} + +.root:hover { + background: var(--color-card-hover); +} + +.iconWrap { + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-1); + background: var(--checkerboard); + background-size: var(--checkerboard-size); +} + +.icon { + width: var(--icon-size); + height: var(--icon-size); + color: var(--color-fg); +} + +.name { + font-size: 13px; + color: var(--color-muted); + text-align: center; + word-break: break-all; +} diff --git a/preview/src/ui/icon-card/types/icon-card.type.ts b/preview/src/ui/icon-card/types/icon-card.type.ts new file mode 100644 index 0000000..f4148e7 --- /dev/null +++ b/preview/src/ui/icon-card/types/icon-card.type.ts @@ -0,0 +1,16 @@ +import type { HTMLAttributes } from 'react' + +/** + * Параметры IconCard. + */ +export type IconCardParams = { + /** Идентификатор иконки. */ + iconId: string + /** Обработчик клика по карточке. */ + onSelect?: (iconId: string) => void +} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = Omit, 'onSelect'> + +export type IconCardProps = RootAttrs & IconCardParams diff --git a/preview/src/ui/icon-grid/icon-grid.ui.tsx b/preview/src/ui/icon-grid/icon-grid.ui.tsx new file mode 100644 index 0000000..0bf99d2 --- /dev/null +++ b/preview/src/ui/icon-grid/icon-grid.ui.tsx @@ -0,0 +1,51 @@ +import cl from 'clsx' +import { IconCard } from '../icon-card' +import type { IconGridProps } from './types/icon-grid.type' +import styles from './styles/icon-grid.module.css' + +/** + * Сетка иконок, сгруппированная по спрайтам. + * + * Используется для: + * - отображения всех иконок из всех спрайтов + * - фильтрации иконок по поисковому запросу + */ +export const IconGrid = (props: IconGridProps) => { + const { groups, searchQuery = '', onIconSelect, className, ...htmlAttr } = props + + const query = searchQuery.toLowerCase() + + return ( +
+ {groups.map((group) => { + const filteredIcons = query + ? group.icons.filter((icon) => icon.id.includes(query)) + : group.icons + + const isGroupHidden = filteredIcons.length === 0 + + return ( +
+

+ {group.name} + {group.mode} + {group.icons.length} +

+
+ {filteredIcons.map((icon) => ( + + ))} +
+
+ ) + })} +
+ ) +} diff --git a/preview/src/ui/icon-grid/index.ts b/preview/src/ui/icon-grid/index.ts new file mode 100644 index 0000000..4222794 --- /dev/null +++ b/preview/src/ui/icon-grid/index.ts @@ -0,0 +1 @@ +export { IconGrid } from './icon-grid.ui' diff --git a/preview/src/ui/icon-grid/styles/icon-grid.module.css b/preview/src/ui/icon-grid/styles/icon-grid.module.css new file mode 100644 index 0000000..97b8d8d --- /dev/null +++ b/preview/src/ui/icon-grid/styles/icon-grid.module.css @@ -0,0 +1,42 @@ +.root { +} + +.group { + margin-bottom: var(--space-10); +} + +.groupHeader { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: 1.1rem; + font-weight: 600; + margin-bottom: var(--space-4); +} + +.badge { + font-size: 11px; + font-weight: 500; + padding: 2px var(--space-2); + border-radius: 10px; + background: var(--color-accent); + color: #ffffff; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.count { + font-size: 13px; + color: var(--color-muted); + font-weight: 400; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: var(--space-3); +} + +._hidden { + display: none; +} diff --git a/preview/src/ui/icon-grid/types/icon-grid.type.ts b/preview/src/ui/icon-grid/types/icon-grid.type.ts new file mode 100644 index 0000000..78038df --- /dev/null +++ b/preview/src/ui/icon-grid/types/icon-grid.type.ts @@ -0,0 +1,19 @@ +import type { HTMLAttributes } from 'react' +import type { SpriteGroup } from '../../../shared/types' + +/** + * Параметры IconGrid. + */ +export type IconGridParams = { + /** Группы спрайтов для отображения. */ + groups: SpriteGroup[] + /** Строка поиска для фильтрации. */ + searchQuery?: string + /** Обработчик выбора иконки. */ + onIconSelect?: (iconId: string) => void +} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type IconGridProps = RootAttrs & IconGridParams diff --git a/preview/src/ui/icon-modal/icon-modal.ui.tsx b/preview/src/ui/icon-modal/icon-modal.ui.tsx new file mode 100644 index 0000000..b085ae1 --- /dev/null +++ b/preview/src/ui/icon-modal/icon-modal.ui.tsx @@ -0,0 +1,235 @@ +import { useState, useEffect, useCallback, useRef } from 'react' +import cl from 'clsx' +import { CodeBlock } from '../code-block' +import { ColorPicker } from '../color-picker' +import { rgbToHex } from '../../shared/lib/rgb-to-hex.util' +import type { IconModalProps } from './types/icon-modal.type' +import styles from './styles/icon-modal.module.css' + +type TabId = 'react' | 'svg' | 'img' | 'css' + +const TABS: { id: TabId; label: string }[] = [ + { id: 'react', label: 'React' }, + { id: 'svg', label: 'SVG' }, + { id: 'img', label: 'IMG' }, + { id: 'css', label: 'CSS' }, +] + +/** + * Модалка деталей иконки с превью, color pickers и табами кода. + * + * Превью иконки рендерится тем же способом, что выбран в табе: + * - React / SVG — через + * - IMG — через + * - CSS — через mask-image на
+ */ +export const IconModal = (props: IconModalProps) => { + const { icon, onClose, defaultSprite, className, ...htmlAttr } = props + + const [activeTab, setActiveTab] = useState('react') + const [colors, setColors] = useState>({}) + const [cssColor, setCssColor] = useState('#000000') + const iconWrapRef = useRef(null) + + const handleOverlayClick = useCallback((e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose() + } + }, [onClose]) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent): void => { + if (e.key === 'Escape') { + onClose() + } + } + + if (icon) { + document.addEventListener('keydown', handleKeyDown) + } + + return () => document.removeEventListener('keydown', handleKeyDown) + }, [icon, onClose]) + + useEffect(() => { + if (!icon) return + + const fg = getComputedStyle(document.documentElement).getPropertyValue('--color-fg').trim() + const fgHex = rgbToHex(fg) + + const initialColors: Record = {} + icon.vars.forEach((v) => { + initialColors[v.varName] = v.isCurrentColor ? fgHex : v.hex + }) + + setColors(initialColors) + setCssColor(fgHex) + setActiveTab('react') + + if (iconWrapRef.current) { + iconWrapRef.current.removeAttribute('style') + } + }, [icon]) + + if (!icon) { + return null + } + + const spriteRef = `${icon.spriteFile}#${icon.id}` + + const isDefaultSprite = icon.group === defaultSprite + const codeReact = isDefaultSprite + ? `` + : `` + const codeSvg = `\n \n` + const codeImg = `${icon.id}` + const codeCss = `.${icon.id} {\n width: 24px;\n height: 24px;\n mask: url('${spriteRef}') no-repeat center / contain;\n -webkit-mask: url('${spriteRef}') no-repeat center / contain;\n background-color: ${cssColor};\n}` + + const codeByTab: Record = { + react: { code: codeReact, language: 'html' }, + svg: { code: codeSvg, language: 'html' }, + img: { code: codeImg, language: 'html' }, + css: { code: codeCss, language: 'css' }, + } + + const handleColorChange = (varName: string, value: string): void => { + setColors((prev) => ({ ...prev, [varName]: value })) + iconWrapRef.current?.style.setProperty(varName, value) + } + + const renderPreview = () => { + switch (activeTab) { + case 'react': + case 'svg': + return ( + + + + ) + + case 'img': + return ( + {icon.id} + ) + + case 'css': + return ( +
+ ) + } + } + + const supportsColorChange = activeTab === 'react' || activeTab === 'svg' + const isMono = icon.vars.length > 0 && icon.vars.every((v) => v.isCurrentColor) + + return ( +
+
+ + +
+
+ {renderPreview()} +
+
+ +
+
{icon.id}
+ {icon.viewBox && ( + + {icon.viewBox.width} × {icon.viewBox.height} + + )} +
+ +
+ {icon.vars.length > 0 && supportsColorChange ? ( + <> +
+ {isMono ? ( + + Иконка наследует цвет текста или задаётся точечно через CSS-переменную. + + ) : ( + + Многоцветная иконка — каждый цвет задаётся отдельной CSS-переменной. + + )} +
+
CSS Variables
+ {icon.vars.map((v) => ( + handleColorChange(v.varName, color)} + label={`${v.varName}: ${v.fallback}`} + className={styles.varRow} + /> + ))} + + ) : activeTab === 'img' ? ( +
+ Управление цветом в режиме IMG невозможно. <img> изолирует SVG — CSS-переменные + и currentColor не проникают внутрь. Подходит для многоцветных изображений с фиксированными цветами. +
+ ) : activeTab === 'css' ? ( + <> +
+ В режиме CSS mask иконка монохромная — цвет задаётся через background-color. + CSS-переменные спрайта не поддерживаются. +
+ + + ) : ( + No color variables + )} +
+ +
+ {TABS.map((tab) => ( + + ))} +
+ + {TABS.map((tab) => ( +
+ +
+ ))} +
+
+ ) +} diff --git a/preview/src/ui/icon-modal/index.ts b/preview/src/ui/icon-modal/index.ts new file mode 100644 index 0000000..377c6e9 --- /dev/null +++ b/preview/src/ui/icon-modal/index.ts @@ -0,0 +1 @@ +export { IconModal } from './icon-modal.ui' diff --git a/preview/src/ui/icon-modal/styles/icon-modal.module.css b/preview/src/ui/icon-modal/styles/icon-modal.module.css new file mode 100644 index 0000000..dfeaa85 --- /dev/null +++ b/preview/src/ui/icon-modal/styles/icon-modal.module.css @@ -0,0 +1,211 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 100; + display: flex; + align-items: flex-start; + justify-content: center; + padding: var(--space-10) 0; + overflow-y: auto; + background: rgba(0, 0, 0, 0.5); +} + +.root { + position: relative; + width: 90%; + max-width: 560px; + padding: var(--space-6); + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-3); +} + +.closeButton { + position: absolute; + top: var(--space-3); + right: var(--space-3); + padding: var(--space-1) var(--space-2); + background: none; + border: none; + color: var(--color-fg); + font-size: 20px; + cursor: pointer; + line-height: 1; +} + +.iconWrap { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-6); + margin-bottom: var(--space-4); + background: var(--color-card-bg); + border-radius: var(--radius-2); +} + +.iconViewBox { + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-1); + background: var(--checkerboard); + background-size: var(--checkerboard-size); +} + +.icon, +.iconImg, +.iconCss { + width: 256px; + height: 256px; +} + +.icon { + color: var(--color-fg); +} + +.iconImg { + object-fit: contain; +} + +.iconCss { + background-color: var(--color-fg); +} + +.nameRow { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + margin-bottom: var(--space-4); +} + +.name { + font-size: 1rem; + font-weight: 600; +} + +.viewBoxBadge { + font-size: 11px; + font-weight: 500; + padding: 2px var(--space-2); + border-radius: 10px; + background: var(--color-card-bg); + border: 1px solid var(--color-border); + color: var(--color-muted); + white-space: nowrap; +} + +.varsSection { + margin-bottom: var(--space-5); +} + +.varsTitle { + font-size: 12px; + font-weight: 600; + color: var(--color-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: var(--space-2); +} + +.varRow { + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: var(--space-2); +} + + + +.noVars { + font-size: 12px; + color: var(--color-muted); + font-style: italic; +} + +.hint { + font-size: 12px; + line-height: 1.5; + color: var(--color-muted); + padding: var(--space-2) var(--space-3); + background: var(--color-card-bg); + border-radius: var(--radius-1); + border-left: 3px solid var(--color-accent); +} + +.colorHint { + margin-bottom: var(--space-3); + padding: var(--space-2) var(--space-3); + font-size: 12px; + line-height: 1.5; + color: var(--color-muted); + background: var(--color-card-bg); + border-radius: var(--radius-1); + border-left: 3px solid var(--color-accent); +} + +.colorHintTitle { + font-size: 12px; + font-weight: 600; + color: var(--color-muted); + margin-bottom: var(--space-1); +} + +.colorHintRow { + display: flex; + align-items: baseline; + gap: var(--space-2); + margin-top: var(--space-1); +} + +.colorHintCode { + font-family: var(--font-mono); + font-size: 11px; + padding: 2px var(--space-1); + background: var(--color-bg); + border-radius: 3px; + white-space: nowrap; +} + +.colorHintDesc { + font-size: 11px; + color: var(--color-muted); +} + +.cssColorRow { + display: flex; + align-items: center; + gap: var(--space-2); + margin-top: var(--space-3); +} + +.tabs { + display: flex; + gap: 0; + border-bottom: 1px solid var(--color-border); + margin-bottom: var(--space-3); +} + +.tab { + padding: var(--space-2) var(--space-4); + font-size: 12px; + font-weight: 600; + cursor: pointer; + border: none; + border-bottom: 2px solid transparent; + color: var(--color-muted); + background: none; +} + +.tab._active { + color: var(--color-accent); + border-bottom-color: var(--color-accent); +} + +.tabContent { + display: none; +} + +.tabContent._active { + display: block; +} diff --git a/preview/src/ui/icon-modal/types/icon-modal.type.ts b/preview/src/ui/icon-modal/types/icon-modal.type.ts new file mode 100644 index 0000000..e325d78 --- /dev/null +++ b/preview/src/ui/icon-modal/types/icon-modal.type.ts @@ -0,0 +1,19 @@ +import type { HTMLAttributes } from 'react' +import type { IconData } from '../../../shared/types' + +/** + * Параметры IconModal. + */ +export type IconModalParams = { + /** Данные иконки для отображения. null — модалка закрыта. */ + icon: IconData | null + /** Имя спрайта по умолчанию (первый из конфига). */ + defaultSprite?: string + /** Обработчик закрытия модалки. */ + onClose: () => void +} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type IconModalProps = RootAttrs & IconModalParams diff --git a/preview/src/ui/search-input/index.ts b/preview/src/ui/search-input/index.ts new file mode 100644 index 0000000..c60a639 --- /dev/null +++ b/preview/src/ui/search-input/index.ts @@ -0,0 +1 @@ +export { SearchInput } from './search-input.ui' diff --git a/preview/src/ui/search-input/search-input.ui.tsx b/preview/src/ui/search-input/search-input.ui.tsx new file mode 100644 index 0000000..be42a0e --- /dev/null +++ b/preview/src/ui/search-input/search-input.ui.tsx @@ -0,0 +1,27 @@ +import cl from 'clsx' +import type { SearchInputProps } from './types/search-input.type' +import styles from './styles/search-input.module.css' + +/** + * Поле поиска. + * + * Используется для: + * - фильтрации иконок по имени + */ +export const SearchInput = (props: SearchInputProps) => { + const { className, onValueChange, ...htmlAttr } = props + + const handleChange = (e: React.ChangeEvent): void => { + onValueChange?.(e.target.value) + } + + return ( + + ) +} diff --git a/preview/src/ui/search-input/styles/search-input.module.css b/preview/src/ui/search-input/styles/search-input.module.css new file mode 100644 index 0000000..c978e94 --- /dev/null +++ b/preview/src/ui/search-input/styles/search-input.module.css @@ -0,0 +1,14 @@ +.root { + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--radius-2); + background: var(--color-card-bg); + color: var(--color-fg); + font-size: 14px; + width: 200px; + outline: none; +} + +.root:focus { + border-color: var(--color-accent); +} diff --git a/preview/src/ui/search-input/types/search-input.type.ts b/preview/src/ui/search-input/types/search-input.type.ts new file mode 100644 index 0000000..c674f4f --- /dev/null +++ b/preview/src/ui/search-input/types/search-input.type.ts @@ -0,0 +1,14 @@ +import type { InputHTMLAttributes } from 'react' + +/** + * Параметры SearchInput. + */ +export type SearchInputParams = { + /** Обработчик изменения значения поиска. */ + onValueChange?: (value: string) => void +} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = Omit, 'onChange'> + +export type SearchInputProps = RootAttrs & SearchInputParams diff --git a/preview/src/vite-env.d.ts b/preview/src/vite-env.d.ts new file mode 100644 index 0000000..feb7ba8 --- /dev/null +++ b/preview/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module '*.module.css' { + const classes: { readonly [key: string]: string } + export default classes +} diff --git a/preview/tsconfig.app.json b/preview/tsconfig.app.json new file mode 100644 index 0000000..7f42e5f --- /dev/null +++ b/preview/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/preview/tsconfig.json b/preview/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/preview/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/preview/tsconfig.node.json b/preview/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/preview/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/preview/vite.config.ts b/preview/vite.config.ts new file mode 100644 index 0000000..891521d --- /dev/null +++ b/preview/vite.config.ts @@ -0,0 +1,32 @@ +import { resolve } from 'node:path' +import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs' +import { defineConfig } from 'vite' +import type { Plugin } from 'vite' +import react from '@vitejs/plugin-react' +import { viteSingleFile } from 'vite-plugin-singlefile' + +/** Переименовывает index.html → preview-template.html после сборки. */ +function renameOutput(): Plugin { + return { + name: 'rename-preview-template', + closeBundle() { + const outDir = resolve(__dirname, '../dist') + const src = resolve(outDir, 'index.html') + const dest = resolve(outDir, 'preview-template.html') + + if (existsSync(src)) { + const content = readFileSync(src, 'utf-8') + writeFileSync(dest, content) + unlinkSync(src) + } + }, + } +} + +export default defineConfig({ + plugins: [react(), viteSingleFile(), renameOutput()], + build: { + outDir: resolve(__dirname, '../dist'), + emptyOutDir: false, + }, +}) diff --git a/src/cli.ts b/src/cli.ts index b88a6be..bf894b6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,78 +1,16 @@ #!/usr/bin/env node -import { defineCommand, runMain } from 'citty' +import { loadConfig } from './config.js' import { generate } from './generate.js' import { log } from './logger.js' -const main = defineCommand({ - meta: { - name: 'svg-sprites', - version: '0.1.0', - description: 'Generate SVG sprites and TypeScript icon name types', - }, - args: { - input: { - type: 'string', - alias: 'i', - description: 'Directory with SVG subfolders (each subfolder = one sprite)', - required: true, - }, - output: { - type: 'string', - alias: 'o', - description: 'Output directory for generated SVG sprite files', - required: true, - }, - types: { - type: 'boolean', - alias: 't', - description: 'Generate TypeScript union types for icon names (default: true)', - default: true, - }, - typesOutput: { - type: 'string', - description: 'Output directory for .generated.ts files (default: same as input)', - }, - removeSize: { - type: 'boolean', - description: 'Remove width/height from root (default: true)', - default: true, - }, - replaceColors: { - type: 'boolean', - description: 'Replace colors with CSS variables var(--icon-color-N) (default: true)', - default: true, - }, - addTransition: { - type: 'boolean', - description: 'Add transition:fill,stroke to colored elements (default: true)', - default: true, - }, - preview: { - type: 'boolean', - alias: 'p', - description: 'Generate HTML preview page with all icons (default: true)', - default: true, - }, - }, - async run({ args }) { - try { - await generate({ - input: args.input, - output: args.output, - types: args.types, - typesOutput: args.typesOutput, - transform: { - removeSize: args.removeSize, - replaceColors: args.replaceColors, - addTransition: args.addTransition, - }, - preview: args.preview, - }) - } catch (error) { - log.error(error instanceof Error ? error.message : String(error)) - process.exit(1) - } - }, -}) +async function main() { + try { + const config = await loadConfig() + await generate(config) + } catch (error) { + log.error(error instanceof Error ? error.message : String(error)) + process.exit(1) + } +} -runMain(main) +main() diff --git a/src/codegen-react.ts b/src/codegen-react.ts new file mode 100644 index 0000000..b28ab51 --- /dev/null +++ b/src/codegen-react.ts @@ -0,0 +1,179 @@ +import fs from 'node:fs' +import path from 'node:path' +import type { SpriteFolder, SpriteResult } from './types.js' + +/** Преобразует kebab-case строку в PascalCase. */ +function toPascalCase(str: string): string { + return str.replace(/(^|[-_])([a-z])/g, (_, __, c: string) => c.toUpperCase()) +} + +/** + * Собирает имена иконок из SpriteFolder. + */ +function getIconNames(folder: SpriteFolder): string[] { + return folder.files + .map((filePath) => path.basename(filePath, '.svg')) + .sort() +} + +/** + * Генерирует [name].tsx и [name].module.css — React-компонент с типами. + * + * Имена файлов берутся из basename папки outputDir. + * Например: outputDir = 'src/ui/svg-sprite' → svg-sprite.tsx + svg-sprite.module.css. + * + * Содержит: + * - union-типы имён иконок для каждого спрайта (IconsIconName, LogosIconName, ...) + * - SpriteMap, SpriteName, IconName + * - компонент SvgSprite с зашитым publicPath + */ +export function generateReactModule( + results: SpriteResult[], + folders: SpriteFolder[], + outputDir: string, + publicPath: string, +): string { + const typeBlocks: string[] = [] + const mapEntries: string[] = [] + const spriteFileEntries: string[] = [] + + for (const result of results) { + const folder = folders.find((f) => f.name === result.name) + if (!folder) continue + + const typeName = `${toPascalCase(result.name)}IconName` + const names = getIconNames(folder) + + typeBlocks.push( + `/** Имена иконок спрайта «${result.name}». */`, + `export type ${typeName} =`, + names.map((n) => ` | '${n}'`).join('\n'), + '', + ) + + mapEntries.push(` ${result.name}: ${typeName}`) + spriteFileEntries.push(` ${result.name}: '${result.name}.sprite.svg',`) + } + + const baseName = path.basename(outputDir) + const defaultSprite = results[0].name + + const lines = [ + '/**', + ' * SVG-спрайты: типы и React-компонент.', + ' * @generated — this file is auto-generated, do not edit manually.', + ' */', + "import type { SVGAttributes, HTMLAttributes } from 'react'", + `import styles from './${baseName}.module.css'`, + '', + ...typeBlocks, + '/** Маппинг имени спрайта на тип его иконок. */', + 'export type SpriteMap = {', + ...mapEntries, + '}', + '', + '/** Имя спрайта. */', + 'export type SpriteName = keyof SpriteMap', + '', + '/** Спрайт по умолчанию. */', + `export type DefaultSprite = '${defaultSprite}'`, + '', + '/** Имя иконки для конкретного спрайта. */', + 'export type IconName = SpriteMap[S]', + '', + `const PUBLIC_PATH = '${publicPath}'`, + `const DEFAULT_SPRITE: SpriteName = '${defaultSprite}'`, + '', + 'const SPRITE_FILES: Record = {', + ...spriteFileEntries, + '}', + '', + 'type IconBaseProps = {', + ' /** Имя иконки. */', + ' icon: IconName', + ' /** Имя спрайта. По умолчанию: первый из конфига. */', + ' sprite?: S', + '}', + '', + 'type IconSvgProps = IconBaseProps & {', + ' wrapped?: false', + '} & SVGAttributes', + '', + 'type IconWrappedProps = IconBaseProps & {', + ' wrapped: true', + '} & HTMLAttributes', + '', + 'export type SvgSpriteProps =', + ' | IconSvgProps', + ' | IconWrappedProps', + '', + '/**', + ' * Иконка из SVG-спрайта.', + ' *', + ' * Используется для:', + ' * - отображения иконки через ``', + ' * - обёртки в `` через проп `wrapped`', + ' *', + ` * Спрайт по умолчанию: «${defaultSprite}».`, + ' */', + 'export const SvgSprite = (props: SvgSpriteProps) => {', + ' const { icon, sprite = DEFAULT_SPRITE as S, wrapped, className, ...rest } = props', + // eslint-disable-next-line no-template-curly-in-string + ' const href = `${PUBLIC_PATH}/${SPRITE_FILES[sprite]}#${icon}`', + '', + ' if (wrapped) {', + ' const { ...htmlAttr } = rest as HTMLAttributes', + ' return (', + ' ', + ' ', + ' ', + ' ', + ' ', + ' )', + ' }', + '', + ' const { ...svgAttr } = rest as SVGAttributes', + ' return (', + ' ', + ' ', + ' ', + ' )', + '}', + '', + ] + + const content = lines.join('\n') + const outputPath = path.join(outputDir, `${baseName}.tsx`) + + fs.mkdirSync(outputDir, { recursive: true }) + fs.writeFileSync(outputPath, content) + + // Генерируем CSS Module + const css = [ + '/* @generated — this file is auto-generated, do not edit manually. */', + '', + '.root {', + ' transition-property: fill, stroke, color;', + ' transition-duration: 0.3s;', + ' transition-timing-function: ease;', + '}', + '', + '.wrap {', + ' display: inline-flex;', + '}', + '', + '.wrap svg {', + ' width: 100%;', + ' height: 100%;', + ' transition-property: fill, stroke, color;', + ' transition-duration: 0.3s;', + ' transition-timing-function: ease;', + '}', + '', + ].join('\n') + + const cssPath = path.join(outputDir, `${baseName}.module.css`) + fs.writeFileSync(cssPath, css) + + return outputPath +} diff --git a/src/compiler.ts b/src/compiler.ts index 64662e2..8784c0a 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -5,28 +5,35 @@ import { createShapeTransform } from './transforms.js' import type { SpriteFolder, SpriteMode, TransformOptions } from './types.js' /** Конфигурация режима для svg-sprite. */ -function getModeConfig(mode: SpriteMode, destDir: string) { +function getModeConfig(mode: SpriteMode, destDir: string, name: string) { return { dest: destDir, - sprite: `sprite.${mode}.svg`, + sprite: `${name}.sprite.svg`, example: false, rootviewbox: false, } } -/** Строит массив shape.transform на основе опций. */ -function buildShapeTransforms(transform: TransformOptions) { +/** Строит массив shape.transform. */ +function buildShapeTransforms(transform: TransformOptions = {}) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const transforms: any[] = ['svgo'] - - const hasCustomTransform = - transform.removeSize !== false || - transform.replaceColors !== false || - transform.addTransition !== false - - if (hasCustomTransform) { - transforms.push(createShapeTransform(transform)) - } + const transforms: any[] = [ + { + svgo: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + ], + }, + }, + createShapeTransform(transform), + ] return transforms } @@ -41,14 +48,12 @@ export async function compileSprite( outputDir: string, transform: TransformOptions = {}, ): Promise { - const destDir = path.join(outputDir, folder.name) - const config = { shape: { transform: buildShapeTransforms(transform), }, mode: { - [folder.mode]: getModeConfig(folder.mode, destDir), + [folder.mode]: getModeConfig(folder.mode, outputDir, folder.name), }, } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..9e2d639 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,67 @@ +import fs from 'node:fs' +import path from 'node:path' +import { createJiti } from 'jiti' +import type { SvgSpritesConfig } from './types.js' + +const CONFIG_NAME = 'svg-sprites.config' + +/** + * Загружает конфиг svg-sprites.config.ts из указанной директории. + * + * Использует jiti для импорта TypeScript-файлов. + */ +export async function loadConfig(cwd: string = process.cwd()): Promise { + const configPath = path.join(cwd, `${CONFIG_NAME}.ts`) + + if (!fs.existsSync(configPath)) { + throw new Error( + `Config file not found: ${configPath}\n` + + `Create a ${CONFIG_NAME}.ts file in the project root.`, + ) + } + + const jiti = createJiti(cwd) + const mod = await jiti.import(configPath) as { default?: SvgSpritesConfig } + + const config = mod.default + + if (!config) { + throw new Error( + `Config file must have a default export: ${configPath}\n` + + 'Use: export default defineConfig({ ... })', + ) + } + + validateConfig(config) + + return config +} + +/** + * Валидирует конфиг на наличие обязательных полей. + */ +function validateConfig(config: SvgSpritesConfig): void { + if (!config.output) { + throw new Error('Config: "output" is required.') + } + + if (!config.sprites || config.sprites.length === 0) { + throw new Error('Config: "sprites" must be a non-empty array.') + } + + for (const sprite of config.sprites) { + if (!sprite.name) { + throw new Error('Config: each sprite must have a "name".') + } + + if (!sprite.input) { + throw new Error(`Config: sprite "${sprite.name}" must have an "input".`) + } + + if (sprite.mode && sprite.mode !== 'stack' && sprite.mode !== 'symbol') { + throw new Error( + `Config: sprite "${sprite.name}" has invalid mode "${sprite.mode}". Supported: stack, symbol.`, + ) + } + } +} diff --git a/src/generate.ts b/src/generate.ts index a374995..6bb1c14 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,40 +1,38 @@ import path from 'node:path' -import { scanSpriteFolders } from './scanner.js' +import { resolveSprites } from './scanner.js' import { compileSprite } from './compiler.js' -import { generateIconTypes } from './codegen.js' +import { generateReactModule } from './codegen-react.js' import { generatePreview } from './preview.js' import { log } from './logger.js' -import type { GenerateOptions, SpriteResult } from './types.js' +import type { SvgSpritesConfig, SpriteResult } from './types.js' /** - * Генерирует SVG-спрайты и (опционально) TypeScript-типы для всех подпапок. + * Генерирует SVG-спрайты из конфига. * * Основная точка входа — используется и из CLI, и из программного API. */ -export async function generate(options: GenerateOptions): Promise { +export async function generate(config: SvgSpritesConfig): Promise { const { - input, output, - types = true, - typesOutput, - transform = {}, + publicPath, preview = true, - } = options + react, + transform = {}, + sprites, + } = config - const inputDir = path.resolve(input) const outputDir = path.resolve(output) - const typesDir = typesOutput ? path.resolve(typesOutput) : inputDir - log.title(`Scanning ${inputDir}...`) + log.title('Resolving sprites...') - const folders = scanSpriteFolders(inputDir) + const folders = resolveSprites(sprites) if (folders.length === 0) { - log.warn('No sprite folders with SVG files found.') + log.warn('No sprites to generate.') return [] } - log.info(`Found ${folders.length} sprite folder(s)\n`) + log.info(`Found ${folders.length} sprite(s)\n`) const results: SpriteResult[] = [] @@ -42,21 +40,21 @@ export async function generate(options: GenerateOptions): Promise() const regex = /var\((--icon-color-\d+),\s*([^)]+)\)/g @@ -31,14 +30,10 @@ function extractIconVars(svgFragment: string): { varName: string; fallback: stri return [...vars.entries()].map(([varName, fallback]) => ({ varName, fallback })) } -/** - * Парсит SVG-спрайт и возвращает маппинг id → SVG-фрагмент для каждой иконки. - */ +/** Парсит SVG-спрайт и возвращает маппинг id → SVG-фрагмент. */ function extractIconFragments(spritePath: string): Map { const content = fs.readFileSync(spritePath, 'utf-8') const fragments = new Map() - - // Матчим ... или ... const regex = /<(?:svg|symbol)\b[^>]*\bid="([^"]+)"[^>]*>[\s\S]*?<\/(?:svg|symbol)>/g let match: RegExpExecArray | null while ((match = regex.exec(content)) !== null) { @@ -47,10 +42,34 @@ function extractIconFragments(spritePath: string): Map { return fragments } +/** Извлекает viewBox из SVG-фрагмента иконки. */ +function extractViewBox(svgFragment: string): { x: number; y: number; width: number; height: number } | null { + const match = svgFragment.match(/viewBox="([^"]+)"/) + if (!match) return null + const parts = match[1].split(/\s+/).map(Number) + if (parts.length !== 4) return null + return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] } +} + +/** Конвертирует CSS-цвет в hex. */ +function colorToHex(color: string): string { + const named: Record = { + red: '#ff0000', blue: '#0000ff', green: '#008000', white: '#ffffff', + black: '#000000', yellow: '#ffff00', cyan: '#00ffff', magenta: '#ff00ff', + orange: '#ffa500', purple: '#800080', pink: '#ffc0cb', gray: '#808080', + grey: '#808080', currentcolor: '#000000', + } + const lower = color.toLowerCase().trim() + if (lower.startsWith('#')) { + if (lower.length === 4) return `#${lower[1]}${lower[1]}${lower[2]}${lower[2]}${lower[3]}${lower[3]}` + return lower + } + return named[lower] || '#000000' +} + /** Подготавливает SVG-спрайт для инлайна — конвертирует вложенные в . */ function prepareInlineSprite(spritePath: string): string { let content = fs.readFileSync(spritePath, 'utf-8') - content = content.replace(/<\?xml[^?]*\?>\s*/g, '') content = content.replace(/
${icon.vars.map((v) => { - const isCurrentColor = v.fallback.toLowerCase() === 'currentcolor' - const hex = colorToHex(v.fallback) - return `` - }).join('')}
` - : '
no color vars
' - - return ` -
-
- -
- ${icon.id} - ${varsHtml} -
` - }) - .join('') - - return ` -
-

${group.name} ${group.mode} ${group.icons.length}

-
${cards} -
-
` - }) - .join('\n') - - return ` - - - - -SVG Sprites Preview — ${totalIcons} icons - - - -${inlineSprites} -
-

SVG Sprites

- ${totalIcons} icons -
- - -
-
-${sections} -
- - -` -} - /** * Генерирует HTML-файл превью для всех спрайтов. * - * Возвращает путь к сгенерированному файлу. + * Использует pre-built React-приложение из dist/preview-template.html, + * инжектирует данные спрайтов и inline SVG. */ export function generatePreview( results: SpriteResult[], outputDir: string, ): string { + // Собираем данные const groups: SpriteGroup[] = results.map((r) => { const fragments = extractIconFragments(r.spritePath) const ids = extractIconIds(r.spritePath) + const spriteFile = `${r.name}.sprite.svg` - const icons: IconData[] = ids.map((id) => ({ - id, - vars: extractIconVars(fragments.get(id) || ''), - })) + const icons: IconData[] = ids.map((id) => { + const fragment = fragments.get(id) || '' + return { + id, + group: r.name, + mode: r.mode, + spriteFile, + viewBox: extractViewBox(fragment), + vars: extractIconVars(fragment).map((v) => ({ + varName: v.varName, + fallback: v.fallback, + hex: colorToHex(v.fallback), + isCurrentColor: v.fallback.toLowerCase() === 'currentcolor', + })), + } + }) - return { - name: r.name, - mode: r.mode, - spritePath: r.spritePath, - icons, - } + return { name: r.name, mode: r.mode, spriteFile, icons } }) - const html = renderHtml(groups) - const outputPath = path.join(outputDir, 'preview.html') + // Inline SVG спрайтов + const inlineSprites = results + .map((r) => prepareInlineSprite(r.spritePath)) + .join('\n') + // Скрипт с данными + DOM injection + const svgEscaped = inlineSprites.replace(/`/g, '\\`').replace(/\$/g, '\\$') + const dataScript = [ + '', + ].join('\n') + + // Читаем шаблон + const templatePath = path.join(__dirname, 'preview-template.html') + + if (!fs.existsSync(templatePath)) { + throw new Error( + `Preview template not found: ${templatePath}\n` + + 'Run "npm run build" in the preview/ directory first.', + ) + } + + let html = fs.readFileSync(templatePath, 'utf-8') + html = html.replace('', dataScript) + + // Записываем результат + const outputPath = path.join(outputDir, 'preview.html') fs.mkdirSync(outputDir, { recursive: true }) fs.writeFileSync(outputPath, html) diff --git a/src/scanner.ts b/src/scanner.ts index c681683..8ee5f7f 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -1,71 +1,81 @@ import fs from 'node:fs' import path from 'node:path' -import type { SpriteFolder, SpriteMode } from './types.js' +import type { SpriteEntry, SpriteFolder } from './types.js' /** - * Парсит имя папки и извлекает режим спрайта. - * - * Формат: `folder-name` → stack (по умолчанию), `folder-name?symbol` → symbol. + * Сканирует папку и возвращает отсортированные абсолютные пути к SVG-файлам. */ -function parseFolderName(fullName: string): { name: string; mode: SpriteMode } { - const hasCustomMode = fullName.includes('?') - if (!hasCustomMode) { - return { name: fullName, mode: 'stack' } +function scanDirectory(dirPath: string): string[] { + if (!fs.existsSync(dirPath)) { + throw new Error(`Input directory does not exist: ${dirPath}`) } - const parts = fullName.split('?') - const mode = parts.pop() as SpriteMode - const name = parts[0] - - if (mode !== 'stack' && mode !== 'symbol') { - throw new Error( - `Unknown sprite mode "${mode}" in folder "${fullName}". Supported: stack, symbol.`, - ) - } - - return { name, mode } + return fs + .readdirSync(dirPath) + .filter((file) => file.endsWith('.svg')) + .sort() + .map((file) => path.join(dirPath, file)) } /** - * Сканирует директорию и возвращает список папок-спрайтов с их SVG-файлами. - * - * Пропускает записи, не являющиеся директориями, и папки без SVG-файлов. + * Резолвит массив путей к SVG-файлам в абсолютные пути. + * Проверяет существование каждого файла. */ -export function scanSpriteFolders(inputDir: string): SpriteFolder[] { - if (!fs.existsSync(inputDir)) { - throw new Error(`Input directory does not exist: ${inputDir}`) - } +function resolveFiles(files: string[]): string[] { + return files.map((filePath) => { + const resolved = path.resolve(filePath) - const entries = fs.readdirSync(inputDir) - const folders: SpriteFolder[] = [] - - for (const entry of entries) { - const fullPath = path.join(inputDir, entry) - - if (!fs.lstatSync(fullPath).isDirectory()) { - continue + if (!fs.existsSync(resolved)) { + throw new Error(`SVG file does not exist: ${resolved}`) } - const svgFiles = fs - .readdirSync(fullPath) - .filter((file) => file.endsWith('.svg')) - .sort() - .map((file) => path.join(fullPath, file)) - - if (svgFiles.length === 0) { - continue + if (!resolved.endsWith('.svg')) { + throw new Error(`File is not an SVG: ${resolved}`) } - const { name, mode } = parseFolderName(entry) + return resolved + }) +} - folders.push({ - fullName: entry, - name, +/** + * Преобразует SpriteEntry из конфига в SpriteFolder для компиляции. + */ +export function resolveSpriteEntry(entry: SpriteEntry): SpriteFolder { + const mode = entry.mode ?? 'stack' + + if (Array.isArray(entry.input)) { + const files = resolveFiles(entry.input) + + if (files.length === 0) { + throw new Error(`Sprite "${entry.name}" has empty input array.`) + } + + return { + name: entry.name, mode, - path: fullPath, - files: svgFiles, - }) + path: null, + files, + } } - return folders + const dirPath = path.resolve(entry.input) + const files = scanDirectory(dirPath) + + if (files.length === 0) { + throw new Error(`Sprite "${entry.name}" has no SVG files in "${dirPath}".`) + } + + return { + name: entry.name, + mode, + path: dirPath, + files, + } +} + +/** + * Преобразует массив SpriteEntry из конфига в массив SpriteFolder. + */ +export function resolveSprites(entries: SpriteEntry[]): SpriteFolder[] { + return entries.map(resolveSpriteEntry) } diff --git a/src/transforms.ts b/src/transforms.ts index 49e0ca6..045c2da 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -6,7 +6,7 @@ * - Добавление transition к элементам с цветом */ -import type { TransformOptions } from './types.js' + /** Элементы, которые могут содержать цвет (fill/stroke). */ const COLORABLE_TAGS = [ @@ -173,13 +173,15 @@ function addTransitions(svg: string): string { }) } +import type { TransformOptions } from './types.js' + /** * Shape transform для svg-sprite. * * Применяет трансформации в зависимости от опций: - * - removeSize: удаление width/height - * - replaceColors: замена цветов на CSS-переменные - * - addTransition: добавление transition к элементам с цветом + * - removeSize: удаление width/height (по умолчанию: true) + * - replaceColors: замена цветов на CSS-переменные (по умолчанию: true) + * - addTransition: добавление transition к элементам с цветом (по умолчанию: true) */ export function createShapeTransform(options: TransformOptions = {}): ( shape: { getSVG: (inline: boolean) => string; setSVG: (svg: string) => void }, diff --git a/src/types.ts b/src/types.ts index b570360..629d49f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,36 +1,25 @@ /** Режим спрайта: stack или symbol. */ export type SpriteMode = 'stack' | 'symbol' -/** Результат парсинга имени папки со спрайтами. */ -export interface SpriteFolder { - /** Полное имя папки (как на диске, включая суффикс ?mode). */ - fullName: string - /** Имя папки без суффикса режима. */ +/** Описание одного спрайта в конфиге. */ +export type SpriteEntry = { + /** Уникальное имя спрайта (используется как имя файла и в типах). */ name: string - /** Режим спрайта. */ - mode: SpriteMode - /** Абсолютный путь к папке. */ - path: string - /** Абсолютные пути к SVG-файлам внутри папки. */ - files: string[] + /** + * Источник SVG-файлов. + * Строка — путь к папке с SVG-файлами. + * Массив — пути к конкретным SVG-файлам. + */ + input: string | string[] + /** + * Режим спрайта. + * По умолчанию: 'stack'. + */ + mode?: SpriteMode } -/** Результат компиляции одного спрайта. */ -export interface SpriteResult { - /** Имя папки (без суффикса режима). */ - name: string - /** Режим спрайта. */ - mode: SpriteMode - /** Путь к сгенерированному SVG-спрайту. */ - spritePath: string - /** Путь к сгенерированному .generated.ts файлу (если включена генерация типов). */ - typesPath: string | null - /** Количество иконок в спрайте. */ - iconCount: number -} - -/** Параметры трансформации SVG. */ -export interface TransformOptions { +/** Параметры трансформации SVG. Все включены по умолчанию. */ +export type TransformOptions = { /** * Удалять width/height с корневого . * По умолчанию: true. @@ -50,30 +39,55 @@ export interface TransformOptions { addTransition?: boolean } -/** Параметры генерации спрайтов. */ -export interface GenerateOptions { - /** Путь к папке с исходными SVG (содержит подпапки-спрайты). */ - input: string +/** Конфигурация генерации SVG-спрайтов. */ +export type SvgSpritesConfig = { /** Путь к папке для сгенерированных SVG-спрайтов. */ output: string /** - * Генерировать ли .generated.ts файлы с union-типами имён иконок. - * По умолчанию: true. + * Публичный путь к спрайтам для использования в коде (href, src, url()). + * Используется в сгенерированном React-компоненте. + * Пример: '/img/sprites'. */ - types?: boolean - /** - * Куда складывать .generated.ts файлы. - * По умолчанию: рядом с исходными папками (в input). - */ - typesOutput?: string - /** - * Настройки трансформации SVG. - * По умолчанию: все трансформации включены. - */ - transform?: TransformOptions + publicPath?: string /** * Генерировать HTML-превью со всеми иконками. * По умолчанию: true. */ preview?: boolean + /** + * Путь для генерации React-компонента. + * Если не задан — компонент и типы не генерируются. + */ + react?: string + /** + * Настройки трансформации SVG. + * По умолчанию: все трансформации включены. + */ + transform?: TransformOptions + /** Список спрайтов для генерации. */ + sprites: SpriteEntry[] +} + +/** Результат парсинга спрайта — данные для компиляции. */ +export type SpriteFolder = { + /** Имя спрайта. */ + name: string + /** Режим спрайта. */ + mode: SpriteMode + /** Абсолютный путь к папке (для input-папки) или null (для input-массива). */ + path: string | null + /** Абсолютные пути к SVG-файлам. */ + files: string[] +} + +/** Результат компиляции одного спрайта. */ +export type SpriteResult = { + /** Имя спрайта. */ + name: string + /** Режим спрайта. */ + mode: SpriteMode + /** Путь к сгенерированному SVG-спрайту. */ + spritePath: string + /** Количество иконок в спрайте. */ + iconCount: number } diff --git a/svg-sprites.config.ts b/svg-sprites.config.ts new file mode 100644 index 0000000..1d45c01 --- /dev/null +++ b/svg-sprites.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from './src/index.js' + +export default defineConfig({ + output: 'preview/public', + publicPath: '', + preview: true, + react: 'test/ui/svg-sprite', + + sprites: [ + { + name: 'icons', + input: 'test/assets/icons', + mode: 'stack', + }, + { + name: 'logos', + input: 'test/assets/logos', + mode: 'stack', + }, + ], +}) diff --git a/sprites/icons/arrow-left.svg b/test/assets/icons/arrow-left.svg similarity index 100% rename from sprites/icons/arrow-left.svg rename to test/assets/icons/arrow-left.svg diff --git a/sprites/icons/arrow-right.svg b/test/assets/icons/arrow-right.svg similarity index 100% rename from sprites/icons/arrow-right.svg rename to test/assets/icons/arrow-right.svg diff --git a/sprites/icons/check.svg b/test/assets/icons/check.svg similarity index 100% rename from sprites/icons/check.svg rename to test/assets/icons/check.svg diff --git a/sprites/icons/discount-shape.svg b/test/assets/icons/discount-shape.svg similarity index 100% rename from sprites/icons/discount-shape.svg rename to test/assets/icons/discount-shape.svg diff --git a/sprites/icons/flash.svg b/test/assets/icons/flash.svg similarity index 100% rename from sprites/icons/flash.svg rename to test/assets/icons/flash.svg diff --git a/sprites/icons/heart-tick.svg b/test/assets/icons/heart-tick (1-я копия).svg similarity index 100% rename from sprites/icons/heart-tick.svg rename to test/assets/icons/heart-tick (1-я копия).svg diff --git a/test/assets/icons/heart-tick.svg b/test/assets/icons/heart-tick.svg new file mode 100644 index 0000000..01a8c3d --- /dev/null +++ b/test/assets/icons/heart-tick.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/sprites/logos/github.svg b/test/assets/logos/github.svg similarity index 100% rename from sprites/logos/github.svg rename to test/assets/logos/github.svg diff --git a/sprites/logos/twitter.svg b/test/assets/logos/twitter.svg similarity index 100% rename from sprites/logos/twitter.svg rename to test/assets/logos/twitter.svg diff --git a/sprites/logos/youtube.svg b/test/assets/logos/youtube.svg similarity index 100% rename from sprites/logos/youtube.svg rename to test/assets/logos/youtube.svg diff --git a/test/ui/svg-sprite/svg-sprite.module.css b/test/ui/svg-sprite/svg-sprite.module.css new file mode 100644 index 0000000..4463761 --- /dev/null +++ b/test/ui/svg-sprite/svg-sprite.module.css @@ -0,0 +1,19 @@ +/* @generated — this file is auto-generated, do not edit manually. */ + +.root { + transition-property: fill, stroke, color; + transition-duration: 0.3s; + transition-timing-function: ease; +} + +.wrap { + display: inline-flex; +} + +.wrap svg { + width: 100%; + height: 100%; + transition-property: fill, stroke, color; + transition-duration: 0.3s; + transition-timing-function: ease; +} diff --git a/test/ui/svg-sprite/svg-sprite.tsx b/test/ui/svg-sprite/svg-sprite.tsx new file mode 100644 index 0000000..c4a8394 --- /dev/null +++ b/test/ui/svg-sprite/svg-sprite.tsx @@ -0,0 +1,96 @@ +/** + * SVG-спрайты: типы и React-компонент. + * @generated — this file is auto-generated, do not edit manually. + */ +import type { SVGAttributes, HTMLAttributes } from 'react' +import styles from './svg-sprite.module.css' + +/** Имена иконок спрайта «icons». */ +export type IconsIconName = + | 'arrow-left' + | 'arrow-right' + | 'check' + | 'discount-shape' + | 'flash' + | 'heart-tick' + | 'heart-tick (1-я копия)' + +/** Имена иконок спрайта «logos». */ +export type LogosIconName = + | 'github' + | 'twitter' + | 'youtube' + +/** Маппинг имени спрайта на тип его иконок. */ +export type SpriteMap = { + icons: IconsIconName + logos: LogosIconName +} + +/** Имя спрайта. */ +export type SpriteName = keyof SpriteMap + +/** Спрайт по умолчанию. */ +export type DefaultSprite = 'icons' + +/** Имя иконки для конкретного спрайта. */ +export type IconName = SpriteMap[S] + +const PUBLIC_PATH = '' +const DEFAULT_SPRITE: SpriteName = 'icons' + +const SPRITE_FILES: Record = { + icons: 'icons.sprite.svg', + logos: 'logos.sprite.svg', +} + +type IconBaseProps = { + /** Имя иконки. */ + icon: IconName + /** Имя спрайта. По умолчанию: первый из конфига. */ + sprite?: S +} + +type IconSvgProps = IconBaseProps & { + wrapped?: false +} & SVGAttributes + +type IconWrappedProps = IconBaseProps & { + wrapped: true +} & HTMLAttributes + +export type SvgSpriteProps = + | IconSvgProps + | IconWrappedProps + +/** + * Иконка из SVG-спрайта. + * + * Используется для: + * - отображения иконки через `` + * - обёртки в `` через проп `wrapped` + * + * Спрайт по умолчанию: «icons». + */ +export const SvgSprite = (props: SvgSpriteProps) => { + const { icon, sprite = DEFAULT_SPRITE as S, wrapped, className, ...rest } = props + const href = `${PUBLIC_PATH}/${SPRITE_FILES[sprite]}#${icon}` + + if (wrapped) { + const { ...htmlAttr } = rest as HTMLAttributes + return ( + + + + + + ) + } + + const { ...svgAttr } = rest as SVGAttributes + return ( + + + + ) +}