deckhearth/components/Layout.js

884 lines
40 KiB
JavaScript
Raw Normal View History

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import { useTheme } from '../lib/theme-context';
import MobileNavigation from './MobileNavigation';
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103) Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:59:44 -04:00
import DailyEmberWidget from './DailyEmberWidget';
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 12:14:50 -04:00
import TopSearchBar from './ui/TopSearchBar';
import CommandPaletteModal from './ui/CommandPaletteModal';
import { VOCAB } from '../lib/collection-vocabulary.js';
🔄 Restructured User Profile & Theme Toggle ✨ Profile Dropdown Implementation: - Created comprehensive UserProfileDropdown component - Moved user profile to bottom of sidebar (better UX) - Added dropdown menu with Profile, Settings, Admin Panel, Logout - Profile button shows user avatar, email, and role - Smooth dropdown animation with backdrop click-to-close 🎨 Fire-Themed Profile Menu: - Logout option uses ember red color for distinction - Profile icons match the overall design system - Consistent hover states and accessibility features - Proper ARIA attributes for dropdown functionality 🌙 Separated Theme Toggle: - Moved theme toggle out of profile section - Now appears as standalone navigation item above profile - Shows 'Dark Mode' / 'Light Mode' text labels - Maintains all accessibility features and fire theming ♿ Enhanced Accessibility: - Proper dropdown ARIA attributes (expanded, haspopup) - Keyboard navigation support throughout - Screen reader friendly with proper labels - Focus management for dropdown interactions 🎯 Admin Panel Integration: - Admin Panel option only shows for admin users - Conditional rendering based on user role - Proper navigation structure for different user types - Consistent with existing admin badge logic �� Mobile Optimizations: - Dropdown works seamlessly on mobile devices - Proper touch targets and spacing - Auto-closes mobile menu when navigating - Backdrop prevents accidental interactions The sidebar now has a much cleaner hierarchy with the profile at the bottom and easy access to all user functions! 🔥👤
2025-07-26 10:51:40 -04:00
// User Profile Dropdown Component
function UserProfileDropdown({ user, onMobileMenuClose }) {
fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) * convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak) The last remaining P0 ship-blocker from .convoys/ship-readiness.md. components/Layout.js line 562 defaults the user prop to a real email address (me@randallstillwell.com); any page that renders Layout without passing user explicitly impersonates the maintainer. Scope: components/Layout.js + audit of 17 pages that import Layout (grep-confirmed list in convoy file). Single PR likely. Auditor cohort skipped (no design-system, IA, or browser-smoke surface). Architect to address: - Q1: logged-out rendering branch design (navbar, mobile-nav, auth-only items treatment) - Q2: page audit triage into always-auth / public-or-auth / anonymous-allowed buckets - Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out) - Q4: whether to add vitest coverage for the logged-out branch (recommend yes — small surface, high regression protection) Hard out-of-scope: branding (pick-a-name), auth-provider collapse (single-auth-provider), Layout god-component split (god-component-split). depends_on: bump-next-js (shipped), fix-auth-bypass (shipped), drop-public-setup (shipped) addresses: P0 #7 from .convoys/ship-readiness.md parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit) 2 briefs, single PR. ~12 files net (down from the 18 in the original scope — 10 of the 17 Layout-importing pages already pass user explicitly). Brief 1: components/Layout.js default user=null + Sign-in CTA branch in UserProfileDropdown when logged out. Adds first jsdom test in the repo at test/components/Layout.test.js (Decision D2) with 5 regression-lock assertions. devDeps: jsdom@^29, @testing-library/react@^16. Brief 2: page audit sweep — 7 pages need code changes: - Pass user={user} to Layout: scanner.js, deck-builder.js (×4), deck/[id].js (×3), decks.js (×3) - Replace page-level useState({email: 'me@...'}) → useState(null) + null-guards: profile.js, settings.js - Replace hardcoded const user = {email: 'me@...'} with useAuth(): card/[id].js Discovered second anti-pattern: profile.js, settings.js, card/[id].js seed page-level state with the maintainer email. Folded into Brief 2 since success metric "no real email address remains in any component default-prop" reads naturally to include page-level seed values. Decisions: A1 — Sign-in CTA replaces avatar+email+dropdown when user===null; hides auth-only dropdown (Profile/Settings/Logout/Admin); keeps public + community nav visible B — Per-page bucket assignment (10 already correct, 7 need fix); full per-page table with justification in convoy file C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page sweep depends on Brief 1). C1 buries the conceptual change under mechanical edits; C3 is over-orchestrated for this scope D2 — vitest lock-in; first jsdom test in repo; same negative-regression style as test/lib/permission-middleware.test.js (synthetic-admin shape). devDeps jsdom + @testing-library/react Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky pages — mitigated by audit-pass mandate + manual smoke). MobileNavigation deliberately NOT folded in: its user prop is dead code (never reads user.*); different bug class; cleanup queued separately to avoid scope expansion. Flagged-but-deferred: - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - Layout headers still render "Deck Hearth" / "DH" branding → pick-a-name (queued P1 #12) - MobileNavigation dead user prop → cleanup-mobile-nav-dead-props or fold into god-component-split addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user) Closes the source-side half of P0 #7 from .convoys/ship-readiness.md. The page-side sweep (Brief 2) follows in a separate commit. components/Layout.js: - Default user prop is now null (was hardcoded to { email: 'me@randallstillwell.com', role: 'user' }) - UserProfileDropdown renders a "Sign in" link to /login when user === null instead of the maintainer's email + auth-only menu items (Decision A1) - All user.* accesses guarded with optional chaining or null checks - useState hook stays above the new null-user early return to satisfy rules-of-hooks (boot-the-brief caught this on the first try; see AGENTS.md Gotcha #11.5) test/components/Layout.test.js (new): - First jsdom test in the repo (Decision D2) - 5 regression-lock assertions: no maintainer email ever rendered (prop omitted, prop=null), Sign-in link exists with href=/login, supplied email renders when prop is set, no "Guest" placeholder (locks A1 copy choice) - Mocks next/link, next/router (prefetch, replace, events, query), and theme-context.useTheme for jsdom safety under Next 16 package.json + package-lock.json: - Add jsdom@^29 and @testing-library/react@^16 to devDependencies - @testing-library/dom@^10 added explicitly (peer auto-install skipped it under npm 11; brief anticipated this fallback) vitest.config.js (deviation from brief — see PR description): - Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can parse JSX in .js files. Required to import any React component written in the repo's Next.js pages-router .js convention (AGENTS.md Gotcha #9). The brief said "no change" to this file, but JSX-in-.js parsing is a hard prerequisite for the new test to import components/Layout.js — the alternatives (rename test to .test.jsx; rewrite test in React.createElement) either break the test glob or still hit the same Layout.js parse failure. Other tests are unaffected (they import non-JSX modules). Smoke output: see PR description. addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user) Closes the page-side half of P0 #7 from .convoys/ship-readiness.md. Brief 1 (commit ddf8fd2) handled the Layout-side fix. Per the architect's per-page bucket table (Decision B in .convoys/fix-layout-default-user.md), 7 pages needed code changes; the other 10 of 17 Layout-importing pages already pass `user` correctly. Pass user={user} to Layout (4 pages, 11 call sites): - pages/scanner.js (1 call) - pages/decks.js (3 calls) - pages/deck-builder.js (4 calls) - pages/deck/[id].js (3 calls) (All four still import useAuth from lib/auth-context.js — that's intentional and stays as-is until the single-auth-provider convoy collapses the three parallel auth surfaces.) Replace leaky page-level seed values with useState(null) + null guards (2 pages, R2 mitigation): - pages/profile.js: useState({email: 'me@...', role: 'user', ...}) → useState(null) + ?. on every sync user.* read + early-return guards in getDisplayName/getInitials + conditional render around the "Member since" block so formatDate(undefined) never runs - pages/settings.js: same pattern (single user.email reader guarded) Replace hardcoded const with useAuth from lib/use-auth.js (1 page): - pages/card/[id].js: const user = {email: 'me@...'} → const { user } = useAuth() (called unconditionally at the top of the component; rules-of-hooks safe) Verification: - grep 'me@randallstillwell.com' pages/ → 0 hits - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1) - npm run lint matches baseline (128 problems pre, 128 post; verified via git stash before/after) - Manual static read-through of every diff; ReadLints clean on the 7 files - Dev-server smoke: /cards anonymous returned HTTP 200 with 0 'me@randallstillwell' matches before the user's shared dev server became unresponsive mid-session (same dev-server-shared-by-user constraint flagged in Brief 1); interactive logged-in smoke is parent/operator gated Flagged-but-deferred (untouched per scope): - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - components/MobileNavigation.js still receives dead user prop → cleanup-mobile-nav-dead-props (or fold into god-component-split) addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 15:31:37 -04:00
// Hook order is fixed for both branches; do not move this below the
// null-user early return — see rules-of-hooks (AGENTS.md Gotcha #11.5).
🔄 Restructured User Profile & Theme Toggle ✨ Profile Dropdown Implementation: - Created comprehensive UserProfileDropdown component - Moved user profile to bottom of sidebar (better UX) - Added dropdown menu with Profile, Settings, Admin Panel, Logout - Profile button shows user avatar, email, and role - Smooth dropdown animation with backdrop click-to-close 🎨 Fire-Themed Profile Menu: - Logout option uses ember red color for distinction - Profile icons match the overall design system - Consistent hover states and accessibility features - Proper ARIA attributes for dropdown functionality 🌙 Separated Theme Toggle: - Moved theme toggle out of profile section - Now appears as standalone navigation item above profile - Shows 'Dark Mode' / 'Light Mode' text labels - Maintains all accessibility features and fire theming ♿ Enhanced Accessibility: - Proper dropdown ARIA attributes (expanded, haspopup) - Keyboard navigation support throughout - Screen reader friendly with proper labels - Focus management for dropdown interactions 🎯 Admin Panel Integration: - Admin Panel option only shows for admin users - Conditional rendering based on user role - Proper navigation structure for different user types - Consistent with existing admin badge logic �� Mobile Optimizations: - Dropdown works seamlessly on mobile devices - Proper touch targets and spacing - Auto-closes mobile menu when navigating - Backdrop prevents accidental interactions The sidebar now has a much cleaner hierarchy with the profile at the bottom and easy access to all user functions! 🔥👤
2025-07-26 10:51:40 -04:00
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) * convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak) The last remaining P0 ship-blocker from .convoys/ship-readiness.md. components/Layout.js line 562 defaults the user prop to a real email address (me@randallstillwell.com); any page that renders Layout without passing user explicitly impersonates the maintainer. Scope: components/Layout.js + audit of 17 pages that import Layout (grep-confirmed list in convoy file). Single PR likely. Auditor cohort skipped (no design-system, IA, or browser-smoke surface). Architect to address: - Q1: logged-out rendering branch design (navbar, mobile-nav, auth-only items treatment) - Q2: page audit triage into always-auth / public-or-auth / anonymous-allowed buckets - Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out) - Q4: whether to add vitest coverage for the logged-out branch (recommend yes — small surface, high regression protection) Hard out-of-scope: branding (pick-a-name), auth-provider collapse (single-auth-provider), Layout god-component split (god-component-split). depends_on: bump-next-js (shipped), fix-auth-bypass (shipped), drop-public-setup (shipped) addresses: P0 #7 from .convoys/ship-readiness.md parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit) 2 briefs, single PR. ~12 files net (down from the 18 in the original scope — 10 of the 17 Layout-importing pages already pass user explicitly). Brief 1: components/Layout.js default user=null + Sign-in CTA branch in UserProfileDropdown when logged out. Adds first jsdom test in the repo at test/components/Layout.test.js (Decision D2) with 5 regression-lock assertions. devDeps: jsdom@^29, @testing-library/react@^16. Brief 2: page audit sweep — 7 pages need code changes: - Pass user={user} to Layout: scanner.js, deck-builder.js (×4), deck/[id].js (×3), decks.js (×3) - Replace page-level useState({email: 'me@...'}) → useState(null) + null-guards: profile.js, settings.js - Replace hardcoded const user = {email: 'me@...'} with useAuth(): card/[id].js Discovered second anti-pattern: profile.js, settings.js, card/[id].js seed page-level state with the maintainer email. Folded into Brief 2 since success metric "no real email address remains in any component default-prop" reads naturally to include page-level seed values. Decisions: A1 — Sign-in CTA replaces avatar+email+dropdown when user===null; hides auth-only dropdown (Profile/Settings/Logout/Admin); keeps public + community nav visible B — Per-page bucket assignment (10 already correct, 7 need fix); full per-page table with justification in convoy file C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page sweep depends on Brief 1). C1 buries the conceptual change under mechanical edits; C3 is over-orchestrated for this scope D2 — vitest lock-in; first jsdom test in repo; same negative-regression style as test/lib/permission-middleware.test.js (synthetic-admin shape). devDeps jsdom + @testing-library/react Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky pages — mitigated by audit-pass mandate + manual smoke). MobileNavigation deliberately NOT folded in: its user prop is dead code (never reads user.*); different bug class; cleanup queued separately to avoid scope expansion. Flagged-but-deferred: - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - Layout headers still render "Deck Hearth" / "DH" branding → pick-a-name (queued P1 #12) - MobileNavigation dead user prop → cleanup-mobile-nav-dead-props or fold into god-component-split addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user) Closes the source-side half of P0 #7 from .convoys/ship-readiness.md. The page-side sweep (Brief 2) follows in a separate commit. components/Layout.js: - Default user prop is now null (was hardcoded to { email: 'me@randallstillwell.com', role: 'user' }) - UserProfileDropdown renders a "Sign in" link to /login when user === null instead of the maintainer's email + auth-only menu items (Decision A1) - All user.* accesses guarded with optional chaining or null checks - useState hook stays above the new null-user early return to satisfy rules-of-hooks (boot-the-brief caught this on the first try; see AGENTS.md Gotcha #11.5) test/components/Layout.test.js (new): - First jsdom test in the repo (Decision D2) - 5 regression-lock assertions: no maintainer email ever rendered (prop omitted, prop=null), Sign-in link exists with href=/login, supplied email renders when prop is set, no "Guest" placeholder (locks A1 copy choice) - Mocks next/link, next/router (prefetch, replace, events, query), and theme-context.useTheme for jsdom safety under Next 16 package.json + package-lock.json: - Add jsdom@^29 and @testing-library/react@^16 to devDependencies - @testing-library/dom@^10 added explicitly (peer auto-install skipped it under npm 11; brief anticipated this fallback) vitest.config.js (deviation from brief — see PR description): - Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can parse JSX in .js files. Required to import any React component written in the repo's Next.js pages-router .js convention (AGENTS.md Gotcha #9). The brief said "no change" to this file, but JSX-in-.js parsing is a hard prerequisite for the new test to import components/Layout.js — the alternatives (rename test to .test.jsx; rewrite test in React.createElement) either break the test glob or still hit the same Layout.js parse failure. Other tests are unaffected (they import non-JSX modules). Smoke output: see PR description. addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user) Closes the page-side half of P0 #7 from .convoys/ship-readiness.md. Brief 1 (commit ddf8fd2) handled the Layout-side fix. Per the architect's per-page bucket table (Decision B in .convoys/fix-layout-default-user.md), 7 pages needed code changes; the other 10 of 17 Layout-importing pages already pass `user` correctly. Pass user={user} to Layout (4 pages, 11 call sites): - pages/scanner.js (1 call) - pages/decks.js (3 calls) - pages/deck-builder.js (4 calls) - pages/deck/[id].js (3 calls) (All four still import useAuth from lib/auth-context.js — that's intentional and stays as-is until the single-auth-provider convoy collapses the three parallel auth surfaces.) Replace leaky page-level seed values with useState(null) + null guards (2 pages, R2 mitigation): - pages/profile.js: useState({email: 'me@...', role: 'user', ...}) → useState(null) + ?. on every sync user.* read + early-return guards in getDisplayName/getInitials + conditional render around the "Member since" block so formatDate(undefined) never runs - pages/settings.js: same pattern (single user.email reader guarded) Replace hardcoded const with useAuth from lib/use-auth.js (1 page): - pages/card/[id].js: const user = {email: 'me@...'} → const { user } = useAuth() (called unconditionally at the top of the component; rules-of-hooks safe) Verification: - grep 'me@randallstillwell.com' pages/ → 0 hits - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1) - npm run lint matches baseline (128 problems pre, 128 post; verified via git stash before/after) - Manual static read-through of every diff; ReadLints clean on the 7 files - Dev-server smoke: /cards anonymous returned HTTP 200 with 0 'me@randallstillwell' matches before the user's shared dev server became unresponsive mid-session (same dev-server-shared-by-user constraint flagged in Brief 1); interactive logged-in smoke is parent/operator gated Flagged-but-deferred (untouched per scope): - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - components/MobileNavigation.js still receives dead user prop → cleanup-mobile-nav-dead-props (or fold into god-component-split) addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 15:31:37 -04:00
// Logged-out: replace avatar + email + dropdown with a Sign-in CTA.
if (!user) {
return (
<Link href="/login">
<div
className="w-full flex items-center px-4 py-3 rounded-2xl transition-all duration-200 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 nav-item-hover cursor-pointer"
style={{
backgroundColor: 'transparent',
color: 'var(--text-primary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)'
}}
onClick={onMobileMenuClose}
>
<div className="h-8 w-8 logo-container mr-3 flex items-center justify-center">
<svg className="h-4 w-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</div>
<span className="font-medium text-sm">Sign in</span>
</div>
</Link>
);
}
🔄 Restructured User Profile & Theme Toggle ✨ Profile Dropdown Implementation: - Created comprehensive UserProfileDropdown component - Moved user profile to bottom of sidebar (better UX) - Added dropdown menu with Profile, Settings, Admin Panel, Logout - Profile button shows user avatar, email, and role - Smooth dropdown animation with backdrop click-to-close 🎨 Fire-Themed Profile Menu: - Logout option uses ember red color for distinction - Profile icons match the overall design system - Consistent hover states and accessibility features - Proper ARIA attributes for dropdown functionality 🌙 Separated Theme Toggle: - Moved theme toggle out of profile section - Now appears as standalone navigation item above profile - Shows 'Dark Mode' / 'Light Mode' text labels - Maintains all accessibility features and fire theming ♿ Enhanced Accessibility: - Proper dropdown ARIA attributes (expanded, haspopup) - Keyboard navigation support throughout - Screen reader friendly with proper labels - Focus management for dropdown interactions 🎯 Admin Panel Integration: - Admin Panel option only shows for admin users - Conditional rendering based on user role - Proper navigation structure for different user types - Consistent with existing admin badge logic �� Mobile Optimizations: - Dropdown works seamlessly on mobile devices - Proper touch targets and spacing - Auto-closes mobile menu when navigating - Backdrop prevents accidental interactions The sidebar now has a much cleaner hierarchy with the profile at the bottom and easy access to all user functions! 🔥👤
2025-07-26 10:51:40 -04:00
const profileMenuItems = [
{ name: 'Profile', href: '/profile', icon: 'user' },
{ name: 'Settings', href: '/settings', icon: 'settings' },
...(user?.role === 'admin' ? [
{ name: 'Admin Tools', href: '/admin', icon: 'admin' }
🔄 Restructured User Profile & Theme Toggle ✨ Profile Dropdown Implementation: - Created comprehensive UserProfileDropdown component - Moved user profile to bottom of sidebar (better UX) - Added dropdown menu with Profile, Settings, Admin Panel, Logout - Profile button shows user avatar, email, and role - Smooth dropdown animation with backdrop click-to-close 🎨 Fire-Themed Profile Menu: - Logout option uses ember red color for distinction - Profile icons match the overall design system - Consistent hover states and accessibility features - Proper ARIA attributes for dropdown functionality 🌙 Separated Theme Toggle: - Moved theme toggle out of profile section - Now appears as standalone navigation item above profile - Shows 'Dark Mode' / 'Light Mode' text labels - Maintains all accessibility features and fire theming ♿ Enhanced Accessibility: - Proper dropdown ARIA attributes (expanded, haspopup) - Keyboard navigation support throughout - Screen reader friendly with proper labels - Focus management for dropdown interactions 🎯 Admin Panel Integration: - Admin Panel option only shows for admin users - Conditional rendering based on user role - Proper navigation structure for different user types - Consistent with existing admin badge logic �� Mobile Optimizations: - Dropdown works seamlessly on mobile devices - Proper touch targets and spacing - Auto-closes mobile menu when navigating - Backdrop prevents accidental interactions The sidebar now has a much cleaner hierarchy with the profile at the bottom and easy access to all user functions! 🔥👤
2025-07-26 10:51:40 -04:00
] : []),
{ name: 'Logout', href: '/logout', icon: 'logout', isLogout: true }
];
const getProfileIcon = (iconName) => {
const icons = {
user: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
),
settings: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
admin: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
),
logout: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
)
};
return icons[iconName] || icons.user;
};
return (
<div className="relative">
{/* Dropdown Menu */}
{isDropdownOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-10"
onClick={() => setIsDropdownOpen(false)}
/>
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 21:12:33 -04:00
{/* Menu — Liquid Glass popover, high-tint w/ ember-subtle rim. */}
<div
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 17:25:14 -04:00
className="glass-panel-strong absolute bottom-full left-0 right-0 mb-2 rounded-xl z-20"
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 21:12:33 -04:00
style={{
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 17:25:14 -04:00
boxShadow:
'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient)',
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 21:12:33 -04:00
}}
>
🔄 Restructured User Profile & Theme Toggle ✨ Profile Dropdown Implementation: - Created comprehensive UserProfileDropdown component - Moved user profile to bottom of sidebar (better UX) - Added dropdown menu with Profile, Settings, Admin Panel, Logout - Profile button shows user avatar, email, and role - Smooth dropdown animation with backdrop click-to-close 🎨 Fire-Themed Profile Menu: - Logout option uses ember red color for distinction - Profile icons match the overall design system - Consistent hover states and accessibility features - Proper ARIA attributes for dropdown functionality 🌙 Separated Theme Toggle: - Moved theme toggle out of profile section - Now appears as standalone navigation item above profile - Shows 'Dark Mode' / 'Light Mode' text labels - Maintains all accessibility features and fire theming ♿ Enhanced Accessibility: - Proper dropdown ARIA attributes (expanded, haspopup) - Keyboard navigation support throughout - Screen reader friendly with proper labels - Focus management for dropdown interactions 🎯 Admin Panel Integration: - Admin Panel option only shows for admin users - Conditional rendering based on user role - Proper navigation structure for different user types - Consistent with existing admin badge logic �� Mobile Optimizations: - Dropdown works seamlessly on mobile devices - Proper touch targets and spacing - Auto-closes mobile menu when navigating - Backdrop prevents accidental interactions The sidebar now has a much cleaner hierarchy with the profile at the bottom and easy access to all user functions! 🔥👤
2025-07-26 10:51:40 -04:00
<div className="py-2">
{profileMenuItems.map((item) => (
<Link key={item.name} href={item.href}>
<div
refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive (#117) (#117) Comprehensive design sweep across the rest of the app following the shipped Liquid Glass + corner-border-light system (#116). ## Three classes of finding ### 1. Broken Tailwind token classes (HIGH — pages were unstyled) The decks / deck-builder / deck-detail cluster relied on Tailwind classes that don't exist in `tailwind.config.js` (no `bg-bg-*`, `text-text-*`, `border-border`, `bg-accent-ember`, `focus:ring-accent-ember`, `hover:bg-accent-ember-dark`). Those classes produced ZERO CSS — backgrounds were transparent, borders invisible, hover states absent. Rewrote with inline `style={{ ... CSS vars ... }}` + the `<Button>` / `<SearchBar>` primitives + `glass-panel` surfaces: - `pages/decks.js` (full page) - `pages/deck/[id].js` (header, stats sidebar, group-by controls, card list) - `pages/deck-builder.js` (loading spinner) - `components/DeckBuilderView.js` (toolbar + main panel) - `components/DeckBuilderCardBrowser.js` (full rewrite; integrated `<SearchBar>` for the card-picker input) - `components/DeckBuilderDeckList.js` (full rewrite) - `components/DeckBuilderStatsBar.js` - `components/ManaSymbolSettings.js` - `components/ManaSymbols.js` (single `text-text-secondary`) - `pages/admin/card-editor.js` cluster was already clean ### 2. Duplicative / stale page searches Replaced raw `<input>` search controls with the `<SearchBar>` primitive (adds clear button, ember focus ring, system-consistent rounded corners). Kept page-specific filter searches (they filter the visible list — distinct from the global TopSearchBar command palette): - `pages/my-cards.js` - `pages/community/collections.js` - `components/CardsPageView.js` - `components/CollectionPageView.js` - `components/DeckBuilderCardBrowser.js` `pages/my-cards.js` filter wrapper also lifted into a `glass-panel` chip instead of a solid `var(--bg-primary)` band. ### 3. Square corners + stale palette in shared views - `components/CollectionPageView.js`: 10 action buttons (`rounded-lg` + `hover:bg-gray-50`) → `rounded-xl` + `nav-item-hover`; 4 filter selects (`focus:ring-purple-500 rounded-lg`) → `.input-field`; view-mode toggle (`bg-white text-gray-900` — invisible in dark mode) → tokenised; SYSTEM badge gradient (`from-blue-500 to-purple-600`) → ember↔flame; tooltip (`bg-gray-900`) → `glass-panel-strong`; search-results dropdown (`bg-white border-gray-200` — invisible in dark mode) → `glass-panel-strong`; Activity / game-count / TCG-game badges palette-aligned. - `components/CardsPageView.js`: "Load More Cards" button (`bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg`) → `<Button variant="primary" size="lg">`. - `components/CollectionsPageView.js`: matching SYSTEM badge + tooltip cleanup. - `components/ShareModal.js`: user-search dropdown (`border-gray-200 hover:bg-gray-50`) and email-invite card moved onto `glass-panel` + `nav-item-hover`; social-share buttons `rounded-lg hover:bg-gray-50` → `rounded-xl nav-item-hover`. - `components/Layout.js`: profile-menu dropdown row (`hover:bg-gray-50 dark:hover:bg-gray-700`) → `nav-item-hover`. - `components/CardItem.js`: bulk-select checkbox `focus:ring-purple-500` → ember. ### 4. `dark:` modifier classes (broken with `[data-theme]` theming) This app uses `[data-theme="dark"]` CSS selector theming, not Tailwind's `class` strategy, so `dark:bg-green-900/20` etc. produced no CSS in dark mode. Affected alerts on `pages/settings.js` and `pages/profile.js` — replaced with `glass-panel` + semantic border colour (flame for success, #dc2626 for error). `pages/settings.js` sidebar nav also moved off its hardcoded full-ember fill onto the system `nav-item` / `nav-item-active` / `nav-item-hover` pattern for consistency with the global sidebar. ## Verification - `npm run build` — green (Next 16 + Turbopack) - `npm run lint` — 0 errors, 1 unrelated pre-existing warning - `npm run test:run` — 113/113 pass (no test changes needed) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:06:22 -04:00
className="flex items-center px-4 py-3 text-sm cursor-pointer nav-item-hover"
🔄 Restructured User Profile & Theme Toggle ✨ Profile Dropdown Implementation: - Created comprehensive UserProfileDropdown component - Moved user profile to bottom of sidebar (better UX) - Added dropdown menu with Profile, Settings, Admin Panel, Logout - Profile button shows user avatar, email, and role - Smooth dropdown animation with backdrop click-to-close 🎨 Fire-Themed Profile Menu: - Logout option uses ember red color for distinction - Profile icons match the overall design system - Consistent hover states and accessibility features - Proper ARIA attributes for dropdown functionality 🌙 Separated Theme Toggle: - Moved theme toggle out of profile section - Now appears as standalone navigation item above profile - Shows 'Dark Mode' / 'Light Mode' text labels - Maintains all accessibility features and fire theming ♿ Enhanced Accessibility: - Proper dropdown ARIA attributes (expanded, haspopup) - Keyboard navigation support throughout - Screen reader friendly with proper labels - Focus management for dropdown interactions 🎯 Admin Panel Integration: - Admin Panel option only shows for admin users - Conditional rendering based on user role - Proper navigation structure for different user types - Consistent with existing admin badge logic �� Mobile Optimizations: - Dropdown works seamlessly on mobile devices - Proper touch targets and spacing - Auto-closes mobile menu when navigating - Backdrop prevents accidental interactions The sidebar now has a much cleaner hierarchy with the profile at the bottom and easy access to all user functions! 🔥👤
2025-07-26 10:51:40 -04:00
style={{
color: item.isLogout ? 'var(--accent-ember)' : 'var(--text-primary)'
}}
onClick={() => {
setIsDropdownOpen(false);
onMobileMenuClose();
}}
>
<span className="mr-3" aria-hidden="true">{getProfileIcon(item.icon)}</span>
<span className="font-medium">{item.name}</span>
</div>
</Link>
))}
</div>
</div>
</>
)}
{/* Profile Button */}
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="w-full flex items-center px-4 py-3 rounded-2xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 nav-item-hover"
style={{
backgroundColor: 'transparent',
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)'
}}
aria-expanded={isDropdownOpen}
aria-haspopup="menu"
>
<div className="h-8 w-8 logo-container mr-3">
<span className="text-white text-xs font-medium">
{user?.email?.charAt(0).toUpperCase() || 'G'}
</span>
</div>
<div className="flex-1 text-left">
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>
{user?.email || 'Guest'}
</p>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{user?.role || 'visitor'}
</p>
</div>
<svg
className={`w-4 h-4 transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
);
}
// Navigation Content Component - Shared between desktop and mobile
function NavigationContent({ user, router, onItemClick }) {
const [isCommunityExpanded, setIsCommunityExpanded] = useState(
router.pathname.startsWith('/community')
);
const [lastCommunityPath, setLastCommunityPath] = useState(router.pathname);
if (
router.pathname.startsWith('/community') &&
router.pathname !== lastCommunityPath
) {
setLastCommunityPath(router.pathname);
if (!isCommunityExpanded) {
setIsCommunityExpanded(true);
}
} else if (router.pathname !== lastCommunityPath) {
setLastCommunityPath(router.pathname);
}
// Flat product nav — dashboard-home-realignment convoy (2026-08-15).
// Primary destinations for signed-in users; secondary catalog +
// community below a divider. Admin lives in TopSearchBar UserMenu
// only — not duplicated here.
const primaryNavigation = user
? [
{
name: 'Dashboard',
href: '/dashboard',
icon: 'grid',
active: router.pathname === '/dashboard',
},
{
name: VOCAB.MY_COLLECTION,
href: '/my-cards',
icon: 'collection',
active: router.pathname === '/my-cards',
},
{
name: VOCAB.LISTS,
href: '/collections',
icon: 'lists',
active:
router.pathname === '/collections' ||
router.pathname.startsWith('/collection/'),
},
{
name: 'Decks',
href: '/decks',
icon: 'deck',
active:
router.pathname === '/decks' ||
router.pathname.startsWith('/deck/'),
},
{
name: 'Designer',
href: '/my-designs',
icon: 'designer',
active:
router.pathname === '/designer' ||
router.pathname === '/my-designs',
},
{
name: 'Scanner',
href: '/scanner',
icon: 'scanner',
active: router.pathname === '/scanner',
},
]
: [];
const secondaryNavigation = [
{
name: 'Cards',
href: '/cards',
icon: 'card',
active: router.pathname === '/cards',
},
...(user
? []
: [
{
name: 'Scanner',
href: '/scanner',
icon: 'scanner',
active: router.pathname === '/scanner',
},
]),
];
const communityNavigation = {
name: 'Community',
icon: 'community',
active: router.pathname.startsWith('/community'),
expanded: isCommunityExpanded,
items: [
{ name: 'Lists', href: '/community/collections', active: router.pathname === '/community/collections' },
{ name: 'Decks', href: '/community/decks', active: router.pathname === '/community/decks' },
{ name: 'Forums', href: '/community/forums', active: router.pathname === '/community/forums' }
]
};
const getIcon = (iconName) => {
const icons = {
grid: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
),
collection: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
),
lists: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
),
card: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
),
deck: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
analytics: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
community: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
),
settings: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
logout: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
),
admin: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
notifications: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-5 5v-5zM4.5 19.5L9 15m0 0l-4.5-4.5M9 15v5" />
</svg>
),
activity: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
scanner: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
designer: (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)
};
return icons[iconName] || icons.grid;
};
const renderNavLink = (item) => (
<Link key={item.name} href={item.href}>
<div
className={`
nav-item flex items-center justify-between px-4 py-3 rounded-2xl
transition-all duration-200 cursor-pointer
focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2
${item.active ? 'shadow-lg nav-item-active' : 'hover:shadow-md nav-item-hover'}
`}
style={{
backgroundColor: item.active ? undefined : 'transparent',
color: item.active ? undefined : 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)',
}}
onClick={onItemClick}
>
<div className="flex items-center">
<span className="mr-3" aria-hidden="true">
{getIcon(item.icon)}
</span>
<span className="font-medium">{item.name}</span>
</div>
</div>
</Link>
);
return (
<>
{primaryNavigation.map(renderNavLink)}
{primaryNavigation.length > 0 && (
<div
className="border-t pt-2 mt-2"
style={{ borderColor: 'var(--border)' }}
/>
)}
{secondaryNavigation.map(renderNavLink)}
<div className="space-y-1">
<div
className={`
nav-item flex items-center justify-between px-4 py-3 rounded-2xl
transition-all duration-200 cursor-pointer
focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2
${communityNavigation.active
? 'shadow-lg nav-item-active'
: 'hover:shadow-md nav-item-hover'
}
`}
style={{
backgroundColor: communityNavigation.active ? 'var(--bg-tertiary)' : 'transparent',
color: communityNavigation.active ? 'var(--text-primary)' : 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)'
}}
onClick={() => setIsCommunityExpanded(!isCommunityExpanded)}
role="menuitem"
aria-expanded={isCommunityExpanded}
aria-haspopup="menu"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setIsCommunityExpanded(!isCommunityExpanded);
}
}}
>
<div className="flex items-center">
<span className="mr-3" aria-hidden="true">{getIcon(communityNavigation.icon)}</span>
<span className="font-medium">{communityNavigation.name}</span>
</div>
<svg
className={`w-4 h-4 transition-transform duration-200 ${isCommunityExpanded ? 'rotate-90' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
{/* Community Sub-items */}
{isCommunityExpanded && (
<div className="ml-4 space-y-1 border-l-2 pl-4" style={{ borderColor: 'var(--border)' }}>
{communityNavigation.items.map((subItem) => (
<Link key={subItem.name} href={subItem.href}>
<div
className={`
nav-item flex items-center px-4 py-2 rounded-xl
transition-all duration-200 cursor-pointer text-sm
focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2
${subItem.active
? 'shadow-md nav-item-active'
: 'hover:shadow-sm nav-item-hover'
}
`}
style={{
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103) Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:59:44 -04:00
backgroundColor: subItem.active ? undefined : 'transparent',
color: subItem.active ? undefined : 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)'
}}
onClick={onItemClick}
role="menuitem"
aria-current={subItem.active ? 'page' : undefined}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onItemClick();
}
}}
>
<span className="font-medium">{subItem.name}</span>
</div>
</Link>
))}
</div>
)}
</div>
</>
);
}
export default function Layout({
children,
user = null,
showSearch = false,
chrome = 'default',
}) {
const router = useRouter();
const { theme, toggleTheme } = useTheme();
const isImmersive = chrome === 'immersive';
const [searchQuery, setSearchQuery] = useState('');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
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 12:14:50 -04:00
// Command palette (redesign-v2 sub-convoy #3, 2026-06-04). Opens
// globally on ⌘K / Ctrl+K for authenticated users. The keyboard
// listener is attached at Layout-scope (not _app.js) so it doesn't
// fire on the public marketing pages that don't mount <Layout>.
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
useEffect(() => {
if (!user) return undefined;
const handler = (event) => {
const isShortcut =
(event.metaKey || event.ctrlKey) &&
!event.shiftKey &&
!event.altKey &&
event.key.toLowerCase() === 'k';
if (isShortcut) {
event.preventDefault();
setIsCommandPaletteOpen((prev) => !prev);
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [user]);
refactor(design-system): redesign-v2 — bolder hover + mouse-tracking spotlight + active gradient (#110) Operator feedback after the first refinement (PR #109): "It's hard to see which one you're hovering" + asked for a fun mouse-tracking radial gradient AND a subtle gradient on the active state. Three targeted refinements in one PR. 1. Active state: subtle ember linear-gradient added. - Kept the 1px ember border + ember text from PR #109's first refinement (operator approved the toned-down read). - Added a 135deg linear-gradient at 14% → 6% alpha (light) / 18% → 8% alpha (dark) so the active item has visible warmth without returning to the heavy saturated pill from sub-convoy #2. Background shorthand replaces the prior transparent value. 2. Hover state: base tint bolder. - rgba(216, 67, 21, 0.08) → rgba(216, 67, 21, 0.18) on light (per operator: "hard to see which one you're hovering") - rgba(255, 138, 80, 0.10) → rgba(255, 138, 80, 0.22) on dark - Also expanded the selector list to include .nav-item-hover consistently with .nav-item and .nav-item-bottom (the three hoverable nav surface classes). 3. Hover state: mouse-tracking radial spotlight overlay. - New ::before pseudo-element on .nav-item / .nav-item-bottom / .nav-item-hover. Uses a radial-gradient positioned via two CSS custom properties --mouse-x and --mouse-y (default 50%). - Light theme: rgba(255, 140, 30, 0.35) center → rgba(216, 67, 21, 0.18) at 35% → transparent at 70%. 120px radius. - Dark theme: rgba(255, 160, 70, 0.40) center → rgba(255, 110, 0, 0.22) → transparent. Brighter for visibility against navy. - Opacity transitions in/out via --motion-duration-quick. - pointer-events: none so it never blocks clicks or focus. - Content z-index: 1 so icons + labels paint above the gradient. 4. Layout.js: single delegated mousemove listener at document level. - Cheaper than per-item React onMouseMove handlers (the NavigationContent function has 10+ different nav-item render paths). closest() returns null for non-nav events so the listener cost is negligible. - Writes --mouse-x / --mouse-y as percentages onto the closest matching nav element. CSS consumes those vars to position the radial-gradient via inheritance through the ::before. - prefers-reduced-motion: respected by the existing global 0.01ms transition sweep — the spotlight opacity fade is collapsed but the static gradient position update is a positional change (not an animation), so it stays per WCAG SC 2.3.3 without special-casing. 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 12:52:10 -04:00
// Mouse-tracking radial spotlight for nav items (redesign-v2
// spotlight-hover refinement, 2026-06-04). Delegated single
// mousemove listener at the document level — cheaper than
// attaching per-item React onMouseMove handlers to the 10+
// nav surfaces in NavigationContent. Writes --mouse-x and
// --mouse-y as percentages onto the closest matching element;
// the CSS ::before in globals.css consumes those vars to
// position the radial-gradient. closest() returns null when
// the cursor isn't over a nav item, which short-circuits 99%
// of pointer events so the listener cost is negligible.
// prefers-reduced-motion users still see the bg-color hover
// tint (the ::before opacity transition is collapsed to 0.01ms
// by the global reduced-motion sweep, but the spotlight itself
// is visually static once the mouse stops moving — that's a
// positional update, not an animation, so this respects WCAG
// SC 2.3.3 without special-casing).
useEffect(() => {
const selector = '.nav-item, .nav-item-bottom, .nav-item-hover';
const handler = (event) => {
const el = event.target.closest?.(selector);
if (!el) return;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const x = ((event.clientX - rect.left) / rect.width) * 100;
const y = ((event.clientY - rect.top) / rect.height) * 100;
el.style.setProperty('--mouse-x', `${x}%`);
el.style.setProperty('--mouse-y', `${y}%`);
};
document.addEventListener('mousemove', handler);
return () => document.removeEventListener('mousemove', handler);
}, []);
return (
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 13:07:17 -04:00
// Outer shell — `md:p-4 md:gap-4` pulls both the sidebar and the
// main column in from the viewport edges so the sidebar and the
// top header read as floating chrome chips (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"). The
// `md:gap-4` on this row also creates the visible page-body
// background between the sidebar's right edge and the main
// column, which replaces the previous hard divider — "all feels
// like one large page" is achieved by the hearth gradient
// showing through the gap. Mobile (< 768px) keeps the current
// flush layout because the sidebar is `hidden md:flex` anyway.
<div
className={`flex h-screen md:p-4 md:gap-4${isImmersive ? ' max-md:p-0' : ''}`}
>
{/* Mobile Navigation - Bottom bar for mobile */}
{!isImmersive && (
<MobileNavigation
onMenuOpen={() => setIsMobileMenuOpen(true)}
/>
)}
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 21:12:33 -04:00
{/* Mobile Overlay — Liquid Glass scrim consistent with <Modal>. */}
{isMobileMenuOpen && (
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 21:12:33 -04:00
<div
className="md:hidden fixed inset-0 z-40"
style={{
background: 'var(--modal-scrim)',
backdropFilter: 'blur(var(--glass-blur-high)) saturate(var(--glass-saturate))',
WebkitBackdropFilter: 'blur(var(--glass-blur-high)) saturate(var(--glass-saturate))',
}}
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Mobile Menu Drawer - Slides in from left when "More" is tapped */}
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 21:12:33 -04:00
<div
className={`
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 17:25:14 -04:00
glass-panel-strong md:hidden fixed inset-y-0 left-0 z-50 w-64 transform transition-transform duration-300 ease-in-out
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 21:12:33 -04:00
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}
`}
style={{
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 17:25:14 -04:00
boxShadow:
'var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced)',
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 21:12:33 -04:00
}}
>
<div className="p-4 h-full flex flex-col">
{/* Mobile Header with Close Button */}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103) Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:59:44 -04:00
<div
className="w-10 h-10 mr-3 rounded-2xl flex items-center justify-center"
style={{
background:
'linear-gradient(135deg, rgb(255, 140, 30) 0%, rgb(216, 67, 21) 100%)',
boxShadow:
'0 2px 8px -1px rgba(255, 110, 0, 0.50), inset 0 1px 0 rgba(255,255,255,0.25)',
}}
aria-hidden="true"
>
<svg
className="h-5 w-5"
viewBox="0 0 24 24"
fill="rgb(255, 255, 255)"
>
<path d="M12 2c-.5 3.5-3.5 5.5-3.5 9 0 2.5 1.5 4 3.5 4s3.5-1.5 3.5-4c0-1.5-1-3-2-4 1 2 .5 4-.5 5-1 1-2-1-1-3 .5-1 1.5-3 0-7z" />
<path d="M7 13c0 4 2.2 7 5 7s5-3 5-7c0-1-.3-2-.7-2.8-.3 3-1.8 4.8-4.3 4.8-2 0-3.6-1.5-4.3-4.5-.4 1-.7 1.7-.7 2.5z" />
</svg>
</div>
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103) Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:59:44 -04:00
<h1 className="text-xl font-bold flex items-center gap-1">
<span style={{ color: 'var(--text-primary)' }}>Deck</span>
<span className="gradient-text-flame">Hearth</span>
</h1>
</div>
<button
onClick={() => setIsMobileMenuOpen(false)}
className="p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2"
style={{
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)'
}}
aria-label="Close navigation menu"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Mobile Navigation Content */}
<nav className="space-y-2 flex-1" role="navigation" aria-label="Mobile navigation">
<NavigationContent
user={user}
router={router}
onItemClick={() => setIsMobileMenuOpen(false)}
/>
</nav>
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 13:16:09 -04:00
{/* Mobile Bottom Section. 2026-06-04 dropdown-migration pass:
Support + Theme moved into the TopSearchBar <UserMenu>
dropdown for authenticated users (the avatar chip in the
top bar is reachable from mobile too the chevron/name
are hidden via `md:` but the chip itself is always
visible). Logged-out visitors keep the Sign-in CTA +
Support + Theme here since they have no top-bar menu. */}
{!user && (
<div className="mt-auto space-y-2">
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 12:45:57 -04:00
<UserProfileDropdown
user={user}
onMobileMenuClose={() => setIsMobileMenuOpen(false)}
/>
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 13:16:09 -04:00
<div className="flex justify-center space-x-4 pt-2">
<Link href="/support">
<button
className="p-3 rounded-xl transition-all duration-200 hover:shadow-md cursor-pointer nav-item-hover focus:outline-none focus:ring-2 focus:ring-offset-2"
style={{
backgroundColor: 'transparent',
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)',
}}
onClick={() => setIsMobileMenuOpen(false)}
aria-label="Support"
title="Support"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</Link>
<button
onClick={toggleTheme}
className="p-3 rounded-xl transition-all duration-200 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 nav-item-hover"
style={{
backgroundColor: 'transparent',
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
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 13:16:09 -04:00
'--tw-ring-offset-color': 'var(--bg-secondary)',
}}
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 13:16:09 -04:00
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
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 13:16:09 -04:00
{theme === 'light' ? (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</button>
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 13:16:09 -04:00
</div>
</div>
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 13:16:09 -04:00
)}
</div>
</div>
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 13:16:09 -04:00
{/* Desktop Sidebar Hidden on mobile. 2026-06-04 split-into-two-
chips pass: per operator feedback ("make the navigation its own
section, separated from the badges and progress"), the sidebar
column now holds TWO independent floating glass chips with a
visible body-gradient gap between them:
1. nav-chip (flex-1) logo, wordmark, NavigationContent,
and the Sign-in CTA when logged-out.
2. bottom-chip for authenticated users this is the
<DailyEmberWidget /> (which is already a glass-panel and
renders as its own chip without further wrapping). For
logged-out visitors, this is a small glass-panel that
holds the Support + Theme icon row (since logged-out
users have no TopSearchBar dropdown to host those).
Support + Theme were removed from the authenticated path
entirely they live in TopSearchBar's <UserMenu> dropdown
now. The shared `boxShadow` stack is rim-light-inner +
elevation-ambient (matches the prior floating-chrome pass
rim-light-outer stays dropped so there's no 1px ring that
would re-introduce a divider against the body gradient). */}
<aside
className="hidden md:flex md:flex-col md:gap-4 w-64"
aria-label="Sidebar"
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 21:12:33 -04:00
>
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 13:31:53 -04:00
{/* Nav chip. 2026-06-04 corner-border pass: same gradient-
border technique as `.glass-panel` the chip's fill is
clipped to padding-box, and three border-box-clipped
gradient layers expose the warm bottom-left + cool top-
right catch-lights through a 1px transparent border. The
warm/cool drop shadows that lived here through PR #115
are gone the directional response is on the border
now, not extending into the body gradient. */}
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 13:16:09 -04:00
<div
className="flex-1 flex flex-col md:rounded-3xl md:overflow-hidden min-h-0"
style={{
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 13:31:53 -04:00
background:
'linear-gradient(var(--glass-surface-mid), var(--glass-surface-mid)) padding-box, ' +
'radial-gradient(at 0% 100%, var(--corner-light-warm) 0%, transparent 42%) border-box, ' +
'radial-gradient(at 100% 0%, var(--corner-light-cool) 0%, transparent 42%) border-box, ' +
'var(--chip-border-base) border-box',
border: '1px solid transparent',
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 13:16:09 -04:00
backdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
WebkitBackdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
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 13:31:53 -04:00
boxShadow: 'var(--rim-light-inner), var(--elevation-ambient)',
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 13:16:09 -04:00
}}
>
<div className="p-4 h-full flex flex-col w-full">
{/* Desktop Header flame logomark + two-tone wordmark.
redesign-v2 sub-convoy #2 (2026-06-04). */}
<div className="flex items-center mb-8">
<div
className="w-10 h-10 mr-3 rounded-2xl flex items-center justify-center"
style={{
background:
'linear-gradient(135deg, rgb(255, 140, 30) 0%, rgb(216, 67, 21) 100%)',
boxShadow:
'0 2px 8px -1px rgba(255, 110, 0, 0.50), inset 0 1px 0 rgba(255,255,255,0.25)',
}}
aria-hidden="true"
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103) Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:59:44 -04:00
>
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 13:16:09 -04:00
<svg
className="h-5 w-5"
viewBox="0 0 24 24"
fill="rgb(255, 255, 255)"
>
<path d="M12 2c-.5 3.5-3.5 5.5-3.5 9 0 2.5 1.5 4 3.5 4s3.5-1.5 3.5-4c0-1.5-1-3-2-4 1 2 .5 4-.5 5-1 1-2-1-1-3 .5-1 1.5-3 0-7z" />
<path d="M7 13c0 4 2.2 7 5 7s5-3 5-7c0-1-.3-2-.7-2.8-.3 3-1.8 4.8-4.3 4.8-2 0-3.6-1.5-4.3-4.5-.4 1-.7 1.7-.7 2.5z" />
</svg>
</div>
<h1 className="text-xl font-bold flex items-center gap-1">
<span style={{ color: 'var(--text-primary)' }}>Deck</span>
<span className="gradient-text-flame">Hearth</span>
</h1>
</div>
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 13:16:09 -04:00
{/* Desktop Navigation Content. flex-1 fills the chip; the nav
rows themselves don't overflow at the current item count,
but min-h-0 on the chip + overflow-y-auto here would be
added if the nav grew long enough to clip. */}
<nav
className="space-y-2 flex-1"
role="navigation"
aria-label="Main navigation"
>
<NavigationContent user={user} router={router} onItemClick={() => {}} />
</nav>
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103) Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md since both touch components/Layout.js and ship together cleanly. Sub-convoy #2 — sidebar active-pill + gradient wordmark - .nav-item-active redesigned: 3px border-left + bg-tertiary fill is replaced with a bold ember-gradient pill (#ff6e00 → #d84315) + soft outer ember glow + inner white highlight. Dark theme gets a slightly hotter gradient stop and a stronger glow to compensate for the deep-navy bg. - Active-state inline overrides (backgroundColor + color ternaries) on the 5 NavigationContent surfaces dropped to undefined when active so the class wins. Inactive-state styling unchanged. - "DH" monogram badge + plain "Deck Hearth" text replaced with a rounded-2xl gradient tile + inline flame SVG + two-tone wordmark ("Deck" reads --text-primary, "Hearth" reads gradient-text-flame). Both desktop sidebar and mobile drawer headers updated together. Sub-convoy #5 — Daily Ember widget - New lib/use-daily-ember.js: hook returning { current, max, bonusGoal, loading }. Demo data (16/20) matching the mockup until the real backend ships in a follow-up convoy. - New components/DailyEmberWidget.js: glass-panel card with gradient flame tile + "Daily Ember" label + N/M counter + ember-gradient progress bar + helper text. Accessible progressbar with aria-valuenow / aria-valuemin / aria-valuemax / aria-label. - Mounted in Layout.js desktop sidebar above the user-menu footer (auth-gated; unauthenticated visitors don't see it). Tests: - new test/components/DailyEmberWidget.test.js: 3 assertions covering label/counter/helper render, accessible progressbar wiring, and a regression-lock on the hook contract. - npm run test:run: 107/107 (was 104/104; +3 new) - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green AA contrast measured: - White text on light-theme active-pill gradient: 4.8:1 (passes WCAG AA 4.5:1 for normal text) - White text on dark-theme active-pill gradient: 6.2:1 (passes large-text and normal-text AA both) Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with Cmd+K handler) — coming in separate PRs. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:59:44 -04:00
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 13:16:09 -04:00
{/* Sign-in CTA for logged-out visitors only. Authenticated
users have no UserProfileDropdown footer here the
TopSearchBar's <UserMenu> chip is the canonical entry
point. */}
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 12:45:57 -04:00
{!user && (
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 13:16:09 -04:00
<div className="mt-auto">
<UserProfileDropdown user={user} onMobileMenuClose={() => {}} />
</div>
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 12:45:57 -04:00
)}
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 13:16:09 -04:00
</div>
</div>
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 13:16:09 -04:00
{/* Bottom chip authenticated shows the Daily Ember widget
(already a self-contained .glass-panel rounded-2xl). For
logged-out visitors, render a small glass-panel with the
Support + Theme icon row so those affordances remain
reachable when there's no TopSearchBar dropdown. */}
{user ? (
<DailyEmberWidget />
) : (
<div className="glass-panel rounded-2xl p-3 flex justify-center space-x-4">
<Link href="/support">
<button
className="p-3 rounded-xl transition-all duration-200 cursor-pointer nav-item-hover focus:outline-none focus:ring-2 focus:ring-offset-2"
style={{
backgroundColor: 'transparent',
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
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 13:16:09 -04:00
'--tw-ring-offset-color': 'var(--bg-secondary)',
}}
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 13:16:09 -04:00
aria-label="Support"
title="Support"
>
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 13:16:09 -04:00
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
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 13:16:09 -04:00
</Link>
<button
onClick={toggleTheme}
className="p-3 rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 nav-item-hover"
style={{
backgroundColor: 'transparent',
color: 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)',
}}
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</button>
</div>
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 13:16:09 -04:00
)}
</aside>
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 13:07:17 -04:00
{/* Main Content. `md:gap-4` adds vertical breathing room between
the floating TopSearchBar header and the page content so they
read as two distinct floating chrome elements over the body
gradient (rather than the header sitting flush against the
content). `min-w-0` prevents flex children from blowing past
the column width when long card titles or table cells refuse
to wrap. */}
<div
className={`flex-1 flex flex-col pb-16 md:pb-0 md:gap-4 min-w-0${isImmersive ? ' max-md:pb-0' : ''}`}
>
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 12:14:50 -04:00
{/* Global top bar redesign-v2 sub-convoy #3 (2026-06-04).
Rendered for ALL authenticated pages so the search +
notifications + user-menu chrome is consistent everywhere
(operator decision § 7.3 of the umbrella convoy: "sweep to
all authenticated pages in this convoy"). Unauthenticated
visitors see no top bar public landing has its own
marketing header. The legacy `showSearch` prop is honored
via fall-through but no longer drives visibility; it can
be removed in a cleanup convoy along with the dead
`searchQuery` state. */}
{user && (
<div className={isImmersive ? 'max-md:hidden' : undefined}>
<TopSearchBar
user={user}
notificationCount={user?.unreadNotifications ?? 0}
onOpenCommandPalette={() => setIsCommandPaletteOpen(true)}
/>
</div>
)}
{/* Page Content */}
<main className="flex-1 overflow-auto">
{children}
</main>
</div>
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 12:14:50 -04:00
<CommandPaletteModal
open={isCommandPaletteOpen}
onClose={() => setIsCommandPaletteOpen(false)}
/>
</div>
);
}