diff --git a/.claude/agents/webkit-adopter.md b/.claude/agents/webkit-adopter.md new file mode 100644 index 0000000000..496585d735 --- /dev/null +++ b/.claude/agents/webkit-adopter.md @@ -0,0 +1,50 @@ +--- +name: webkit-adopter +description: Runs and explains the @aziontech/webkit adoption flow — `npx @aziontech/webkit init`. Sets up deps, lint configs, pre-commit, the webkit MCP, and the Claude Code bundle in this project, idempotently. +scope: general +--- + +# Agent: webkit-adopter + +## Role + +You own onboarding this project onto `@aziontech/webkit`. You run the adoption command, explain what each step did, and verify the wiring — safely and idempotently. + +## The command + +``` +npx @aziontech/webkit init +``` + +Options: + +- `--dry-run` — print the plan without writing anything. Always offer this first if the user is unsure. +- `--strict` (default) / `--recommended` — which ESLint preset the generated config uses. + +## What `init` does + +1. Records the design-system dependencies in `package.json` — `@aziontech/webkit`, `@aziontech/theme`, `@aziontech/icons`, and dev tools (`@aziontech/eslint-plugin-webkit`, `@aziontech/webkit/stylelint-config`, `eslint`, `stylelint`, `vue-eslint-parser`, `postcss-html`, `postcss-scss`, `husky`). It does not run an install — remind the user to run their package manager. +2. Writes `eslint.config.mjs` (flat) wiring the webkit preset, unless an ESLint config already exists — in which case it prints a merge snippet instead of overwriting. +3. Writes `.stylelintrc.json` (extending `@aziontech/webkit/stylelint-config`, with the `.vue` / `.scss` custom syntaxes wired), unless a Stylelint config already exists — in which case it prints a merge snippet. +4. Merges the `webkit` server into `.mcp.json`. +5. Adds a `prepare` script (`husky`) to `package.json` and writes `.husky/pre-commit` to lint on commit. Running the package-manager install runs `prepare`, which activates the hooks. +6. Copies the Claude Code bundle (rules, the `webkit-usage` skill, agents) into `.claude/` — only files that are missing, never overwriting local edits. +7. Appends a `@aziontech/webkit` fragment to `CLAUDE.md`, guarded by a marker so it is added once. +8. If a `src/main.ts` / `src/main.js` exists without the theme import, advises adding `import '@aziontech/theme'` and `import '@aziontech/icons'`. + +## Idempotency + +`init` is safe to run repeatedly. Existing files are skipped or merged, never clobbered; the MCP server and the CLAUDE.md fragment are added only if absent. Re-run it after upgrading without fear. + +## How you work + +1. Offer `--dry-run` first so the user sees the plan. +2. Run `init`, then relay the printed actions honestly (what was written vs skipped). +3. Remind the user to run their package-manager install — it fetches the deps and runs `prepare` (husky), which activates the git hooks. +4. Point them at the copied rules and the `webkit-usage` skill for day-to-day usage. + +## What you do not do + +- Do not hand-edit the consumer's entry file automatically — advise the import; let them apply it. +- Do not overwrite an existing ESLint config or existing `.claude/` files. +- Do not run a package install on their behalf unless they ask. diff --git a/.claude/agents/webkit-adoption-auditor.md b/.claude/agents/webkit-adoption-auditor.md new file mode 100644 index 0000000000..b135295419 --- /dev/null +++ b/.claude/agents/webkit-adoption-auditor.md @@ -0,0 +1,48 @@ +--- +name: webkit-adoption-auditor +description: Measures how much of an app already renders through @aziontech/webkit versus hand-rolled UI, and produces a prioritized backlog of custom components that have a webkit equivalent to adopt. +scope: general +--- + +# Agent: webkit-adoption-auditor + +## Role + +You measure design-system adoption. Given the app source (or a chosen set of screens), you produce a **coverage scorecard** — the share of rendered UI that comes from `@aziontech/webkit` versus hand-rolled or third-party custom UI — and a **prioritized adoption backlog** of custom UI that already has a webkit equivalent it should switch to. You are the executable companion to the `webkit-ds-adoption` skill, and you pair with the `webkit-prefer-over-custom` lint. + +## What you check + +### Coverage (the count) + +- Scan `.vue` files (templates + ` + + +``` + +## Wrong + + +```html + +
Open settings
+ + + + + + +
…
+``` diff --git a/.claude/rules/webkit-component-states.md b/.claude/rules/webkit-component-states.md new file mode 100644 index 0000000000..0be39bce97 --- /dev/null +++ b/.claude/rules/webkit-component-states.md @@ -0,0 +1,78 @@ +# Rule: render every component state through data-\* and webkit's state components + +Every interactive component has a **state surface** — the set of conditions it must visibly +handle. webkit components own the rendering of their states; your screen supplies only the +**trigger** (a boolean prop, or the async data your app owns). The same applies to components +you author in this project: decide which states each one handles and render them through +`data-*` attributes and webkit's own state components (`Skeleton`, `EmptyState`, +`HelperText`) — never an ad-hoc spinner or "no results" string improvised per screen. +This pattern is convention + code review in this project; the tokens used inside state +styling are still checked mechanically by the stylelint config and the +`webkit/no-hardcoded-color` ESLint rule. + +The canonical states and how each renders: + +| State | Trigger | How it renders | +| ----------------- | ------------------------- | --------------------------------------------------------------------- | +| `disabled` | `disabled` prop | `data-disabled` + disabled tokens; not focusable | +| `loading` | `loading` prop | `data-loading` + `Skeleton` / spinner affordance; interaction blocked | +| `invalid` | validation (form field) | `data-invalid` + error tokens + a `HelperText` message | +| `readonly` | `readonly` prop | `data-readonly`; value visible, not editable | +| `empty` | no data (data-driven) | the `EmptyState` component, not a hand-written "no results" string | +| `error` | load/action failure | an error slot / message, never a bare thrown error | +| `open` / `closed` | `open` (+ `v-model:open`) | `data-state="open"` / `data-state="closed"` (overlays) | + +Not every component has every state — a button has `disabled` + `loading`; a data table adds +`empty` + `error`. Handle exactly the states the component owns, and handle all of them. + +## Do + +- Switch state styling with a `data-*` attribute on the element, styled by a Tailwind + `data-[...]:` variant inline in its `class` — the state decision lives in the markup. +- Reuse the shipped state components: `Skeleton` for loading placeholders, `EmptyState` for + no-data, `HelperText` for validation messages. +- Keep the trigger in the app: fetch data in your page/store/composable and pass `loading`, + `items`, or `error` down as props. Components render states; they do not fetch. +- When authoring a local component, list its state surface up front and render every state — + a silent empty list or an invisible error is a missing state, not a smaller component. + +## Do not + +- Never render a state with a `:class` ternary (`loading ? 'opacity-80' : ''`) — use + `data-loading` + a `data-[loading]:` variant. +- Never hand-roll a spinner, shimmer, or "Nothing here" message that `Skeleton` / + `EmptyState` already provides. +- Never fetch data or own async state inside a shared component just to drive its states — + the trigger always comes from the consumer. +- Never ship a data-driven view that only handles the happy path — `loading`, `empty`, and + `error` are part of the component, not per-screen afterthoughts. + +## Correct + + +```html + +
+ + + +
+``` + +## Wrong + + +```html + +
+ +

Nothing here

+ +
+``` diff --git a/.claude/rules/webkit-component-structure.md b/.claude/rules/webkit-component-structure.md new file mode 100644 index 0000000000..0ae5fc70cd --- /dev/null +++ b/.claude/rules/webkit-component-structure.md @@ -0,0 +1,95 @@ +# Rule: one folder layout, one ` +``` + +## Wrong + + +```vue + + + + +``` diff --git a/.claude/rules/webkit-composables.md b/.claude/rules/webkit-composables.md new file mode 100644 index 0000000000..57f789875d --- /dev/null +++ b/.claude/rules/webkit-composables.md @@ -0,0 +1,88 @@ +# Rule: composables — one shape, `readonly` outward, cleanup on scope dispose + +A composable (`useXxx`) is a public building block: imported, typed, and relied on across +the app, so it follows one contract — a predictable surface, a predictable lifetime, and +no surprises on destructure. The `webkit/authoring-standards` ESLint rule blocks the two +mechanical violations (returning a `reactive()` object, authoring the file as `.js`); the +rest of the contract is convention, enforced in review. + +## Do + +- Name it `useXxx`, one composable per file, kebab filename, always `.ts` + (`use-controllable.ts`). Co-locate it in the component's own `composables/` folder when + it is specific to that component; put shared ones in your project's `src/composables/`. +- Return an **object of refs / computed / functions**. Wrap reactive state that escapes + in `readonly()`; expose writes only through returned setter functions. +- Accept arguments as `MaybeRefOrGetter` and resolve them with `toValue()`, so a + caller may pass a ref, a getter, or a raw value. +- Use generics and an **explicit return type**; export the return type when a consumer + needs to hold it (`UseControllableReturn`). +- Tear down everything you start: remove every listener, timer, `requestAnimationFrame`, + and observer in `onScopeDispose` — it also fires inside a detached `effectScope()`, + unlike `onUnmounted`, so the composable never leaks between tests. +- Call lifecycle composables **synchronously in `setup`** — Vue can only bind lifecycle + hooks during synchronous setup. +- Use `useId()` for generated ids (SSR-stable). Reuse VueUse primitives + (`useEventListener`, `onClickOutside`) — they already own their cleanup. +- Make instance vs singleton deliberate: state created **inside** the function is + per-instance; state at **module scope** is a shared singleton — say which one it is in + the name and the doc comment, never by accident of where a `ref` was declared. +- For state shared root-to-leaf in a compound component, write a context composable: a + typed `InjectionKey` next to the root, and a `useXxxContext()` that injects and + **throws a clear error when used outside its provider**. Only the root calls + `provide()`; leaves only inject through the composable. + +## Do not + +- Never return a `reactive()` object — destructuring loses reactivity. Blocked by lint. +- Never author a composable as `.js` — consumers need the derivable return type. Blocked + by lint. +- Never expose mutable state outward, and never accept a `reactive` object as an + argument — `readonly()` out, `MaybeRefOrGetter` + `toValue()` in. +- Never use `any` or omit the return type. +- Never register a listener/timer/observer without `onScopeDispose` cleanup (or a VueUse + primitive that owns it). +- Never call a lifecycle composable after an `await` or outside `setup`. +- Never roll your own id counter — use `useId()`. +- Never `provide()` from anything other than a context composable invoked at a component + root. +- Never mix data fetching, HTTP clients, or store logic into a UI composable — that + belongs in your data layer; components reach loading/empty/error states through props. + +## Correct + + +```ts +// use-controllable.ts — pure, per-instance, readonly outward +export interface UseControllableReturn { + value: Readonly> + setValue: (next: T) => void +} + +export function useControllable( + modelValue: MaybeRefOrGetter, + fallback: T, + emit: (value: T) => void +): UseControllableReturn { + const internal = ref(fallback) as Ref + const value = computed(() => toValue(modelValue) ?? internal.value) + const setValue = (next: T) => { + internal.value = next + emit(next) + } + return { value: readonly(value) as Readonly>, setValue } +} +``` + +## Wrong + + +```ts +// Returns reactive() (destructure loses reactivity), exposes mutable +// state directly, and registers a listener that is never removed. +export function useDropdown(open) { + const state = reactive({ open }) + window.addEventListener('keydown', onKeydown) + return state +} +``` diff --git a/.claude/rules/webkit-construction-standards.md b/.claude/rules/webkit-construction-standards.md new file mode 100644 index 0000000000..f6f2e55b84 --- /dev/null +++ b/.claude/rules/webkit-construction-standards.md @@ -0,0 +1,38 @@ +# Rule: building your own Vue components — the construction standards + +When this project builds a component of its own (a screen part the design system does not +ship), it follows the same construction standards the design system's components are built +with. One pattern per concern — follow ✅, avoid ❌. + +| Pattern | ❌ Avoid | ✅ Do | +| --------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| Two-way value | `defineProps({ modelValue })` + `emit('update:modelValue')` | `const model = defineModel()` | +| Props | runtime `defineProps({ kind: { type: String } })` · `any` | typed `withDefaults(defineProps(), {…})` + JSDoc per prop | +| Prop vocabulary | `variant` · `sm/md/lg` · `isDisabled` · `closeable` | `kind` · `small/medium/large` · `disabled` · `dismissible` | +| Emits | `defineEmits(['click'])` · `emit('click', item)` | typed `defineEmits<{ 'item-click': [e, item] }>()` — DOM event first | +| Slots | `` with no declaration | typed `defineSlots<{ default(): unknown }>()` | +| Composables | `return reactive({…})` · listeners with no cleanup | `return { value: readonly(v), set }` · args via `toValue()` · cleanup in `onScopeDispose` | +| Styling | `const kindClasses = {…}` · ` +``` diff --git a/.claude/rules/webkit-testid.md b/.claude/rules/webkit-testid.md new file mode 100644 index 0000000000..3845a73703 --- /dev/null +++ b/.claude/rules/webkit-testid.md @@ -0,0 +1,80 @@ +# Rule: `data-testid` — derived name on the root, overridable + +Every webkit component root carries a **`data-testid`** derived from the component's own +name — **`-`**, or **`input-`** for input components (`input-chip`, +`content-badge`, `feedback-toast`, `data-table`). Your tests target that attribute instead +of inventing selectors, and you override it per instance by passing `data-testid` on the +tag. Components you author in this project follow the same pattern. This is a convention +enforced by review — no lint rule checks it, so apply it deliberately. + +## Do + +- Target components in tests via their derived `data-testid`, never via DOM structure or + internal classes. +- Override the testid per instance when a page renders more than one of the same + component: `