diff --git a/.claude/AGENTS.md b/.claude/AGENTS.md index a5eb44801..77b965531 100644 --- a/.claude/AGENTS.md +++ b/.claude/AGENTS.md @@ -42,7 +42,6 @@ pnpm storybook:dev pnpm storybook:build pnpm storybook:preview pnpm icons:build -pnpm webkit:build:dts ``` ## 4) Where to change what @@ -96,10 +95,7 @@ Run the minimum relevant validations for the scope of your change. - `pnpm webkit:lint` - `pnpm webkit:type-check` - `pnpm webkit:type-coverage` - - `pnpm webkit:build:dts` - `pnpm storybook:build` -- Other webkit typings/API surface change: - - `pnpm webkit:build:dts` If a full validation is too expensive, run targeted checks and document what was/was not validated. @@ -153,7 +149,7 @@ The legacy monolithic skill `skills/component-create.md` has been **removed**. N | Focused skills (one per phase) | [`.claude/skills/`](.claude/skills/) — `spec-create`, `spec-validate`, `figma-discover`, `token-map`, `reuse-audit`, `structure-decide`, `component-scaffold`, `storybook-write`, `code-connect-write`, `validate-component`, `echo-report` | | Isolated sub-agent prompts | [`.claude/agents/`](.claude/agents/) — one per skill, no chat history, no cross-talk | | Immutable rules (injected verbatim) | [`.claude/rules/`](.claude/rules/) — `no-invention`, `migration`, `dependencies` | -| Centralized design docs (sources of truth) | [`.claude/docs/`](.claude/docs/) — `DESIGN.md` (tokens + animations + forbidden list), `COMPONENT_REQUIREMENTS.md` (component pattern + Storybook discipline), `PRIMEVUE_ABSTRACTION.md` | +| Centralized design docs (sources of truth) | [`.claude/docs/`](.claude/docs/) — `DESIGN.md` (tokens + animations + forbidden list), `COMPONENT_REQUIREMENTS.md` (component pattern + Storybook discipline) | | Run logs (audit trail) | [`.claude/logs/.jsonl`](.claude/logs/) | **How `/component-create` runs:** @@ -166,7 +162,7 @@ The legacy monolithic skill `skills/component-create.md` has been **removed**. N 6. **storybook-writer** — minimal stories only (Default + per kind + per size + Disabled). 7. **code-connect-writer** — skips if `@figma/code-connect` missing. 8. **echo-reporter** (cross-check) — independent parser; disagreement with the hook marks the run degraded. -9. **validate-component** — `pnpm webkit:lint && type-check && type-coverage && build:dts && storybook:build`. +9. **validate-component** — `pnpm webkit:lint && type-check && type-coverage && storybook:build`. 10. **Finalize** — `status: approved → implemented`, refresh checksum, close log. **Why this is mandatory:** the previous workflow gave a single 749-line skill creative latitude — it sometimes added props, variants, or imports the user did not request. The new pipeline removes that latitude structurally: sub-agents see only the spec + their narrow rules; two hooks (`enforce-spec-exists`, `validate-spec-compliance`) verify the `.vue` matches the spec 1-to-1. @@ -195,7 +191,7 @@ The trigger logic now lives in the orchestrator command [`.claude/commands/compo - New components use ` - - -``` - -### 8. Template Structure +### 7. Template Structure Organize template logically: @@ -316,53 +296,15 @@ Organize template logically: --- -## Package.json Requirements - -Each component directory MUST have a `package.json` file with the following structure: - -```json -{ - "main": "./.vue", - "module": "./.vue", - "types": "./.vue.d.ts", - "browser": { - "./sfc": "./.vue" - }, - "sideEffects": ["*.vue"] -} -``` - -**Example for `field-text`:** +## Package Configuration -```json -{ - "main": "./field-text.vue", - "module": "./field-text.vue", - "types": "./field-text.vue.d.ts", - "browser": { - "./sfc": "./field-text.vue" - }, - "sideEffects": ["*.vue"] -} -``` +Components do **not** have a per-folder `package.json`. Module resolution goes through the root [`packages/webkit/package.json#exports`](../package.json) map, which points every public path directly at a source file (`.vue`/`.ts`/`.js`). The root `package.json` also declares `"sideEffects": ["**/*.vue", "**/*.css"]` once for the whole package. To publish a component, add its export entry (see [Export Configuration](#export-configuration) below). --- ## TypeScript Declarations -TypeScript declaration files (`.d.ts`) are **auto-generated** using Vue TSC. - -### Generation Commands - -Run from the webkit package root: - -```bash -# Clean existing declarations -npm run clean:dts - -# Generate new declarations -npm run build:dts -``` +TypeScript declaration files (`.d.ts`) are **auto-generated** using Vue TSC. They are emitted at publish time by `packages/webkit/.releaserc`'s `prepareCmd` (`vue-tsc --declaration --emitDeclarationOnly`) and ship to npm consumers; declaration-emit is validated in CI by `type-check` (`vue-tsc --noEmit`). **DO NOT manually edit `.d.ts` or `.d.ts.map` files.** @@ -534,7 +476,7 @@ If your component needs Vue Router: ### 3. Styling -- ✅ **DO:** Use PrimeVue styling conventions +- ✅ **DO:** Use the webkit token + `data-*` styling conventions - ✅ **DO:** Support custom classes via `attrs.class` (`useAttrs()` with `inheritAttrs: false`; do not declare `class` as a prop) - ✅ **DO:** Use Tailwind CSS utility classes - ❌ **DON'T:** Hardcode colors or spacing @@ -586,9 +528,7 @@ When creating a new component, ensure you've completed all requirements: - [ ] Define events with `defineEmits()` - [ ] Integrate VeeValidate (for form components) - [ ] Add `data-testid` attributes -- [ ] Create `package.json` in component directory - [ ] Add export to main `package.json` -- [ ] Run `npm run build:dts` to generate TypeScript declarations - [ ] Test component in consuming application ### Regular Components @@ -599,9 +539,7 @@ When creating a new component, ensure you've completed all requirements: - [ ] Define props with types and defaults - [ ] Define events with `defineEmits()` - [ ] Add `data-testid` attributes -- [ ] Create `package.json` in component directory - [ ] Add export to main `package.json` -- [ ] Run `npm run build:dts` to generate TypeScript declarations - [ ] Test component in consuming application ### Vue Router Components (Additional) @@ -622,7 +560,7 @@ When creating a new component, ensure you've completed all requirements: - - -``` - -### ConfirmDialog - -```vue - - - -``` - -**Props:** - -- `group` (String) - Optional group name -- `breakpoints` (Object) - Responsive breakpoints - -### Tooltip Directive - -Already registered globally by `WebkitPlugin`. For standalone use: - -```js -import { Tooltip } from '@aziontech/webkit/tooltip' - -app.directive('tooltip', Tooltip) -``` - ---- - -## Exports Reference - -| Export Path | Type | Description | -| ---------------------------------- | ---------- | --------------------------------------- | -| `@aziontech/webkit/plugin` | Plugin | `WebkitPlugin` - unified PrimeVue setup | -| `@aziontech/webkit/use-toast` | Composable | `useToast` with shortcut methods | -| `@aziontech/webkit/use-dialog` | Composable | `useDialog` wrapper | -| `@aziontech/webkit/api` | Utility | `FilterMatchMode` constants | -| `@aziontech/webkit/dynamic-dialog` | Component | DynamicDialog wrapper | -| `@aziontech/webkit/confirm-dialog` | Component | ConfirmDialog wrapper | -| `@aziontech/webkit/tooltip` | Directive | Tooltip re-export | - ---- - -## Migration Guide - -### Step 1: Replace plugin setup in main.js - -**Before:** - -```js -import PrimeVue from 'primevue/config' -import Tooltip from 'primevue/tooltip' -import ToastService from 'primevue/toastservice' -import DialogService from 'primevue/dialogservice' - -app.use(PrimeVue) -app.directive('tooltip', Tooltip) -app.use(ToastService) -app.use(DialogService) -``` - -**After:** - -```js -import { WebkitPlugin } from '@aziontech/webkit/plugin' - -app.use(WebkitPlugin) -``` - -### Step 2: Replace imports across codebase - -| Before | After | -| ---------------------------------------------------- | -------------------------------------------------------------- | -| `import { useToast } from 'primevue/usetoast'` | `import { useToast } from '@aziontech/webkit/use-toast'` | -| `import { useDialog } from 'primevue/usedialog'` | `import { useDialog } from '@aziontech/webkit/use-dialog'` | -| `import { FilterMatchMode } from 'primevue/api'` | `import { FilterMatchMode } from '@aziontech/webkit/api'` | -| `import DynamicDialog from 'primevue/dynamicdialog'` | `import DynamicDialog from '@aziontech/webkit/dynamic-dialog'` | -| `import ConfirmDialog from 'primevue/confirmdialog'` | `import ConfirmDialog from '@aziontech/webkit/confirm-dialog'` | -| `import Toast from 'primevue/toast'` | `import Toast from '@aziontech/webkit/toast'` | -| `import Dialog from 'primevue/dialog'` | `import Dialog from '@aziontech/webkit/dialog'` | -| `import Button from 'primevue/button'` | `import Button from '@aziontech/webkit/button'` | - -### Step 3: Remove primevue from package.json - -After all imports point to `@aziontech/webkit/*`, remove `primevue` from `dependencies`. It becomes a transitive dependency through webkit. - ---- - -## Architecture - -``` -Consumer App (console-kit) - │ - ├── import { WebkitPlugin } from '@aziontech/webkit/plugin' - ├── import { useToast } from '@aziontech/webkit/use-toast' - ├── import { useDialog } from '@aziontech/webkit/use-dialog' - └── import Button from '@aziontech/webkit/button' - │ - ▼ - @aziontech/webkit - │ - ├── src/plugins/primevue/index.js → WebkitPlugin - ├── src/composables/use-toast/index.js → useToast wrapper - ├── src/composables/use-dialog/index.js → useDialog wrapper - ├── src/services/primevue-api/index.js → FilterMatchMode - ├── src/directives/tooltip/index.js → Tooltip - └── src/core/primevue/*/ → Component wrappers - │ - ▼ - primevue (transitive dependency) -``` - -### How PrimeVue services work internally - -``` -WebkitPlugin useToast() component - │ │ │ - ▼ ▼ ▼ -app.provide(Symbol, svc) inject(Symbol) → svc ToastEventBus.on('add') - │ │ │ - └──────────────────────────────┘ │ - │ │ - ▼ │ - svc.add(msg) ──→ ToastEventBus.emit('add') ───────────┘ -``` - -The webkit wrappers use the **same PrimeVue Symbols** internally, so components and composables communicate seamlessly. - ---- - -## Troubleshooting - -### useToast/useDialog throws "not provided" error - -**Problem:** `No PrimeVue Toast provided!` or similar error. - -**Solution:** Ensure `WebkitPlugin` is registered before the component mounts: - -```js -app.use(WebkitPlugin) // Must come before app.mount() -app.mount('#app') -``` - -### DynamicDialog doesn't open - -**Problem:** `dialog.open()` is called but nothing appears. - -**Solution:** Ensure `` is mounted at root level (e.g., `App.vue`): - -```vue - -``` - -### Toast styles not applied - -**Problem:** Toast appears but without custom styling via `pt` prop. - -**Solution:** The webkit Toast wrapper explicitly forwards the `pt` prop. Ensure you're passing it correctly: - -```vue - - - -``` - -### PrimeVue version mismatch - -**Problem:** Unexpected behavior after updating webkit. - -**Solution:** Check which PrimeVue version webkit uses: - -```bash -cat node_modules/@aziontech/webkit/package.json | grep primevue -``` - -Consumer apps should not have `primevue` in their own `package.json` to avoid version conflicts. - ---- - -**Last Updated:** 2026-04-02 -**Version:** 1.0.0 -**Maintainer:** Azion WebKit Team diff --git a/.claude/hooks/README.md b/.claude/hooks/README.md index da050e5cc..d7685a270 100644 --- a/.claude/hooks/README.md +++ b/.claude/hooks/README.md @@ -1,20 +1,22 @@ # `.claude/hooks/` -`PreToolUse` hooks for the Azion Webkit monorepo. Wired up in [`../settings.json`](../settings.json). Each hook runs **before** the matching tool call and can either approve (exit 0) or block (exit 2) the operation. +`PreToolUse`/`PostToolUse` hooks for the Azion Webkit monorepo. Wired up in [`../settings.json`](../settings.json). Each hook runs around the matching tool call and can either approve (exit 0) or block (exit 2) the operation. The hooks fail open on unexpected errors (invalid JSON input, missing files, etc.) — they never silently break the workflow. +Hooks do not own their logic: the checks live in **shared engines** ([`_lib/`](./_lib/) shims re-exporting [`packages/webkit/src/eslint-plugin/*.js`](../../packages/webkit/src/eslint-plugin/), plus the DS-internal `_lib/spec-compliance-checks.mjs` and `_lib/story-source-checks.mjs`) that the CI ratchet ([`packages/webkit/scripts/check-authoring.mjs`](../../packages/webkit/scripts/check-authoring.mjs)) re-runs repo-wide. The rule ⇄ gate pairing is declared in [`_lib/standards.mjs`](./_lib/standards.mjs) and asserted in CI by [`packages/webkit/test/standards/invariant.test.mjs`](../../packages/webkit/test/standards/invariant.test.mjs). + ## Hooks ### [`validate-tokens.mjs`](./validate-tokens.mjs) -Blocks `Write`/`Edit`/`MultiEdit` on `.vue` / `.css` / `.scss` / `.ts` files under `packages/webkit/src/components/webkit/**` when the **new** content introduces any of: +Blocks `Write`/`Edit`/`MultiEdit` on `.vue` / `.css` / `.scss` / `.ts` files under `packages/webkit/src/components/**` when the **new** content introduces any of: - Hex/RGB/HSL colors - Tailwind palette names (`bg-gray-500`, `text-violet-600`, ...) - Raw Tailwind text sizes (`text-xs|sm|base|lg|...`) - Raw typography tokens (`text-[length:var(--text-*)]`, `leading-*`, `tracking-*`, `font-family`, `font-sora`, ...) -- PrimeVue color utilities (`text-color`, `surface-*`) +- External/legacy color utilities (`text-color`, `surface-*`) - `class` declared in `defineProps` - `any` type - `// @ts-ignore` / `// @ts-nocheck` / `// @ts-expect-error` @@ -32,13 +34,33 @@ Blocks `Write`/`Edit`/`MultiEdit` on any `.vue` / `.ts` / `.js` / `.mjs` / `.cjs Prevents the agent from hallucinating paths or speculatively importing modules that have not been installed yet. +### [`validate-authoring.mjs`](./validate-authoring.mjs) + +Blocks `Write`/`Edit`/`MultiEdit` when the resulting file introduces a construction-standard violation ([`props`](../rules/props.md), [`v-model`](../rules/v-model.md), [`emits`](../rules/emits.md), [`slots`](../rules/slots.md), [`composables`](../rules/composables.md), [`deprecation`](../rules/deprecation.md)): + +- Hand-rolled `modelValue` prop + `update:modelValue` emit (use `defineModel`) +- Runtime `defineProps({...})` / `defineEmits([...])` (use the typed generic forms) +- `` in the template without a typed `defineSlots` +- A composable returning `reactive()` state or authored as `.js` +- `@deprecated` without a named replacement + removal version + +Engine: [`_lib/authoring-checks.mjs`](./_lib/authoring-checks.mjs) → [`packages/webkit/src/eslint-plugin/authoring-checks.js`](../../packages/webkit/src/eslint-plugin/authoring-checks.js) — the same module the CI ratchet and the consumer ESLint rule (`authoring-standards`) run. + +### [`validate-story-source.mjs`](./validate-story-source.mjs) + +Blocks `Write`/`Edit`/`MultiEdit` on any `*.stories.*` file whose **resulting content** violates the [storybook-source](../rules/storybook-source.md) contract — the Docs "Show code" must be a single runnable PascalCase SFC. **Strict**: the whole stories tree is on the canonical pattern, so every check applies to the full result of the write (no "newly-introduced-only" grandfathering). 13 checks: `docs-not-literal`, `handrolled-transform`, `dynamic-source`, `nested-template`, `lowercase-tag` (native HTML tags exempt), `import-binding-mismatch`, `argtypes-regex`, `legacy-csf2-assignment`, `figma-reference`, `args-destructure`, `missing-helper`, `missing-source-code`, `missing-sourcestate` (foundations pages exempt from the helper/`toSfc` requirement). + +Also runs standalone: `node .claude/hooks/validate-story-source.mjs --all` audits the entire stories tree and exits non-zero on any violation. + +Engine: [`_lib/story-source-checks.mjs`](./_lib/story-source-checks.mjs) — shared with the CI ratchet. + ### [`enforce-component-create.mjs`](./enforce-component-create.mjs) -Blocks the **first** `Write` that creates a new `.vue` under `packages/webkit/src/components/webkit///` when the session transcript shows no reference to the spec-driven pipeline. Forces the agent to run `/spec-create` then `/component-create`, which load the orchestrator at [`../commands/component-create.md`](../commands/component-create.md) and the focused skills under [`../skills/`](../skills/). +Blocks the **first** `Write` that creates a new `.vue` under `packages/webkit/src/components///` when the session transcript shows no reference to the spec-driven pipeline. Forces the agent to run `/spec-create` then `/component-create`, which load the orchestrator at [`../commands/component-create.md`](../commands/component-create.md) and the focused skills under [`../skills/`](../skills/). ### [`enforce-spec-exists.mjs`](./enforce-spec-exists.mjs) -PreToolUse hook on `Write` of any `packages/webkit/src/components/webkit///.vue`. Blocks when: +PreToolUse hook on `Write` of any `packages/webkit/src/components///.vue`. Blocks when: - `.specs/.md` is missing. - The spec's `status` is not `approved` or `implemented` (drafts cannot drive code generation; locked specs require a `spec_version` bump). @@ -59,6 +81,10 @@ PostToolUse hook on `Write|Edit|MultiEdit` of the same `.vue` paths. Re-reads th Bypassed for legacy components (same whitelist). Shared parser library: [`_lib/spec.mjs`](./_lib/spec.mjs). +### [`enforce-test-exists.mjs`](./enforce-test-exists.mjs) + +PostToolUse hook on `Write` of a **root** component `.vue` (`packages/webkit/src/components///.vue` — file name equals its folder). Warns (exit 2, surfaced to the agent) when the co-located `.test.ts` is missing, so every component ships the browser-mode functional suite mandated by [`../rules/testing.md`](../rules/testing.md). PostToolUse (not Pre) so it never deadlocks `/component-create`, which writes the `.vue` before a test can exist. Composition sub-components are skipped (tested through their root). Bypassed for legacy components (same whitelist). + ## Adding a new hook 1. Create `.mjs` in this directory. diff --git a/.claude/hooks/__tests__/run.mjs b/.claude/hooks/__tests__/run.mjs index b74caa259..2b6b20e60 100644 --- a/.claude/hooks/__tests__/run.mjs +++ b/.claude/hooks/__tests__/run.mjs @@ -15,6 +15,7 @@ import { validateFrontmatter, getSection, parseTable, + defaultCellIsStringUndefined, bodyChecksum, constraintsBlockHasCanonicalBullets, parseVueSfc, @@ -149,6 +150,13 @@ group('lib: body parsing', () => { assertTrue(propNames.includes('size'), 'size missing') assertTrue(propNames.includes('disabled'), 'disabled missing') }) + test('defaultCellIsStringUndefined flags quoted undefined/null only', () => { + assertTrue(defaultCellIsStringUndefined("`'undefined'`") === true, "quoted 'undefined' should flag") + assertTrue(defaultCellIsStringUndefined("`'null'`") === true, "quoted 'null' should flag") + assertTrue(defaultCellIsStringUndefined('`undefined`') === false, 'unquoted undefined is legitimate') + assertTrue(defaultCellIsStringUndefined("`''`") === false, 'empty string is legitimate') + assertTrue(defaultCellIsStringUndefined("`'medium'`") === false, 'real string default is legitimate') + }) }) group('lib: checksum', () => { @@ -327,6 +335,106 @@ group('hook: validate-spec-compliance.mjs', () => { }) }) +group('hook: validate-story-source.mjs', () => { + const story = (content) => ({ tool_name: 'Write', tool_input: { file_path: resolve(ROOT, 'x.stories.js'), content } }) + + test('non-story path passes through (exit 0)', () => { + const r = runHook('.claude/hooks/validate-story-source.mjs', { + tool_name: 'Write', + tool_input: { file_path: resolve(ROOT, 'random/file.vue'), content: '' } + }) + assertEqual(r.code, 0) + }) + test('new story with literal docs + toSfc + PascalCase passes (exit 0)', () => { + const r = runHook( + '.claude/hooks/validate-story-source.mjs', + story( + [ + "import Foo from '@aziontech/webkit/foo'", + "import { toSfc } from '../../_shared/story-source'", + "tags: ['autodocs']", + 'const T = ``', + 'docs: { canvas: { sourceState: "shown" }, source: { code: toSfc(IMPORT, T) } }' + ].join('\n') + ) + ) + assertEqual(r.code, 0) + }) + test('docs as a function call blocks (exit 2)', () => { + const r = runHook( + '.claude/hooks/validate-story-source.mjs', + story( + [ + "import Foo from '@aziontech/webkit/foo'", + "import { toSfc } from '../../_shared/story-source'", + "tags: ['autodocs']", + "docs: runnableDocs({ imports: IMPORT })" + ].join('\n') + ) + ) + assertEqual(r.code, 2) + assertTrue(/docs-not-literal/.test(r.stderr), 'should flag docs function call') + }) + test('lowercase/kebab component tag blocks (exit 2)', () => { + const r = runHook( + '.claude/hooks/validate-story-source.mjs', + story( + [ + "import EmptyState from '@aziontech/webkit/empty-state'", + "import { toSfc } from '../../_shared/story-source'", + "tags: ['autodocs']", + 'const T = ``', + 'docs: { canvas: { sourceState: "shown" }, source: { code: toSfc(IMPORT, T) } }' + ].join('\n') + ) + ) + assertEqual(r.code, 2) + assertTrue(/lowercase-tag/.test(r.stderr), 'should flag lowercase tag') + }) + test('import binding not matching export subpath blocks (exit 2)', () => { + const r = runHook( + '.claude/hooks/validate-story-source.mjs', + story( + [ + "import Chip from '@aziontech/webkit/chips'", + "import { toSfc } from '../../_shared/story-source'", + "tags: ['autodocs']", + 'const T = ``', + 'docs: { canvas: { sourceState: "shown" }, source: { code: toSfc(IMPORT, T) } }' + ].join('\n') + ) + ) + assertEqual(r.code, 2) + assertTrue(/import-binding-mismatch/.test(r.stderr), 'should flag binding/subpath mismatch') + }) + test('hand-rolled transform blocks (exit 2)', () => { + const r = runHook( + '.claude/hooks/validate-story-source.mjs', + story( + ["import Foo from '@aziontech/webkit/foo'", "tags: ['autodocs']", 'docs: { source: { transform: (code) => code } }'].join('\n') + ) + ) + assertEqual(r.code, 2) + assertTrue(/handrolled-transform|missing-helper/.test(r.stderr), 'should flag hand-rolled / missing helper') + }) + test('nested