plugins/acss-kit/skills/kit-core/SKILL.md
Internal orchestrator for /kit-create, /kit-list, /kit-sync, /kit-update and Form/HTML/Style-Tune modes. Per-component generation lives in component-<name> skills; do not auto-trigger for component requests.
npx skillsauth add shawn-sandy/acss-plugins kit-coreInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generate fpkit-style React components directly into a developer's project. No @fpkit/acss npm package required. Only React + sass needed.
This skill generates self-contained, production-quality React components from markdown specs that embed the actual TSX/SCSS code as fenced code blocks alongside accessibility documentation. The developer owns the generated code and can freely modify it. Components use local imports — never @fpkit/acss.
sass or sass-embedded in devDependenciesIf the session is in plan mode, call ExitPlanMode before doing anything else. Every subsequent step writes files (ui.tsx, component TSX/SCSS), edits .acss-target.json, or runs Python scripts via Bash — plan mode would block all of it.
Stay in plan mode only when it is absolutely necessary — i.e. the user explicitly asked for a dry-run / preview ("show me the plan first", "what would /kit-add do", "don't generate yet"). In that case, narrate the dependency tree and file list from Step B4 without invoking Write/Edit/Bash, and wait for approval before re-entering this skill.
Run this check at the start of every /kit-add invocation.
Read tsconfig.json and package.json to confirm React + TypeScript is present.
Read package.json. Look for sass or sass-embedded in devDependencies.
If neither is found, output:
sass or sass-embedded not found in devDependencies.
Run: npm install -D sass
Then re-run: /kit-add <component>
Stop. Do not generate any files.
Run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/detect_target.py <project_root> to read or initialize .acss-target.json.
If the script returns "source": "generated", use the reported componentsDir. Skip the prompt.
If the script returns "source": "none", ask:
Where should components be generated? (default: src/components/fpkit/)
After the developer answers (or accepts the default), write .acss-target.json at the project root:
{ "componentsDir": "src/components/fpkit" }
Commit this file — /kit-add reads it on subsequent runs as the source of truth for import paths.
Remember the answer for the current session as well, so subsequent /kit-add calls don't re-read the file unnecessarily.
Run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/detect_stack.py <project_root> to classify framework, bundler, CSS pipeline, and entrypoint. Capture the JSON.
If source: "detected", merge the result into .acss-target.json under a stack key (preserve existing componentsDir/utilitiesDir):
{
"componentsDir": "src/components/fpkit",
"stack": {
"framework": "vite",
"bundler": "vite",
"cssPipeline": ["sass"],
"tsconfig": true,
"entrypointFile": "src/main.tsx",
"detectedAt": "2026-05-01T00:00:00Z"
}
}
Skip re-detection on later runs unless package.json's mtime is newer than stack.detectedAt.
If source: "unknown", surface the reasons array verbatim and ask the developer to confirm framework + entrypoint by hand. Record their answer under stack so subsequent runs skip the prompt.
If source: "none" (no React project root), halt — /kit-add cannot proceed.
Use stack.cssPipeline to tailor advice: when it contains "tailwind", note that fpkit components and Tailwind utilities coexist but the user should not migrate component SCSS into @apply. When it omits "sass", fall through to Step A2's install instruction.
Check whether ui.tsx and foundation.css exist in the target directory.
Three cases:
First-run (neither ui.tsx nor foundation.css present):
${CLAUDE_PLUGIN_ROOT}/assets/foundation/ui.tsx into <target>/ui.tsx${CLAUDE_PLUGIN_ROOT}/assets/foundation/foundation.css into <target>/foundation.css${CLAUDE_PLUGIN_ROOT}/assets/foundation/sass/ tree into <target>/foundation/sass/Created ui.tsx (foundation component — do not delete)
Created foundation.css + foundation/sass/ (fpkit base layer — import once in your app entry)
Add to your app entry:
import './components/fpkit/foundation.css'
Existing install (ui.tsx present but foundation.css absent):
foundation.css (CSS reset, base typography, spacing tokens, @layer ordering)
was not found in this project. Adding it will:
- Apply a CSS reset and base element styles
- Set @layer foundation, components, utilities, theme ordering
- Add --spacing-*, --shadow-*, and font-scale tokens
To revert: delete foundation.css and remove its import.
Add foundation.css now?
foundation.css and sass/ tree; print the import hint above.Already installed (both ui.tsx and foundation.css present):
The canonical layer order for consumer projects is:
@layer foundation, components, utilities, theme;
foundation.css emits this declaration at the top. Components generated by
/kit-add wrap their SCSS in @layer components { }. Theme files
(light.css / dark.css) must declare into @layer theme. The
utility-class bridge and utility files (utilities.css, token-bridge.css,
per-family partials) must declare into @layer utilities.
Cascade outcome: theme > utilities > components > foundation.
Read the component's reference doc:
../../component-{name}/reference.mdreferences/inline-components.md (Badge, Tag, Heading, Text, Details, Progress)If the component is not found, inform the developer. Run /kit-list to show available components.
Every reference doc has a Generation Contract section:
## Generation Contract
export_name: ComponentName
file: component-name.tsx
scss: component-name.scss
imports: UI from '../ui'
dependencies: [dep1, dep2]
This tells Claude exactly what files to create and what dependencies to resolve.
Reference docs follow the canonical embedded-markdown shape with three required sections beyond the Generation Contract — read them all before writing any files:
## TSX Template — fenced tsx block with the full component implementation. Copy this verbatim into the generated .tsx file. Substitute {{IMPORT_SOURCE:...}} / {{NAME}} / {{FIELDS}} placeholders at write time when present.## SCSS Template — fenced scss block with the canonical styles. Copy verbatim into the generated .scss file.## Accessibility — WCAG 2.2 AA criteria the component addresses (keyboard, ARIA, focus, contrast, target size). Don't strip a11y patterns out of the TSX/SCSS during generation; they're load-bearing.If a reference doc is missing any of these three sections, fall back to the older "Key Pattern" / "Full Implementation Reference" / "SCSS Pattern" shape. The verification banner at the top of each reference.md records its verification status against fpkit source; treat any without a banner as legacy and synthesize from the available pieces.
Walk dependencies recursively using each dependency's own Generation Contract. Build the full list of files that will be created.
Example for Dialog:
dialog.tsx + dialog.scss
→ button.tsx + button.scss
→ icon-button.tsx + icon-button.scss
→ icon.tsx (no scss)
Before generating any files, display:
Generating the following files in src/components/fpkit/:
New:
ui.tsx (foundation — React only)
icon.tsx
button.tsx + button.scss
icon-button.tsx + icon-button.scss
dialog.tsx + dialog.scss
Skipped (already exist):
(none)
Proceed? [Enter to continue, Ctrl+C to cancel]
Wait for confirmation before proceeding.
Generate leaf dependencies first, then composite components.
Order example:
icon.tsx (no deps)button.tsx + button.scssicon-button.tsx + icon-button.scssdialog.tsx + dialog.scssFor each file:
.tsx)Imports:
// Always import UI from local path
import UI from '../ui'
import React from 'react'
// Other local deps
import Button from '../button/button'
Types:
// Inline all types in the component file
// Never import types from other generated components
export type ButtonProps = {
children?: React.ReactNode
disabled?: boolean
// ...
} & React.ComponentPropsWithoutRef<'button'>
No external imports other than React and local project files.
Condensed utilities:
useDisabledState — Inline the condensed ~50-line version from references/accessibility.mdresolveDisabledState — Inline as a one-liner: const resolveDisabledState = (d?: boolean, id?: boolean) => d ?? id ?? false.scss)Always use CSS custom properties with hardcoded fallbacks:
.btn {
font-size: var(--btn-fs, 0.9375rem);
padding-block: var(--btn-padding-block, calc(var(--btn-fs, 0.9375rem) * 0.5));
padding-inline: var(--btn-padding-inline, calc(var(--btn-fs, 0.9375rem) * 1.5));
border-radius: var(--btn-radius, 0.375rem);
background: var(--btn-bg, transparent);
color: var(--btn-color, var(--color-text, currentColor));
// Global token references MUST have fallbacks:
background: var(--btn-primary-bg, var(--color-primary, #0066cc));
}
Rules:
--{component}-{element?}-{variant?}-{property}--color-primary) always get hardcoded fallbacksreferences/css-variables.md for full naming conventionsAlways use aria-disabled instead of the native disabled attribute for buttons and interactive elements.
Why: Native disabled removes the element from keyboard tab order — keyboard and screen-reader users can't reach the control to discover it's disabled or access any explanation. aria-disabled keeps it focusable so screen readers can announce the disabled state.
Condensed useDisabledState (inline in button.tsx and any interactive component):
// Condensed useDisabledState — WCAG 2.1.1 compliant disabled pattern
// Uses aria-disabled instead of native disabled to maintain keyboard access
function useDisabledState(
disabled: boolean | undefined,
handlers: { onClick?: React.MouseEventHandler<HTMLButtonElement>; onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement> } = {}
) {
const isDisabled = Boolean(disabled)
const disabledProps = {
'aria-disabled': isDisabled,
className: isDisabled ? 'is-disabled' : '',
}
const wrappedHandlers = {
onClick: handlers.onClick
? (e: React.MouseEvent<HTMLButtonElement>) => {
if (isDisabled) { e.preventDefault(); e.stopPropagation(); return }
handlers.onClick!(e)
}
: undefined,
onKeyDown: handlers.onKeyDown
? (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (isDisabled) { e.preventDefault(); e.stopPropagation(); return }
handlers.onKeyDown!(e)
}
: undefined,
}
return { disabledProps, handlers: wrappedHandlers }
}
SCSS disabled styling:
.btn {
&[aria-disabled="true"],
&.is-disabled {
opacity: var(--btn-disabled-opacity, 0.6);
cursor: var(--btn-disabled-cursor, not-allowed);
pointer-events: none;
}
}
Always include visible focus indicators:
.btn:focus-visible {
outline: var(--btn-focus-outline, 2px solid currentColor);
outline-offset: var(--btn-focus-outline-offset, 2px);
}
Prefer semantic elements over roles:
<button> not <div role="button"><nav> not <div role="navigation"><dialog> not <div role="dialog">// {Component} component
// CSS variables with fallback defaults — override in :root or scoped selectors
.{component} {
// Layout
display: var(--{component}-display, block);
// Spacing
padding-block: var(--{component}-padding-block, 1rem);
padding-inline: var(--{component}-padding-inline, 1rem);
// Typography
font-size: var(--{component}-fs, 1rem);
font-weight: var(--{component}-fw, 400);
// Visual
background: var(--{component}-bg, transparent);
color: var(--{component}-color, currentColor);
border: var(--{component}-border, none);
border-radius: var(--{component}-radius, 0);
}
fpkit uses data-* attributes for variants (not BEM modifiers):
// Size variants via data-btn attribute
.btn[data-btn~="sm"] { font-size: var(--btn-size-sm, 0.8125rem); }
.btn[data-btn~="lg"] { font-size: var(--btn-size-lg, 1.125rem); }
.btn[data-btn~="block"] { width: 100%; }
// Style variants via data-style attribute
.btn[data-style="outline"] {
background: var(--btn-outline-bg, transparent);
border: var(--btn-outline-border, 1px solid currentColor);
}
// Color variants via data-color attribute
.btn[data-color="primary"] {
background: var(--btn-primary-bg, var(--color-primary, #0066cc));
color: var(--btn-primary-color, var(--color-text-inverse, #fff));
}
The [data-btn~="value"] selector matches space-separated words — data-btn="sm block" matches both [data-btn~="sm"] and [data-btn~="block"].
After all files are generated, show:
Generated components in src/components/fpkit/:
Created:
button/button.tsx
button/button.scss
Skipped (already existed):
(none)
Import and usage:
import Button from './components/fpkit/button/button'
import './components/fpkit/button/button.scss'
<Button type="button" onClick={handleClick}>Click me</Button>
<Button type="button" disabled>Disabled (stays focusable)</Button>
<Button type="button" data-color="primary" data-btn="lg">Primary Large</Button>
After Step F, run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/verify_integration.py <project_root> to check that the user's entrypoint actually imports the artifacts that were just written.
reasons array. Print each reason as a numbered fix-up list. Do not auto-edit the entrypoint — the developer must add imports themselves so they retain ownership of the wiring.The verifier reads stack.entrypointFile from .acss-target.json, so Step A3.1 must have run successfully. If stack.entrypointFile is missing or stale, the verifier exits 1 with a reason pointing back to detect_stack.py.
/kit-list workflow — read-only inspectionThis is a separate command flow from /kit-add (Steps A–G above). /kit-list never writes files. It globs ../../component-*/SKILL.md for the full component list and reads individual reference.md files for per-component detail.
Glob ../../component-*/SKILL.md and read each skill's name: and description: frontmatter fields to enumerate all available components. Append [HTML] to any component whose reference.md contains a ## HTML Template section (i.e. /kit-add --target=html can generate it). Components without that section exist as React only and /kit-add --target=html will warn.
Output format:
Available Components (acss-kit)
Simple (no dependencies):
badge — Status indicator with count or text
tag — Categorical label with optional removal
heading — Semantic heading (h1-h6) with styles
text — Inline/block text with variants
Interactive (useDisabledState pattern):
button — Primary interactive element (all variants) [HTML]
link — Accessible anchor with hover/visited states
Layout:
card — Compound component (Card.Title, Card.Content, Card.Footer) [HTML]
nav — Navigation landmark with compound Nav.List, Nav.Item
Complex (multiple dependencies):
alert — Severity-aware notification (needs icon) [HTML]
dialog — Modal dialog with focus trap (needs button, icon) [HTML]
form — Form controls (input, textarea, select, checkbox, toggle)
Run /kit-add <component> to generate React components, or /kit-add --target=html <component> for static HTML versions of the [HTML]-marked entries.
Read the component's reference.md from ../../component-<name>/reference.md (or its entry in references/inline-components.md for inline-only components) and display:
reference.md has a ## HTML Template section; print Verified if present (i.e. /kit-add --target=html can generate it), otherwise Not yet — React onlyExample output for /kit-list badge:
Component: Badge
File: badge.tsx + badge.scss
Dependencies: none (simple component)
HTML output: Not yet — React only (/kit-add --target=html will warn)
Props:
children? ReactNode — Content (typically numbers or short text)
variant? 'rounded' — Visual variant
...UI props — All standard <sup>-element HTML props
CSS Variables:
--badge-bg Background color (default: #e9ecef)
--badge-color Text color (default: #212529)
--badge-fs Font size (default: 0.75rem)
--badge-fw Font weight (default: 600)
--badge-padding-inline Horizontal padding (default: 0.375rem)
--badge-padding-block Vertical padding (default: 0.125rem)
--badge-radius Border radius (default: 0.25rem)
Usage:
import Badge from './badge/badge'
import './badge/badge.scss'
<Badge aria-label="3 unread messages">3</Badge>
<Badge variant="rounded" aria-label="99+ notifications">99+</Badge>
Run /kit-add badge to generate this component.
If the component name is unknown, print "Component '<name>' not found. Run /kit-list (no args) to see the full catalog." and stop.
Read these before generating components:
| Document | Purpose |
|----------|---------|
| references/architecture.md | UI base component, polymorphic pattern, as prop |
| references/css-variables.md | CSS variable naming conventions, fallback strategy |
| references/accessibility.md | WCAG patterns, aria-disabled, condensed useDisabledState |
| references/composition.md | Compound components, generation decision tree |
| references/inline-components.md | Inline-only components (Badge, Tag, Heading, Text, Details, Progress) |
| ../../component-button/reference.md | Button — canonical shape ✓ |
| ../../component-icon-button/reference.md | IconButton (wraps Button + XOR aria-label/aria-labelledby) — canonical shape ✓ |
| ../../component-alert/reference.md | Alert with severity levels, auto-dismiss — canonical shape ✓ |
| ../../component-card/reference.md | Card compound component (Title, Content, Footer) — canonical shape ✓ |
| ../../component-dialog/reference.md | Dialog with native <dialog> — canonical shape ✓ |
| ../../component-popover/reference.md | Popover via native HTML Popover API — canonical shape ✓ |
| ../../component-table/reference.md | Table compound (Caption, Head, Body, Row, HeaderCell, Cell) — canonical shape ✓ |
| ../../component-img/reference.md | Img with lazy loading + SVG-gradient placeholder — canonical shape ✓ |
| ../../component-icon/reference.md | Icon with built-in 9-icon SVG dispatch — canonical shape ✓ |
| ../../component-link/reference.md | Link with auto security defaults — canonical shape ✓ |
| ../../component-list/reference.md | List + List.ListItem (ul/ol/dl) — canonical shape ✓ |
| ../../component-field/reference.md | Field (label + control wrapper) — canonical shape ✓ |
| ../../component-input/reference.md | Input with validation states — canonical shape ✓ |
| ../../component-checkbox/reference.md | Checkbox (wraps Input) — canonical shape ✓ |
| references/form.md | Form composition (legacy bundled reference; superseded by Form Mode in this skill) |
| ../../component-nav/reference.md | Nav compound component (List, Item) — legacy shape |
@fpkit/acss imports — all imports are localvar(--token) has a hardcoded fallbackdisabled for interactive componentsWhen adding or updating a component reference doc, follow the canonical embedded-markdown shape.
Every component reference doc must contain (in order):
**Verified against fpkit source:**. Records the upstream ref (e.g. @fpkit/[email protected]) and any intentional divergences from upstream (inlined hooks, simplified compound APIs, dropped subcomponents). Future maintainers read this to understand why the vendored version diverges.## Overview — one-paragraph summary of the component's purpose.## Generation Contract — export_name, file, scss, imports, dependencies. The /kit-add workflow reads these fields verbatim.## Props Interface — TypeScript interface or type alias the component accepts.## TSX Template — fenced tsx block containing the full component code. Self-contained: imports only UI from '../ui', React, and other vendored components via relative paths. Never @fpkit/acss.## CSS Variables — fenced scss block listing the component's CSS custom properties with default values.## SCSS Template — fenced scss block containing the actual SCSS rules.## Accessibility — required. Document keyboard interaction, ARIA, focus management, target size, color contrast, and the WCAG 2.2 AA criteria addressed. The Accessibility section is load-bearing — don't strip a11y patterns out of the TSX/SCSS during generation.## Usage Examples — fenced tsx block showing common usage patterns.Every component lives in its own component-<name>/ skill directory (SKILL.md + reference.md). New components are added via /acss-kit-component-author <name>, which scaffolds both files.
Form generation and natural-language component creation are handled by the Form Mode and Creator Mode sections of this skill — see below.
Every component-<name>/reference.md carries a verification banner at the top of the file:
> **Verified against fpkit source:** `@fpkit/acss@<version>`. Intentional divergences: <none or description>.
This banner is the single source of truth for which fpkit version a component was verified against and what intentional divergences exist.
Before authoring or backfilling a reference doc:
@fpkit/acss ceiling version to the matching git tag/SHA in the shawn-sandy/acss repo. If no matching tag exists for that npm version, use the closest tag and document the gap in the verification banner.https://github.com/shawn-sandy/acss/blob/<tag-or-sha>/packages/fpkit/src/<component>/... (full GitHub URL per repo policy — never blob/main).@fpkit/acss.Generate a paste-ready TSX snippet (or standalone component file) from a plain-English description. Resolves the user's words against the matched component's Props Interface — never invents props or variants that aren't in the reference doc.
Delegates to whichever component reference doc matches the description. Each reference doc carries its own
@fpkit/[email protected]verification line.
Supported components: Button, IconButton, Alert, Card, Dialog, Popover, Link, Img, Icon, List, Table, Field, Input, Checkbox, Nav — any component with a dedicated component-<name>/reference.md skill. Components that exist only as inline entries in references/inline-components.md (Badge, Tag, Heading, Text/Paragraph, Details, Progress) are not supported; promote them via /acss-kit-component-author <name> first.
Form-shaped requests ("signup form", "contact form with email and password") are handled by the Form Mode section below — not creator mode.
Examples:
aria-label 'Close'."Call ExitPlanMode before parsing. Step CM-B may delegate to /kit-add (writes TSX/SCSS) and Step CM-E (file mode) writes a standalone component file — plan mode blocks both.
Stay in plan mode only when the user explicitly asked for a parse-only preview. In that case, narrate the resolved spec (component, props, content) from CM-A1–CM-A5 without writing files, and wait for approval.
Match the description's component noun against per-component skill directories (../../component-<name>/reference.md). Each component has its own component-<name> skill and reference.md.
| Phrase contains | Resolves to | Reference doc |
|-----------------|-------------|---------------|
| button, btn, cta, call to action | Button | ../../component-button/reference.md |
| icon button, icon-button | IconButton | ../../component-icon-button/reference.md |
| alert, banner, notification | Alert | ../../component-alert/reference.md |
| card, panel, tile | Card | ../../component-card/reference.md |
| dialog, modal | Dialog | ../../component-dialog/reference.md |
| popover, floating card | Popover | ../../component-popover/reference.md |
| link, anchor, hyperlink | Link | ../../component-link/reference.md |
| image, img, picture | Img | ../../component-img/reference.md |
| icon (standalone, not "icon button") | Icon | ../../component-icon/reference.md |
| list, bullet list, ordered list | List | ../../component-list/reference.md |
| table, data table, grid (tabular) | Table | ../../component-table/reference.md |
| field, form field, labelled control | Field | ../../component-field/reference.md |
| input, text field, email field | Input | ../../component-input/reference.md |
| checkbox, tickbox | Checkbox | ../../component-checkbox/reference.md |
| nav, navigation, menu bar | Nav | ../../component-nav/reference.md |
When no mapping is found, halt: "No acss-kit component matches '<phrase>'. Run /kit-list to see the catalog."
For multi-component compositions ("a card with a button inside"), match the outer component first; the inner component is a refinement turn (CM-G).
Read the matched reference doc and parse:
## Generation Contract — yields export_name, file, dependencies.## Props Interface — yields the prop set, types, and JSDoc. Union-literal types are the canonical vocabulary.## Usage Examples — used to detect compound API (e.g. Card.Title, Table.Body).First match wins. The only silent defaults are the state-control carve-outs (CM-A3.5) and component-declared safe defaults (CM-A3.6).
Colour family (applies to props named color, severity, kind, tone, palette, or with a colour-like union):
| Synonym | Maps to |
|---------|---------|
| primary, main, cta | primary |
| secondary | secondary |
| tertiary, accent | tertiary |
| info, informational | info |
| success, confirm | success |
| warning, caution | warning |
| danger, destructive, delete, error | danger (or error if that's the prop's literal) |
| neutral, default, muted | default (or neutral) |
Halt if the resolved synonym is not in the prop's union literal. Never silently substitute the closest one.
Size family (applies to props named size, scale, density, or with a size-like union):
| Synonym | Maps to |
|---------|---------|
| extra small, xs, tiny | xs |
| small, sm, compact | sm |
| medium, md, regular | md |
| large, lg, big | lg |
| extra large, xl | xl |
| huge, 2xl | 2xl |
Halt if the resolved size is not in the prop's union. Some components accept only a subset — the union literal is authoritative.
Per-component union literals — common adjective synonyms:
| Synonym group | Canonical target |
|---------------|------------------|
| pill, rounded, round, capsule | pill |
| outline, outlined, bordered, ghost | outline or outlined — literal match wins |
| filled, solid | filled |
| soft, subtle, tonal | soft |
| text, link-style, flat | text |
| dismissible, closable, with close button | dismissible: true |
Resolution rule: (1) Literal match wins. (2) If not literal but one canonical spelling exists, use it. (3) If both spellings exist and the user gave a non-literal synonym, halt via AskUserQuestion. (4) If the synonym maps to nothing on this component, halt listing the actual union members.
Boolean props — set to true when the description contains an affirmative phrase for the prop name (disabled, block, dismissible, external, etc.). Booleans not mentioned are omitted.
Slot / content props (children, title, body, aria-label) — extract from: (1) quoted strings in order; (2) that says <X> / labelled <X> / with text <X> → children; (3) imperative verb-phrase fallback. Never write a component with placeholder content — halt via AskUserQuestion if a slot is unresolvable.
Some required props represent state bindings. Emit an explicit demo default and document in the summary as a wire-up TODO.
| Prop | Demo default | Summary note |
|------|--------------|--------------|
| open | true | Wire to caller state (e.g. useState) |
| expanded | true | (same) |
| visible | true | (same) |
| checked | false | Wire to caller state |
Pair each with a no-op () => {} callback when a matching on* callback exists in the Props Interface.
Button's type prop always defaults to "button" — detected by reading the Props Interface JSDoc for Required — ... paired with a default in the TSX Template signature. Any other required prop follows the halt-on-unresolved rule from CM-A5.
Halt via AskUserQuestion when: a required prop is unresolved (excluding CM-A3.5/CM-A3.6 carve-outs); a colour-family prop is unresolved; a synonym maps to two different prop axes; two synonyms conflict on the same axis; a resolved value is not in the prop's union.
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/detect_target.py <project_root>
source: "generated" → probe componentsDir for the matched component's files and all dependencies from its Generation Contract. Run /kit-add <component> [...dependencies] if any are missing.source: "none" → run /kit-add <component> [...dependencies] to bootstrap.After /kit-add completes, re-run detect_target.py to confirm source is "generated", then continue.
Ask once via AskUserQuestion (skip if the user already specified):
src/components/<Name>.tsx where <Name> is derived from the resolved content (e.g. "Add to cart" button → AddToCartButton).Run the CM-H validation matrix before generating. Any halt rule means: stop, print the offending combination, do not write. Any confirm rule means: round-trip through AskUserQuestion, only continue once the user accepts.
Single-element components (Button, IconButton, Alert, Link, Img, Icon, Input, Checkbox, Field, Popover):
// Children present:
<{{COMPONENT}} {{PROPS}}>
{{CHILDREN}}
</{{COMPONENT}}>
// No children (Img, Icon, Input, Checkbox when children absent):
<{{COMPONENT}} {{PROPS}} />
Branch selection: (1) No children slot in Props Interface → always self-closing. (2) children present and resolved → open/close. (3) children optional and empty → self-closing. (4) children required and empty → CM-A5 already halted.
Compound components (Card, Table, List) — emit root + slots the description named, in document order per the reference doc's Usage Examples. Skip empty slots — never emit a placeholder.
Snippet mode imports — resolve path from stack.entrypointFile (in .acss-target.json) to componentsDir. Fallback: project-root-relative path with a comment to adjust import to the paste destination.
File mode — emit import lines + typed function wrapper + JSX at src/components/<Name>.tsx. {{HANDLER_SIGNATURE}} is the typed callback prop forwarded when the component declares one (e.g. onClick for Button, onDismiss for Alert).
Atomic generation — build entire output in memory; write to disk only on success.
The generated component is WCAG 2.2 AA by construction (delegates to the vendored component). Enforce during generation:
aria-label on icon-only controls, alt on Img, labelFor on Field.disabled through the component's typed disabled prop — not raw HTML disabled — to preserve the aria-disabled + tab-order pattern via useDisabledState (WCAG 2.1.1).After a successful generation, the next user turn is a refinement (not a fresh CM-A) when both: (1) it doesn't name a different component, and (2) it reads as a delta on the existing spec.
| Phrase | Effect |
|--------|--------|
| make it larger / bigger | size-family prop → next step up (halt at ceiling) |
| make it smaller | size-family prop → next step down (halt at floor) |
| swap to <colour> / make it <X> | colour-family prop → resolved from CM-A3 colour table |
| make it <variant> | variant prop → resolved from synonym table |
| add full width / stretch it | block: true |
| disable it | disabled: true |
| change the text to "<X>" | primary content slot → <X> |
| start over / reset / forget that | clear in-memory spec; treat next turn as fresh CM-A |
A refinement re-runs CM-A5 → CM-D → CM-E. Steps B and C are skipped. In file mode, rewrite the same file. In snippet mode, print the full new JSX.
| Combination | Action |
|-------------|--------|
| Required prop unresolved (excluding CM-A3.5/CM-A3.6) | Halt |
| Resolved value not in prop's union literal | Halt — list supported values |
| Two same-axis synonyms in one description | Halt — reject as conflicting |
| Slot content empty or whitespace-only | Halt |
| Slot content > 80 chars | Confirm — long inline labels usually mean a different component |
| ## Generation Notes — Creator Mode block in matched reference doc | Apply its halt/confirm entries verbatim |
Button:
"Create a primary pill button that says 'Add to cart'."
import Button from './fpkit/button/button'
import './fpkit/button/button.scss'
<Button type="button" color="primary" variant="pill">
Add to cart
</Button>
Alert:
"Make me a soft warning alert titled 'Heads up' with body 'Your card expires next month' that's dismissible."
import Alert from './fpkit/alert/alert'
import './fpkit/alert/alert.scss'
<Alert open={true} severity="warning" variant="soft" title="Heads up" dismissible onDismiss={() => {}}>
Your card expires next month
</Alert>
(open and onDismiss are demo defaults — wire to caller state.)
Card (compound):
"Build a card with a heading 'Plan' and content 'Premium tier with all features.'"
import Card from './fpkit/card/card'
import './fpkit/card/card.scss'
<Card>
<Card.Title>Plan</Card.Title>
<Card.Content>Premium tier with all features.</Card.Content>
</Card>
Anti-patterns — creator mode must never: silently default a colour-family prop; substitute a literal the component doesn't declare; bake the description into a code comment; carry a spec the user dropped; write to disk on an un-confirmed confirm; hard-code the components path (always run detect_target.py); emit compound slots the user didn't name.
Generate a self-contained, accessible React form composed from the Field, Input, Button, and (when needed) Checkbox reference components. If any of those don't yet exist in the target directory, this mode walks through /kit-add field input checkbox button first.
Verified against fpkit source:
@fpkit/[email protected]. Follows upstreamcomponents/form/form.tsxcomposition pattern, targeting a single self-contained generated file.
Examples:
Call ExitPlanMode before resolving the field list. Step FM-B may delegate to /kit-add, and Step FM-C writes the form file — plan mode blocks both.
Stay in plan mode only when the user explicitly asked for a preview. In that case, narrate the resolved field list and the file that would be generated, then wait for approval.
If the description is vague (e.g. "a contact form" with no specified fields), pause with AskUserQuestion. Safe defaults by form type:
| Form type | Default fields | |-----------|----------------| | Signup | email (required, autoComplete=email), password (required, minLength=8, autoComplete=new-password) | | Login | email (required, autoComplete=email), password (required, autoComplete=current-password) | | Contact | name, email (required), message (textarea, rows=4) | | Newsletter | email (required, autoComplete=email) |
Confirm with the user before proceeding.
{
name: string, // form field name
label: string, // visible label
type: 'text' | 'email' | 'password' | 'tel' | 'url'
| 'number' | 'date'
| 'textarea' | 'select' | 'checkbox' | 'radio',
required?: boolean, // adds aria-required + visible *
autoComplete?: string,
options?: { value, label }[], // required for select and radio
rows?: number, // textarea default 4
minLength?: number,
}
For unsupported types (file, color, range), note in the summary and generate a plain <input> directly.
Derive PascalCase from description: "signup form" → SignupForm, "contact us" → ContactForm. Confirm only if ambiguous.
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/detect_target.py <project_root>
source: "generated" → probe componentsDir for field/field.tsx, input/input.tsx, button/button.tsx, checkbox/checkbox.tsx (only if any field has type: 'checkbox'), and ui.tsx.source: "none" → skip probe, proceed to bootstrap.Run /kit-add field input button (and checkbox if needed) when source is "none" or any file is missing. Re-run detect_target.py to confirm source is "generated", then continue.
Write to src/forms/<FormName>.tsx by default (or wherever the user specifies).
// {{NAME}}.tsx — generated by kit-core skill (form mode)
import { useState, type FormEvent } from 'react'
{{IMPORT_SOURCE:Field,Input,Checkbox,Button}}
export type {{NAME}}Values = {
{{FIELD_TYPES}}
}
export type {{NAME}}Errors = Partial<Record<keyof {{NAME}}Values, string>>
export default function {{NAME}}({
onSubmit,
}: {
onSubmit?: (values: {{NAME}}Values) => void | Promise<void>
}) {
const [errors, setErrors] = useState<{{NAME}}Errors & { _form?: string }>({})
const [submitting, setSubmitting] = useState(false)
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
if (submitting) return
setSubmitting(true)
setErrors({})
try {
const formData = new FormData(e.currentTarget)
const raw = Object.fromEntries(formData.entries()) as Record<string, FormDataEntryValue>
const values = {
...raw,
{{CHECKBOX_COERCION}}
{{RADIO_COERCION}}
} as unknown as {{NAME}}Values
await onSubmit?.(values)
} catch (err) {
setErrors({ _form: (err as Error).message })
} finally {
setSubmitting(false)
}
}
return (
<form
onSubmit={handleSubmit}
noValidate
aria-labelledby="{{NAME_KEBAB}}-heading"
className="form"
>
<h2 id="{{NAME_KEBAB}}-heading">{{HEADING}}</h2>
{errors._form && (
<div role="alert" className="form-error">{errors._form}</div>
)}
{{FIELDS}}
<Button
type="submit"
disabled={submitting}
data-color="primary"
>
{submitting ? 'Submitting…' : '{{SUBMIT_LABEL}}'}
</Button>
</form>
)
}
The submit Button uses useDisabledState internally — aria-disabled gates pointer/keyboard while submitting is true, combined with the if (submitting) return guard in handleSubmit to prevent double-submits.
| Placeholder | Substitute with |
|-------------|-----------------|
| {{NAME}} | PascalCase form name (e.g. SignupForm) |
| {{NAME_KEBAB}} | kebab-case form name (e.g. signup-form); prefix for all control ids |
| {{HEADING}} | Visible form heading (e.g. Create your account) |
| {{SUBMIT_LABEL}} | Submit button label (e.g. Create account) |
| {{FIELD_TYPES}} | One TS line per field: fieldName: string (or boolean for checkbox) |
| {{FIELDS}} | Rendered field elements — see FM-D below |
| {{IMPORT_SOURCE:Field,Input,Checkbox,Button}} | Resolved local import block from componentsDir; drop Checkbox when absent |
| {{CHECKBOX_COERCION}} | Per checkbox: <name>: formData.get('<name>') === 'on', |
| {{RADIO_COERCION}} | Per radio: <name>: String(formData.get('<name>') ?? ''), |
Compute the relative path from src/forms/<FormName>.tsx to componentsDir. Default src/components/fpkit gives ../components/fpkit.
import Field from '<relative>/field/field'
import Input from '<relative>/input/input'
import Checkbox from '<relative>/checkbox/checkbox' // omit if no checkbox field
import Button from '<relative>/button/button'
import '<relative>/field/field.scss'
import '<relative>/input/input.scss'
import '<relative>/checkbox/checkbox.scss' // omit if no checkbox field
import '<relative>/button/button.scss'
Build the entire form in memory; write to disk only on success.
Substitute into {{FIELDS}} with 6-space indentation. All renderers use {{form_name_kebab}}-{{name}} as the control's id.
Text-like inputs (text, email, password, tel, url, number, date):
<Field labelFor="{{form_name_kebab}}-{{name}}" label="{{label}}">
<Input
id="{{form_name_kebab}}-{{name}}"
name="{{name}}"
type="{{type}}"
{{REQUIRED_PROP}}
{{AUTOCOMPLETE_PROP}}
{{MINLENGTH_PROP}}
/>
</Field>
Textarea:
<Field labelFor="{{form_name_kebab}}-{{name}}" label="{{label}}">
<textarea
id="{{form_name_kebab}}-{{name}}"
name="{{name}}"
{{ROWS_ATTR}}
{{REQUIRED_ATTR}}
{{ARIA_REQUIRED_ATTR}}
/>
</Field>
Select:
<Field labelFor="{{form_name_kebab}}-{{name}}" label="{{label}}">
<select
id="{{form_name_kebab}}-{{name}}"
name="{{name}}"
{{REQUIRED_ATTR}}
{{ARIA_REQUIRED_ATTR}}
>
<option value="">Select…</option>
{{OPTIONS}}
</select>
</Field>
Checkbox:
<Checkbox
id="{{form_name_kebab}}-{{name}}"
name="{{name}}"
label="{{label}}"
{{REQUIRED_PROP}}
/>
(Checkbox renders its own label — do not wrap in Field.)
Radio (group):
<fieldset>
<legend>{{label}}</legend>
{{OPTIONS_AS_RADIOS}}
</fieldset>
Each radio option:
<label>
<input
type="radio"
id="{{form_name_kebab}}-{{name}}-{{value}}"
name="{{name}}"
value="{{value}}"
{{REQUIRED_ATTR}}
/>
{{option_label}}
</label>
Halt before writing if select or radio has no options.
Conditional attributes:
| Property | Expansion |
|----------|-----------|
| required: true | required, aria-required={true} |
| autoComplete: "email" | autoComplete="email" |
| minLength: 8 | minLength={8} |
| rows: 6 | rows={6} (textarea; omit → rows={4}) |
Field-types map for {{FIELD_TYPES}}:
| Field type | TypeScript type |
|--------------|-----------------|
| text, email, password, tel, url, textarea, select, radio | string |
| number, date | string (FormData serialises both as strings; cast at validation time) |
| checkbox | boolean |
The generated form is WCAG 2.2 AA by construction:
<form noValidate> — disables native validation; error truth lives in aria-describedby / errorMessage.aria-labelledby on the form references the <h2> so screen readers announce the form's purpose on entry.<div role="alert"> — form-level submission failure announced immediately.Field provides <label htmlFor> association for every Input, Textarea, and Select.useDisabledState — aria-disabled keeps it focusable while submitting (WCAG 2.1.1).WCAG 2.2 AA criteria: 1.3.1, 2.1.1, 2.4.3, 2.4.7, 3.3.1, 3.3.2, 4.1.2, 4.1.3.
Generated src/forms/<FormName>.tsx
Imports: Field, Input, Button [, Checkbox]
Field summary:
email (email, required, autoComplete=email)
password (password, required, minLength=8)
Next steps:
- Wire onSubmit handler in your route/page
- Add per-field validation (structure is scaffolded; logic is application-specific)
- Style overrides via CSS variables — see field.scss / input.scss
Invoked by /style-tune when the subject resolves to a component. See style-tune/SKILL.md Step A for intent parsing and dispatch.
Run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/detect_target.py <project_root>. Require source: "generated".
Probe <componentsDir>/<component>/<component>.scss. If missing, halt: "Component <name> isn't vendored yet. Run /kit-add <name> first — this is a styling task, not a scaffolding task."
Supported components: button, card, alert, dialog, input, nav. For others, halt: "Component <name> doesn't have a token mapping yet."
For each (component, family, delta) from style-tune/SKILL.md Step A:
Grep the component SCSS for the targeted token name(s) and read the current value(s).[0.125rem, 1rem]; padding multipliers, etc.).--alert-bg: var(--color-surface, …)): do NOT edit this declaration. Route the edit to the underlying theme role via styles/SKILL.md Style-Tune Mode, and note in Step F that tuning the component token requires changing the theme role.preset: true in vocabulary): expand into the listed multi-family deltas and apply each independently.var(--x, fallback) wrappers — only the declaration's RHS may change.Build the entire updated SCSS file in memory; Edit atomically. When one modifier touches multiple tokens, batch into one Edit pass per file.
Safety rules:
var() wrapper — only the RHS may change.var(--color-*, …) reference exists.--{c}-* declarations.Structural check after each Edit:
Grep for var( occurrences — count must be unchanged before and after.Grep for each edited token name — must appear exactly once on a declaration LHS.On failure, restore from the in-memory pre-edit copy and halt.
Idempotency: if the computed value equals the current value within tolerance (hex equality, or rem within 0.0001), skip the write and report "already at target" in Step F. Note that cumulative drift is still possible across iteration passes — × 0.75 then × 1.25 lands at × 0.9375, not the original. Document this when a chroma or scale modifier is applied.
Generate static HTML versions of fpkit-style components for projects that don't use React — server-rendered apps, static sites, design-system docs, email templates, prototypes.
Triggers: user asks for /kit-add --target=html, "static HTML components", "HTML version of <component>", or mentions a non-React project.
Both this section and the React workflow above read the same reference docs at ../../component-<name>/reference.md. The React workflow extracts ## TSX Template; this section extracts ## HTML Template and (for stateful components) ## Vanilla JS. The ## SCSS Template block is identical for both.
Run this check at the start of every HTML-target invocation.
HT-A1. Determine target directory
Run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/detect_target.py --target=html <project_root>.
"source": "configured" → use the reported componentsHtmlDir. Skip the prompt.
"source": "none" → ask:
Where should HTML components be generated? (default: components/html)
After the developer answers, write .acss-html-target.json at the project root:
{ "componentsHtmlDir": "components/html" }
Commit this file — subsequent runs read it.
HT-A2. Copy the foundation helper
Check if _stateful.js exists in <componentsHtmlDir>. If not:
${CLAUDE_PLUGIN_ROOT}/assets/html-foundation/_stateful.js into <componentsHtmlDir>/_stateful.js.Created _stateful.js (foundation helper — required by stateful components).HT-B1. Look up the component — same lookup as the React workflow: ../../component-<name>/reference.md.
HT-B2. Read canonical sections — a reference doc that supports HTML output contains:
## Generation Contract — export_name, file, scss, dependencies. Reuse verbatim.## HTML Template — fenced html block. Copy verbatim into <name>.html.## SCSS Template — fenced scss block. Copy verbatim into <name>.scss.## Vanilla JS — fenced js block. Present on stateful components only. Copy verbatim into <name>.js.## Accessibility — read it. Do not strip ARIA attributes.If ## HTML Template is missing, warn the developer and offer to author markup from the TSX template by hand. Do not silently skip.
HT-B3. Resolve dependencies — same algorithm as React Step B3.
HT-B4. Show dependency tree and wait for confirmation — same format as React Step B4, using the componentsHtmlDir path.
HT-B5. Generate files bottom-up — leaf dependencies first. Skip existing files.
The HTML output is a fragment — no <html>/<head>/<body> wrapper. Slot placeholders use HTML comments: <!-- slot: children -->.
HTML (.html): Fragment. Same class names, data-* attributes, and ARIA as the TSX output. Slot placeholders as HTML comments. Multiple variants separated by <!-- variant: <name> -->.
SCSS (.scss): Byte-identical to the React generator output. Rules: rem only, --{component}-{element?}-{variant?}-{property} naming, hardcoded fallbacks on global tokens, [aria-disabled="true"] on every interactive component.
JS (.js) — stateful components only: Emitted for Button, Card (interactive variant), Alert, Dialog, Popover, Checkbox, Input, IconButton. Plain ES module, no bundler required. Imports wireDisabled from ./_stateful.js where applicable. Exports an idempotent init() function.
Generated HTML components in components/html/:
Created:
button.html button.scss button.js
Skipped (already existed):
_stateful.js
How to wire it up:
1. Compile SCSS: npx sass components/html/button.scss components/html/button.css
Then: <link rel="stylesheet" href="components/html/button.css">
(Or @import the .scss from your existing Sass entrypoint.)
2. <script type="module" src="components/html/button.js"></script>
3. Paste the markup from button.html into your page or template.
Run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/verify_integration.py --target=html <project_root>.
.scss/.js artifact is referenced by at least one page. No action.reasons entry as a numbered fix-up list. Do not auto-edit user pages.*.html snippets are listed but not checked — they're copy-paste fragments.
<html>/<body> wrappers._stateful.js is the disabled-state helper — copied once per project.data-ai
Use when the user asks to generate, create, or scaffold a Table — accessible data table with caption, scope headers, responsive scroll wrapper, and sortable column support.
tools
Use when the user asks to generate, create, or scaffold a Popover — accessible tooltip/popover using the Popover API with focus trap, aria-expanded, and light-dismiss.
tools
Use when the user asks to generate, create, or scaffold a Nav — accessible navigation landmark with aria-label, current-page link marking, and horizontal/vertical layout.
tools
Use when the user asks to generate, create, or scaffold a List — accessible ordered/unordered list with role="list" reset, item slots, and inline/stacked layout variants.