240 lines
11 KiB
Markdown
240 lines
11 KiB
Markdown
|
|
---
|
|||
|
|
name: liquid-glass-form-primitives
|
|||
|
|
classification: feature
|
|||
|
|
success_metric: |
|
|||
|
|
`<Button>`, `<Input>`, `<SearchBar>` primitives ship under
|
|||
|
|
`components/ui/`; the .btn-primary / .btn-flame / .btn-ember /
|
|||
|
|
.btn-gold / .input-field / .search-bar utility classes either become
|
|||
|
|
thin aliases of the new primitives' styling OR are deprecated for #8
|
|||
|
|
to delete; every consumer of those classes is migrated; focus-rings
|
|||
|
|
use the new ember-rim tokens; lint + vitest + smoke green.
|
|||
|
|
skip:
|
|||
|
|
- ia
|
|||
|
|
status: in-progress-brief-1-merged
|
|||
|
|
created: 2026-06-03
|
|||
|
|
conductor_started: 2026-06-03
|
|||
|
|
brief_1_merged: 2026-06-03
|
|||
|
|
depends_on:
|
|||
|
|
- liquid-glass-design-tokens
|
|||
|
|
umbrella: liquid-glass-redesign
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Convoy: liquid-glass-form-primitives
|
|||
|
|
|
|||
|
|
Sub-convoy #3 of the `liquid-glass-redesign` epic. Introduces the
|
|||
|
|
button, input, and search-bar primitives — the second half of the
|
|||
|
|
foundational reusable kit (after the modal + surface primitives of #2).
|
|||
|
|
Parallel-safe with #2 after #1 merges.
|
|||
|
|
|
|||
|
|
## Why
|
|||
|
|
|
|||
|
|
The repo defines five button utility classes (`.btn-primary`,
|
|||
|
|
`.btn-flame`, `.btn-ember`, `.btn-gold`, `.btn-secondary`) and two
|
|||
|
|
input classes (`.input-field`, `.search-bar`) directly in
|
|||
|
|
`styles/globals.css`. Each uses opaque ember/flame gradient fills + a
|
|||
|
|
single drop shadow — i.e. the "warm panel" aesthetic the redesign is
|
|||
|
|
moving away from.
|
|||
|
|
|
|||
|
|
Buttons and inputs are the densest interactive surface in the app. If
|
|||
|
|
every other surface goes glass and these stay opaque, the visual
|
|||
|
|
hierarchy fights itself.
|
|||
|
|
|
|||
|
|
A small primitive set lets callers express *intent* (primary action,
|
|||
|
|
ghost secondary, ember rim-light hover state) without composing
|
|||
|
|
Tailwind class strings or wiring ad-hoc `style={{}}` objects.
|
|||
|
|
|
|||
|
|
## Scope
|
|||
|
|
|
|||
|
|
### In scope — primitives
|
|||
|
|
|
|||
|
|
- `components/ui/Button.js` (new). Props:
|
|||
|
|
- `variant`: `'primary'` (ember rim-light glass pill), `'ghost'`
|
|||
|
|
(glass with no accent until hover), `'ember'` (solid ember for
|
|||
|
|
destructive / high-emphasis CTAs), `'gold'` (celebration / rarity
|
|||
|
|
CTAs), `'icon'` (square glass for header icon-only buttons).
|
|||
|
|
- `size`: `'sm' | 'md' | 'lg'`.
|
|||
|
|
- `loading` (boolean) — shows an inline spinner; disables button.
|
|||
|
|
- `iconLeft`, `iconRight` — slot for SVG icons.
|
|||
|
|
- Standard `<button>` props (type, onClick, disabled, aria-label,
|
|||
|
|
...rest).
|
|||
|
|
- Renders a native `<button>` with the glass styling + focus ring +
|
|||
|
|
the appropriate ARIA when used as icon-only.
|
|||
|
|
- `components/ui/Input.js` (new). Props:
|
|||
|
|
- `label` (required for a11y; visible by default; can be
|
|||
|
|
`srOnly={true}`).
|
|||
|
|
- `id` (auto-generated if absent).
|
|||
|
|
- `error` (string; renders connected via `aria-describedby` per the
|
|||
|
|
a11y finding in `.convoys/ship-readiness.md` § Role-a11y-auditor:
|
|||
|
|
*"login/signup form errors are visually red but not connected to
|
|||
|
|
inputs via `aria-describedby`"* — this primitive CLOSES that
|
|||
|
|
finding).
|
|||
|
|
- `description` (optional hint text).
|
|||
|
|
- Standard `<input>` props.
|
|||
|
|
- `components/ui/SearchBar.js` (new). Wraps `<Input>` with the
|
|||
|
|
search-magnifier icon, ⌘K keyboard hint slot, and a dedicated focus
|
|||
|
|
state (the search bar is currently the most visually distinct input
|
|||
|
|
in the app).
|
|||
|
|
- Tests under `test/components/`:
|
|||
|
|
- `Button.test.js` — variants render, `loading` disables, icon-only
|
|||
|
|
requires `aria-label`, focus ring visible on `:focus-visible`.
|
|||
|
|
- `Input.test.js` — label association, error -> `aria-describedby`
|
|||
|
|
wiring, visually-hidden label via `srOnly`.
|
|||
|
|
|
|||
|
|
### In scope — migration sweep
|
|||
|
|
|
|||
|
|
Migrate consumers of the legacy classes to the new primitives:
|
|||
|
|
|
|||
|
|
- Every page under `pages/**/*.js` that uses `className="btn-primary"`
|
|||
|
|
/ `"btn-flame"` / `"btn-ember"` / `"btn-gold"` / `"action-btn-primary"`
|
|||
|
|
/ `"action-btn-secondary"`.
|
|||
|
|
- Every component under `components/**/*.js` that uses those classes.
|
|||
|
|
- `input-field` / `search-bar` / `theme-toggle` / `header-icon`
|
|||
|
|
consumers.
|
|||
|
|
|
|||
|
|
The legacy utility classes in `styles/globals.css` are **NOT deleted
|
|||
|
|
here** — they remain as thin aliases (or stub no-ops) until sub-convoy
|
|||
|
|
#8 deletes them as a batch. This keeps the diff per PR readable.
|
|||
|
|
|
|||
|
|
### Out of scope
|
|||
|
|
|
|||
|
|
- Modal / Surface primitives — sub-convoy #2.
|
|||
|
|
- Layout shell — sub-convoy #4.
|
|||
|
|
- Card surfaces — sub-convoy #5.
|
|||
|
|
- New form patterns (multi-step wizards, etc.) — orthogonal scope.
|
|||
|
|
|
|||
|
|
## Roles invoked
|
|||
|
|
|
|||
|
|
1. `role-architect` — primitive API, brief decomposition (likely 3–4
|
|||
|
|
briefs by file cluster).
|
|||
|
|
2. `role-a11y-auditor` — pre-implementation review of Button + Input
|
|||
|
|
ARIA contracts (especially `<Input>` error association).
|
|||
|
|
3. `role-design-system-auditor` — sign-off on variant shape +
|
|||
|
|
focus-ring recipe.
|
|||
|
|
4. `role-implementer` — multiple briefs.
|
|||
|
|
5. Post-PR audit fleet.
|
|||
|
|
|
|||
|
|
## Architecture (ratified 2026-06-03)
|
|||
|
|
|
|||
|
|
**Primitives:**
|
|||
|
|
- `components/ui/Button.js` — `forwardRef`. Variants: `primary` (ember gradient w/ ember-rim-pronounced + rim-light-inner; hover scales 1.02; active scales 0.98), `secondary` (glass-surface-high + rim-subtle), `danger` (#dc2626), `ghost` (transparent w/ ember-tinted hover). Sizes: sm/md/lg. Built-in `loading` (aria-busy + spinner replaces leading icon), `disabled` (opacity 0.5 + pointer-events-none), `leadingIcon` + `trailingIcon`, ember focus-visible ring.
|
|||
|
|
- `components/ui/Input.js` — `forwardRef`. Glass-surface-high background, ember focus ring, supports `label` (semantic htmlFor/id pairing), `error` (red border + red message + aria-invalid + aria-describedby), `helperText` (mutually exclusive with error), `leadingIcon` (decorative pointer-events-none), `trailingAction` (interactive). All native input props pass through.
|
|||
|
|
- `components/ui/SearchBar.js` — `forwardRef`. Wraps Input with leading search icon, conditional clear button (renders only when value non-empty AND onClear provided). Defaults type="search", placeholder "Search…".
|
|||
|
|
|
|||
|
|
**Test plan:** `test/components/ui-primitives.test.js` — 10 cases. Button: children/onClick, loading state (aria-busy + disabled), disabled suppresses click, all 4 variants render. Input: label/htmlFor pairing, error sets aria-invalid + describedby + visible message, helperText path with no error. SearchBar: search-type input + placeholder, clear button conditional on value + onClear, no clear when onClear missing.
|
|||
|
|
|
|||
|
|
## Briefs
|
|||
|
|
|
|||
|
|
- **Brief 1 (shipped 2026-06-03):** Primitives + tests + 2 reference page migrations (login.js, signup.js — both smoke-tested critical paths). 7 inputs + 2 submit buttons migrated. Vitest 104/104 green (+10 new primitive tests). Lint 0 errors. Existing `test/pages/login.test.js` assertion ("Sign in to Deck Hearth" button text) preserved.
|
|||
|
|
- **Brief 2 (queued for follow-up):** Sweep remaining form-bearing surfaces — profile/settings pages, deck-builder text inputs, scanner search field, card-editor admin form, all collection-cluster modal forms (Brief 2 here lands AFTER #2's Brief 2 so the modal shell is already in place). Mechanical migration following the login/signup pattern.
|
|||
|
|
|
|||
|
|
## Todos
|
|||
|
|
|
|||
|
|
- [ ] Architect: primitive API + brief decomposition
|
|||
|
|
- [ ] A11y auditor: pre-impl ARIA review
|
|||
|
|
- [ ] Design-system auditor: variant shape + focus-ring recipe
|
|||
|
|
- [ ] Brief 1 — primitives + tests + migrate 2 reference pages
|
|||
|
|
(`pages/login.js`, `pages/signup.js` — the most form-dense
|
|||
|
|
auth surfaces, also covered by smoke spec #2)
|
|||
|
|
- [ ] Brief 2 — migrate `components/**` consumers (cluster by neighbor)
|
|||
|
|
- [ ] Brief 3 — migrate `pages/**` consumers (cluster by neighbor)
|
|||
|
|
- [ ] Post-PR audit per brief
|
|||
|
|
|
|||
|
|
## Decisions to ratify (architect)
|
|||
|
|
|
|||
|
|
1. **`<Button variant>` set** — proposal: `primary | ghost | ember |
|
|||
|
|
gold | icon`. Confirm or trim.
|
|||
|
|
2. **Loading state shape** — inline spinner vs button-shaped
|
|||
|
|
skeleton. Recommended: inline spinner that replaces `iconLeft` slot.
|
|||
|
|
3. **Focus ring recipe** — `box-shadow: 0 0 0 3px var(--ember-rim-pronounced)`
|
|||
|
|
on `:focus-visible` (NOT `:focus` — keep mouse-click focus clean).
|
|||
|
|
Confirm.
|
|||
|
|
4. **Input error wiring** — `aria-invalid="true"` + `aria-describedby`
|
|||
|
|
pointing at the error `<span>`. Confirm.
|
|||
|
|
5. **`<Input srOnly>` rendering** — `class="sr-only"` on the label,
|
|||
|
|
not removed from DOM. Required for screen readers.
|
|||
|
|
6. **Migration approach for ad-hoc inline buttons** — many pages
|
|||
|
|
compose `<button className="bg-[...] text-[...] ...">` directly with
|
|||
|
|
no utility class. Architect inventories these during architect pass
|
|||
|
|
and decides whether to fold into briefs or leave for sub-convoy #8's
|
|||
|
|
hex sweep.
|
|||
|
|
|
|||
|
|
## Acceptance criteria
|
|||
|
|
|
|||
|
|
1. `<Button>`, `<Input>`, `<SearchBar>` exist under `components/ui/`.
|
|||
|
|
2. Tests pass.
|
|||
|
|
3. Every consumer of the 7 legacy utility classes is migrated OR
|
|||
|
|
marked for #8.
|
|||
|
|
4. `<Input>` error states wire `aria-describedby` (closes a11y
|
|||
|
|
finding).
|
|||
|
|
5. Lint + vitest + smoke green.
|
|||
|
|
6. Visual-diff baselines re-seeded per brief.
|
|||
|
|
|
|||
|
|
## CI impact
|
|||
|
|
|
|||
|
|
| Workflow / job | Behavior |
|
|||
|
|
| --- | --- |
|
|||
|
|
| `preview-smoke.yml` | Fires. Smoke spec 2 (`'sign-in page renders'`) defends Button migration on `/login` post-Brief 1. |
|
|||
|
|
| `visual-diff.yml` | **Fires + LOUD** — buttons appear everywhere. |
|
|||
|
|
| `lint` | Fires. |
|
|||
|
|
| `test:` (vitest) | Fires + new Button/Input assertions lock primitive contract. |
|
|||
|
|
| New grep gates | None for this convoy; #8 may add a `forbidden-legacy-btn-class` grep gate post-cleanup. |
|
|||
|
|
|
|||
|
|
## Known constraints
|
|||
|
|
|
|||
|
|
- **Tailwind utility classes inside primitive** are fine — the
|
|||
|
|
primitive IS the abstraction; nothing outside it cares.
|
|||
|
|
- **No new third-party form library.** Plain `<button>` / `<input>`
|
|||
|
|
underneath; no Formik, no react-hook-form. Existing forms in the
|
|||
|
|
codebase manage state with `useState`; that pattern stays.
|
|||
|
|
- **Theme tokens** — primitives consume ONLY tokens from #1; no
|
|||
|
|
hardcoded hex.
|
|||
|
|
|
|||
|
|
## Multitask dispatch
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
slice_dependencies:
|
|||
|
|
- brief: 1
|
|||
|
|
depends_on: []
|
|||
|
|
files:
|
|||
|
|
- components/ui/Button.js
|
|||
|
|
- components/ui/Input.js
|
|||
|
|
- components/ui/SearchBar.js
|
|||
|
|
- components/ui/index.js
|
|||
|
|
- test/components/Button.test.js
|
|||
|
|
- test/components/Input.test.js
|
|||
|
|
- pages/login.js
|
|||
|
|
- pages/signup.js
|
|||
|
|
- brief: 2
|
|||
|
|
depends_on: [1]
|
|||
|
|
files:
|
|||
|
|
# Architect-curated cluster of component consumers
|
|||
|
|
- components/CardItem.js
|
|||
|
|
- components/CardDetailView.js
|
|||
|
|
- components/CardsPageView.js
|
|||
|
|
- components/CollectionPageView.js
|
|||
|
|
- components/CollectionsPageView.js
|
|||
|
|
- components/ScannerPageView.js
|
|||
|
|
# ... (architect completes inventory)
|
|||
|
|
- brief: 3
|
|||
|
|
depends_on: [1]
|
|||
|
|
files:
|
|||
|
|
# Architect-curated cluster of page consumers
|
|||
|
|
- pages/index.js
|
|||
|
|
- pages/profile.js
|
|||
|
|
- pages/settings.js
|
|||
|
|
- pages/my-cards.js
|
|||
|
|
- pages/collections.js
|
|||
|
|
# ... (architect completes inventory)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**After Brief 1 merges:** `/multitask role-implementer briefs 2, 3`
|
|||
|
|
(disjoint file sets if architect partitions correctly).
|
|||
|
|
|
|||
|
|
## Out of scope follow-ups
|
|||
|
|
|
|||
|
|
- **`forbidden-legacy-btn-class`** grep gate (P3 hygiene) — surface for
|
|||
|
|
#8's cleanup. Forbid `className="btn-(primary|flame|ember|gold|
|
|||
|
|
secondary)"` post-migration.
|
|||
|
|
- **`form-validation-library-adoption`** — react-hook-form vs Zod vs
|
|||
|
|
homegrown. Out of scope; would be its own architect-led convoy.
|