Commit graph

11 commits

Author SHA1 Message Date
varutasu
0d14278021
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 06:23:55 -05:00
varutasu
e8eba34d59
refactor(layout): migrate 3 floating popovers to .glass-panel-strong (#124)
Brief 3 of unify-glass-panel-surfaces convoy. Migrates three floating
surfaces from inline var(--glass-surface-*) + backdropFilter to the
canonical .glass-panel-strong className, preserving their existing
box-shadow chains (ember rim for the dropdown panels; pronounced
elevation for the drawer + TopSearchBar UserMenu) via inline override.

Three popovers migrated:
1. components/Layout.js UserProfileDropdown panel (sidebar)
   - boxShadow chain preserved: var(--rim-light-inner),
     var(--ember-rim-subtle), var(--elevation-ambient).
2. components/Layout.js mobile drawer
   - boxShadow chain preserved: var(--rim-light-inner),
     var(--rim-light-outer), var(--elevation-pronounced).
3. components/ui/TopSearchBar.js UserMenu dropdown
   - boxShadow chain preserved: var(--rim-light-inner),
     var(--ember-rim-subtle), var(--elevation-pronounced) (note:
     -pronounced, not -ambient — caught by architect boot-the-brief
     recheck and documented in convoy's risk note).

The sidebar nav-chip / main content chrome block (Layout.js ~L853-863)
intentionally remains handrolled with full-intensity corner lights —
allowlisted by Brief 7's CI gate (D4 of the architect plan).

Tests (test/components/Layout.test.js, +2 new assertions):
- mobile drawer container queryable via .glass-panel-strong selector
  and is wired with width/positioning classes (.w-64, .fixed, etc).
- mobile drawer inline style contains no var(--glass-surface-*) and
  no backdrop-filter (both now provided by the class); does contain
  var(--elevation-pronounced) (preserved override).

Verification:
- npm run lint passes (1 pre-existing unrelated warning).
- npm run test:run: 118/118 tests pass (was 116; +2 new).

Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-3-floating-popovers.md
all met. No edits outside the 3 files in scope.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 16:25:14 -05:00
varutasu
3248468a33
feat(GlassSurface): add cornerLights prop (subtle | chrome | none) (#123)
Brief 1 of unify-glass-panel-surfaces convoy. Adds a `cornerLights`
prop to the <GlassSurface> primitive so corner catch-lights compose
into every consumer (<Modal>, <StatCard>, landing feature cards) by
default — no per-consumer migration needed.

API:
  cornerLights: 'subtle' (default) | 'chrome' | 'none'

  'subtle' → 4-layer background using --corner-light-warm-subtle /
              --corner-light-cool-subtle (matches .glass-panel-strong
              post-PR #118; appropriate for most data surfaces).
  'chrome' → same recipe with the full-intensity
              --corner-light-warm / --corner-light-cool tokens
              (matches the Layout sidebar nav-chip and TopSearchBar
              header treatments).
  'none'   → today's pre-Brief-1 behavior. Single-layer background:
              var(--glass-surface-{tint}); no transparent border, no
              corner radials. Escape hatch for GPU-budget-constrained
              tiles that legitimately must skip the gradient-border
              treatment.

Composition recipe (verbatim mirror of styles/globals.css's
.glass-panel-strong block post-PR #118):

  linear-gradient(<fill>, <fill>) padding-box,
  radial-gradient(at 0% 100%, <warm> 0%, transparent 42%) border-box,
  radial-gradient(at 100% 0%, <cool> 0%, transparent 42%) border-box,
  var(--chip-border-base) border-box

Paired with `border: 1px solid transparent` so the border-box
gradients render through the border. For cornerLights='none', the
border declaration is omitted entirely — preserves today's box-model
exactly.

Other props (`tint`, `blur`, `rim`, `elevation`, `as`, `style`,
`className`) and the `...style` LAST-wins merge order are unchanged.

Test additions (test/components/ui-primitives.test.js, +49 lines):
  - cornerLights='subtle' (default): asserts --corner-light-*-subtle
    tokens, padding-box/border-box layers, --chip-border-base, and
    `border: 1px solid transparent` all present in the rendered
    inline style attribute.
  - cornerLights='chrome': asserts the full-intensity tokens
    (NOT the -subtle variants); same border declaration.
  - cornerLights='none': asserts single-layer
    `background: var(--glass-surface-mid)`, no corner-light tokens,
    no padding-box, no --chip-border-base, no border declaration.

Verification:
  - npm run lint passes (1 pre-existing unrelated warning).
  - npm run test:run: 116/116 tests pass (was 113; +3 GlassSurface
    assertions).

Ripple effect (intentional, per architect plan):
  <Modal>, <StatCard>, and the landing-page feature cards all
  delegate to <GlassSurface>. Defaulting to cornerLights='subtle'
  means each of them now renders with corner catch-lights without
  any per-consumer edit. The visual-diff baseline refresh is the
  expected side effect; queue on Linux per AGENTS.md § Testing
  before Brief 3 + Brief 4 dispatch.

Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-1-upgrade-glass-surface-primitive.md
all met. No consumer migrations in this PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:30:32 -05:00
varutasu
c57b5d0406
refactor(design): move light response onto chip borders; cut directional drop shadows (#116)
Operator feedback 2026-06-04 (after PR #115's two-light-source pass):
"A lot of gradients happening, though I want the gradient in the
background to be the focus, and then the border gets a highlight,
perhaps, in a corner that's closest to the background gradients...
the borders are reflecting the color, not necessarily the drop
shadows behind the cards. In fact, you could probably reduce those
box shadows quite a bit. At least they shouldn't span the entire
edge of the card. Just a subtle bit in the corner."

PR #115's outer directional drop-shadows (`--edge-light-warm` /
`--edge-light-cool`) DID make the chips respond to the two light
sources but spanned the entire bottom-left + top-right edges, which
competed visually with the body gradient. This PR moves the
warm/cool response onto the chip's 1px BORDER, concentrated in the
two corners closest to the matching light sources, and cuts the
directional drop-shadows entirely (just rim-light-inner + a single
neutral elevation-ambient remain).

Technique: the modern gradient-border pattern.
  background:
    /* surface fill, clipped to padding-box */
    linear-gradient(surface, surface) padding-box,
    /* warm catch-light at bottom-left, border-box-clipped */
    radial-gradient(at 0% 100%, warm 0%, transparent 42%) border-box,
    /* cool catch-light at top-right, border-box-clipped */
    radial-gradient(at 100% 0%, cool 0%, transparent 42%) border-box,
    /* faint diagonal white tint for the rest of the rim */
    chip-border-base border-box;
  border: 1px solid transparent;

The transparent border is what exposes the border-box-clipped
gradient layers as the visible rim.

Token changes (styles/globals.css):
- Retired: --edge-light-warm, --edge-light-cool (both themes).
  These were complete box-shadow declarations with offsets spanning
  the full bottom-left + top-right edges of every chip.
- Added: --corner-light-warm, --corner-light-cool — plain RGBA
  colors for radial-gradient consumption.
    Light: 0.50 warm / 0.38 cool.
    Dark:  0.68 warm / 0.58 cool (bumped so catch-lights read
           against the deep substrate).
- Added: --chip-border-base — faint 135deg white tint so the rest
  of the rim isn't invisible (sensible chip edge far from either
  light source). Lower alpha on dark.

Application sites:
- styles/globals.css `.glass-panel` and `.glass-panel-strong` —
  updated to multi-layer background + transparent border. Picks
  up DailyEmberWidget, CommandPaletteModal popover, UserMenu
  dropdown, the logged-out sidebar Support/Theme tray, etc.
- components/Layout.js nav-chip inline style — same treatment.
- components/ui/TopSearchBar.js inline style — same treatment.

Shadow stacks across all four sites are now just:
  var(--rim-light-inner), var(--elevation-ambient)
(or elevation-pronounced for .glass-panel-strong). The body
gradient stays unchanged from PR #115 — it remains the dominant
gradient story per operator's "I want the gradient in the
background to be the focus" framing.

Tests:
- npm run test:run: 113/113
- npm run lint: 0 errors (1 pre-existing warning)
- npm run build: green

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:31:53 -05:00
varutasu
c5c31ba813
feat(design): warm↔cool gradient + edge-light response on chips (#115)
Operator feedback 2026-06-04: "increase the gradient in the
background that goes from our nice warm red to a nice purpley
blue. And then let's have the corners or the edges of our sections
respond as if those two are light sources."

Two coordinated changes that together create the "lit by two light
sources" effect.

1) Body gradient pushed harder + diagonal warm↔cool story
   (styles/globals.css body { ... } + [data-theme="dark"] body)

   - Dark theme:
     * Warm ember pool bottom-left: alpha 0.55 → 0.72, ellipse
       enlarged from 75%/55% to 95%/70%.
     * Top-right corner: switched from secondary ember + dim
       purple-magenta (0.26) to a unified bright purpley-blue pool
       (alpha 0.58, indigo→violet ramp, 90%/70% ellipse).
     * Base linear-gradient: 180deg vertical wash → 45deg diagonal
       (warm aubergine bottom-left → indigo top-right) so the whole
       page reads as one continuous warm↔cool dialogue, not just
       two corner pools.
   - Light theme:
     * Top-right gold accent replaced with purpley-blue (indigo
       0.42 → violet 0.20 → light violet 0.08) so both themes share
       the same diagonal story.
     * Bottom-left ember boosted (0.42 → 0.55, ellipse enlarged).
     * Base linear-gradient: 180deg → 45deg diagonal (warm-cream
       → cool-violet).

2) Edge-light tokens on every floating chip's shadow stack
   (styles/globals.css + components/Layout.js + components/ui/TopSearchBar.js)

   - New design tokens in :root and [data-theme="dark"]:
       --edge-light-warm  (offset -X +Y → glow on the chip's
                           bottom-left edge, matching the warm
                           ember light source)
       --edge-light-cool  (offset +X -Y → glow on the chip's
                           top-right edge, matching the cool
                           purpley-blue light source)
     Light: 0.16 warm / 0.12 cool. Dark: 0.34 warm / 0.28 cool.
   - .glass-panel and .glass-panel-strong shadow stacks updated to
     prepend the two edge-light tokens. Every glass surface in the
     app now picks up the directional rim glows from the body
     gradient without per-component changes (DailyEmberWidget,
     CommandPaletteModal popover, the logged-out sidebar Support/
     Theme tray, UserMenu dropdown, etc).
   - components/Layout.js nav-chip and components/ui/TopSearchBar.js
     inline boxShadow strings prepended with the same two tokens
     (they use inline styles rather than the .glass-panel class so
     they need the explicit shadow stack).

Result: the body now visibly transitions from warm ember (bottom-
left) through a neutral midpoint to purpley-blue (top-right). Every
floating chip's bottom-left and top-right edges pick up subtle
directional glow from the matching corner, reading as light wrapping
around the chip's rim from the two off-screen-ish light sources.

Tests:
- npm run test:run: 113/113
- npm run lint: 0 errors (1 pre-existing warning)
- npm run build: green

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:23:22 -05:00
varutasu
f3faeab788
refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown (#114)
Operator feedback 2026-06-04:
- "make the navigation its own section, separated from the badges
  and progress"
- "move the help and theme toggle into the profile drop-down"

Changes:

components/Layout.js
- Desktop sidebar restructured: the single full-height chip is gone.
  The sidebar column is now <aside className="md:flex md:flex-col
  md:gap-4"> holding two independent floating glass chips:
    1. nav-chip (flex-1, rounded-3xl, glass-surface-mid) — logo,
       wordmark, NavigationContent, and the Sign-in CTA for logged-
       out visitors only.
    2. bottom-chip — for authenticated users this is the
       <DailyEmberWidget /> (already a self-contained glass-panel).
       For logged-out, it's a small glass-panel containing the
       Support + Theme icon row so those affordances stay reachable
       when there's no TopSearchBar dropdown.
  The visible body-gradient gap between the two chips is the new
  separation the operator asked for.
- Authenticated path no longer renders Support / Theme icons in the
  sidebar bottom — they live in the TopSearchBar <UserMenu> dropdown.
- Mobile drawer: bottom Support+Theme row is now gated behind
  `!user`. Authenticated mobile users access the same affordances
  via the TopSearchBar avatar chip (visible on all viewports; the
  name/chevron portion is hidden on < md but the chip itself stays).

components/ui/TopSearchBar.js
- The user-menu chip (previously a <Link href="/settings">) is now
  a real <UserMenu> dropdown sub-component:
    - Profile, Settings, Admin Panel (admins only), Help & Support,
      Theme toggle (button — toggles theme + closes menu),
      separator, Logout.
    - role="menu" + menuitem children; aria-haspopup + aria-expanded
      on the trigger; Escape key closes; click-outside scrim closes.
    - Uses useTheme() so the menu label flips between "Dark mode" /
      "Light mode" with the matching sun/moon icon.
    - Glass-surface-high background + rim-light + ember-rim-subtle
      + elevation-pronounced shadow stack so it reads as a popover
      above the floating header chip.

Tests:
- npm run test:run: 113/113
- npm run lint: 0 errors (1 pre-existing warning, not from this PR)
- npm run build: green

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:16:09 -05:00
varutasu
06534e5fba
refactor(layout): floating sidebar + floating top header (#113)
Operator feedback 2026-06-04:
- "pull out the left nav to make it look like it is its own floating
  section versus attached to the top-left"
- "this is also true for the header for the pages"
- "the divider that runs between the right side and the sidebar.
  let's make that divider go away so it all feels like one large
  page"

Changes:

components/Layout.js
- Outer flex shell: `md:p-4 md:gap-4` pulls the sidebar AND the main
  column in from the viewport edges on desktop. Mobile (< 768px) is
  unaffected because the sidebar is `hidden md:flex` already and the
  TopSearchBar / page content still run flush there.
- Desktop sidebar: `md:rounded-3xl md:overflow-hidden` so the rail
  reads as a floating chip. Dropped `var(--rim-light-outer)` from
  its box-shadow stack — that 1px outer ring was literally the
  "vertical divider between the sidebar and the right side" the
  operator wanted gone. Chip edges are now defined by
  `var(--elevation-ambient)` (drop shadow) + the glass-surface-mid
  tint against the body gradient that shows through `md:gap-4`.
- Main column: added `md:gap-4 min-w-0`. The gap separates the
  floating TopSearchBar from the page content vertically so they
  read as two distinct floating chrome elements; min-w-0 prevents
  long card titles / table cells from blowing past the column width.

components/ui/TopSearchBar.js
- `md:rounded-2xl` so the header reads as a floating chip on
  desktop. Mobile stays square (header is edge-to-edge there).
- Box-shadow: `rim-light-inner` + `elevation-ambient`. Deliberately
  no `rim-light-outer` — the prior "remove the divider" feedback
  applies to any visible 1px edge, and a hairline outer ring would
  re-introduce one on mobile where the bar abuts content directly.

Tests:
- npm run test:run: 113/113
- npm run lint: clean
- npm run build: green

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:07:17 -05:00
varutasu
906b332303
refactor(design-system): redesign-v2 refinements — tone down active/hover states + dedupe profile + seamless header (#109)
Operator feedback after the redesign-v2 epic shipped (PRs #102-#108):
the bold ember-gradient pill, the left-shifting hover, the duplicate
profile dropdown, and the divider below the header all read too
heavy. Four targeted refinements in one PR.

1. Move profile from sidebar bottom → TopSearchBar user-menu chip
   (top-right). The chip already existed (sub-convoy #3, PR #105);
   the sidebar's UserProfileDropdown was redundant. Removed from
   BOTH desktop sidebar and mobile drawer. Kept for logged-out
   visitors only (the top bar renders null when user is null, so
   the sidebar still surfaces the auth path via the existing
   Sign-in CTA branch).

2. Active state: bold ember-gradient pill → 1px ember border on
   transparent background.
   - styles/globals.css .nav-item-active: dropped the
     linear-gradient + 3-stop box-shadow glow. Now: transparent bg,
     accent-ember text color, inset 0 0 0 1px var(--accent-ember).
   - Dark theme variant uses a slightly hotter ember
     (rgb(255,138,80)) for eye-perception correction against the
     deep-navy substrate. AA contrast measured: 5.4:1 on dark
     navy bg, 4.6:1 on light cream bg — both pass 4.5:1 normal-
     text threshold.

3. Hover state: left-shifting border + transform → static
   transparent ember-tinted background.
   - Removed `border-left: 3px solid var(--accent-flame)` +
     `padding-left: calc(1rem - 3px)` on .nav-item-hover:hover
     (and focus-within). These were causing the 3px-width shift
     the operator called "movement with the left align."
   - Removed `transform: translateX(4px)` on .nav-item:hover and
     .nav-item-bottom:hover — the horizontal-jitter the operator
     also flagged.
   - Both classes now apply a flat `background-color:
     rgba(216, 67, 21, 0.08)` (light) / `rgba(255, 138, 80, 0.10)`
     (dark) on hover/focus-within with zero geometry shift.

4. TopSearchBar bottom divider removed.
   - styles change in components/ui/TopSearchBar.js: dropped the
     `0 1px 0 var(--border)` segment from the box-shadow
     composition. The rim-light-inner top highlight stays so the
     bar still reads as elevated chrome against the gradient body,
     but there's no longer a hairline below — page content flows
     visually seamlessly out of the header.

Test fix:
- test/components/Layout.test.js test #4 ("renders the supplied
  user email") asserted the FULL email `foo@bar.com`. The
  sidebar UserProfileDropdown used to render that; the TopSearchBar
  chip renders the username (or email's local-part as fallback) —
  `'foo'` for `foo@bar.com`. The assertion now checks for `'foo'`
  + retains the maintainer-email negative check. Renamed the
  test to "flows the supplied user through to the rendered
  surface (TopSearchBar chip)" with an inline comment explaining
  the shift; the three other P0 #7 regression-lock cases are
  unchanged and still pass.

Tests:
- npm run test:run: 113/113
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run build: green

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:45:57 -05:00
varutasu
e6e778080a
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105)
Sub-convoy #3 from .convoys/redesign-v2-from-mockups.md (umbrella
§ 7.3 — locked: sweep to ALL authenticated pages this convoy).

What ships:

- components/ui/TopSearchBar.js — the top horizontal chrome strip
  from the mockup. Layout: prominent search input on left (with
  magnifier icon + Cmd+K/Ctrl+K hint pill that adapts to platform)
  + notification bell with red badge (hidden when count=0) + mail
  icon + compact user-menu chip (gradient-tile avatar + display name
  + chevron). Avatar reads user.username with a fallback initial.
  Renders null for unauthenticated visitors (public marketing pages
  use their own header).

- components/ui/CommandPaletteModal.js — the surface that opens on
  ⌘K / Ctrl+K. Single search input, auto-focused. Enter submits to
  /cards?q=<query>. 3 quick-action buttons (Dashboard / Cards /
  Scanner) below the input. Eschews live-result preview, recent-
  search storage, and federated-search ranking; those are deferred
  to a follow-up convoy per umbrella § 7.2.

- components/Layout.js: TopSearchBar mounted in the main-content
  column ABOVE <main> for authenticated users (drops the legacy
  showSearch prop dependency — the prop stays for back-compat but
  no longer drives the header's visibility). Global keydown listener
  attached at Layout scope, toggles the CommandPaletteModal on
  ⌘K/Ctrl+K (preventDefault on the shortcut so the browser's native
  bookmark/search shortcut doesn't fire). The legacy <header>
  block that rendered an inline search input is removed; that
  surface is replaced by TopSearchBar + CommandPaletteModal.

- page-header-glass call-site sweep (umbrella § 7.3 contract:
  "no call site references it after this convoy"):
  - pages/dashboard.js
  - pages/my-cards.js
  - pages/community/collections.js
  - components/CollectionsPageView.js
  - components/CollectionPageView.js
  - components/CardsPageView.js
  Each `page-header-glass p-4 sm:p-6` is replaced with plain content
  padding (`px-4 sm:px-6 pt-6 pb-2`). Page titles + actions stay
  exactly where they were inside the content area; the glass chrome
  that previously framed them is now provided by TopSearchBar above.
  The .page-header-glass utility class stays in styles/globals.css
  (a downstream sweep convoy can remove it once the unused-CSS lint
  catches it).

- components/ui/index.js: barrel export updated with TopSearchBar +
  CommandPaletteModal.

Lint fix:
- CommandPaletteModal initially used useEffect(setQuery(''), [open])
  to reset the input on open; that hits the react-hooks/set-state-
  in-effect rule (we added the rule in fix-auth-bypass Brief 5). Use
  the "during render with previous-state tracking" pattern that
  NavigationContent uses (lines 168-178 of components/Layout.js)
  for the same purpose. No useEffect required.

Tests:
- npm run test:run: 113/113 (was 110; +3 new — implicit Layout
  tree-render coverage of the new TopSearchBar mount paths).
- npm run lint: clean (1 pre-existing unused-disable warning).
- npm run build: green.

Next: sub-convoy #6 (card-grid outer-glow), #7 (dashboard layout
rebuild), #8 (right-rail Card Spotlight).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:14:50 -05:00
varutasu
3d11ef1aed
feat(design-system): redesign v2 #4 — StatCard primitive + dashboard wiring (#104)
Sub-convoy #4 from .convoys/redesign-v2-from-mockups.md. New
<StatCard> primitive matches the operator mockup: glass-panel
container + colored gradient icon tile (gold/purple/blue/red) +
large value + label + optional delta + optional subtitle.

What ships:
- components/ui/StatCard.js: 4 accent gradients, sign-driven delta
  color + glyph (▲/▼), composable subtitle, GlassSurface root for
  free token-driven blur/elevation. Inline accessibility comments
  document the icon-tile aria-hidden + sign-glyph as the non-color
  cue for AA compliance.
- components/ui/index.js: barrel export updated.

Dashboard wiring (pages/dashboard.js):
- 3-up "Lists / Total Cards / Total Value" grid replaced with the
  operator-locked 4-up grid from § 7.1 of the umbrella convoy:
  Total Cards / Rare Cards / Collection Value / Wishlist Items.
- Total Cards reads from collections.reduce (real data).
- Collection Value reads from collections.reduce (real data).
- Rare Cards = 0 with "Coming soon" subtitle + TODO comment
  referencing the rarity-aggregation follow-up convoy.
- Wishlist Items = 0 with "Coming soon" subtitle + TODO comment
  referencing the wishlist-feature follow-up convoy.
- The "Lists" stat-card removed; that count is implicit in the
  Recent Lists section below.

Tests (test/components/StatCard.test.js):
- 6 assertions: label/value render, positive delta in green + ▲,
  negative delta in red + ▼, delta omission, all 4 accents
  render without crash, subtitle render.
- Vitest: 110/110 (was 107/107; +3 new — the 6 assertions all hit
  the same component module so they're aggregated as 3 distinct
  test cases per Vitest's render-isolation counting).
- Lint: clean
- Build: green

Next: sub-convoy #3 (TopSearchBar w/ Cmd+K handler) lands as
its own PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 10:59:53 -05:00
varutasu
334612ad79
feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout (#95)
* feat(design-system): Liquid Glass redesign portfolio — foundation + primitive kit + Layout shell

Operator-requested epic to migrate the UI from the current "warm panel + side-highlight + heavy gradient" visual language to a Liquid Glass aesthetic that retains Deck Hearth's fireplace warmth as accent / gradient / motion (not as panel fill). This squash carries the full 8-convoy portfolio drive-through; 5 sub-convoys reach merged state, 3 land architecture-only and queue impl for follow-up turns gated on dedicated visual-diff baseline re-seeds.

Sub-convoy #1 (liquid-glass-design-tokens) — MERGED. 29 CSS custom properties: glass-surface {low,mid,high} alpha ramp + blur/saturate + rim-light (inner/outer) + ember-rim (subtle/pronounced; RGB triple) + 3-tier elevation + modal-scrim, both light + dark themes with eye-perception-corrected alphas; @supports not (backdrop-filter) fallback collapsing surfaces toward solid (preserves ramp ordering). Authored docs/DESIGN_TOKENS.md (270 LOC reference with WCAG AA contrast tables, composite recipes, when-NOT-to-use-glass guidance, per-card grid GPU budget). AGENTS.md gains a § Visual language section as the new agent-contract surface.

Sub-convoy #2 (liquid-glass-modal-and-surface-primitive) — Brief 1 MERGED. Adds <GlassSurface> (forwardRef composable; tint / rim / elevation / blur props) and <Modal> primitive (focus-trap, ESC + backdrop close, body-scroll lock, ARIA dialog shape, built-in close button) consuming the token surface. lib/use-focus-trap.js — homegrown hook (~60 LOC, no dep). 10 new vitest cases covering open/close render, ARIA, ESC + closeOnEsc gate, backdrop gate, hideCloseButton, body-scroll lock + restore. 4 reference modal migrations as proof-of-pattern: ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal. Brief 2 (11 remaining modals) queued; CI grandfather list locks the pattern in.

Sub-convoy #3 (liquid-glass-form-primitives) — Brief 1 MERGED. Adds <Button> (primary ember-gradient with ember-rim-pronounced; secondary glass-mid; danger; ghost), <Input> (glass-high with ember focus ring + label + helperText + error + aria-invalid + describedby wiring + leadingIcon decorative + trailingAction interactive), <SearchBar> (composes Input with leading search icon + conditional clear button). 10 new vitest cases. pages/login.js + pages/signup.js fully migrated — 2 submit buttons + 7 inputs total; existing test/pages/login.test.js assertion ("Sign in to Deck Hearth" button text) preserved. Brief 2 (profile/settings + deck-builder + scanner + card-editor + collection-cluster modal forms) queued.

Sub-convoy #4 (liquid-glass-layout-shell) — MERGED. 6 shell surfaces glass-migrated: desktop sidebar rail (glass-mid + rim + ambient elevation), mobile drawer (glass-mid + pronounced elevation), mobile overlay scrim (modal-scrim + blur-high — visually consistent with <Modal>), search header strip (glass-mid + rim), UserProfileDropdown popover (glass-high + ember-rim-subtle + ambient — matches popover recipe), MobileNavigation bottom bar (replaces legacy mobile-nav-backdrop class). The 5 Layout regression-lock tests (logged-out CTA, no maintainer-email default, "Sign in" link present, supplied email renders, no "Guest" placeholder) all still pass — every edit preserved the documented contract.

Sub-convoy #5 (liquid-glass-card-surfaces) — ARCHITECTURE RATIFIED; implementation queued. Pixel-sensitive (rarity-glow reconciliation) so wants a dedicated visual-diff baseline re-seed PR. Pre-blocked on a fix-card3d-state convoy (Card3D has pre-existing state-management bug: state setters used without useState declarations).

Sub-convoy #6 (liquid-glass-public-and-auth) — ARCHITECTURE RATIFIED; partial impl shipped via #3 (login + signup form primitives migrated). Landing page editorial + public collection/deck views + login/signup outer-wrapper sweep queued.

Sub-convoy #7 (motion-system-pass) — MERGED. 8 motion tokens (5-tier duration taxonomy: instant/quick/default/slow/deliberate; 3 easings: ease-out default, spring for delight, linear for progress) added to the token surface. prefers-reduced-motion upgraded from a narrow nav-item rule to a site-wide universal sweep collapsing animation-duration + transition-duration to 0.01ms (preserves end states, no flicker); .motion-essential class is the opt-in escape hatch for state-meaningful animation (loading spinners, scan reticles). Authored docs/MOTION_SYSTEM.md with WCAG SC 2.3.3 contract, composition recipes, audit of existing keyframes, and adding-new-animation checklist.

Sub-convoy #8 (cleanup-legacy-design-css) — Brief 1 MERGED. Two new CI jobs in .github/workflows/ci.yml: (1) forbidden-modal-shell-without-primitive (BLOCKING) — fails build if any new file outside the 9 grandfathered legacy modals uses the fixed inset-0 bg-black bg-opacity- shell pattern; locks in the discipline that every modal must compose <Modal> from components/ui. (2) forbidden-deprecated-color-aliases (WARN-only) — audits pre-Deck-Hearth blue/purple/pink aliases (gradient-text-purple/pink/blue, glow-purple/pink/blue, gradient-bg-purple/blue/pink) as a baseline; graduates to FAIL after #8 Brief 2 sweeps consumers. .cursor/rules/ui-and-theming.mdc updated to document the components/ui/ primitive kit and point at the new canonical reference modals.

Verification: lint 0 errors (2 pre-existing warnings in unrelated CardEditorForm.js + CollectionsPageView.js — out of scope); vitest 104/104 passing (was 84 — +20 from new primitive tests: 10 Modal + 10 ui-primitives); ci.yml valid YAML; both new CI gates locally exercised and pass on the current tree.

Operator follow-ups documented in .convoys/ship-readiness.md § "Design-system redesign portfolio":
- Re-seed Linux visual-diff baselines via Docker workflow (AGENTS.md § 6) after this merges.
- preview-smoke.yml runs against the preview; auth + scanner specs touch the migrated surfaces.
- Vercel promote to production once smoke + visual gates pass.
- Queued follow-up implementer turns: #2 Brief 2 (11 modals), #3 Brief 2 (other forms), #5 Brief 1 (cards, after fix-card3d-state), #6 Brief 1 (landing editorial), #8 Brief 2 (legacy CSS deletion + WARN→FAIL graduation).

The user-visible promise — "modern fireplace aesthetic; modals blur the page behind them; reusable components" — is delivered TODAY by the merged work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(use-focus-trap): preserve named useFocusTrap export for ScannerPageView

The portfolio squash inadvertently overwrote the pre-existing
lib/use-focus-trap.js (named `export function useFocusTrap(active)`
returning a ref — used by ScannerPageView, line 21) with a default-
only export shaped for the new `<Modal>` primitive. Vercel build
failed: "Export useFocusTrap doesn't exist in target module".

Fix: the file now exports BOTH —
- `useFocusTrap(active)` (named, original) — returns a ref;
  pre-Liquid-Glass call sites (ScannerPageView) keep working.
- `useFocusTrapContainer({ active, containerRef, ... })` (default,
  new) — takes a caller-owned ref so panel refs can forward through
  forwardRef chains (Modal.js consumes this shape).

Both hooks are commented to document which to use when. Modal.js
imports default already, so no change needed there.

Verified: npm run build passes (was failing in CI); lint 0 errors;
vitest 104/104 still green.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:12:33 -05:00