83 lines
5.9 KiB
Markdown
83 lines
5.9 KiB
Markdown
|
|
---
|
||
|
|
convoy: unify-glass-panel-surfaces
|
||
|
|
brief_number: 1
|
||
|
|
depends_on: []
|
||
|
|
files:
|
||
|
|
- components/ui/GlassSurface.js
|
||
|
|
- test/components/ui-primitives.test.js
|
||
|
|
---
|
||
|
|
|
||
|
|
# Brief 1: Upgrade `<GlassSurface>` primitive with `cornerLights` prop
|
||
|
|
|
||
|
|
## Goal (1 sentence)
|
||
|
|
|
||
|
|
Add a `cornerLights = 'subtle' | 'chrome' | 'none'` prop to `<GlassSurface>` and compose the 4-layer gradient-border pattern (padding-box fill + two border-box corner radials + `--chip-border-base` base) on top of the existing `tint` / `blur` / `rim` / `elevation` props so every consumer (Modal, StatCard, landing feature cards) picks up corner catch-lights automatically.
|
||
|
|
|
||
|
|
## Files in scope (do not edit anything else)
|
||
|
|
|
||
|
|
- `components/ui/GlassSurface.js` — add the prop, compose the 4-layer `background` and the `border: 1px solid transparent` when the prop is not `'none'`.
|
||
|
|
- `test/components/ui-primitives.test.js` — add 3 corner-light assertions to the existing `describe('GlassSurface', ...)` block (or add the block if it doesn't exist; today the file covers Button / Input / SearchBar only, so a new top-level `describe('GlassSurface', ...)` block at the end of the file is correct).
|
||
|
|
|
||
|
|
**Out of scope:** every consumer of `<GlassSurface>` — `components/ui/Modal.js`, `components/ui/StatCard.js`, `pages/index.js` landing cards. Their behavior changes via the new default; no edits required. Do NOT modify them in this PR.
|
||
|
|
|
||
|
|
## Conventions to follow
|
||
|
|
|
||
|
|
- **Verbatim 4-layer recipe** (mirrors `.glass-panel-strong` post-PR #118; see `styles/globals.css`):
|
||
|
|
```js
|
||
|
|
// When cornerLights === 'subtle' (default):
|
||
|
|
background:
|
||
|
|
`linear-gradient(var(--glass-surface-${tint}), var(--glass-surface-${tint})) padding-box,
|
||
|
|
radial-gradient(at 0% 100%, var(--corner-light-warm-subtle) 0%, transparent 42%) border-box,
|
||
|
|
radial-gradient(at 100% 0%, var(--corner-light-cool-subtle) 0%, transparent 42%) border-box,
|
||
|
|
var(--chip-border-base) border-box`
|
||
|
|
// When cornerLights === 'chrome':
|
||
|
|
// Same recipe; swap --corner-light-warm-subtle → --corner-light-warm
|
||
|
|
// and --corner-light-cool-subtle → --corner-light-cool.
|
||
|
|
// When cornerLights === 'none':
|
||
|
|
// Today's behavior — single layer background: var(--glass-surface-${tint}).
|
||
|
|
// No border declaration.
|
||
|
|
```
|
||
|
|
- **Border declaration**: when `cornerLights !== 'none'`, also set
|
||
|
|
`border: '1px solid transparent'` on the composed style. This is what
|
||
|
|
reveals the border-box gradient layers. When `'none'`, omit the
|
||
|
|
border entirely (preserve today's box-model).
|
||
|
|
- **Shadow stack**: unchanged. The existing `RIM_SHADOWS` /
|
||
|
|
`ELEVATION_SHADOWS` composition logic stays exactly as written. The
|
||
|
|
corner-light layers live in `background`, not `box-shadow`.
|
||
|
|
- **Default value**: `cornerLights = 'subtle'`. This is the new default
|
||
|
|
for every consumer that doesn't pass the prop explicitly.
|
||
|
|
- **Backdrop filter**: unchanged. `backdropFilter` /
|
||
|
|
`WebkitBackdropFilter` continue to compose from `blur` / `tint` exactly
|
||
|
|
as today. Do not touch this line.
|
||
|
|
- **`style` prop merging**: today the composed style spreads
|
||
|
|
`...style` LAST so caller overrides win. Preserve that exactly — a
|
||
|
|
caller passing `style={{ border: '1px solid #f00' }}` overrides the
|
||
|
|
transparent border. This is the explicit escape hatch any consumer
|
||
|
|
who needs a real border can use without setting `cornerLights='none'`.
|
||
|
|
- **AGENTS.md / no-go zones**: `styles/globals.css` is untouched in
|
||
|
|
this brief (Brief 5 owns the `.card` deletion; no other CSS edits in
|
||
|
|
this convoy). The `--corner-light-warm-subtle` /
|
||
|
|
`--corner-light-cool-subtle` / `--corner-light-warm` /
|
||
|
|
`--corner-light-cool` / `--chip-border-base` tokens all exist
|
||
|
|
in `:root` and `[data-theme="dark"]` per PR #118 — verified.
|
||
|
|
|
||
|
|
## Acceptance criteria
|
||
|
|
|
||
|
|
- [ ] `<GlassSurface>` accepts a `cornerLights` prop with values `'subtle'` (default), `'chrome'`, `'none'`. Any other value falls back to `'subtle'` (mirror the existing `RIM_SHADOWS[rim] ?? []` defensive fallback shape).
|
||
|
|
- [ ] When `cornerLights='subtle'`, rendered output includes:
|
||
|
|
- `background:` containing both `var(--corner-light-warm-subtle)` and `var(--corner-light-cool-subtle)`.
|
||
|
|
- `border:` value `1px solid transparent`.
|
||
|
|
- [ ] When `cornerLights='chrome'`, rendered output includes:
|
||
|
|
- `background:` containing `var(--corner-light-warm)` and `var(--corner-light-cool)` (NOT the `-subtle` variants).
|
||
|
|
- `border:` value `1px solid transparent`.
|
||
|
|
- [ ] When `cornerLights='none'`, rendered output is identical to today: single-layer `background: var(--glass-surface-${tint})`, no `border` declaration in the composed style.
|
||
|
|
- [ ] `tint` / `blur` / `rim` / `elevation` / `as` / `style` / `className` props all behave exactly as before. Existing consumers continue to compile + render without prop changes.
|
||
|
|
- [ ] `test/components/ui-primitives.test.js` has 3 new assertions (one per `cornerLights` value) verifying the inline style attribute or the rendered element's `style.background` / `style.border` accordingly. Use the existing `render` + `screen.getByRole` / `getByText` / `container` patterns from the file; don't pull in new test deps.
|
||
|
|
- [ ] `npm run lint` passes.
|
||
|
|
- [ ] `npm run test:run` is 22+/22+ (today's count is 21; this brief adds at least 3 — exact count depends on whether you split assertions across multiple `it(...)` blocks).
|
||
|
|
- [ ] No edits to files outside the two listed in `files:` above. **No consumer migrations in this PR.** Brief 3 + 4 will compose against the new default; Briefs 2 + 5 + 6 don't use the primitive.
|
||
|
|
|
||
|
|
## Rationale (≤3 sentences)
|
||
|
|
|
||
|
|
The primitive is the single largest leverage point in the convoy — upgrading it ripples through `<Modal>`, `<StatCard>`, and the landing-page feature cards in one commit. Defaulting to `'subtle'` matches the PR #118 token-tier decision and means downstream consumers don't need to opt in. The `'chrome'` + `'none'` values exist so the prop is future-proof: floating chrome can opt up without handrolling, and any GPU-constrained tile (e.g. a future <CardItem>-like surface) can opt out cleanly.
|