diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..df3235f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# AGENTS.md + +Instructions for AI coding agents working in this repository **or** in projects that depend on `gnexus-ui-kit`. + +## If you are building UI with GNexus UI Kit + +1. **Check the catalog before writing any markup**: [`docs/catalog.json`](docs/catalog.json) maps every need (button, modal, status label, ...) to its component, props and when to use it. If a component exists, use it — do not invent your own markup for what the kit already covers. +2. Read the full agent guide: [`docs/ai-guide.md`](docs/ai-guide.md) — component selection, variant/icon rules, and the recipe for building a custom component that matches the GNexus style (tokens, `hard_panel`, uppercase titles, `focus_ring`, `hover_touch`). +3. In Vue projects, use the adapter: `import { GnButton, GnModal } from "gnexus-ui-kit/vue"`. Full Vue contract: [`docs/vue/ai-usage-guide.md`](docs/vue/ai-usage-guide.md). +4. Icons are Phosphor only, always with the base class: `icon="ph-house"` in Vue, `` in HTML. +5. Variants are a closed set: `primary, secondary, accent, success, warning, danger, error, info`. + +Quick copy-paste block for the consuming project's own CLAUDE.md/AGENTS.md is at the bottom of [`docs/ai-guide.md`](docs/ai-guide.md). + +## If you are working on this repository + +Follow [`CLAUDE.md`](CLAUDE.md) — it is the maintainer reference: demo sync rules (vanilla vs Vue demo must stay pixel-identical), build/verification commands (`npm run release:check`), screenshot policy, and the Vue adapter lessons. The component catalog table in CLAUDE.md is generated from `docs/catalog.json` — edit the JSON, then run `npm run gen:catalog`. + +## Single sources of truth + +| Source | Generated from it | +|--------|-------------------| +| `docs/catalog.json` | catalog table in `CLAUDE.md`, selection table in `docs/ai-guide.md` | +| `src/scss/_palette-colors.scss`, `_design-tokens.scss`, `_spacing.scss` | all component styling | +| `demo/partials/*.html` | both demo pages (vanilla + Vue) | \ No newline at end of file diff --git a/README.md b/README.md index a920f8d..4889014 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,16 @@ Человеческая документация по подключению, стилю, JS API и компонентам находится в [`docs/`](docs/index.md). Официальный Vue 3 adapter описан в [`docs/vue.md`](docs/vue.md). +## Использование с ИИ-агентами + +Если над проектом работает AI-агент (Claude, Cursor, Copilot, Codex, ...), укажите ему на: + +- [`AGENTS.md`](AGENTS.md) — точка входа для агентов (файл едет в npm-пакет). +- [`docs/ai-guide.md`](docs/ai-guide.md) — полный гайд: как выбирать компоненты из каталога, какие варианты/иконки допустимы, и рецепт «как сделать кастомный компонент в стиле GNexus» (токены, `hard_panel`, uppercase-заголовки, `focus_ring`, `hover_touch`). +- [`docs/catalog.json`](docs/catalog.json) — машиночитаемый каталог: need → component → props → useWhen. + +Быстрый способ — вставить готовый блок правил в `CLAUDE.md`/`AGENTS.md` проекта-потребителя (сниппет в конце [`docs/ai-guide.md`](docs/ai-guide.md)). + ## Быстрый старт ```bash diff --git a/docs/ai-guide.md b/docs/ai-guide.md new file mode 100644 index 0000000..bdb8a06 --- /dev/null +++ b/docs/ai-guide.md @@ -0,0 +1,181 @@ +# AI Agent Guide + +This guide is for AI agents (Claude, Cursor, Copilot, Codex, ...) building interfaces with GNexus UI Kit — in Vue projects, plain HTML pages, or inside this repository. + +It answers the two most common failure modes: + +1. **The agent invents its own component** instead of using one from the kit → see [Component selection](#component-selection). +2. **The agent builds a custom component that drifts from the styleguide** → see [Building a custom component in the GNexus style](#building-a-custom-component-in-the-gnexus-style). + +## The one rule + +> **Before writing ANY markup, check the [component selection table](#component-selection) (or `docs/catalog.json`). If a component exists for the need, use it. Do not hand-roll buttons, modals, tabs, toasts, forms, badges or tables.** + +Raw markup is acceptable only for layout wrappers and content with no adapter component yet. + +## Required setup (once per app) + +```js +import "gnexus-ui-kit/dist/css/kit.css"; +import "gnexus-ui-kit/dist/assets/fonts/phosphor-icons/src/css/icons.css"; +``` + +Vue: use the adapter (`gnexus-ui-kit/vue`) — see [`docs/vue/ai-usage-guide.md`](vue/ai-usage-guide.md) for the full Vue contract. Vanilla JS: the bundle publishes `GNexusUIKit.*` on `window` — see `docs/javascript.md`. + +## Component selection + +Every mapping below is generated from `docs/catalog.json` — the single source of truth. If your need is not in this table, search `docs/catalog.json` before inventing markup. + + + +| Need | Use | When | +|------|-----|------| +| Button | `GnButton` | Any clickable command or submit action | +| Icon-only button | `GnIconButton` | Action that shows only an icon (always pass `label` for accessibility) | +| Copy button | `GnCopyButton` | Copy-to-clipboard action with built-in success feedback | +| Status label | `GnBadge` | Small colored status text (active, pending, failed) — never a hand-styled span | +| Message block | `GnAlert` | Inline info/warning/error message inside page content | +| Card / panel | `GnCard` | Framed content block with optional title and footer | +| Horizontal card | `GnHorizontalCard` | Media-left preview card (image + linked title) | +| Login card | `GnLoginCard` | Username/password sign-in form | +| User card | `GnUserCard` | Person or account profile block with avatar, email and role | +| Page title bar | `GnPageHeader` | Screen-level title with kicker, subtitle, meta and action buttons | +| Text field | `GnInput` | Single-line text input with label, icon and validation state | +| Textarea | `GnTextarea` | Multiline text input | +| Select dropdown | `GnSelect` | Choose one option from a short list via native dropdown | +| Checkbox | `GnCheckbox` | Boolean opt-in/opt-out choice | +| Toggle switch | `GnSwitch` | Instant on/off setting toggle | +| Radio | `GnRadio` / `GnRadioGroup` | Single choice from a small, fully visible option set | +| Range slider | `GnRange` | Numeric value picked on a min–max scale | +| File upload | `GnFileUpload` | File picker with previews; it owns preview object URLs | +| Searchable select | `GnCombobox` | Choose one option from a large or filterable list | +| Tag / multi-value input | `GnTagInput` | Enter multiple free-form values as chips | +| Tabs | `GnTabs` | Switch between views within one page | +| Router tabs | `GnRouterTabs` | Tabs mapped to vue-router routes | +| Accordion | `GnAccordion` | Collapsible disclosure groups | +| Modal dialog | `GnModal` | Blocking dialog; Escape, focus trap and focus return are handled for you | +| Side drawer | `GnDrawer` | Slide-in panel for details, quick edit or secondary flows | +| Toasts | `GnToastProvider` + `useToast` | Transient global notification; one toast at a time by design | +| Confirm dialog | `GnConfirmDialog` | Yes/no confirmation before a destructive or important action | +| Table | `GnTable` | Structured data rows with per-cell customization slots | +| Toolbar | `GnToolbar` | Title/meta + action buttons row above content | +| Input group | `GnInputGroup` | Input with prefix/suffix addon or embedded action button | +| Search field | `GnSearchField` | Search input with clear button | +| Pagination | `GnPagination` | Page switcher for tables and lists | +| Empty state | `GnEmptyState` | No-data placeholder with icon, text and actions | +| Skeleton loader | `GnSkeleton` | Loading placeholder blocks while content loads | +| Key-value list | `GnDescriptionList` | Read-only key-value metadata pairs | +| Progress bar | `GnProgress` | Determinate progress from 0 to max | +| Usage meter card | `GnUsageMeter` | Limit/quota usage card with progress bar | +| Staged progress | `GnProgressStages` | Segmented progress across named stages | +| Wizard steps | `GnSteps` | Step-by-step wizard flow indicator (horizontal or vertical) | +| Chip / tag | `GnChip` | Small label that can be selectable or removable | +| Chip group | `GnChipGroup` | Row/wrap container for a group of chips | +| Avatar | `GnAvatar` | User/entity avatar from image, initials or icon, with optional status | +| Identity row | `GnIdentity` | Compact avatar + title + meta row | +| Avatar stack | `GnAvatarStack` | Overlapping group of avatars | +| Timeline | `GnTimeline` | Vertical event history with state markers | +| Activity log | `GnActivityLog` | Compact audit or job log rows | +| Basic list | `GnList` | Simple item list | +| Definition list | `GnDefinitionList` | Term/description pairs | +| Action list | `GnActionList` | List of clickable actions | +| Loader / spinner | `GnLoader` | Indeterminate loading spinner | +| Status card | `GnStatusCard` | Card communicating a state (success/warning/error/info) | +| Metric card | `GnMetricCard` | KPI number with delta and meta | +| Action card | `GnActionCard` | Card with text and action buttons | +| Dropdown menu | `GnDropdown` | Menu of actions revealed from a trigger button | +| Popover panel | `GnPopover` | Small anchored panel with title and text | +| Tooltip | `GnTooltip` | Short hover hint | +| Navigation list | `GnNavList` | Vertical navigation links list (router-aware) | +| App shell | `GnNavigationShell` | Whole-app topbar/drawer/footer navigation frame | + + + +## Rules that prevent drift + +- **Variants**: only `primary, secondary, accent, success, warning, danger, error, info`. Never invent a variant name (`danger` and `error` render the same in most components). +- **Icons**: Phosphor only, always with the base `ph` class. Vue props: `icon="ph-house"` (prefix required). Raw HTML: ``. +- **Colors**: use design tokens, never raw hex values. In SCSS: `$surface-panel-muted`, `$color-text-light`, `$color-primary`, `$color-secondary`, `$color-success/warning/error/info`. In plain CSS (no build step): use the kit's own component classes and utility classes (`p-*`, `m-*`, `g-*`, `row`, `d-flex`, `text-*`) instead of restyling. +- **Spacing**: `$space-1` … `$space-12` scale, never arbitrary pixel values. +- **Focus**: every interactive element shows a visible focus ring (`@include focus_ring` in SCSS). +- **Hover**: wrap hover styling in `@include hover_touch` so touch devices fall back to `:active`. +- **Motion**: `$motion-fast` (0.15s) / `$motion-base` (0.2s) / `$motion-slow` (0.28s) with `ease`. No decorative animation. + +## Building a custom component in the GNexus style + +If — and only if — the catalog has no component for the need, build one, but it must look native. Checklist: + +1. **Surface**: `@include hard_panel` — `$surface-panel-muted` background, 2px solid border (`$border-width-base`), zero border-radius. Sharp corners are non-negotiable. +2. **Accent**: important panels get a thick left accent — `@include hard_panel($border-color-muted, $border-width-accent)` (6px left border). +3. **Title**: small, bold, uppercase (`$font-size-sm`, `$font-weight-bold`, `text-transform: uppercase`). No hero-size headings inside panels. +4. **Text**: `$color-text-light` for primary text, `$color-text-medium` for secondary/meta. Meta text uses `$font-size-xs`/`$font-size-sm`. +5. **Typeface**: IBM Plex Mono is already the base font — do not override `font-family`. +6. **Interactive states**: `@include focus_ring` on `:focus-visible`, `@include hover_touch` for hover affordances, transitions on `$motion-fast`/`$motion-base`. +7. **Icons**: ``, sizes from `$icon-size-xs` … `$icon-size-xl`. +8. **State colors**: `$color-success`, `$color-warning`, `$color-error`, `$color-info` — the same four the kit's badges/alerts/status cards use. + +Do (matches the kit's own panels): + +```scss +.my-widget { + @include hard_panel($border-color-muted, $border-width-accent); + padding: $space-4; + transition-duration: $motion-base; + transition-property: border-color; + transition-timing-function: $motion-ease; + + @include hover_touch { + border-color: $color-secondary; + } + + &:focus-visible { + @include focus_ring; + } +} + +.my-widget-title { + font-size: $font-size-sm; + font-weight: $font-weight-bold; + text-transform: uppercase; +} +``` + +Don't (the classic drift): + +```scss +// WRONG: invented palette, rounded corners, random spacing, no focus ring +.my-widget { + background: #1a1b26; + border: 1px solid #333; + border-radius: 8px; + padding: 13px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); +} +``` + +Full token reference: [`docs/style-guide.md`](style-guide.md). + +## Paste this into the consuming project + +Add this block to the **consuming project's** `CLAUDE.md` / `AGENTS.md` (whichever your agent reads), so the agent knows the kit exists: + +```markdown +## GNexus UI Kit + +This project uses GNexus UI Kit (npm: `gnexus-ui-kit`). Rules: + +- Before writing ANY UI markup, check the component catalog in + `node_modules/gnexus-ui-kit/docs/catalog.json` (columns: need, component, + props, useWhen). If a component exists for the need, use it. +- In Vue: import components from `gnexus-ui-kit/vue` (GnButton, GnModal, ...). + In vanilla HTML: use the kit's documented classes, not custom lookalikes. +- Icons: Phosphor only — `icon="ph-house"` in Vue, `` in HTML. +- Variants: only `primary, secondary, accent, success, warning, danger, error, info`. +- Custom components must follow the GNexus recipe: + `node_modules/gnexus-ui-kit/docs/ai-guide.md` ("Building a custom component"). +- Full agent guide: `node_modules/gnexus-ui-kit/docs/ai-guide.md` +``` + +## For maintainers of this repository + +When working **inside this repo**, follow [`CLAUDE.md`](../CLAUDE.md) — it carries the repo workflow rules (demo sync, build verification, screenshot policy) on top of everything above. \ No newline at end of file diff --git a/docs/catalog.json b/docs/catalog.json index 1116dbb..135c99e 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -1,60 +1,60 @@ [ - { "need": "Button", "component": "GnButton", "props": ["variant", "size", "icon", "loading", "disabled"] }, - { "need": "Icon-only button", "component": "GnIconButton", "props": ["icon", "label", "size"] }, - { "need": "Copy button", "component": "GnCopyButton", "props": ["text", "icon", "successIcon", "duration", "label", "size"] }, - { "need": "Status label", "component": "GnBadge", "props": ["variant", "outline"] }, - { "need": "Message block", "component": "GnAlert", "props": ["variant"] }, - { "need": "Card / panel", "component": "GnCard", "props": ["title (slot)", "footer (slot)"] }, - { "need": "Horizontal card", "component": "GnHorizontalCard", "props": ["image", "title", "titleHref", "icon", "footer (slot)"] }, - { "need": "Login card", "component": "GnLoginCard", "props": ["title", "logoIcon", "usernameLabel", "passwordLabel", "rememberMe", "submitText", "error", "forgotHref", "signupHref"] }, - { "need": "User card", "component": "GnUserCard", "props": ["name", "email", "role", "avatar", "href", "compact", "actions"] }, - { "need": "Page title bar", "component": "GnPageHeader", "props": ["kicker", "title", "subtitle", "meta", "actions (slot)"] }, - { "need": "Text field", "component": "GnInput", "props": ["v-model", "label", "icon", "state", "help"] }, - { "need": "Textarea", "component": "GnTextarea", "props": ["v-model", "label", "state", "help"] }, - { "need": "Select dropdown", "component": "GnSelect", "props": ["v-model", "label", "options"] }, - { "need": "Checkbox", "component": "GnCheckbox", "props": ["v-model", "label", "disabled"] }, - { "need": "Toggle switch", "component": "GnSwitch", "props": ["v-model", "label", "disabled"] }, - { "need": "Radio", "component": "GnRadio / GnRadioGroup", "props": ["v-model", "options"] }, - { "need": "Range slider", "component": "GnRange", "props": ["v-model", "label", "min", "max"] }, - { "need": "File upload", "component": "GnFileUpload", "props": ["v-model", "badge", "multiple", "accept"] }, - { "need": "Searchable select", "component": "GnCombobox", "props": ["v-model", "label", "options", "placeholder"] }, - { "need": "Tag / multi-value input", "component": "GnTagInput", "props": ["v-model", "label", "placeholder", "separator", "unique", "maxItems"] }, - { "need": "Tabs", "component": "GnTabs", "props": ["v-model", "items"] }, - { "need": "Router tabs", "component": "GnRouterTabs", "props": ["`items` (with `to`)", "activeMatch"] }, - { "need": "Accordion", "component": "GnAccordion", "props": ["items", "v-model", "multiple"] }, - { "need": "Modal dialog", "component": "GnModal", "props": ["v-model:open", "title", "closeOnBackdrop"] }, - { "need": "Side drawer", "component": "GnDrawer", "props": ["v-model:open", "title", "position"] }, - { "need": "Toasts", "component": "GnToastProvider + useToast", "propsNote": "Wrap app once; call `toast.success({ title, text })`" }, - { "need": "Confirm dialog", "component": "GnConfirmDialog", "props": ["v-model:open", "title", "message", "confirmVariant"] }, - { "need": "Table", "component": "GnTable", "props": ["columns", "rows", "caption", "emptyText"] }, - { "need": "Toolbar", "component": "GnToolbar", "props": ["title", "meta", "actions (slot)"] }, - { "need": "Input group", "component": "GnInputGroup", "props": ["addon (slot)", "action (slot)"] }, - { "need": "Search field", "component": "GnSearchField", "props": ["v-model"] }, - { "need": "Pagination", "component": "GnPagination", "props": ["page", "total-pages"] }, - { "need": "Empty state", "component": "GnEmptyState", "props": ["title", "text", "icon", "actions (slot)"] }, - { "need": "Skeleton loader", "component": "GnSkeleton", "props": [] }, - { "need": "Key-value list", "component": "GnDescriptionList", "props": ["items"] }, - { "need": "Progress bar", "component": "GnProgress", "props": ["value", "label", "animated"] }, - { "need": "Usage meter card", "component": "GnUsageMeter", "props": ["title", "value", "max", "meta"] }, - { "need": "Staged progress", "component": "GnProgressStages", "props": ["items"] }, - { "need": "Wizard steps", "component": "GnSteps", "props": ["items", "vertical"] }, - { "need": "Chip / tag", "component": "GnChip", "props": ["variant", "selected", "removable"] }, - { "need": "Chip group", "component": "GnChipGroup", "props": [] }, - { "need": "Avatar", "component": "GnAvatar", "props": ["src", "alt", "initials", "icon", "size", "status"] }, - { "need": "Identity row", "component": "GnIdentity", "propsNote": "`title`, `meta`, `avatar` props" }, - { "need": "Avatar stack", "component": "GnAvatarStack", "props": ["items"] }, - { "need": "Timeline", "component": "GnTimeline", "props": ["items"] }, - { "need": "Activity log", "component": "GnActivityLog", "props": ["items"] }, - { "need": "Basic list", "component": "GnList", "props": ["items"] }, - { "need": "Definition list", "component": "GnDefinitionList", "props": ["items"] }, - { "need": "Action list", "component": "GnActionList", "props": ["items"] }, - { "need": "Loader / spinner", "component": "GnLoader", "props": [] }, - { "need": "Status card", "component": "GnStatusCard", "props": ["title", "text", "icon", "variant"] }, - { "need": "Metric card", "component": "GnMetricCard", "props": ["label", "value", "icon", "delta", "negative", "meta"] }, - { "need": "Action card", "component": "GnActionCard", "props": ["title", "text", "actions (slot)"] }, - { "need": "Dropdown menu", "component": "GnDropdown", "props": ["items", "label", "variant"] }, - { "need": "Popover panel", "component": "GnPopover", "props": ["title", "text", "label"] }, - { "need": "Tooltip", "component": "GnTooltip", "props": ["text"] }, - { "need": "Navigation list", "component": "GnNavList", "props": ["items"] }, - { "need": "App shell", "component": "GnNavigationShell", "props": ["brand", "items", "current", "footerLeft", "footerRight"] } + { "need": "Button", "component": "GnButton", "props": ["variant", "size", "icon", "loading", "disabled"], "useWhen": "Any clickable command or submit action" }, + { "need": "Icon-only button", "component": "GnIconButton", "props": ["icon", "label", "size"], "useWhen": "Action that shows only an icon (always pass `label` for accessibility)" }, + { "need": "Copy button", "component": "GnCopyButton", "props": ["text", "icon", "successIcon", "duration", "label", "size"], "useWhen": "Copy-to-clipboard action with built-in success feedback" }, + { "need": "Status label", "component": "GnBadge", "props": ["variant", "outline"], "useWhen": "Small colored status text (active, pending, failed) — never a hand-styled span" }, + { "need": "Message block", "component": "GnAlert", "props": ["variant"], "useWhen": "Inline info/warning/error message inside page content" }, + { "need": "Card / panel", "component": "GnCard", "props": ["title (slot)", "footer (slot)"], "useWhen": "Framed content block with optional title and footer" }, + { "need": "Horizontal card", "component": "GnHorizontalCard", "props": ["image", "title", "titleHref", "icon", "footer (slot)"], "useWhen": "Media-left preview card (image + linked title)" }, + { "need": "Login card", "component": "GnLoginCard", "props": ["title", "logoIcon", "usernameLabel", "passwordLabel", "rememberMe", "submitText", "error", "forgotHref", "signupHref"], "useWhen": "Username/password sign-in form" }, + { "need": "User card", "component": "GnUserCard", "props": ["name", "email", "role", "avatar", "href", "compact", "actions"], "useWhen": "Person or account profile block with avatar, email and role" }, + { "need": "Page title bar", "component": "GnPageHeader", "props": ["kicker", "title", "subtitle", "meta", "actions (slot)"], "useWhen": "Screen-level title with kicker, subtitle, meta and action buttons" }, + { "need": "Text field", "component": "GnInput", "props": ["v-model", "label", "icon", "state", "help"], "useWhen": "Single-line text input with label, icon and validation state" }, + { "need": "Textarea", "component": "GnTextarea", "props": ["v-model", "label", "state", "help"], "useWhen": "Multiline text input" }, + { "need": "Select dropdown", "component": "GnSelect", "props": ["v-model", "label", "options"], "useWhen": "Choose one option from a short list via native dropdown" }, + { "need": "Checkbox", "component": "GnCheckbox", "props": ["v-model", "label", "disabled"], "useWhen": "Boolean opt-in/opt-out choice" }, + { "need": "Toggle switch", "component": "GnSwitch", "props": ["v-model", "label", "disabled"], "useWhen": "Instant on/off setting toggle" }, + { "need": "Radio", "component": "GnRadio / GnRadioGroup", "props": ["v-model", "options"], "useWhen": "Single choice from a small, fully visible option set" }, + { "need": "Range slider", "component": "GnRange", "props": ["v-model", "label", "min", "max"], "useWhen": "Numeric value picked on a min–max scale" }, + { "need": "File upload", "component": "GnFileUpload", "props": ["v-model", "badge", "multiple", "accept"], "useWhen": "File picker with previews; it owns preview object URLs" }, + { "need": "Searchable select", "component": "GnCombobox", "props": ["v-model", "label", "options", "placeholder"], "useWhen": "Choose one option from a large or filterable list" }, + { "need": "Tag / multi-value input", "component": "GnTagInput", "props": ["v-model", "label", "placeholder", "separator", "unique", "maxItems"], "useWhen": "Enter multiple free-form values as chips" }, + { "need": "Tabs", "component": "GnTabs", "props": ["v-model", "items"], "useWhen": "Switch between views within one page" }, + { "need": "Router tabs", "component": "GnRouterTabs", "props": ["`items` (with `to`)", "activeMatch"], "useWhen": "Tabs mapped to vue-router routes" }, + { "need": "Accordion", "component": "GnAccordion", "props": ["items", "v-model", "multiple"], "useWhen": "Collapsible disclosure groups" }, + { "need": "Modal dialog", "component": "GnModal", "props": ["v-model:open", "title", "closeOnBackdrop"], "useWhen": "Blocking dialog; Escape, focus trap and focus return are handled for you" }, + { "need": "Side drawer", "component": "GnDrawer", "props": ["v-model:open", "title", "position"], "useWhen": "Slide-in panel for details, quick edit or secondary flows" }, + { "need": "Toasts", "component": "GnToastProvider + useToast", "propsNote": "Wrap app once; call `toast.success({ title, text })`", "useWhen": "Transient global notification; one toast at a time by design" }, + { "need": "Confirm dialog", "component": "GnConfirmDialog", "props": ["v-model:open", "title", "message", "confirmVariant"], "useWhen": "Yes/no confirmation before a destructive or important action" }, + { "need": "Table", "component": "GnTable", "props": ["columns", "rows", "caption", "emptyText"], "useWhen": "Structured data rows with per-cell customization slots" }, + { "need": "Toolbar", "component": "GnToolbar", "props": ["title", "meta", "actions (slot)"], "useWhen": "Title/meta + action buttons row above content" }, + { "need": "Input group", "component": "GnInputGroup", "props": ["addon (slot)", "action (slot)"], "useWhen": "Input with prefix/suffix addon or embedded action button" }, + { "need": "Search field", "component": "GnSearchField", "props": ["v-model"], "useWhen": "Search input with clear button" }, + { "need": "Pagination", "component": "GnPagination", "props": ["page", "total-pages"], "useWhen": "Page switcher for tables and lists" }, + { "need": "Empty state", "component": "GnEmptyState", "props": ["title", "text", "icon", "actions (slot)"], "useWhen": "No-data placeholder with icon, text and actions" }, + { "need": "Skeleton loader", "component": "GnSkeleton", "props": [], "useWhen": "Loading placeholder blocks while content loads" }, + { "need": "Key-value list", "component": "GnDescriptionList", "props": ["items"], "useWhen": "Read-only key-value metadata pairs" }, + { "need": "Progress bar", "component": "GnProgress", "props": ["value", "label", "animated"], "useWhen": "Determinate progress from 0 to max" }, + { "need": "Usage meter card", "component": "GnUsageMeter", "props": ["title", "value", "max", "meta"], "useWhen": "Limit/quota usage card with progress bar" }, + { "need": "Staged progress", "component": "GnProgressStages", "props": ["items"], "useWhen": "Segmented progress across named stages" }, + { "need": "Wizard steps", "component": "GnSteps", "props": ["items", "vertical"], "useWhen": "Step-by-step wizard flow indicator (horizontal or vertical)" }, + { "need": "Chip / tag", "component": "GnChip", "props": ["variant", "selected", "removable"], "useWhen": "Small label that can be selectable or removable" }, + { "need": "Chip group", "component": "GnChipGroup", "props": [], "useWhen": "Row/wrap container for a group of chips" }, + { "need": "Avatar", "component": "GnAvatar", "props": ["src", "alt", "initials", "icon", "size", "status"], "useWhen": "User/entity avatar from image, initials or icon, with optional status" }, + { "need": "Identity row", "component": "GnIdentity", "propsNote": "`title`, `meta`, `avatar` props", "useWhen": "Compact avatar + title + meta row" }, + { "need": "Avatar stack", "component": "GnAvatarStack", "props": ["items"], "useWhen": "Overlapping group of avatars" }, + { "need": "Timeline", "component": "GnTimeline", "props": ["items"], "useWhen": "Vertical event history with state markers" }, + { "need": "Activity log", "component": "GnActivityLog", "props": ["items"], "useWhen": "Compact audit or job log rows" }, + { "need": "Basic list", "component": "GnList", "props": ["items"], "useWhen": "Simple item list" }, + { "need": "Definition list", "component": "GnDefinitionList", "props": ["items"], "useWhen": "Term/description pairs" }, + { "need": "Action list", "component": "GnActionList", "props": ["items"], "useWhen": "List of clickable actions" }, + { "need": "Loader / spinner", "component": "GnLoader", "props": [], "useWhen": "Indeterminate loading spinner" }, + { "need": "Status card", "component": "GnStatusCard", "props": ["title", "text", "icon", "variant"], "useWhen": "Card communicating a state (success/warning/error/info)" }, + { "need": "Metric card", "component": "GnMetricCard", "props": ["label", "value", "icon", "delta", "negative", "meta"], "useWhen": "KPI number with delta and meta" }, + { "need": "Action card", "component": "GnActionCard", "props": ["title", "text", "actions (slot)"], "useWhen": "Card with text and action buttons" }, + { "need": "Dropdown menu", "component": "GnDropdown", "props": ["items", "label", "variant"], "useWhen": "Menu of actions revealed from a trigger button" }, + { "need": "Popover panel", "component": "GnPopover", "props": ["title", "text", "label"], "useWhen": "Small anchored panel with title and text" }, + { "need": "Tooltip", "component": "GnTooltip", "props": ["text"], "useWhen": "Short hover hint" }, + { "need": "Navigation list", "component": "GnNavList", "props": ["items"], "useWhen": "Vertical navigation links list (router-aware)" }, + { "need": "App shell", "component": "GnNavigationShell", "props": ["brand", "items", "current", "footerLeft", "footerRight"], "useWhen": "Whole-app topbar/drawer/footer navigation frame" } ] \ No newline at end of file diff --git a/package.json b/package.json index c3bef6f..f39df3c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "src/vue/", "docs/", "README.md", - "CLAUDE.md" + "CLAUDE.md", + "AGENTS.md" ], "exports": { ".": { diff --git a/scripts/generate-catalog.mjs b/scripts/generate-catalog.mjs index e7099fd..122c1d4 100644 --- a/scripts/generate-catalog.mjs +++ b/scripts/generate-catalog.mjs @@ -5,7 +5,7 @@ // src/vue/components/*.js. // // node scripts/generate-catalog.mjs regenerate + validate -// node scripts/generate-catalog.mjs --check fail if CLAUDE.md is stale +// node scripts/generate-catalog.mjs --check fail if CLAUDE.md or ai-guide is stale import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -14,8 +14,11 @@ const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const catalogPath = join(root, "docs/catalog.json"); const claudePath = join(root, "CLAUDE.md"); +const aiGuidePath = join(root, "docs/ai-guide.md"); const START = ""; const END = ""; +const AI_START = ""; +const AI_END = ""; const rows = JSON.parse(readFileSync(catalogPath, "utf8")); @@ -63,6 +66,10 @@ }; for(const row of rows) { + if(!row.useWhen) { + warn(`row "${row.need}" (${row.component}) is missing "useWhen"`); + } + if(row.propsNote) { continue; // free-form note, nothing to validate } @@ -121,6 +128,13 @@ ...rows.map(row => `| ${row.need} | ${renderComponent(row.component)} | ${renderProps(row)} |`) ].join("\n"); +const selectionTable = [ + "| Need | Use | When |", + "|------|-----|------|", + ...rows.map(row => `| ${row.need} | ${renderComponent(row.component)} | ${row.useWhen ?? "—"} |`) +].join("\n"); + +// --- write CLAUDE.md -------------------------------------------------------- const claude = readFileSync(claudePath, "utf8"); const pattern = new RegExp(`${escapeRegExp(START)}[\\s\\S]*${escapeRegExp(END)}`); if(!pattern.test(claude)) { @@ -128,17 +142,29 @@ process.exit(1); } -const updated = claude.replace(pattern, `${START}\n\n${table}\n\n${END}`); +const updatedClaude = claude.replace(pattern, `${START}\n\n${table}\n\n${END}`); + +// --- write docs/ai-guide.md ------------------------------------------------- +const aiGuide = readFileSync(aiGuidePath, "utf8"); +const aiPattern = new RegExp(`${escapeRegExp(AI_START)}[\\s\\S]*${escapeRegExp(AI_END)}`); +if(!aiPattern.test(aiGuide)) { + console.error(`[catalog] markers not found in docs/ai-guide.md; wrap the selection table with:\n${AI_START}\n...\n${AI_END}`); + process.exit(1); +} + +const updatedAiGuide = aiGuide.replace(aiPattern, `${AI_START}\n\n${selectionTable}\n\n${AI_END}`); if(process.argv.includes("--check")) { - if(updated !== claude) { - console.error("[catalog] CLAUDE.md is stale — run `npm run gen:catalog`"); + if(updatedClaude !== claude || updatedAiGuide !== aiGuide) { + const stale = [updatedClaude !== claude && "CLAUDE.md", updatedAiGuide !== aiGuide && "docs/ai-guide.md"].filter(Boolean).join(" and "); + console.error(`[catalog] ${stale} stale — run \`npm run gen:catalog\``); process.exit(1); } - console.log("[catalog] CLAUDE.md is up to date"); + console.log("[catalog] CLAUDE.md and docs/ai-guide.md are up to date"); } else { - writeFileSync(claudePath, updated); - console.log(`[catalog] CLAUDE.md updated (${rows.length} rows)`); + writeFileSync(claudePath, updatedClaude); + writeFileSync(aiGuidePath, updatedAiGuide); + console.log(`[catalog] CLAUDE.md and docs/ai-guide.md updated (${rows.length} rows)`); } if(warnings > 0) {