refactor: заменить shiki на самописный highlighter и обновить архитектуру
- Удалён shiki (9.5→0 МБ), создан regex-токенизатор для html/css/xml - CLI переведён с аргументов на конфиг-файл svg-sprites.config.ts - Превью переработано: React-приложение вместо инлайн HTML - Добавлен футер с названием пакета и ссылкой на репозиторий - Исправлена загрузка dev-data.js для Vite 8 - Футер прижат к низу, содержимое центрировано
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
public/
|
public/
|
||||||
|
test/public/
|
||||||
.tmp/
|
.tmp/
|
||||||
*.generated.ts
|
*.generated.ts
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|||||||
206
README.md
Normal file
206
README.md
Normal file
@@ -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'
|
||||||
|
|
||||||
|
// Иконка из первого спрайта (по умолчанию)
|
||||||
|
<SvgSprite icon="check" />
|
||||||
|
|
||||||
|
// Иконка из другого спрайта
|
||||||
|
<SvgSprite icon="github" sprite="logos" />
|
||||||
|
|
||||||
|
// Обёртка в <span> (удобно для inline-элементов)
|
||||||
|
<SvgSprite icon="arrow-left" wrapped />
|
||||||
|
```
|
||||||
|
|
||||||
|
Компонент полностью типизирован — автодополнение работает для имён иконок и спрайтов.
|
||||||
|
|
||||||
|
## Управление цветом
|
||||||
|
|
||||||
|
При сборке цвета иконок заменяются на 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'
|
||||||
|
// → <use href="/img/sprites/icons.sprite.svg#check" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### `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 с <svg>
|
||||||
|
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 `<use>` | CSS-переменные, `color` | `<SvgSprite icon="check" />` |
|
||||||
|
| CSS `mask-image` | `background-color` (монохром) | `.icon { mask: url(...); background-color: red; }` |
|
||||||
|
| `<img>` | нет | `<img src="icons.sprite.svg#check">` |
|
||||||
|
|
||||||
|
## Программный 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
|
||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -9,8 +9,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"citty": "^0.1.6",
|
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
"svg-sprite": "^2.0.4"
|
"svg-sprite": "^2.0.4"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1302,15 +1302,6 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"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": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -1438,6 +1429,7 @@
|
|||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||||
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.10.0"
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
@@ -1796,6 +1788,15 @@
|
|||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/joycon": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||||
|
|||||||
@@ -16,10 +16,11 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup && npm run build:preview",
|
||||||
|
"build:preview": "cd preview && npx vite build",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"sprite": "node dist/cli.js --input sprites --output public",
|
"sprite": "node dist/cli.js",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -35,8 +36,8 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"citty": "^0.1.6",
|
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
"svg-sprite": "^2.0.4"
|
"svg-sprite": "^2.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
25
preview/.gitignore
vendored
Normal file
25
preview/.gitignore
vendored
Normal file
@@ -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?
|
||||||
1
preview/.templates/business/{{name.kebabCase}}/index.ts
Normal file
1
preview/.templates/business/{{name.kebabCase}}/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { {{name.pascalCase}}Business } from './{{name.kebabCase}}.business'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры бизнес-модуля {{name.pascalCase}}.
|
||||||
|
*/
|
||||||
|
export type {{name.pascalCase}}BusinessParams = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}BusinessProps = RootAttrs & {{name.pascalCase}}BusinessParams
|
||||||
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { {{name.pascalCase}}Infra } from './{{name.kebabCase}}.infra'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры инфраструктурного модуля {{name.pascalCase}}.
|
||||||
|
*/
|
||||||
|
export type {{name.pascalCase}}InfraParams = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}InfraProps = RootAttrs & {{name.pascalCase}}InfraParams
|
||||||
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
preview/.templates/layout/{{name.kebabCase}}/index.ts
Normal file
1
preview/.templates/layout/{{name.kebabCase}}/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { {{name.pascalCase}}Layout } from './{{name.kebabCase}}.layout'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры {{name.pascalCase}}Layout.
|
||||||
|
*/
|
||||||
|
export type {{name.pascalCase}}LayoutParams = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}LayoutProps = RootAttrs & {{name.pascalCase}}LayoutParams
|
||||||
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
preview/.templates/module/{{name.kebabCase}}/index.ts
Normal file
1
preview/.templates/module/{{name.kebabCase}}/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { {{name.pascalCase}} } from './{{name.kebabCase}}'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры {{name.pascalCase}}.
|
||||||
|
*/
|
||||||
|
export type {{name.pascalCase}}Params = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params
|
||||||
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
preview/.templates/screen/{{name.kebabCase}}/index.ts
Normal file
1
preview/.templates/screen/{{name.kebabCase}}/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { {{name.pascalCase}}Screen } from './{{name.kebabCase}}.screen'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры экрана {{name.pascalCase}}.
|
||||||
|
*/
|
||||||
|
export type {{name.pascalCase}}ScreenParams = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}ScreenProps = RootAttrs & {{name.pascalCase}}ScreenParams
|
||||||
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
2
preview/.templates/store/{{name.kebabCase}}/index.ts
Normal file
2
preview/.templates/store/{{name.kebabCase}}/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { use{{name.pascalCase}}Store } from './{{name.kebabCase}}.store'
|
||||||
|
export type { {{name.pascalCase}}State } from './{{name.kebabCase}}.type'
|
||||||
@@ -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>()(() => ({
|
||||||
|
|
||||||
|
}))
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Состояние {{name.pascalCase}}.
|
||||||
|
*/
|
||||||
|
export interface {{name.pascalCase}}State {
|
||||||
|
|
||||||
|
}
|
||||||
1
preview/.templates/ui/{{name.kebabCase}}/index.ts
Normal file
1
preview/.templates/ui/{{name.kebabCase}}/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { {{name.pascalCase}} } from './{{name.kebabCase}}.ui'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры {{name.pascalCase}}.
|
||||||
|
*/
|
||||||
|
export type {{name.pascalCase}}Params = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params
|
||||||
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
preview/.templates/widget/{{name.kebabCase}}/index.ts
Normal file
1
preview/.templates/widget/{{name.kebabCase}}/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { {{name.pascalCase}}Widget } from './{{name.kebabCase}}.widget'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры виджета {{name.pascalCase}}.
|
||||||
|
*/
|
||||||
|
export type {{name.pascalCase}}WidgetParams = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}WidgetProps = RootAttrs & {{name.pascalCase}}WidgetParams
|
||||||
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
preview/AGENTS.md
Normal file
43
preview/AGENTS.md
Normal file
@@ -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/` целиком или строить по ней собственную карту.
|
||||||
|
- Смешивать правила из разных ролей в одном ответе.
|
||||||
|
- Додумывать правила, которых нет в твоём файле роли.
|
||||||
96
preview/ai/DEVELOP.md
Normal file
96
preview/ai/DEVELOP.md
Normal file
@@ -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 | Расширения, настройки редактора, сниппеты |
|
||||||
5
preview/ai/applied/api.md
Normal file
5
preview/ai/applied/api.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
scope: applied
|
||||||
|
keywords: [api, запрос, fetch, SWR, эндпоинт, REST, клиент]
|
||||||
|
when: "Работа с API: запросы, клиенты, обработка ответов"
|
||||||
|
---
|
||||||
118
preview/ai/applied/components.md
Normal file
118
preview/ai/applied/components.md
Normal file
@@ -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<HTMLDivElement>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`container/index.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export { Container } from './container.ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Дочерние компоненты
|
||||||
|
|
||||||
|
Если модулю нужны внутренние подкомпоненты — генерировать их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
||||||
5
preview/ai/applied/fonts.md
Normal file
5
preview/ai/applied/fonts.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
scope: applied
|
||||||
|
keywords: [шрифт, font, next/font, подключение шрифта, woff]
|
||||||
|
when: "Подключение и настройка шрифтов"
|
||||||
|
---
|
||||||
5
preview/ai/applied/hooks.md
Normal file
5
preview/ai/applied/hooks.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
scope: applied
|
||||||
|
keywords: [хук, hook, use, кастомный хук, useState, useEffect]
|
||||||
|
when: "Создание или использование кастомных хуков"
|
||||||
|
---
|
||||||
5
preview/ai/applied/images-sprites.md
Normal file
5
preview/ai/applied/images-sprites.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
scope: applied
|
||||||
|
keywords: [изображение, картинка, image, next/image, public, оптимизация]
|
||||||
|
when: "Работа с изображениями: подключение, оптимизация"
|
||||||
|
---
|
||||||
5
preview/ai/applied/localization.md
Normal file
5
preview/ai/applied/localization.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
scope: applied
|
||||||
|
keywords: [i18n, локализация, перевод, язык, i18next, namespace]
|
||||||
|
when: "Локализация: добавление переводов, работа с i18next"
|
||||||
|
---
|
||||||
68
preview/ai/applied/page-level.md
Normal file
68
preview/ai/applied/page-level.md
Normal file
@@ -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 <ProfileScreen id={id} />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`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 <ErrorScreen error={error} reset={reset} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorPage
|
||||||
|
```
|
||||||
101
preview/ai/applied/project-structure.md
Normal file
101
preview/ai/applied/project-structure.md
Normal file
@@ -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_` доступны в клиентском коде. Остальные доступны только на сервере.
|
||||||
5
preview/ai/applied/stores.md
Normal file
5
preview/ai/applied/stores.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
scope: applied
|
||||||
|
keywords: [стор, store, zustand, состояние, глобальное состояние]
|
||||||
|
when: "Работа с глобальным состоянием: создание стора, подписка"
|
||||||
|
---
|
||||||
285
preview/ai/applied/styles.md
Normal file
285
preview/ai/applied/styles.md
Normal file
@@ -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/` запрещены.
|
||||||
7
preview/ai/applied/svg-sprites.md
Normal file
7
preview/ai/applied/svg-sprites.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: SVG-спрайты
|
||||||
|
scope: applied
|
||||||
|
keywords: [svg, спрайт, иконка, icon, sprite]
|
||||||
|
when: "Работа с SVG-иконками и спрайтами"
|
||||||
|
---
|
||||||
|
# SVG-спрайты
|
||||||
174
preview/ai/applied/templates-generation.md
Normal file
174
preview/ai/applied/templates-generation.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
---
|
||||||
|
title: Шаблоны и генерация кода
|
||||||
|
scope: applied
|
||||||
|
keywords: [шаблон, генерация, template, scaffold, plop, hygen, .templates]
|
||||||
|
when: "Генерация кода из шаблонов, создание новых шаблонов"
|
||||||
|
---
|
||||||
|
<!-- @formatter:off -->
|
||||||
|
::: 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<HTMLDivElement>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```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` |
|
||||||
|
|
||||||
|
## Когда создавать новый шаблон
|
||||||
|
|
||||||
|
- Повторяющаяся структура появляется больше одного раза.
|
||||||
|
- Существующий шаблон не покрывает нужный тип модуля.
|
||||||
|
|
||||||
5
preview/ai/applied/video.md
Normal file
5
preview/ai/applied/video.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
scope: applied
|
||||||
|
keywords: [видео, video, плеер, mp4]
|
||||||
|
when: "Встраивание и работа с видео"
|
||||||
|
---
|
||||||
89
preview/ai/applied/vscode.md
Normal file
89
preview/ai/applied/vscode.md
Normal file
@@ -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` с общими для команды настройками.
|
||||||
665
preview/ai/basics/architecture.md
Normal file
665
preview/ai/basics/architecture.md
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
---
|
||||||
|
title: Архитектура
|
||||||
|
scope: basics
|
||||||
|
keywords: [SLM Design, слой, модуль, сегмент, архитектура, FSD, scoped layered module]
|
||||||
|
when: "Организация кода: слои, модули, зависимости между модулями"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- /index -->
|
||||||
|
# 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.
|
||||||
|
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||||
|
|
||||||
|
<!-- /reference/layers -->
|
||||||
|
## Слои
|
||||||
|
|
||||||
|
Раздел описывает слои 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-состояния
|
||||||
|
|
||||||
|
<!-- /reference/modules -->
|
||||||
|
## Модули
|
||||||
|
|
||||||
|
Раздел описывает модули 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 (
|
||||||
|
<div>
|
||||||
|
<ChatBadge count={messages.length} />
|
||||||
|
{messages.map(m => <MessageBubble key={m.id} {...m} />)}
|
||||||
|
<MessageInput onSend={sendMessage} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Жизненный цикл
|
||||||
|
|
||||||
|
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
||||||
|
|
||||||
|
- Нужен на одной странице → `screens/{name}/parts/`
|
||||||
|
- Появился в 2+ местах → поднимается по природе:
|
||||||
|
- абстрактный UI → `ui/`
|
||||||
|
- блок с данными/логикой → `widgets/`
|
||||||
|
- представление бизнес-домена → `business/{area}/parts/`
|
||||||
|
|
||||||
|
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||||
|
|
||||||
|
<!-- /reference/segments -->
|
||||||
|
## Сегменты
|
||||||
|
|
||||||
|
Раздел описывает сегменты 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
|
||||||
|
```
|
||||||
154
preview/ai/basics/code-style.md
Normal file
154
preview/ai/basics/code-style.md
Normal file
@@ -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
|
||||||
|
<input type="text" placeholder="Введите имя" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Плохо**
|
||||||
|
```ts
|
||||||
|
// Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки.
|
||||||
|
const label = "Сохранить";
|
||||||
|
const title = 'Привет, ' + name;
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Плохо: одинарные кавычки в JSX-атрибутах.
|
||||||
|
<input type='text' placeholder='Введите имя' />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Точки с запятой и запятые
|
||||||
|
|
||||||
|
- Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным.
|
||||||
|
- В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна.
|
||||||
|
|
||||||
|
## Импорты
|
||||||
|
|
||||||
|
- В именованных импортах использовать пробелы внутри фигурных скобок.
|
||||||
|
- Типы импортировать через `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 } };
|
||||||
|
```
|
||||||
136
preview/ai/basics/documentation.md
Normal file
136
preview/ai/basics/documentation.md
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
```
|
||||||
149
preview/ai/basics/naming.md
Normal file
149
preview/ai/basics/naming.md
Normal file
@@ -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.<suffix>.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<string, User>;
|
||||||
|
const userIds = ['u1', 'u2'];
|
||||||
|
const ordersMap = new Map<string, Order>();
|
||||||
|
const featureFlagsDict = { beta: true, legacy: false } as Record<string, boolean>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Плохо**
|
||||||
|
```ts
|
||||||
|
// Плохо: имя не отражает, что это коллекция.
|
||||||
|
const user = [];
|
||||||
|
// Плохо: словарь назван как массив.
|
||||||
|
const usersMap = [];
|
||||||
|
// Плохо: по имени непонятно, что это словарь.
|
||||||
|
const users = {} as Record<string, User>;
|
||||||
|
```
|
||||||
43
preview/ai/basics/tech-stack.md
Normal file
43
preview/ai/basics/tech-stack.md
Normal file
@@ -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` — шаблонизатор для создания слоёв и других файлов из шаблонов.
|
||||||
58
preview/ai/basics/typing.md
Normal file
58
preview/ai/basics/typing.md
Normal file
@@ -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;
|
||||||
|
```
|
||||||
35
preview/ai/triggers/develop/add-api-request.md
Normal file
35
preview/ai/triggers/develop/add-api-request.md
Normal file
@@ -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 — компонент, использующий данные
|
||||||
|
|
||||||
|
## Проверь себя
|
||||||
|
|
||||||
|
- [ ] Типы запроса и ответа описаны
|
||||||
|
- [ ] Хук для использования в компоненте создан
|
||||||
|
- [ ] Обработка ошибок реализована
|
||||||
24
preview/ai/triggers/develop/add-dependency.md
Normal file
24
preview/ai/triggers/develop/add-dependency.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: Добавить зависимость
|
||||||
|
---
|
||||||
|
|
||||||
|
# Добавить зависимость
|
||||||
|
|
||||||
|
Инструкция по добавлению новой npm-зависимости в проект. Проверь допустимость перед установкой.
|
||||||
|
|
||||||
|
## Прочитай перед началом
|
||||||
|
|
||||||
|
- basics/tech-stack.md — разрешённый стек, допустимые библиотеки
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
1. Проверь, что библиотека не дублирует уже используемую (→ basics/tech-stack.md).
|
||||||
|
|
||||||
|
2. Проверь, что библиотека входит в разрешённый список или обоснуй необходимость.
|
||||||
|
|
||||||
|
3. Установи как `dependency` или `devDependency` в зависимости от назначения.
|
||||||
|
|
||||||
|
## Проверь себя
|
||||||
|
|
||||||
|
- [ ] Библиотека не дублирует уже используемую
|
||||||
|
- [ ] Библиотека входит в разрешённый список (→ basics/tech-stack.md)
|
||||||
28
preview/ai/triggers/develop/add-font.md
Normal file
28
preview/ai/triggers/develop/add-font.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Подключить шрифт
|
||||||
|
---
|
||||||
|
|
||||||
|
# Подключить шрифт
|
||||||
|
|
||||||
|
Инструкция по подключению и настройке шрифта в проекте.
|
||||||
|
|
||||||
|
## Прочитай перед началом
|
||||||
|
|
||||||
|
- applied/fonts.md — правила подключения шрифтов: форматы, загрузка, CSS-переменные
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
1. Подготовь файлы шрифта (woff2).
|
||||||
|
|
||||||
|
2. Подключи шрифт по правилам (→ applied/fonts.md).
|
||||||
|
|
||||||
|
3. Зарегистрируй CSS-переменную для шрифта.
|
||||||
|
|
||||||
|
## Смежные триггеры
|
||||||
|
|
||||||
|
- triggers/develop/style-component.md — использование шрифта в стилях
|
||||||
|
|
||||||
|
## Проверь себя
|
||||||
|
|
||||||
|
- [ ] Файл шрифта в формате woff2
|
||||||
|
- [ ] CSS-переменная для шрифта зарегистрирована
|
||||||
29
preview/ai/triggers/develop/add-icon.md
Normal file
29
preview/ai/triggers/develop/add-icon.md
Normal file
@@ -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)
|
||||||
30
preview/ai/triggers/develop/add-image.md
Normal file
30
preview/ai/triggers/develop/add-image.md
Normal file
@@ -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)
|
||||||
28
preview/ai/triggers/develop/add-localization.md
Normal file
28
preview/ai/triggers/develop/add-localization.md
Normal file
@@ -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)
|
||||||
33
preview/ai/triggers/develop/add-server-data.md
Normal file
33
preview/ai/triggers/develop/add-server-data.md
Normal file
@@ -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
|
||||||
|
- [ ] Типы запроса и ответа описаны
|
||||||
|
- [ ] Данные передаются через пропсы, не через глобальное состояние
|
||||||
27
preview/ai/triggers/develop/add-video.md
Normal file
27
preview/ai/triggers/develop/add-video.md
Normal file
@@ -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)
|
||||||
31
preview/ai/triggers/develop/connect-store.md
Normal file
31
preview/ai/triggers/develop/connect-store.md
Normal file
@@ -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 — хук-обёртка над стором
|
||||||
|
|
||||||
|
## Проверь себя
|
||||||
|
|
||||||
|
- [ ] Используется селектор, а не подписка на весь стор
|
||||||
|
- [ ] Выбор локальное/глобальное состояние обоснован
|
||||||
38
preview/ai/triggers/develop/create-component.md
Normal file
38
preview/ai/triggers/develop/create-component.md
Normal file
@@ -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)
|
||||||
34
preview/ai/triggers/develop/create-entity.md
Normal file
34
preview/ai/triggers/develop/create-entity.md
Normal file
@@ -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`
|
||||||
37
preview/ai/triggers/develop/create-feature.md
Normal file
37
preview/ai/triggers/develop/create-feature.md
Normal file
@@ -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)
|
||||||
36
preview/ai/triggers/develop/create-hook.md
Normal file
36
preview/ai/triggers/develop/create-hook.md
Normal file
@@ -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 модуля
|
||||||
34
preview/ai/triggers/develop/create-layout.md
Normal file
34
preview/ai/triggers/develop/create-layout.md
Normal file
@@ -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-компонента
|
||||||
36
preview/ai/triggers/develop/create-page.md
Normal file
36
preview/ai/triggers/develop/create-page.md
Normal file
@@ -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
|
||||||
52
preview/ai/triggers/develop/create-project.md
Normal file
52
preview/ai/triggers/develop/create-project.md
Normal file
@@ -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)
|
||||||
36
preview/ai/triggers/develop/create-store.md
Normal file
36
preview/ai/triggers/develop/create-store.md
Normal file
@@ -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 модуля
|
||||||
32
preview/ai/triggers/develop/create-widget.md
Normal file
32
preview/ai/triggers/develop/create-widget.md
Normal file
@@ -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`
|
||||||
36
preview/ai/triggers/develop/generate-module.md
Normal file
36
preview/ai/triggers/develop/generate-module.md
Normal file
@@ -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)
|
||||||
24
preview/ai/triggers/develop/setup-vscode.md
Normal file
24
preview/ai/triggers/develop/setup-vscode.md
Normal file
@@ -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` проверены
|
||||||
35
preview/ai/triggers/develop/style-component.md
Normal file
35
preview/ai/triggers/develop/style-component.md
Normal file
@@ -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`
|
||||||
23
preview/eslint.config.js
Normal file
23
preview/eslint.config.js
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
preview/index.html
Normal file
13
preview/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>SVG Sprites Preview</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- __SPRITES_INJECT__ -->
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3103
preview/package-lock.json
generated
Normal file
3103
preview/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
preview/package.json
Normal file
34
preview/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
163
preview/scripts/generate-dev-data.js
Normal file
163
preview/scripts/generate-dev-data.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Генерирует dev-данные для preview из реальных спрайтов основного пакета.
|
||||||
|
*
|
||||||
|
* Запуск: node scripts/generate-dev-data.js
|
||||||
|
*
|
||||||
|
* Результат:
|
||||||
|
* public/dev-data.js — window.__SPRITES_DATA__ с метаданными иконок
|
||||||
|
* public/dev-sprites.svg — инлайновые <symbol> из всех спрайтов
|
||||||
|
*/
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Подготавливает спрайт для инлайна — вложенные <svg> → <symbol>. */
|
||||||
|
function prepareInlineSprite(spritePath) {
|
||||||
|
let content = fs.readFileSync(spritePath, 'utf-8')
|
||||||
|
content = content.replace(/<\?xml[^?]*\?>\s*/g, '')
|
||||||
|
content = content.replace(/<style>:root>svg\{display:none\}:root>svg:target\{display:block\}<\/style>/g, '')
|
||||||
|
|
||||||
|
let depth = 0
|
||||||
|
content = content.replace(/<(\/?)svg\b([^>]*?)(\s*\/?)>/g, (_full, slash, attrs) => {
|
||||||
|
if (slash) {
|
||||||
|
depth--
|
||||||
|
return depth > 0 ? '</symbol>' : '</svg>'
|
||||||
|
}
|
||||||
|
depth++
|
||||||
|
if (depth > 1) {
|
||||||
|
const cleanAttrs = attrs.replace(/\s*xmlns="[^"]*"/g, '')
|
||||||
|
return `<symbol${cleanAttrs}>`
|
||||||
|
}
|
||||||
|
return `<svg${attrs} style="display:none">`
|
||||||
|
})
|
||||||
|
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("<div>" + svg + "</div>", "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`)
|
||||||
108
preview/src/App.tsx
Normal file
108
preview/src/App.tsx
Normal file
@@ -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<string | null>(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 && (
|
||||||
|
<Banner variant="warn">
|
||||||
|
<strong>file://</strong> — Preview opened from local file. External SVG
|
||||||
|
references won't work in code snippets. Use a local server for full
|
||||||
|
functionality.
|
||||||
|
</Banner>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<header className={styles.header}>
|
||||||
|
<h1 className={styles.title}>SVG Sprites</h1>
|
||||||
|
<span className={styles.count}>{totalIcons} icons</span>
|
||||||
|
<div className={styles.toolbar}>
|
||||||
|
<SearchInput
|
||||||
|
placeholder="Search icons..."
|
||||||
|
onValueChange={setSearchQuery}
|
||||||
|
/>
|
||||||
|
<button type="button" className={styles.themeButton} onClick={toggle}>
|
||||||
|
◑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<IconGrid
|
||||||
|
groups={data.groups}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onIconSelect={setSelectedIconId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconModal
|
||||||
|
icon={selectedIcon}
|
||||||
|
defaultSprite={data.groups[0]?.name}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<span className={styles.footerText}>@gromlab/svg-sprites</span>
|
||||||
|
<a
|
||||||
|
className={styles.footerLink}
|
||||||
|
href="https://gromlab.ru/gromov/svg-sprites"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Repository
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
preview/src/app/styles/app.module.css
Normal file
64
preview/src/app/styles/app.module.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
26
preview/src/app/styles/globals.css
Normal file
26
preview/src/app/styles/globals.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
60
preview/src/app/styles/variables.css
Normal file
60
preview/src/app/styles/variables.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
40
preview/src/infrastructure/theme/hooks/use-theme.hook.ts
Normal file
40
preview/src/infrastructure/theme/hooks/use-theme.hook.ts
Normal file
@@ -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<Theme>(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 }
|
||||||
|
}
|
||||||
2
preview/src/infrastructure/theme/index.ts
Normal file
2
preview/src/infrastructure/theme/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { useTheme } from './hooks/use-theme.hook'
|
||||||
|
export type { Theme } from './types/theme.type'
|
||||||
2
preview/src/infrastructure/theme/styles/theme.module.css
Normal file
2
preview/src/infrastructure/theme/styles/theme.module.css
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
4
preview/src/infrastructure/theme/theme.infra.tsx
Normal file
4
preview/src/infrastructure/theme/theme.infra.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Theme infrastructure module — реэкспортирует хук.
|
||||||
|
* Компонент не требуется, тема управляется через data-theme на documentElement.
|
||||||
|
*/
|
||||||
11
preview/src/infrastructure/theme/types/theme.type.ts
Normal file
11
preview/src/infrastructure/theme/types/theme.type.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Тема приложения.
|
||||||
|
*/
|
||||||
|
export type Theme = 'light' | 'dark'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры Theme.
|
||||||
|
*/
|
||||||
|
export type ThemeParams = {}
|
||||||
|
|
||||||
|
export type ThemeProps = ThemeParams
|
||||||
28
preview/src/main.tsx
Normal file
28
preview/src/main.tsx
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
if (import.meta.env.DEV && !window.__SPRITES_DATA__) {
|
||||||
|
await loadDevScript()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDevData().then(() => {
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
})
|
||||||
16
preview/src/shared/lib/rgb-to-hex.util.ts
Normal file
16
preview/src/shared/lib/rgb-to-hex.util.ts
Normal file
@@ -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'
|
||||||
|
}
|
||||||
6
preview/src/shared/types/index.ts
Normal file
6
preview/src/shared/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type {
|
||||||
|
IconVar,
|
||||||
|
IconData,
|
||||||
|
SpriteGroup,
|
||||||
|
SpritesData,
|
||||||
|
} from './sprites-data.type'
|
||||||
63
preview/src/shared/types/sprites-data.type.ts
Normal file
63
preview/src/shared/types/sprites-data.type.ts
Normal file
@@ -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[]
|
||||||
|
}
|
||||||
20
preview/src/ui/banner/banner.ui.tsx
Normal file
20
preview/src/ui/banner/banner.ui.tsx
Normal file
@@ -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 (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, variant && styles[`_${variant}`], className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
preview/src/ui/banner/index.ts
Normal file
1
preview/src/ui/banner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Banner } from './banner.ui'
|
||||||
29
preview/src/ui/banner/styles/banner.module.css
Normal file
29
preview/src/ui/banner/styles/banner.module.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user