← back to Norma
components/InboxThemeBanner.tsx
149 lines
'use client';
// Top-of-page inbox-theme banner. Full-width strip at the very top of the
// AppShell with prev/next cycle buttons + dropdown so you can scrub through
// the full 32-theme set. Writes <body data-inbox-theme="..."> + persists to
// localStorage. CSS skins in globals.css scope down to .gmail-crm-tab so
// only the Gmail CRM tab visual changes.
import { useEffect, useState, useCallback } from 'react';
import { ChevronLeft, ChevronRight, Palette, Shuffle } from 'lucide-react';
import { INBOX_THEMES, INBOX_THEME_LS_KEY, DEFAULT_INBOX_THEME } from '@/lib/inbox-themes';
export default function InboxThemeBanner() {
const [theme, setTheme] = useState<string>(DEFAULT_INBOX_THEME);
const [mounted, setMounted] = useState(false);
// Hydrate from localStorage post-mount (avoids SSR mismatch)
useEffect(() => {
setMounted(true);
try {
const s = localStorage.getItem(INBOX_THEME_LS_KEY);
if (s && INBOX_THEMES.some(t => t.id === s)) setTheme(s);
} catch {}
}, []);
// Apply to <body data-inbox-theme="..."> + persist
useEffect(() => {
if (!mounted) return;
if (typeof document !== 'undefined') document.body.dataset.inboxTheme = theme;
try { localStorage.setItem(INBOX_THEME_LS_KEY, theme); } catch {}
}, [theme, mounted]);
const currentIdx = Math.max(0, INBOX_THEMES.findIndex(t => t.id === theme));
const current = INBOX_THEMES[currentIdx];
const step = useCallback((delta: number) => {
const n = INBOX_THEMES.length;
const next = ((currentIdx + delta) % n + n) % n; // wrap both directions
setTheme(INBOX_THEMES[next].id);
}, [currentIdx]);
const randomize = useCallback(() => {
const others = INBOX_THEMES.filter(t => t.id !== theme);
setTheme(others[Math.floor(Math.random() * others.length)].id);
}, [theme]);
return (
<div
role="toolbar"
aria-label="Gmail CRM theme switcher"
style={{
position: 'sticky',
top: 0,
zIndex: 110,
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 16px',
background: 'var(--color-surface)',
borderBottom: '1px solid var(--color-border)',
fontSize: 12,
color: 'var(--color-text-secondary)',
flexWrap: 'wrap',
}}
>
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
fontWeight: 700, color: 'var(--color-text)', fontSize: 11.5,
letterSpacing: '0.06em', textTransform: 'uppercase',
paddingRight: 10, borderRight: '1px solid var(--color-border)',
}}>
<Palette size={14} />
Gmail CRM theme
</div>
{/* Prev / counter / Next cycle */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<button
onClick={() => step(-1)}
className="btn btn-ghost btn-sm"
aria-label="Previous theme"
title="Previous theme (←)"
style={{ padding: '4px 8px' }}
>
<ChevronLeft size={14} />
</button>
<span style={{
minWidth: 56, textAlign: 'center',
fontFamily: 'ui-monospace, Menlo, monospace',
fontSize: 11.5, color: 'var(--color-text-muted)',
}}>
{currentIdx + 1} / {INBOX_THEMES.length}
</span>
<button
onClick={() => step(1)}
className="btn btn-ghost btn-sm"
aria-label="Next theme"
title="Next theme (→)"
style={{ padding: '4px 8px' }}
>
<ChevronRight size={14} />
</button>
<button
onClick={randomize}
className="btn btn-ghost btn-sm"
aria-label="Random theme"
title="Random theme"
style={{ padding: '4px 8px' }}
>
<Shuffle size={13} />
</button>
</div>
{/* Dropdown for direct jump */}
<select
value={theme}
onChange={e => setTheme(e.target.value)}
className="input"
aria-label="Pick inbox theme"
title={current?.description || 'Pick a theme'}
style={{
fontSize: 12, padding: '4px 8px',
minWidth: 220, maxWidth: 280,
}}
>
{INBOX_THEMES.map(t => (
<option key={t.id} value={t.id}>🎨 {t.label}</option>
))}
</select>
{/* Current theme name + description (caption to the right) */}
<span style={{ flex: 1, minWidth: 0, fontStyle: 'italic', opacity: 0.85, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{current ? `${current.label.replace(/^\d+ · /, '')} — ${current.description}` : ''}
</span>
<a
href="/mockups/inbox/index.html"
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost btn-sm"
title="Open the full 32-mockup gallery in a new tab"
style={{ fontSize: 11, padding: '4px 10px' }}
>
Gallery ↗
</a>
</div>
);
}