diff --git a/craft/README.md b/craft/README.md index e0ec276a69..03ac27fa2d 100644 --- a/craft/README.md +++ b/craft/README.md @@ -49,6 +49,7 @@ skill — so the loud failure mode is not worth the friction. | `typography.md` | `typography` | Any skill that emits typed content (~all skills) | | `color.md` | `color` | Any skill that emits styled output (~all skills) | | `anti-ai-slop.md` | `anti-ai-slop` | Marketing pages, landing pages, decks | +| `state-coverage.md` | `state-coverage` | Any skill with stateful UI (dashboards, mobile apps, forms, list/table views) | More sections (`motion`, `icons`, `craft-details`) will be added in follow-up PRs as we wire the linter side. diff --git a/craft/state-coverage.md b/craft/state-coverage.md new file mode 100644 index 0000000000..b8114d6df4 --- /dev/null +++ b/craft/state-coverage.md @@ -0,0 +1,115 @@ +# State coverage craft rules + +Universal rules for what every interactive surface must render. The active +`DESIGN.md` decides how each state looks; this file decides which states must +exist and what they must contain. The single most reliable AI-design failure +is shipping only the populated state. + +> Distilled from WCAG 2.2, NN/g, Material Design 3, Apple HIG, and Baymard +> Institute checkout research. + +## The five required states + +Every surface that fetches, transforms, or accepts data must render all five. + +| State | Triggered when | Must contain | +|---|---|---| +| **Loading** | Data is in flight | Skeleton, spinner, or shell — plus a 15 s "taking longer than expected" fallback | +| **Empty** | No records yet, or query returned nothing | Headline, plain explanation, primary CTA | +| **Error** | Fetch failed, server failure, validation rejection | Plain-language cause, recovery action, preserved user input | +| **Populated** | Data present, primary case | The state the design was actually drawn for | +| **Edge** | Extreme volume, long strings, missing optional fields, RTL or long-word content, partial network | Layout that does not break | + +Render-and-screenshot test: every list, table, card, form, and panel in the +artifact has all five. Missing states are the most common silent failure of +AI-generated UI. + +## Form-specific states + +Forms add three states on top of the five. + +| State | Triggered when | Behavior | +|---|---|---| +| **Untouched** | Field has not yet had focus | Default styling; no validation messages | +| **Dirty (valid)** | User typed and field passes validation | Persistent helper text remains; no success-coloring | +| **Submitted-pending** | Submit clicked, awaiting server | Submit button enters loading state; fields lock against re-submission | + +Validation timing: validate **on blur**, not on first keystroke. For password +and similar live fields, validate on each keystroke *only after the first +blur*. Remove the error message the instant input becomes valid. + +## Empty state composition + +Empty is not the absence of state. It is its own state with a job. + +- **First-use empty** — illustration + headline + value sentence + primary CTA. The empty is the onboarding moment. +- **No-results empty** — echo the query, suggest alternatives, never leave a true blank. +- **Cleared empty** — celebratory phrasing, optional next-action. +- **Error-as-empty** — never. An error is its own state with recovery information; do not collapse error into empty. + +## Error state composition + +Every error must answer three questions, in this order: + +1. **What happened.** "Your card was declined." Not "Something went wrong." +2. **Why, if knowable.** "Insufficient funds." Or "Network unreachable — check your connection." +3. **What the user can do.** A retry button, an alternative path, or a support link. + +Preserve user input across the error. The form must not clear on submit +failure. + +Severity tiers: + +- **Field-level** — red border, inline message, focus moves to the field. +- **Form-level** — error summary banner at top + per-field markers. +- **Section-level** — inline panel with retry, surrounding sections still functional. +- **Page-level** — full error state with illustration and recovery CTA. +- **App-level** — persistent banner or modal for critical loss-of-functionality. + +Match severity to surface scope. A field validation failure does not warrant +a page-level error. + +## Loading state thresholds + +Pick the indicator by expected duration, not by what's available in the +component library. + +| Duration | Indicator | +|---|---| +| 0–300 ms | None. Render synchronously; users perceive no delay. | +| 300 ms – 2 s | Subtle spinner or skeleton. | +| 2 – 10 s | Skeleton matched to expected layout, or labelled spinner ("Loading payments…"). | +| 10 – 30 s | Determinate progress bar with cancel option. | +| 30 – 60 s | Progress bar + "Taking longer than expected." | +| 60 s+ | Stop animation. Show error with retry, cancel, or continue. | + +Never leave a spinner running indefinitely. Start a timeout on every request. + +## ARIA and focus rules + +State changes must be announced and focused correctly. + +| Change | ARIA | Focus action | +|---|---|---| +| Inline error on submit | `role="alert"` on the message | Move focus to first error field | +| Toast / non-urgent confirmation | `role="status"` (polite live region) | Do not move focus | +| Critical error or destructive confirmation | `role="alertdialog"` (assertive) | Move focus to dialog | +| Loading begins | `role="status"` announcement ("Loading…") | Do not move focus to spinner | +| Loading ends, content appears | — | Move focus to loaded content if action was user-initiated | + +Live region containers must exist in the DOM before content is injected. +Adding `aria-live` simultaneously with content does not trigger an +announcement. + +## Common mistakes (lint these) + +- Surface renders only the populated state; loading, empty, error, and edge are absent. +- Empty state is a literal blank or "No data" text with no headline, explanation, or action. +- Error message reads "Something went wrong" with no cause or recovery. +- Spinner with no timeout; runs indefinitely on slow or failed requests. +- Submit clears form fields on validation failure, forcing re-entry. +- Inline validation fires on first keystroke instead of on blur. +- Full-page loading replaces the chrome when only one section is fetching. +- Toast appears at a different screen position than previous toasts in the same artifact. +- Color alone conveys error state — no icon, no text label. +- Auto-dismissing toast cannot be paused on hover or focus (WCAG SC 2.2.1). diff --git a/skills/dashboard/SKILL.md b/skills/dashboard/SKILL.md index 31695ee7ec..828544f3ea 100644 --- a/skills/dashboard/SKILL.md +++ b/skills/dashboard/SKILL.md @@ -22,6 +22,8 @@ od: design_system: requires: true sections: [color, typography, layout, components] + craft: + requires: [state-coverage] --- # Dashboard Skill diff --git a/skills/kanban-board/SKILL.md b/skills/kanban-board/SKILL.md index 944d777ab0..c220fee7b2 100644 --- a/skills/kanban-board/SKILL.md +++ b/skills/kanban-board/SKILL.md @@ -22,6 +22,8 @@ od: design_system: requires: true sections: [color, typography, layout, components] + craft: + requires: [state-coverage] example_prompt: "Make me a kanban board for a 5-person growth squad mid-sprint — backlog, doing, review, done." --- diff --git a/skills/mobile-app/SKILL.md b/skills/mobile-app/SKILL.md index 6616a801c1..1ab0de83ef 100644 --- a/skills/mobile-app/SKILL.md +++ b/skills/mobile-app/SKILL.md @@ -24,6 +24,8 @@ od: design_system: requires: true sections: [color, typography, layout, components] + craft: + requires: [state-coverage] --- # Mobile App Skill