mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(craft): add form-validation + opt-ins on saas-landing, mobile-onboarding Module 5 of 5 in the behavioral craft series proposed in #501. Modules 1-4 merged: state-coverage (#502), animation-discipline (#515), accessibility-baseline (#587), rtl-and-bidi (#595). Picks up where accessibility-baseline.md ends (label + describedby + invalid + role=alert for inline errors) and connects the four layers a real form spans: WHATWG Constraint Validation as the platform floor, validation timing as a state machine on the input, WCAG 3.3.x as the announcement and recovery contract, schema as the cross-stack truth. Sections: input state machine; validation timing (4 rules anchored on :user-invalid Baseline 2023); Constraint Validation API rules (setCustomValidity, requestSubmit vs submit, readonly + #11841, inputmode); error wiring beyond the baseline (adaptive messages, error summary without role=alert, preserve user input on error); schema as cross-stack contract (Standard Schema, server-authoritative, Zod 4 z.email() form); WCAG 3.3.3 / 3.3.4 / 3.3.8 / 3.3.9; native mobile parity (UIKit, SwiftUI, Compose, Flutter, RN); common mistakes. Reviewed in 3 loops with Claude CLI Opus 4.7 xhigh effort: - Loop 1: 6 P0s caught (SwiftUI Form validity claim, SwiftUI announcement primitive, Compose semantics syntax, UIKit UIAlertController, contradictory Baymard stats, 3.3.8 CAPTCHA framing reversed) + 11 P1/P2s; all addressed. - Loop 2: verified P0 fixes; flagged 1 P1 (RN table row scrambled) + 4 P2s; all addressed. - Loop 3: SHIP verdict. Three P2 nits applied (Zod 4 z.email() form, WebAIM Million 2026 stat woven in: 51% page-level, 33.1% input-level). WebAIM Million 2026 numbers verified directly against webaim.org/projects/million/. Skill opt-ins: saas-landing (lead capture form), mobile-onboarding (sign-in screen). Skill bodies do not contain validation-specific instructions that would override craft guidance — opt-in alone is sufficient. README updated. Refs #501. * fix(craft+skills): form-validation review fixes (lefarcen + mrcfps P2s) Both non-blocking findings addressed: - Drop form-validation from saas-landing.craft.requires. The skill body produces a CTA-driven landing page with no JS and no interactive form. Adding form-validation injected ~221 lines of irrelevant prompt pressure and conflicted with the README opt-in rule ("primary artifact contains an interactive form"). mobile-onboarding keeps the opt-in — sign-in screen is a real form. - Reword timing rule 4 (async checks). Previous "never block submit on a network round-trip" was too broad and conflicted with the schema-layer "server is the truth" rule. Split into two paths: background preflight (uniqueness, address lookup) doesn't gate the form; authoritative submit-path server validation must await the server response and surface its field errors. The rule is "don't let a slow background check freeze the form," not "don't ever wait for the server." * fix(craft): form-validation mrcfps round-2 (novalidate trade-off, Flutter RTL) Two non-blocking precision items: - novalidate trade-off: previous wording said keeping required/pattern/type preserves no-JS PE, but a literal server-rendered <form novalidate> disables the browser's submit-blocking and validation UI even when JS is unavailable — losing the no-JS constraint-validation floor. Reworded to spell out the two safe patterns: (A) render <form> without novalidate server-side and have the form library set form.noValidate = true after hydration, or (B) ship novalidate from the start only when the submit path reaches server validation without JS. Either way, keep the constraint attributes. - Flutter announcement example: hardcoded TextDirection.ltr would announce Arabic/Hebrew/Persian validation messages with wrong bidi direction when this craft is combined with rtl-and-bidi. Switched to SemanticsService.announce(message, Directionality.of(context)) with an explicit warning never to hardcode the direction. * fix(craft): form-validation mrcfps round-3 (readonly safety, Compose error message) Two non-blocking precision items: - Non-input readonly fallback: previous text said `aria-readonly` plus hidden mirror input was an option for non-input controls that need to submit. But `aria-readonly` doesn't actually stop a `<select>` or custom widget from being changed, so the visible control can drift while the hidden input ships a stale value — user sees one option, server gets another. Tightened: prefer `disabled` plus a same-named hidden input, or non-editable text plus hidden input. If using `aria-readonly`, the interaction must also be blocked or the two values kept in sync. - Compose error message: previous rule was too absolute about avoiding `Modifier.semantics { error("…") }`. `isError = true` flips the field state but does not carry the localized error message; Android Compose accessibility guidance pairs `isError` with `semantics { error(message) }` so the accessibility service gets the real text. The trap is duplication, not the API itself. Reframed the rule: use both, source the message from the same state field as `supportingText` so they stay in sync. * fix(craft): form-validation Compose live-region API name Compose row in the native-mobile parity table named a "LiveRegion" semantic that doesn't exist. Real API is `Modifier.semantics { liveRegion = LiveRegionMode.Polite }` on the supporting-text node. Also replaced the generic `view.announceForAccessibility(…)` with the Compose-idiomatic `LocalView.current.announceForAccessibility(message)` so generated snippets compile.
84 lines
4.5 KiB
Markdown
84 lines
4.5 KiB
Markdown
# Craft references
|
|
|
|
Brand-agnostic craft knowledge. Each file is a small, dense rulebook on one
|
|
dimension of professional UI craft (typography, color, motion, …). Skills
|
|
opt into the references they need; the daemon injects only the requested
|
|
ones into the system prompt above the active skill body.
|
|
|
|
## Why a third axis next to `skills/` and `design-systems/`
|
|
|
|
| Axis | Scope | Example |
|
|
|---|---|---|
|
|
| `skills/` | Artifact shape | `saas-landing`, `dashboard`, `pricing-page` |
|
|
| `design-systems/` | Brand visual language (the 9-section `DESIGN.md`) | `linear-app`, `apple`, `notion` |
|
|
| `craft/` | **Universal** craft knowledge — true regardless of brand | letter-spacing rules, accent-overuse caps, anti-AI-slop |
|
|
|
|
`DESIGN.md` tells the agent which colors and fonts a brand uses. `craft/`
|
|
tells the agent the universal rules a competent designer applies on top —
|
|
e.g. ALL CAPS always needs ≥0.06em tracking, regardless of the brand.
|
|
|
|
## How a skill opts in
|
|
|
|
Add an `od.craft.requires` array to the skill's front-matter. Only the
|
|
listed sections are injected, so a skill that needs only typography pays
|
|
no token cost for color/motion content.
|
|
|
|
```yaml
|
|
od:
|
|
craft:
|
|
requires: [typography, color, anti-ai-slop]
|
|
```
|
|
|
|
Allowed values match the file names in this directory minus the `.md`
|
|
extension. Unknown values are silently ignored (forward-compatible).
|
|
|
|
### Why silent fallback instead of fail-fast?
|
|
|
|
A skeptical reader will ask: "If a skill requests a planned-but-not-yet-vendored
|
|
section and the corresponding file doesn't exist yet, shouldn't we warn
|
|
the user?" We chose forward-compatibility over fail-fast: a skill
|
|
authored today can list a planned slug and start benefiting the moment
|
|
the matching `craft/<slug>.md` is vendored in a follow-up PR, with no
|
|
skill edit needed. The cost of a missed reference is a missing
|
|
paragraph in the system prompt, not a broken skill — so the loud
|
|
failure mode is not worth the friction.
|
|
|
|
Note for skill authors arriving from older guidance: an earlier draft
|
|
used `motion` as the future-slug placeholder. The shipped equivalent
|
|
today is `animation-discipline`. Use that one if your skill emits
|
|
motion.
|
|
|
|
### Enforcement levels
|
|
|
|
Craft files mix auto-checked rules and guidance.
|
|
|
|
- **Auto-checked.** Rules wired into `apps/daemon/src/lint-artifact.ts` — currently the P0 list in `anti-ai-slop.md` (Tailwind-indigo accent, two-stop hero gradients, emoji-as-icons, etc.). The linter reports these as findings back to the UI (for P0/P1 badges) and to the agent (as a system reminder for self-correction). Artifact persistence is not currently hard-blocked on P0 hits.
|
|
- **Guidance.** The rest. The agent reads the rules, reviewers apply them, the linter doesn't check them.
|
|
|
|
A purely behavioral craft file (state-coverage, animation-discipline) is guidance unless a specific rule is later promoted into `lint-artifact.ts`.
|
|
|
|
## Files
|
|
|
|
| File | Section name | When to require |
|
|
|---|---|---|
|
|
| `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) |
|
|
| `animation-discipline.md` | `animation-discipline` | Any skill that ships motion: mobile apps, multi-screen flows, gamified UI, transitions, microinteractions |
|
|
| `accessibility-baseline.md` | `accessibility-baseline` | Any skill that ships interactive UI: dashboards, forms, mobile flows, anything with focus/labels/keyboard paths |
|
|
| `rtl-and-bidi.md` | `rtl-and-bidi` | Any skill that ships localized text or layout: blogs, docs, financial tables, mobile apps, anything that may render Arabic / Hebrew / Persian |
|
|
| `form-validation.md` | `form-validation` | Any skill whose primary artifact contains an interactive form: lead capture, sign-in, signup, settings, multi-step intake |
|
|
|
|
**Partial-stateful skills.** A skill that's mostly static but contains an embedded form, data table, or query surface should opt in. State-coverage rules apply to the stateful component, not the whole page.
|
|
|
|
More sections (`icons`, `craft-details`) will be added in follow-up
|
|
PRs as we wire the linter side.
|
|
|
|
## Attribution
|
|
|
|
Craft content is adapted from the MIT-licensed
|
|
[refero_skill](https://github.com/referodesign/refero_skill) project
|
|
(© Refero Design), with edits to fit Open Design's house style and link
|
|
back to OD's design tokens (`var(--accent)` etc.) instead of generic
|
|
Tailwind hex values.
|