diff --git a/.claude/CLAUDE-v1.6.md b/.claude/CLAUDE-v1.6.md new file mode 100644 index 0000000..2cf230a --- /dev/null +++ b/.claude/CLAUDE-v1.6.md @@ -0,0 +1,751 @@ +# CLAUDE.md — Angular UI Kit (@cdek-it/angular-ui-kit) + +Инструкции для Claude Code по генерации компонентов, обёрток и сторисов. + +--- + +## Стек и версии + +| Технология | Версия | +|----------------|---------| +| Angular | 20 | +| PrimeNG | 20 | +| Storybook | 10 | +| Tailwind CSS | 3 | +| TypeScript | 5 | + +--- + +## Структура проекта + +``` +src/ +├── lib/ +│ └── components/ +│ └── {name}/ +│ └── {name}.component.ts ← компонент-обёртка +├── stories/ +│ └── components/ +│ └── {name}/ +│ ├── {name}.stories.ts ← сторисы +│ └── examples/ ← примеры для сторисов +├── prime-preset/ +│ └── tokens/ +│ └── components/ +│ └── {name}.ts ← CSS-токены компонента +└── styles.scss ← Tailwind + иконки + шрифты +``` + +--- + +## Паттерн компонента-обёртки + +Каждый компонент — standalone Angular-компонент, оборачивающий PrimeNG. + +### Правила + +1. Файл: `src/lib/components/{name}/{name}.component.ts` +2. `selector` — с приставкой `extra-` + имя компонента строчными буквами (например `selector: 'extra-button'`) +3. Импортировать PrimeNG-компонент и указать его в `imports: []` +4. Для каждого типа Input создавать отдельный `type`-алиас +5. Все `@Input()` отражают **свой** API обёртки, не PrimeNG напрямую +6. Computed-геттеры маппят API обёртки → PrimeNG API +7. Шаблон компонента использует только геттеры, не сырые инпуты + +### Эталон — ButtonComponent + +```typescript +import { Component, Input } from '@angular/core'; +import { Button, ButtonSeverity as PrimeButtonSeverity } from 'primeng/button'; + +// Типы — отдельные алиасы, не inline union +export type ButtonVariant = 'primary' | 'secondary' | 'outlined' | 'text' | 'link'; +export type ButtonSeverity = 'success' | 'warning' | 'danger' | 'info' | null; +export type ButtonSize = 'small' | 'base' | 'large' | 'xlarge'; +export type ButtonIconPos = 'prefix' | 'postfix' | null; +export type BadgeSeverity = 'success' | 'info' | 'warning' | 'danger' | 'secondary' | 'contrast' | null; + +@Component({ + selector: 'extra-button', + standalone: true, + imports: [Button], + template: ` + + ` +}) +export class ButtonComponent { + @Input() label = 'Button'; + @Input() variant: ButtonVariant = 'primary'; + @Input() severity: ButtonSeverity = null; + @Input() size: ButtonSize = 'base'; + @Input() rounded = false; + @Input() iconPos: ButtonIconPos = null; + @Input() iconOnly = false; + @Input() icon = ''; + @Input() disabled = false; + @Input() loading = false; + @Input() badge = ''; + @Input() badgeSeverity: BadgeSeverity = null; + @Input() showBadge = false; + @Input() fluid = false; + @Input() ariaLabel: string | undefined = undefined; + @Input() autofocus = false; + @Input() tabindex: number | undefined = undefined; + @Input() text = false; + + // Геттеры — маппинг в PrimeNG API + get primeSize(): 'small' | 'large' | undefined { + if (this.size === 'small') return 'small'; + if (this.size === 'large') return 'large'; + return undefined; + } + + get primeIconPos(): 'left' | 'right' { + return this.iconPos === 'postfix' ? 'right' : 'left'; + } + + get primeSeverity(): PrimeButtonSeverity | null { + if (this.variant === 'secondary') return 'secondary'; + if (this.severity === 'warning') return 'warn'; + return this.severity; + } + + get primeBadgeSeverity() { + if (this.badgeSeverity === 'warning') return 'warn'; + return this.badgeSeverity; + } +} +``` + +--- + +## Паттерн сторисов + +### Файл: `src/stories/components/{name}/{name}.stories.ts` + +**Все тексты описаний — на русском языке.** + +### Правило: title сториса + +Формат: `Components/{Category}/{ComponentName}` + +Категории соответствуют группировке на [primeng.org](https://primeng.org/): + +| Категория | Компоненты | +|-----------|-----------------------------------------------------------------------------------------------| +| Button | Button, SpeedDial, SplitButton | +| Data | DataTable, DataView, OrderList, OrgChart, Paginator, PickList, Timeline, Tree, TreeTable | +| Form | AutoComplete, Checkbox, ColorPicker, DatePicker, InputMask, InputNumber, InputOtp, InputText, Knob, Listbox, MultiSelect, Password, RadioButton, Rating, Select, SelectButton, Slider, Textarea, ToggleButton, ToggleSwitch, TreeSelect | +| Menu | Breadcrumb, ContextMenu, Dock, Menu, Menubar, MegaMenu, PanelMenu, Steps, TabMenu, TieredMenu | +| Messages | Message, Toast | +| Misc | Avatar, Badge, BlockUI, Chip, Inplace, MeterGroup, ProgressBar, ProgressSpinner, ScrollTop, Skeleton, Tag | +| Overlay | ConfirmDialog, ConfirmPopup, Dialog, Drawer, Popover, Tooltip | +| Panel | Accordion, Card, Divider, Fieldset, Panel, ScrollPanel, Splitter, Stepper, Tabs | +| Media | Carousel, Galleria, Image, ImageCompare | + +```typescript +// ❌ Запрещено +title: 'Prime/Button' +title: 'Components/Button' + +// ✅ Правильно +title: 'Components/Button/Button' +title: 'Components/Panel/Card' +title: 'Components/Panel/Divider' +title: 'Components/Form/InputText' +``` + +### Полный шаблон сториса + +```typescript +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { XxxComponent } from '../../../lib/components/xxx/xxx.component'; + +// Расширяем тип для Events, которых нет в @Output() +type XxxArgs = XxxComponent & { onClick?: (event: MouseEvent) => void }; + +const meta: Meta = { + title: 'Components/{Category}/Xxx', + component: XxxComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ imports: [XxxComponent] }) + ], + parameters: { + docs: { + description: { + // 1. Одна строка — для чего компонент + // 2. Ссылка на Figma + // 3. Блок импорта + component: `Описание компонента. [Figma Design](https://www.figma.com/design/...). + +\`\`\`typescript +import { XxxModule } from 'primeng/xxx'; +\`\`\``, + }, + }, + }, + argTypes: { + // ── Props ──────────────────────────────────────────────── + propName: { + control: 'text' | 'boolean' | 'select' | 'number', + options: [...], // только для select + description: 'Описание на русском', + table: { + category: 'Props', + defaultValue: { summary: 'значение' }, + type: { summary: "'тип1' | 'тип2'" }, + }, + }, + // ── Badge ──────────────────────────────────────────────── + badge: { + // ... category: 'Badge' + }, + // ── Events ─────────────────────────────────────────────── + onClick: { + control: false, + description: 'Событие клика', + table: { + category: 'Events', + type: { summary: 'EventEmitter' }, + }, + }, + }, + args: { + // Дефолты для полей, которые нужно явно инициализировать + }, +}; + +// commonTemplate — для сторисов-вариаций (НЕ для Default) +const commonTemplate = ` + +`; + +export default meta; +type Story = StoryObj; + +// ── Default ────────────────────────────────────────────────────────────────── +// Динамический render: template генерируется из текущих args. +// Storybook Angular захватывает template как source code → +// при смене controls сниппет обновляется автоматически. + +export const Default: Story = { + name: 'Default', + render: (args) => { + const parts: string[] = []; + + if (args.label) parts.push(`label="${args.label}"`); + if (args.variant) parts.push(`variant="${args.variant}"`); + if (args.severity) parts.push(`severity="${args.severity}"`); + // ... остальные пропсы + if (args.rounded) parts.push(`[rounded]="true"`); + if (args.disabled) parts.push(`[disabled]="true"`); + + const template = parts.length + ? `` + : ``; + + return { props: args, template }; + }, + args: { label: 'Label' }, + parameters: { + docs: { + description: { + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.', + }, + }, + }, +}; + +// ── Сторисы-вариации ───────────────────────────────────────────────────────── +// Каждая сторис — ОДИН вариант компонента. +// Используют commonTemplate + props: args → controls работают. +// source.code — статичный минимальный пример. + +export const Sizes: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { label: 'Button', size: 'large' }, + parameters: { + docs: { + description: { story: 'Описание вариации.' }, + source: { + code: ``, + }, + }, + }, +}; +``` + +--- + +## Паттерн examples/ + +### Назначение + +Папка `src/stories/components/{name}/examples/` содержит **standalone Angular-компоненты** — каждый инкапсулирует один вариант использования компонента. +В блоке **Source** в Storybook показывается полноценный Angular-компонент (TypeScript), который пользователь библиотеки может скопировать к себе как есть. + +Это принципиально отличается от подхода в `{name}.stories.ts`, где `source.code` показывает просто HTML-шаблон. + +### Структура файла + +```typescript +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { XxxComponent } from '../../../../lib/components/xxx/xxx.component'; + +// 1. Шаблон выносится в const — чтобы переиспользовать в source.code +const template = ` +
+ +
+`; +const styles = ''; + +// 2. Standalone-компонент с реальным шаблоном +@Component({ + selector: 'app-xxx-variant', + standalone: true, + imports: [XxxComponent], + template, + styles, +}) +export class XxxVariantComponent {} + +// 3. StoryObj — рендерит компонент; source.code — код компонента для копирования +export const Variant: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { story: 'Описание на русском.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { XxxComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-xxx-variant', + standalone: true, + imports: [XxxComponent], + template: \` + + \`, +}) +export class XxxVariantComponent {} + `, + }, + }, + }, +}; +``` + +### Правила + +1. Каждый файл — **одна вариация**, один `@Component` + один `StoryObj` +2. Шаблон выносится в `const template` — чтобы использовать в `source.code` +3. `render: () => ({ template: '' })` — **только** для статичных примеров, где controls не нужны (форм-контролы с `ngModel`, группы компонентов). Для простых prop-вариаций controls не будут работать при таком подходе — см. правило ниже. +4. `source.code` содержит полный TypeScript-код компонента с импортами из `@cdek-it/angular-ui-kit` +5. Обёртка `
` — фон preview; **`p-4` не добавлять** +6. Именование файлов: `{name}-{variant}.component.ts` (например `avatar-label.component.ts`) +7. Именование selector компонента: `app-{name}-{variant}` (например `app-avatar-label`) +8. Класс компонента: `{Name}{Variant}Component` (например `AvatarLabelComponent`) + +### Когда создавать examples/ + +`examples/` создаётся **обязательно для каждого компонента** — для всех вариационных сторисов (кроме `Default`). + +`Default` (интерактивный playground) в examples/ **не дублируется** — он живёт только в `{name}.stories.ts`. + +Каждая вариационная сторис (`WithIcon`, `Removable`, `Disabled` и т.д.) имеет соответствующий файл в `examples/`. + +--- + +## Структура сторисов — обязательные разделы + +| Порядок | Сторис | Описание | +|---------|-------------|-----------------------------------------------------------------------| +| 1 | **Default** | Интерактивный playground. Динамический render. Все пропсы в Controls. | +| 2+ | Вариации | По одному варианту: Sizes, Icons, Rounded, Loading, Disabled, и т.д. | + +### Правило: один экземпляр компонента на сторис + +**Каждая сторис показывает ровно один вариант компонента. Все остальные виды компонента настраиваются с помощью пропсов через `args`.** + +В каждой вариационной сторис шаблон содержит **ровно один** экземпляр компонента. +Это правило распространяется как на сторисы в `{name}.stories.ts`, так и на компоненты в `examples/`. +Вариация демонстрируется через `args` (пропсы), а не через дублирование тегов. + +```typescript +// ❌ Запрещено — несколько экземпляров компонента в шаблоне +export const Sizes: Story = { + render: (args) => ({ + props: args, + template: ` + + + + `, + }), +}; + +// ❌ Запрещено — то же нарушение внутри examples/ +@Component({ template: ` + + + +` }) +export class TagSeveritiesComponent {} + +// ✅ Правильно — один экземпляр, вариация задаётся через args +export const Sizes: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { label: 'Button', size: 'large' }, +}; + +export const Severity: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { value: 'Success', severity: 'success' }, +}; +``` + +**Исключение**: составные компоненты (например группа радиокнопок / чекбоксов), где несколько дочерних элементов — это **сам смысл компонента**, а не визуальное перечисление вариантов. + +### Обязательные вариации для большинства компонентов +- `Sizes` — один компонент с нужным `size` в `args` +- `Icons` — один компонент с иконкой в `args` +- `Loading` — один компонент с `loading: true` в `args` +- `Rounded` — один компонент с `rounded: true` в `args` +- `Severity` — один компонент с нужным `severity` в `args` +- `Disabled` — один компонент с `disabled: true` в `args` + +### Правило: controls (пропсы) работают во всех вариационных сторисах + +**Вариационные сторисы ВСЕГДА рендерят через `commonTemplate + args`** — это единственный способ, при котором Storybook передаёт значения controls в компонент. + +`render: () => ({ template: '' })` — статичный рендер, controls **не работают**. Такой подход применим только для форм-контролов с `ngModel` и групп компонентов. + +```typescript +// ❌ Controls не работают — props не передаются в статичный компонент +export const Rounded: Story = { + render: () => ({ + template: ``, + }), +}; + +// ✅ Controls работают — props передаются через args +export const Rounded: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { value: 'Rounded', severity: 'success', rounded: true }, + parameters: { + docs: { + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { TagComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-tag-rounded', + standalone: true, + imports: [TagComponent], + template: \\\` + + \\\`, +}) +export class TagRoundedComponent {} + `, + }, + }, + }, +}; +``` + +**Если для вариации существует example-файл:** example-компонент регистрируется в `moduleMetadata`, но сторис рендерит через `commonTemplate + args`. В `source.code` сторис показывает TypeScript-код из example-файла (дублируется вручную). + +--- + +## Ключевые технические решения + +### Почему Default story использует динамический render + +В Storybook 10 Angular `source.transform` **не реактивен** — вызывается один раз. +Единственный способ обновлять code-сниппет при смене controls: +генерировать `template` строку прямо в `render(args)`. +Storybook Angular использует `template` из render как source code. + +```typescript +// ✅ Правильно — template обновляется при смене controls +render: (args) => { + const template = ``; + return { props: args, template }; +}, + +// ❌ Неправильно — source.transform не реактивен +parameters: { + docs: { source: { transform: (src, ctx) => ... } } +} +``` + +### Почему НЕ используется самозакрывающийся тег + +Angular JIT-компилятор запрещает ``. +` + +``` + +Поиск иконок: https://tabler.io/icons + +--- + +## Стилизация компонентов + +### Порядок слоёв + +``` +Компонент-обёртка → PrimeNG (p-button) → PrimeNG Aura тема +→ Токены (src/prime-preset/tokens/components/{name}.ts) +→ Tailwind CSS +``` + +### Добавление CSS-токенов + +Файл: `src/prime-preset/tokens/components/{name}.ts` + +Структура токенов соответствует PrimeNG Aura preset. +Кастомные расширения — через префикс `--p-{name}-extend-*`. + +### Tailwind в шаблонах сторисов + +```html + +
+ +
+``` + +--- + +## Референс Vue UI Kit + +Vue UI Kit (PrimeVue) — источник референса по структуре сторисов и вариациям компонентов: + +- **Репозиторий**: `~/Downloads/vue-ui-kit-3/src/plugins/prime/stories/` +- **Запущен локально**: `http://localhost:6006` + +### Что брать как референс + +| Vue файл | Что переносить в Angular | +|-----------------------------------|--------------------------------------------------| +| `Button/Button.stories.js` | argTypes, stories args, описания | +| `Button/Button.template.js` | Шаблоны вариаций (grid-матрица размеров/severity)| +| `Button/Button.mdx` | Структура документации, порядок сторисов | + +### Как адаптировать Vue → Angular + +| Vue | Angular | +|-----------------------------|----------------------------------------------| +| `v-bind="args"` | `[prop]="prop"` (через `props: args`) | +| `variant="text"` | Остаётся `variant="text"` (статичный шаблон) | +| `