2026-06-04 17:47:26 -04:00
|
|
|
---
|
refactor(glass): migrate Button.secondary + Input + MobileNav off bespoke glass-surface (#131)
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 07:23:55 -04:00
|
|
|
status: closed
|
2026-06-04 17:47:26 -04:00
|
|
|
classification: server-only-no-actually-just-frontend-styles-cleanup
|
|
|
|
|
parent_convoy: unify-glass-panel-surfaces
|
|
|
|
|
blocked_by: []
|
|
|
|
|
size: small
|
|
|
|
|
budget_hours: 2-3
|
refactor(glass): migrate Button.secondary + Input + MobileNav off bespoke glass-surface (#131)
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 07:23:55 -04:00
|
|
|
created: 2026-06-04
|
|
|
|
|
closed: 2026-06-05
|
|
|
|
|
prs:
|
|
|
|
|
- 131 # Single-PR convoy — all 3 migrations + CI allowlist reduction
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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:
|
|
|
|
|
|
|
|
|
|
- [x] `Button.js` secondary variant uses `.btn-glass-secondary`.
|
|
|
|
|
- [x] `Input.js` outer wrapper uses `.glass-input`.
|
|
|
|
|
- [x] `MobileNavigation.js` bottom-nav backdrop uses `<GlassSurface>`.
|
|
|
|
|
- [x] `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).
|
|
|
|
|
- [x] `.github/workflows/ci.yml`'s `GLASS_ALLOWLIST` reduced from
|
|
|
|
|
6 entries to 3 (the 3 chrome blocks).
|
|
|
|
|
- [x] `npm run lint` passes (1 pre-existing unrelated warning).
|
|
|
|
|
- [x] `npm run test:run`: 118/118 tests pass.
|
|
|
|
|
- [x] 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.
|
|
|
|
|
|
2026-06-04 17:47:26 -04:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 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.
|