deckhearth/.convoys/migrate-button-input-mobilenav-to-glass-primitive.md
Randall Stillwell 44c94243c2 refactor(glass): migrate Button.secondary + Input + MobileNav off bespoke glass-surface
Closes the migrate-button-input-mobilenav-to-glass-primitive convoy
(seeded by PR #127). All 3 residual handrolled var(--glass-surface-*)
inline-style usages migrated to either purpose-built utility classes
or the <GlassSurface> primitive. CI allowlist reduced from 6 entries
to 3 (chrome only).

Architect decisions (D1-D3, ratified):

D1 — Button.secondary → new .btn-glass-secondary utility class.
  NOT <GlassSurface>: the primitive sets `background` inline via
  composedStyle, which CSS :hover rules can't override without
  !important. The new class composes the same high-tint
  gradient-border that .glass-panel-strong uses, plus a pure-CSS
  :hover swap (high → mid fill on the padding-box layer).
  Identical visual contract; the hover behavior is now driven by
  CSS, not Tailwind's `hover:bg-[var(...)]` arbitrary class.

D2 — Input → new .glass-input utility class.
  NOT <GlassSurface as="input"> and NOT <GlassSurface as="div"> wrap.
  Reason: <GlassSurface>'s gradient-border trick requires
  `border: 1px solid transparent` to expose the border-box layers,
  which conflicts with <Input>'s conditional error-state
  `1px solid #dc2626` red border. The new class adopts only the
  tint + blur layer; the visible 1px border + focus ring stay in
  JSX (class-controlled, not inline). Same visual contract as
  before for both normal AND error states.

D3 — MobileNavigation → <GlassSurface as="div" tint="mid" blur="mid"
  rim="subtle" elevation="flat" cornerLights="chrome">.
  NOT .page-header-glass (the seed's first recommendation):
  .page-header-glass uses var(--glass-surface-high) (wrong tint —
  MobileNav uses mid) and sets a bottom-border separator (wrong
  for a fixed-bottom-nav where the bottom edge is the viewport
  edge). <GlassSurface> is the better fit AND brings the
  chrome-tier corner-light bleed that the parent convoy is
  unifying across all chrome surfaces.

Implementation choice — single PR (not 3 parallel briefs):
  The seed recommended 3 small parallel-safe briefs (one per file).
  D1 and D2 both need styles/globals.css to gain new utility
  classes, so those 2 changes can't run truly in parallel without
  merge conflicts. Single PR is faster, simpler to review
  end-to-end, and the natural shape for a 2-3 hour convoy with
  tightly-coupled artifacts.

Files changed (4):

styles/globals.css (+50 / -1):
  - Adds .btn-glass-secondary (with :hover variant) — D1.
  - Adds .glass-input — D2.
  - Both classes documented inline with architect-decision references.

components/ui/Button.js (+2 / -10):
  - Replaces inline variantStyle + Tailwind hover arbitrary class
    for `variant === 'secondary'` with `variantClass =
    'btn-glass-secondary font-medium'`. variantStyle now `{}`.
  - Other variants (primary, danger, ghost) UNCHANGED.

components/ui/Input.js (+1 / -7):
  - Adds `glass-input` to the className list.
  - Removes inline `background` + `backdropFilter` +
    `WebkitBackdropFilter` from the input's style block.
  - Conditional `border: inputBorder` stays in JSX (error swap).
  - All other props/behavior preserved.

components/MobileNavigation.js (+11 / -8):
  - Adds `import { GlassSurface } from './ui'`.
  - Replaces the inline-styled backdrop <div> with
    <GlassSurface as="div" ...>. Same className ("absolute inset-0"),
    same visible behavior, plus the chrome-tier corner-light bleed.
  - Comment block updated to reference the convoy + decision.

.github/workflows/ci.yml (+8 / -22):
  - forbidden-patterns Check 7/7 GLASS_ALLOWLIST reduced from 6
    entries to 3 (chrome only). The TODO comments referencing this
    convoy are deleted (work is done).

.convoys/migrate-button-input-mobilenav-to-glass-primitive.md
  (+74 / -3):
  - status: queued → closed, closed: 2026-06-05, prs: [131].
  - Architect ratifications D1-D3 written into front-matter docs.
  - Closeout checklist with all acceptance criteria checked.
  - Note that parent convoy unify-glass-panel-surfaces is now
    fully closed — no residual handrolled glass-surface usage
    outside the 3 chrome blocks.

Verification:
- POSITIVE TEST: post-migration grep with the reduced 3-entry
  allowlist returns 0 violations. 
- grep on raw files: only Layout.js + TopSearchBar.js still match
  the literal regex (GlassSurface.js uses template literal which
  doesn't match — intentional, allowlist is forward-compat).
- YAML parses (python3 yaml.safe_load).
- npm run lint passes (1 pre-existing unrelated warning).
- npm run test:run: 118/118 tests pass.

Visual diff to be verified by reviewer in light + dark mode for:
- <Button variant="secondary"> default + hover state.
- <Input> default + error state.
- Mobile bottom-nav backdrop.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 06:23:00 -05:00

9.2 KiB

status classification parent_convoy blocked_by size budget_hours created closed prs
closed server-only-no-actually-just-frontend-styles-cleanup unify-glass-panel-surfaces
small 2-3 2026-06-04 2026-06-05
131

Architect ratifications (2026-06-05)

The seed posed 3 open questions. Ratified decisions:

D1 — Button secondary → New .btn-glass-secondary utility class in styles/globals.css (NOT <GlassSurface>). Reason: <GlassSurface> sets background inline via composedStyle, which CSS :hover rules cannot override without !important. A purpose-built class with a pure-CSS :hover swap (high → mid fill on the padding-box layer of the gradient-border composition) keeps the hover semantics clean. The class composes the same high-tint gradient-border that .glass-panel-strong uses.

D2 — Input → New .glass-input utility class in styles/globals.css (NOT <GlassSurface as="input"> and NOT a wrapping <GlassSurface as="div">). Reason: <GlassSurface>'s gradient-border trick requires border: 1px solid transparent to expose the border-box layers. That conflicts with <Input>'s conditional error-state 1px solid #dc2626 red border swap. The new class adopts only the tint + blur layer; the visible 1px border + focus ring stay in JSX (class-controlled, not inline).

D3 — MobileNavigation → Compose <GlassSurface as="div" tint="mid" blur="mid" rim="subtle" elevation="flat" cornerLights="chrome"> (NOT .page-header-glass). Reason: the seed recommended .page-header-glass reuse, but that class uses var(--glass-surface-high) (wrong tint — MobileNav uses mid) and sets a bottom-border separator (wrong for a fixed-bottom-nav where the bottom edge is the viewport edge). <GlassSurface> is the better fit AND brings the chrome-tier corner-light bleed that the parent convoy is unifying across all chrome surfaces.

Implementation choice: single PR, not 3

The seed recommended 3 small parallel-safe briefs (one per file). On execution that's the wrong decomposition — D1 and D2 BOTH need the same styles/globals.css to gain new utility classes, so those 2 changes can't run truly in parallel without merge conflicts. With the CI billing block still active, each PR also requires an admin-merge cycle. Shipping all 3 migrations + the CSS additions + the CI allowlist reduction in a single PR was faster, simpler to review end-to-end, and the natural shape for a small (2-3 hour) convoy with tightly-coupled artifacts.

Closeout

All acceptance criteria from the seed met:

  • Button.js secondary variant uses .btn-glass-secondary.
  • Input.js outer wrapper uses .glass-input.
  • MobileNavigation.js bottom-nav backdrop uses <GlassSurface>.
  • grep -lE "var\(--glass-surface-(low|mid|high)\)" pages components -r --include='*.js' returns only Layout.js + TopSearchBar.js (GlassSurface.js uses a template-literal ${tint} that doesn't match the static regex — intentional).
  • .github/workflows/ci.yml's GLASS_ALLOWLIST reduced from 6 entries to 3 (the 3 chrome blocks).
  • npm run lint passes (1 pre-existing unrelated warning).
  • npm run test:run: 118/118 tests pass.
  • Visual diff against main — to be verified by reviewer in light + dark mode for the 3 migrated surfaces.

The parent convoy unify-glass-panel-surfaces is now FULLY closed — no residual handrolled glass-surface usage outside the 3 chrome blocks, and the gate (Check 7/7 of forbidden-patterns) enforces that contract going forward.


migrate-button-input-mobilenav-to-glass-primitive

Why

The unify-glass-panel-surfaces convoy's Brief 7 added a CI gate that forbids bespoke var(--glass-surface-*) inline-style usage outside a documented allowlist. When the gate was being added, three files turned out to still handroll their own glass surfaces and had to be admitted to the allowlist to ship the gate now:

  • components/ui/Button.js — the secondary variant carries style={{ background: 'var(--glass-surface-high)', backdropFilter: '...' }} and a Tailwind arbitrary class hover:bg-[var(--glass-surface-mid)].
  • components/ui/Input.js — the input fill is style={{ background: 'var(--glass-surface-high)', backdropFilter: '...' }} on the outer wrapper of the input control.
  • components/MobileNavigation.js — the bottom-nav background layer is style={{ background: 'var(--glass-surface-mid)', backdropFilter: '...' }}.

The pattern (inline background: var(--glass-surface-X) + inline backdropFilter) is exactly what the convoy spent six briefs eliminating elsewhere. These three are the residual.

Goal (1 sentence)

Migrate components/ui/Button.js's secondary variant, components/ui/Input.js, and components/MobileNavigation.js to compose <GlassSurface> (with the right cornerLights + blur + tint props from Brief 1) or the appropriate .glass-panel-* / .page-header-glass class, then delete the three entries from the forbidden-bespoke-glass-surface allowlist in .github/workflows/ci.yml so the gate covers them too.

Files in scope

  • components/ui/Button.js — the secondary variant block only; leave primary, danger, ghost, etc. as-is unless they legitimately need the same migration (they don't today).
  • components/ui/Input.js — the outer wrapper style only.
  • components/MobileNavigation.js — the bottom-nav backdrop layer only.
  • .github/workflows/ci.yml — the forbidden-bespoke-glass-surface check (now Check 7/7 of the consolidated forbidden-patterns job). Delete the three pending entries from GLASS_ALLOWLIST, leaving only the 3 chrome blocks.

Open questions for the architect

  1. <Button variant="secondary"><GlassSurface> or class? The button uses a complex backdropFilter + boxShadow stack matching glass-panel-strong's look. Composing <GlassSurface tint="high" blur="low" cornerLights="subtle"> keeps it tokenized and means cornerLights ripples in for free. The hover variant (hover:bg-[var(--glass-surface-mid)]) needs a different solution — either a hover prop on <GlassSurface>, or wrap the hover state in a separate utility class. Recommend pulling the hover into a CSS variable swap on the :hover pseudo-class of a new utility class (.glass-surface-hover-shift or similar), authored in styles/globals.css.

  2. <Input><GlassSurface as="div"> wrapping the native <input>? That's the most consistent shape, but the current <Input> API takes inline-style props the wrapper would have to forward. Easier alternative: add .glass-input utility class to styles/globals.css mirroring .glass-panel-strong's shape but with border-radius: 8px and the input-specific focus ring.

  3. <MobileNavigation>.page-header-glass? That class was designed for the desktop top-of-page strip; the bottom-nav has the same "full-bleed translucent chrome" semantics inverted vertically. Either reuse the class (simplest), or introduce a .glass-bottom-nav mirror. Recommend reuse since the visual contract is identical aside from vertical anchoring (controlled by the consumer's <div className="fixed bottom-0 ...">).

Acceptance criteria (draft — architect to ratify)

  • Button.js secondary variant uses <GlassSurface> or a documented .glass-* class.
  • Input.js outer wrapper uses <GlassSurface> or a documented .glass-input class.
  • MobileNavigation.js bottom-nav backdrop uses .page-header-glass (or .glass-bottom-nav if the architect decides on a mirror).
  • grep -lE "var\(--glass-surface-(low|mid|high)\)" pages components -r --include='*.js' returns only the 3 chrome files (Layout, TopSearchBar, GlassSurface).
  • .github/workflows/ci.yml's GLASS_ALLOWLIST is reduced from 6 entries to 3.
  • Visual diff against main shows no regression in the secondary button, the <Input> control, or the bottom-nav surface in both themes.
  • npm run lint + npm run test:run both green.

Out of scope

  • Other Button variants (primary, danger, ghost) — they don't use var(--glass-surface-*).
  • The <GlassSurface> primitive itself — Brief 1 already shipped the cornerLights prop; this convoy just adopts it in 3 places.
  • Any other component the grep doesn't currently flag — if a new file appears in the grep result after this convoy lands, that's a separate convoy (per the forbidden-bespoke-glass-surface gate's own friction principle).

Pre-work the conductor should verify

  • Brief 1 (<GlassSurface> cornerLights prop) has merged. — PR #123.
  • Brief 7 (this convoy's parent gate) has merged.
  • The 3 target files still contain var(--glass-surface-*) inline styles (re-run the grep at kickoff).

Notes for future agents

The 3 files are independent — there's no shared abstraction across them. Recommend treating this as 3 small briefs (one per file) the architect can dispatch in parallel after deciding the migration shape per file in the open questions above. If the architect chooses the "add .glass-input and .glass-bottom-nav mirror utility classes" path, those style additions belong in a 4th brief that ships first.