feat(components): add Form Field to the token-proof bundle (#87) #219

Merged
peterswimm merged 1 commit from feat/87-form-field-component into main 2026-10-20 16:16:34 +00:00
Owner

closes #87

Scope

Adds exactly one component to the brand-agnostic bundle, as ville decided in #87: a Form Field (components/form-field.html) with label, text input, help text, and default / focus / invalid / disabled states. It exists to prove the one DTCG tier Button and Card don't touch, border.json plus the focus ring.

  • Reads var(--*) only. No literal colours, sizes or radii. The few fallbacks are other vars (--border-interactive falls back to --text-secondary, --focus-ring to --text-primary), because five short-var skins (chestnutstand, cosmiccrisp, pardner, sundered, truancy) define no --border-interactive, and pardner has no --focus-ring.
  • Focus: :focus-visible draws a --border-width-emphasis (3px) solid outline in --focus-ring, offset by --space-1 so it never merges with the border. Contrast is checked in the test below.
  • Invalid: aria-invalid="true" turns the border --error, and the help line switches to error text, so colour is not the only signal.
  • _bundle.js now carries a components registry (button, card, form-field).
  • tools/export_public_repo.py ships the new file and the same registry in the public kit. Without that the kit would lack the component.

Not added, per ville: Badge/Tag, nav, hero, footer, ForgeBlock or product-specific components.

Testing

New generator/tests/unit/test_components.py (32 cases, 30 pass, 2 skip because pardner's --focus-ring is not a plain hex). Full suite pytest generator tools: 560 passed, 2 skipped. Covers:

  • the registry in _bundle.js equals the components/*.html files on disk, and equals the public-kit export;
  • no literal colour or length in any Form Field declaration, and fallbacks are only vars;
  • the focus rule is an offset outline of at least 2px in --focus-ring, and is never none;
  • --focus-ring against --bg-surface is at least 3:1 for every short-var product, light and dark;
  • every unfallbacked var is defined by every short-var skin;
  • --focus-ring and --error differ between the toilville and chestnutstand skins, with the same markup;
  • the markup names no product.

Control (checks fail when the fix is broken, then restored):

  • literal 3px radius and #666 colour in the component: fails;
  • Form Field dropped from _bundle.js: 3 fail;
  • focus outline replaced with outline: none: fails;
  • toilville light --focus-ring set to #FFFFFF: contrast test fails;
  • exporter no longer copies form-field: fails.

Not run: a browser visual pass. The skin-switch and contrast checks read the generated skin.css values, not rendered pixels.

Impact Statement

  • Existing features: none. Button, Card and every skin are untouched.
  • Public APIs / contracts: ToilvilleDesignSystem.components is a new list on the bundle namespace; the public kit gets the same under DesignKit.components.
  • Schemas: none. No token added or changed; no dist/ change.
  • Data / artifacts: one new component file in components/ and in the public kit export.
  • Integrations: publish-public-design-kit.yml already triggers on components/** and tools/export_public_repo.py, so the public kit picks it up on merge.
  • Mappings: none.
  • Backward compatibility: additive only.
closes #87 ## Scope Adds exactly one component to the brand-agnostic bundle, as ville decided in #87: a **Form Field** (`components/form-field.html`) with label, text input, help text, and default / focus / invalid / disabled states. It exists to prove the one DTCG tier Button and Card don't touch, `border.json` plus the focus ring. - Reads `var(--*)` only. No literal colours, sizes or radii. The few fallbacks are other vars (`--border-interactive` falls back to `--text-secondary`, `--focus-ring` to `--text-primary`), because five short-var skins (chestnutstand, cosmiccrisp, pardner, sundered, truancy) define no `--border-interactive`, and pardner has no `--focus-ring`. - Focus: `:focus-visible` draws a `--border-width-emphasis` (3px) solid outline in `--focus-ring`, offset by `--space-1` so it never merges with the border. Contrast is checked in the test below. - Invalid: `aria-invalid="true"` turns the border `--error`, and the help line switches to error text, so colour is not the only signal. - `_bundle.js` now carries a `components` registry (`button`, `card`, `form-field`). - `tools/export_public_repo.py` ships the new file and the same registry in the public kit. Without that the kit would lack the component. Not added, per ville: Badge/Tag, nav, hero, footer, ForgeBlock or product-specific components. ## Testing New `generator/tests/unit/test_components.py` (32 cases, 30 pass, 2 skip because pardner's `--focus-ring` is not a plain hex). Full suite `pytest generator tools`: 560 passed, 2 skipped. Covers: - the registry in `_bundle.js` equals the `components/*.html` files on disk, and equals the public-kit export; - no literal colour or length in any Form Field declaration, and fallbacks are only vars; - the focus rule is an offset outline of at least 2px in `--focus-ring`, and is never `none`; - `--focus-ring` against `--bg-surface` is at least 3:1 for every short-var product, light and dark; - every unfallbacked var is defined by every short-var skin; - `--focus-ring` and `--error` differ between the toilville and chestnutstand skins, with the same markup; - the markup names no product. **Control (checks fail when the fix is broken, then restored):** - literal `3px` radius and `#666` colour in the component: fails; - Form Field dropped from `_bundle.js`: 3 fail; - focus outline replaced with `outline: none`: fails; - toilville light `--focus-ring` set to `#FFFFFF`: contrast test fails; - exporter no longer copies `form-field`: fails. Not run: a browser visual pass. The skin-switch and contrast checks read the generated `skin.css` values, not rendered pixels. ## Impact Statement - Existing features: none. Button, Card and every skin are untouched. - Public APIs / contracts: `ToilvilleDesignSystem.components` is a new list on the bundle namespace; the public kit gets the same under `DesignKit.components`. - Schemas: none. No token added or changed; no `dist/` change. - Data / artifacts: one new component file in `components/` and in the public kit export. - Integrations: `publish-public-design-kit.yml` already triggers on `components/**` and `tools/export_public_repo.py`, so the public kit picks it up on merge. - Mappings: none. - Backward compatibility: additive only.
feat(components): add Form Field to the token-proof bundle (#87)
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 1s
Test Generator / lint-workflows (pull_request) Successful in 27s
Test Generator / lint-prose (pull_request) Successful in 53s
Security Scans / Trivy filesystem scan (pull_request) Successful in 1m10s
Security Scans / Gitleaks secret scan (pull_request) Successful in 1m8s
Block Validation / validate (pull_request) Successful in 1m12s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 17s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 59s
Test Generator / test (pull_request) Successful in 4m27s
8cb4da74ab
peterswimm deleted branch feat/87-form-field-component 2026-10-20 16:16:35 +00:00
Sign in to join this conversation.
No description provided.