Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/scaffolder.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ You are the `scaffolder` sub-agent. You execute the `component-scaffold` skill v
1. Read the `=== SPEC ===` block. Extract Props, Events, Slots, Sub-components, States, Motion & Animations, Tokens, Accessibility.
2. Write the root `<name>.vue` using the canonical skeleton in the skill. Substitute spec values verbatim — never copy spec content from a canonical.
3. (Composition only) Write each sub-component `.vue` plus `injection-key.ts`.
4. Update `packages/webkit/package.json#exports` with one entry per public component.
4. Update `packages/webkit/package.json#exports` with one entry per public component. (Composition: the compound root → `index.ts`, the standalone `./<name>-root` → root `.vue` for tree-shaking, and one per public sub-component. See `.claude/rules/compound-api.md`.)
5. Stop. Do not run pnpm, do not write the story, do not write the `.figma.ts`.

## What you may NOT do
Expand Down
17 changes: 12 additions & 5 deletions .claude/rules/compound-api.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Rule: compound API — composition first, props for data, names from anatomy

Composition (`structure: composition`) components in the webkit layer expose a **compound API**: every public sub-component is importable on its own **and** attached to the root for dot-notation (`<Table.Row>`, `<Paginator.Button>`). This rule fixes how that API is shaped, named, and typed so every composition component reads the same way.
Composition (`structure: composition`) components in the webkit layer expose a **compound API**: every public sub-component is importable on its own **and** attached to the root for dot-notation (`<Table.Row>`, `<Paginator.Button>`). The root itself stays importable two ways — as the **compound** (`@aziontech/webkit/table`, every sub-component attached) and as a **standalone, tree-shakeable root** (`@aziontech/webkit/table-root`, nothing attached). This rule fixes how that API is shaped, named, and typed so every composition component reads the same way.

It rests on three decisions, in priority order:

Expand Down Expand Up @@ -28,12 +28,13 @@ export default Object.assign(Table, {

`Object.assign` returns `typeof Table & { Row: ..., Cell: ... }`, so the generated `index.d.ts` types `<Table.Row>` / `<Table.Cell>` with no manual annotation. **Do not hand-write `index.d.ts`** — `.d.ts` is gitignored (a build artifact); a hand-written one is never committed, and `vue-tsc` cannot derive types from a plain `index.js` (the package does not enable `allowJs`). The index must be `.ts`.

Both forms stay available to the consumer:
All forms stay available to the consumer:

```vue
<script setup>
import Table from '@aziontech/webkit/table' // compound — leads the docs
import TableRow from '@aziontech/webkit/table-row' // standalone — for tree-shaking
import Table from '@aziontech/webkit/table' // compound — leads the docs, dot-notation
import TableRoot from '@aziontech/webkit/table-root' // standalone root — tree-shaking (no sub-components attached)
import TableRow from '@aziontech/webkit/table-row' // standalone sub-component — tree-shaking
</script>

<template>
Expand All @@ -52,6 +53,11 @@ The dot-notation **must** use a PascalCase root binding (`Table`). `table` (lowe
```json
"./data/table": "./src/components/data/table/index.ts"
```
- **Standalone root export (tree-shaking)** — alongside the compound, add a flat `<name>-root` key pointing **directly at the root `.vue`**, bypassing `index.ts`:
```json
"./data/table-root": "./src/components/data/table/table.vue"
```
The compound path cannot be tree-shaken: `Object.assign(Table, { Row, Cell, ... })` in `index.ts` references every sub-component, so a bundler retains them all even when the consumer only renders `<Table>`. The `-root` key resolves straight to the root `.vue`, so a root-only import pulls in nothing else. Sub-components are already individually importable; this gives the **root** the same tree-shakeable path. The compound (`./table` → `index.ts`) still leads the docs — the `-root` key is the opt-in for consumers who want only the root.
- **Sub-component exports stay flat and unchanged** — one entry per public sub-component pointing at its `.vue` (`"./data/table-row": ".../table-row/table-row.vue"`).
- **The package ships source and is consumed as source** (the exports map points at `./src/...`, and `.vue` files already import `.ts` such as `injection-key.ts`). A `.ts` index is therefore safe — the consumer's build transpiles it the same way it transpiles every `.vue` and the shared `injection-key.ts`. `.d.ts` files are gitignored and generated at publish time by `.releaserc`'s `prepareCmd` (`vue-tsc --declaration --emitDeclarationOnly`), not in dev or CI.

Expand Down Expand Up @@ -91,6 +97,7 @@ Interactivity alone does not justify a sub-component. A clickable row is `@click
## Hard prohibitions

- Do not export composition sub-components without also attaching them to the root compound (`index.ts` via `Object.assign`).
- Do not ship the compound `index.ts` root export without **also** adding the standalone `<name>-root` export pointing at the root `.vue`. The compound path retains every sub-component (`Object.assign`), so a root-only consumer has no tree-shakeable way in without the `-root` key.
- Do not make the index a plain `index.js` — `vue-tsc` cannot derive types from it (no `allowJs`), so `<Root.Part>` ends up untyped. It must be `index.ts`.
- Do not hand-write `index.d.ts` — it is gitignored and generated at publish time (via `.releaserc`'s `prepareCmd`) from `index.ts`, not committed or built in dev.
- Do not invent a `Trigger` (or any overlay part name) on a component that has no `data-state="open|closed"`.
Expand All @@ -99,7 +106,7 @@ Interactivity alone does not justify a sub-component. A clickable row is `@click

## Enforcement

- `scaffolder` emits `index.ts` for every `structure: composition` component (the exports target plus the `Object.assign` compound) and adds its entry to `packages/webkit/package.json#exports`; it writes no per-component `package.json` (see [`.claude/skills/component-scaffold/SKILL.md`](../skills/component-scaffold/SKILL.md)).
- `scaffolder` emits `index.ts` for every `structure: composition` component (the exports target plus the `Object.assign` compound), adds **both** the compound root entry (`./<name>` → `index.ts`) **and** the standalone `./<name>-root` entry (→ root `.vue`) to `packages/webkit/package.json#exports`, and writes no per-component `package.json` (see [`.claude/skills/component-scaffold/SKILL.md`](../skills/component-scaffold/SKILL.md)).
- The spec's `## Usage` block (composition) **leads with the compound dot-notation** form; the standalone imports are documented as the tree-shaking alternative. See [`.specs/_template.md`](../../.specs/_template.md).
- `validate-references.mjs` blocks any phantom `@aziontech/webkit/*` import; the standalone sub-component paths must exist in `packages/webkit/package.json#exports`.
- Sub-agent prompts inject this rule alongside [`no-invention.md`](./no-invention.md), [`styling.md`](./styling.md), [`dependencies.md`](./dependencies.md), and [`migration.md`](./migration.md). Any deviation surfaces as `BLOCKED:` and stops the run.
12 changes: 7 additions & 5 deletions .claude/skills/component-scaffold/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Convert an approved `.specs/<name>.md` into:
- (Composition only) **One folder per sub-component** under the root component directory — `<name>/<name>-<part>/<name>-<part>.vue`. The full file name is preserved (`dialog-trigger.vue`, not `index.vue`) so error traces and editor breadcrumbs are unambiguous.
- (Composition only) `packages/webkit/src/components/webkit/<category>/<name>/injection-key.ts` at the root level (shared by every sub-component).
- (Composition only) `packages/webkit/src/components/webkit/<category>/<name>/index.ts` — the **compound API** that attaches every sub-component to the root (`<Root.Part>`) via `Object.assign`; because it is a `.ts` file, `vue-tsc` generates the adjacent `index.d.ts` (do not hand-write it — `.d.ts` is gitignored). See [`.claude/rules/compound-api.md`](../../rules/compound-api.md).
- New entry/entries in `packages/webkit/package.json#exports` — one per public component (root + each public sub-component). The public path keeps the short, flat form (`./overlay/dialog-trigger`) regardless of the folder nesting. For composition, the **root** export points at `index.ts`, not the root `.vue`.
- New entry/entries in `packages/webkit/package.json#exports` — one per public component (root + each public sub-component). The public path keeps the short, flat form (`./overlay/dialog-trigger`) regardless of the folder nesting. For composition, the **root** export points at `index.ts` (the compound), **plus** a standalone `./<name>-root` export pointing at the root `.vue` for tree-shaking — see [`.claude/rules/compound-api.md`](../../rules/compound-api.md).

Nothing else. The story, the Code Connect file, and the validation pass live in other skills.

Expand Down Expand Up @@ -197,12 +197,13 @@ packages/webkit/src/components/webkit/actions/button/

**The index is `.ts`, not `.js`** — `vue-tsc` cannot derive declarations from a plain `.js` (no `allowJs`), so `<Dialog.Trigger>` would be untyped. **Do not hand-write `index.d.ts`** — it is gitignored and generated at publish time (via `.releaserc`), not in dev. The package is consumed as source (the exports map points at `./src/...`, and `.vue` files already import `.ts` like `injection-key.ts`), so the consumer transpiles `index.ts` the same way.

5. **Update `packages/webkit/package.json#exports`** — add one entry per public component (root + each public sub-component) preserving alphabetical order inside the category. The **public export path stays flat** (`./<name>-<part>`) so consumers don't see the folder nesting; only the right-hand side changes:
5. **Update `packages/webkit/package.json#exports`** — add one entry per public component (the compound root, the standalone root, and each public sub-component) preserving alphabetical order inside the category. The **public export path stays flat** (`./<name>-<part>`) so consumers don't see the folder nesting; only the right-hand side changes:

The **composition root** points at `index.ts` (the compound); monolithic roots point at `<name>.vue`:
The **composition root** points at `index.ts` (the compound); monolithic roots point at `<name>.vue`. Composition also gets a **standalone `./<name>-root`** key pointing straight at the root `.vue` — the tree-shaking path, since the compound `index.ts` retains every sub-component through `Object.assign` (see [`.claude/rules/compound-api.md`](../../rules/compound-api.md)):

```json
"./<name>": "./src/components/webkit/<category>/<name>/index.ts",
"./<name>-root": "./src/components/webkit/<category>/<name>/<name>.vue",
"./<name>-trigger": "./src/components/webkit/<category>/<name>/<name>-trigger/<name>-trigger.vue",
"./<name>-content": "./src/components/webkit/<category>/<name>/<name>-content/<name>-content.vue"
```
Expand All @@ -211,6 +212,7 @@ packages/webkit/src/components/webkit/actions/button/

```json
"./overlay/popover": "./src/components/webkit/overlay/popover/index.ts",
"./overlay/popover-root": "./src/components/webkit/overlay/popover/popover.vue",
"./overlay/popover-trigger": "./src/components/webkit/overlay/popover/popover-trigger/popover-trigger.vue",
"./overlay/popover-content": "./src/components/webkit/overlay/popover/popover-content/popover-content.vue"
```
Expand All @@ -224,7 +226,7 @@ packages/webkit/src/components/webkit/actions/button/
## Rules

- Every prop, event, slot, and sub-component MUST come from the spec — no inventions. (`validate-spec-compliance.mjs` enforces this on Write; it will reject the run if you stray.)
- (Composition only) Emit the compound `index.ts` (vue-tsc generates `index.d.ts` at publish; never hand-write it) and point the root export → `index.ts`; member names mirror the component's anatomy. Never invent overlay part names (`Trigger`/`Content`) on a component with no open/closed state. See [`.claude/rules/compound-api.md`](../../rules/compound-api.md).
- (Composition only) Emit the compound `index.ts` (vue-tsc generates `index.d.ts` at publish; never hand-write it) and point the root export → `index.ts`; **also add the standalone `./<name>-root` export → root `.vue`** for tree-shaking. Member names mirror the component's anatomy. Never invent overlay part names (`Trigger`/`Content`) on a component with no open/closed state. See [`.claude/rules/compound-api.md`](../../rules/compound-api.md).
- Use the canonicals (`button.vue`, `card-pricing.vue`) as the **shape** reference — but substitute spec content, never copy spec content from a canonical.
- `withDefaults` mirrors the spec's Default column **exactly**. An optional string prop that holds renderable text defaults to `''` (`value: ''`, `label: ''`) — never `value: undefined`, never the literal `'undefined'`. Reserve `undefined` (unquoted) for props where absence ≠ empty: controlled state (`open`, `modelValue`) or an optional resource whose presence toggles rendering (`src`). When the spec's Default cell is wrong on this point, stop with `BLOCKED: prop <name> defaults to 'undefined' — should be '' (empty string)`; do not silently copy it.
- All visual tokens come from [`.claude/docs/DESIGN.md`](../../docs/DESIGN.md). No HEX, no Tailwind palette, no raw typography.
Expand Down Expand Up @@ -267,7 +269,7 @@ packages/webkit/src/components/webkit/actions/button/
- [ ] Composition: every sub-component is **its own folder** under the component root — `<name>/<name>-<part>/<name>-<part>.vue`.
- [ ] Composition: `injection-key.ts` written at the root level (sibling of `<name>.vue`), not inside any sub-component folder. Root imports it via `./injection-key`; sub-components via `../injection-key`.
- [ ] Composition: `index.ts` written at the root level (sibling of `<name>.vue`), attaching every sub-component to the root via `Object.assign` (no hand-written `index.d.ts` — vue-tsc generates it at publish). Member names mirror the spec's anatomy (no invented `Trigger`/`Content` on a non-overlay component).
- [ ] New entries added to `packages/webkit/package.json#exports`. Public paths stay flat (`./<name>-<part>`); right-hand paths reflect the folder nesting. The composition root export points at `index.ts`.
- [ ] New entries added to `packages/webkit/package.json#exports`. Public paths stay flat (`./<name>-<part>`); right-hand paths reflect the folder nesting. The composition root export points at `index.ts`; a standalone `./<name>-root` export points at the root `.vue` (tree-shaking).
- [ ] No HEX / Tailwind palette / raw typography / `any` / `@ts-ignore`.
- [ ] `defineOptions.name` is PascalCase and matches the directory.
- [ ] `data-testid` fallback equals `'<category>-<name>'` on the root and `'<category>-<name>__<part>'` on each sub-component.
Expand Down
10 changes: 6 additions & 4 deletions .specs/_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ import MyComponent from '@aziontech/webkit/<name>'
<!-- Composition example (structure: composition):
LEAD with the compound dot-notation form — one import of the root, every
sub-component reached as `<Root.Part>`. The standalone imports remain
available (and are the tree-shaking path) but the docs lead with compound.
available (and are the tree-shaking path) — the root via `<name>-root`,
each sub-component via `<name>-<part>` — but the docs lead with compound.
The root binding MUST be PascalCase (`Dialog`); a lowercase root would
collide with a native element and not resolve. See
`.claude/rules/compound-api.md`.
Expand All @@ -72,11 +73,12 @@ import DialogClose from '@aziontech/webkit/dialog-close'
</template>
```

Tree-shaking alternative — the same sub-components as standalone imports:
Tree-shaking alternative — the standalone root + each sub-component from its own
entry (no `Object.assign` compound pulled in):

```vue
<script setup>
import Dialog from '@aziontech/webkit/dialog'
import Dialog from '@aziontech/webkit/dialog-root'
import DialogTrigger from '@aziontech/webkit/dialog-trigger'
import DialogContent from '@aziontech/webkit/dialog-content'
</script>
Expand Down Expand Up @@ -261,7 +263,7 @@ Canonical layout — matches `apps/storybook/src/stories/webkit/actions/button/B
- Do not duplicate the `## Usage` block from the spec inside the Storybook story body. The block is injected once into `parameters.docs.description.component` by the storybook-write skill; copy it nowhere else.
- Do not edit `.claude/docs/DESIGN.md`, `.claude/docs/COMPONENT_REQUIREMENTS.md`, or `.claude/docs/PRIMEVUE_ABSTRACTION.md`.
- Do not edit the root `package.json` or `.github/workflows/*`.
- Do not export composition sub-components without attaching them to the root compound (`index.ts` via `Object.assign`; vue-tsc generates `index.d.ts` — never hand-write it); the root export and the root `package.json` main/module point at `index.ts`, `types` at `index.d.ts`. Do not invent overlay part names (`Trigger` / `Content`) on a component with no `data-state=open|closed`, and do not collapse a slot-shaped concern into a config-array prop. See `.claude/rules/compound-api.md`.
- Do not export composition sub-components without attaching them to the root compound (`index.ts` via `Object.assign`; vue-tsc generates `index.d.ts` — never hand-write it); the root export points at `index.ts`, and a standalone `./<name>-root` export points at the root `.vue` (tree-shaking). Do not invent overlay part names (`Trigger` / `Content`) on a component with no `data-state=open|closed`, and do not collapse a slot-shaped concern into a config-array prop. See `.claude/rules/compound-api.md`.
- Do not change `structure` after `status: approved`. To change structure, bump `spec_version` and re-author the spec.
- Do not create files outside the paths declared by your task (the orchestrator tells you exactly which files to write).
- Do not run `git` commands, `pnpm install`, or any command that changes the lockfile.
Expand Down
Loading