Closes the migrate-button-input-mobilenav-to-glass-primitive convoy (seeded by PR #127). All 3 residual handrolled var(--glass-surface-*) inline-style usages migrated to either purpose-built utility classes or the <GlassSurface> primitive. CI allowlist reduced from 6 entries to 3 (chrome only). Architect decisions (D1-D3, ratified): D1 — Button.secondary → new .btn-glass-secondary utility class. NOT <GlassSurface>: the primitive sets `background` inline via composedStyle, which CSS :hover rules can't override without !important. The new class composes the same high-tint gradient-border that .glass-panel-strong uses, plus a pure-CSS :hover swap (high → mid fill on the padding-box layer). Identical visual contract; the hover behavior is now driven by CSS, not Tailwind's `hover:bg-[var(...)]` arbitrary class. D2 — Input → new .glass-input utility class. NOT <GlassSurface as="input"> and NOT <GlassSurface as="div"> wrap. Reason: <GlassSurface>'s gradient-border trick requires `border: 1px solid transparent` to expose the border-box layers, which conflicts with <Input>'s conditional error-state `1px solid #dc2626` red border. The new class adopts only the tint + blur layer; the visible 1px border + focus ring stay in JSX (class-controlled, not inline). Same visual contract as before for both normal AND error states. D3 — MobileNavigation → <GlassSurface as="div" tint="mid" blur="mid" rim="subtle" elevation="flat" cornerLights="chrome">. NOT .page-header-glass (the seed's first recommendation): .page-header-glass uses var(--glass-surface-high) (wrong tint — MobileNav uses mid) and sets a bottom-border separator (wrong for a fixed-bottom-nav where the bottom edge is the viewport edge). <GlassSurface> is the better fit AND brings the chrome-tier corner-light bleed that the parent convoy is unifying across all chrome surfaces. Implementation choice — single PR (not 3 parallel briefs): The seed recommended 3 small parallel-safe briefs (one per file). D1 and D2 both need styles/globals.css to gain new utility classes, so those 2 changes can't run truly in parallel without merge conflicts. Single PR is faster, simpler to review end-to-end, and the natural shape for a 2-3 hour convoy with tightly-coupled artifacts. Files changed (4): styles/globals.css (+50 / -1): - Adds .btn-glass-secondary (with :hover variant) — D1. - Adds .glass-input — D2. - Both classes documented inline with architect-decision references. components/ui/Button.js (+2 / -10): - Replaces inline variantStyle + Tailwind hover arbitrary class for `variant === 'secondary'` with `variantClass = 'btn-glass-secondary font-medium'`. variantStyle now `{}`. - Other variants (primary, danger, ghost) UNCHANGED. components/ui/Input.js (+1 / -7): - Adds `glass-input` to the className list. - Removes inline `background` + `backdropFilter` + `WebkitBackdropFilter` from the input's style block. - Conditional `border: inputBorder` stays in JSX (error swap). - All other props/behavior preserved. components/MobileNavigation.js (+11 / -8): - Adds `import { GlassSurface } from './ui'`. - Replaces the inline-styled backdrop <div> with <GlassSurface as="div" ...>. Same className ("absolute inset-0"), same visible behavior, plus the chrome-tier corner-light bleed. - Comment block updated to reference the convoy + decision. .github/workflows/ci.yml (+8 / -22): - forbidden-patterns Check 7/7 GLASS_ALLOWLIST reduced from 6 entries to 3 (chrome only). The TODO comments referencing this convoy are deleted (work is done). .convoys/migrate-button-input-mobilenav-to-glass-primitive.md (+74 / -3): - status: queued → closed, closed: 2026-06-05, prs: [131]. - Architect ratifications D1-D3 written into front-matter docs. - Closeout checklist with all acceptance criteria checked. - Note that parent convoy unify-glass-panel-surfaces is now fully closed — no residual handrolled glass-surface usage outside the 3 chrome blocks. Verification: - POSITIVE TEST: post-migration grep with the reduced 3-entry allowlist returns 0 violations. ✅ - grep on raw files: only Layout.js + TopSearchBar.js still match the literal regex (GlassSurface.js uses template literal which doesn't match — intentional, allowlist is forward-compat). - YAML parses (python3 yaml.safe_load). - npm run lint passes (1 pre-existing unrelated warning). - npm run test:run: 118/118 tests pass. Visual diff to be verified by reviewer in light + dark mode for: - <Button variant="secondary"> default + hover state. - <Input> default + error state. - Mobile bottom-nav backdrop. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
No EOL
7.3 KiB
JavaScript
180 lines
No EOL
7.3 KiB
JavaScript
import { useRouter } from 'next/router';
|
|
import Link from 'next/link';
|
|
import { useState } from 'react';
|
|
import { GlassSurface } from './ui';
|
|
|
|
export default function MobileNavigation({ onMenuOpen }) {
|
|
const router = useRouter();
|
|
|
|
// Navigation items for the bottom bar
|
|
const navigationItems = [
|
|
{
|
|
name: 'Cards',
|
|
href: '/cards',
|
|
icon: 'cards',
|
|
active: router.pathname === '/cards'
|
|
},
|
|
{
|
|
name: 'Decks',
|
|
href: '/decks',
|
|
icon: 'decks',
|
|
active: router.pathname === '/decks'
|
|
},
|
|
{
|
|
name: 'Dashboard',
|
|
href: '/dashboard',
|
|
icon: 'dashboard',
|
|
active: router.pathname === '/dashboard' || router.pathname === '/collections' || router.pathname === '/my-cards' || router.pathname === '/analytics',
|
|
isPrimary: true // This will be the raised center button
|
|
},
|
|
{
|
|
name: 'Community',
|
|
href: '/community',
|
|
icon: 'community',
|
|
active: router.pathname.startsWith('/community')
|
|
},
|
|
{
|
|
name: 'More',
|
|
href: '#',
|
|
icon: 'more',
|
|
onClick: onMenuOpen,
|
|
active: false
|
|
}
|
|
];
|
|
|
|
const getIcon = (iconName, isActive = false, isPrimary = false) => {
|
|
const iconClass = isPrimary ? "h-7 w-7" : "h-6 w-6";
|
|
const strokeWidth = isActive ? 2.5 : 2;
|
|
|
|
const icons = {
|
|
cards: (
|
|
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} 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>
|
|
),
|
|
decks: (
|
|
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} 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>
|
|
),
|
|
dashboard: (
|
|
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} 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>
|
|
),
|
|
community: (
|
|
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} 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>
|
|
),
|
|
more: (
|
|
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
)
|
|
};
|
|
|
|
return icons[iconName] || icons.dashboard;
|
|
};
|
|
|
|
return (
|
|
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50">
|
|
{/* Liquid Glass background — mid-tint surface with chrome-tier
|
|
corner-light bleed, matching the rest of the floating chrome
|
|
(sidebar nav-chip, TopSearchBar). Composed via <GlassSurface>
|
|
post-migrate-button-input-mobilenav-to-glass-primitive
|
|
convoy (2026-06-05) — `rim="subtle"` reproduces the previous
|
|
rim-light-inner + rim-light-outer halo; `cornerLights="chrome"`
|
|
brings in the corner-light bleed that the convoy is
|
|
unifying across all chrome surfaces. */}
|
|
<GlassSurface
|
|
className="absolute inset-0"
|
|
tint="mid"
|
|
blur="mid"
|
|
rim="subtle"
|
|
elevation="flat"
|
|
cornerLights="chrome"
|
|
/>
|
|
|
|
{/* Navigation Content */}
|
|
<div className="relative px-4 py-2">
|
|
<div className="flex items-center justify-around">
|
|
{navigationItems.map((item) => {
|
|
if (item.isPrimary) {
|
|
// Primary/Dashboard button - raised and prominent
|
|
return (
|
|
<Link key={item.name} href={item.href}>
|
|
<div className="relative">
|
|
{/* Raised background circle */}
|
|
<div
|
|
className="absolute inset-0 rounded-full shadow-lg transform -translate-y-2"
|
|
style={{
|
|
background: item.active
|
|
? 'var(--gradient-primary)'
|
|
: 'var(--gradient-secondary)',
|
|
width: '56px',
|
|
height: '56px'
|
|
}}
|
|
/>
|
|
|
|
{/* Button content */}
|
|
<button
|
|
className="relative flex flex-col items-center justify-center w-14 h-14 transform -translate-y-2 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
style={{
|
|
color: 'white',
|
|
'--tw-ring-color': 'var(--accent-ember)',
|
|
'--tw-ring-offset-color': 'var(--bg-primary)'
|
|
}}
|
|
aria-label={item.name}
|
|
>
|
|
{getIcon(item.icon, item.active, true)}
|
|
</button>
|
|
|
|
{/* Label below raised button */}
|
|
<span
|
|
className="absolute top-12 left-1/2 transform -translate-x-1/2 text-xs font-medium whitespace-nowrap"
|
|
style={{
|
|
color: item.active ? 'var(--accent-ember)' : 'var(--text-secondary)'
|
|
}}
|
|
>
|
|
{item.name}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
);
|
|
} else {
|
|
// Regular navigation buttons
|
|
const ButtonComponent = item.onClick ? 'button' : Link;
|
|
const buttonProps = item.onClick
|
|
? { onClick: item.onClick }
|
|
: { href: item.href };
|
|
|
|
return (
|
|
<ButtonComponent key={item.name} {...buttonProps}>
|
|
<div
|
|
className="flex flex-col items-center justify-center py-2 px-3 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"
|
|
style={{
|
|
color: item.active ? 'var(--accent-ember)' : 'var(--text-secondary)',
|
|
'--tw-ring-color': 'var(--accent-ember)',
|
|
'--tw-ring-offset-color': 'var(--bg-secondary)'
|
|
}}
|
|
>
|
|
<div className="mb-1">
|
|
{getIcon(item.icon, item.active)}
|
|
</div>
|
|
<span className="text-xs font-medium">
|
|
{item.name}
|
|
</span>
|
|
</div>
|
|
</ButtonComponent>
|
|
);
|
|
}
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Safe area padding for devices with home indicator */}
|
|
<div className="h-safe-area-inset-bottom" />
|
|
</div>
|
|
);
|
|
} |