Template
Central context for the settings a template app shares instead of prop drilling: theme, viewMode, sidebarCollapsed and the search palette, with the ⌘K/Ctrl+K shortcut moved out of App. - settings persist on user action only (an effect-written theme would freeze the OS preference for users who never toggled) - theme still boots from the <html data-theme> attribute the pre-paint script set; viewMode mirrors to data-view-mode as the audience-preset hook and deliberately gates nothing yet - Topbar: viewMode segmented control, theme toggle and search now read the context; useAppState throws outside the provider - tests/appState.test.tsx: defaults, persistence, rehydration, garbage storage values, ⌘K shortcut
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import { Icon } from './Icon'
|
|
import { VIEW_MODES, useAppState, type ViewMode } from '../app'
|
|
|
|
/** `viewMode` is the template's audience preset (see src/app/appState.ts). */
|
|
const VIEW_MODE_LABELS: Record<ViewMode, string> = {
|
|
default: 'Default',
|
|
compact: 'Compact',
|
|
}
|
|
|
|
type TopbarProps = {
|
|
onMenuToggle: () => void
|
|
}
|
|
|
|
export function Topbar({ onMenuToggle }: TopbarProps) {
|
|
const { theme, toggleTheme, viewMode, setViewMode, openSearch } = useAppState()
|
|
|
|
return (
|
|
<header className="topbar">
|
|
<div className="brand">
|
|
<div className="brand-mark">
|
|
<span />
|
|
</div>
|
|
<b>northstar</b>
|
|
<span className="version">v0.8.2</span>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="top-search"
|
|
onClick={openSearch}
|
|
aria-label="Search components, tokens, docs (Press ⌘K)"
|
|
>
|
|
<Icon name="search" size={16} />
|
|
<span className="top-search-placeholder">Search components, tokens, docs...</span>
|
|
<kbd>⌘ K</kbd>
|
|
</button>
|
|
|
|
<nav className="top-links">
|
|
<a href="#docs">Docs</a>
|
|
<a href="#github">GitHub ↗</a>
|
|
|
|
{/* Audience preset held by the app state. Nothing gates on it yet — it
|
|
is wired here so the value is reachable and persisted from the UI. */}
|
|
<div className="segmented top-view-mode" role="group" aria-label="View mode">
|
|
{VIEW_MODES.map(mode => (
|
|
<button
|
|
key={mode}
|
|
type="button"
|
|
className={mode === viewMode ? 'selected' : undefined}
|
|
aria-pressed={mode === viewMode}
|
|
title={`Switch to ${VIEW_MODE_LABELS[mode].toLowerCase()} view`}
|
|
onClick={() => setViewMode(mode)}
|
|
>
|
|
{VIEW_MODE_LABELS[mode]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={toggleTheme}
|
|
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
|
|
>
|
|
<Icon name={theme === 'dark' ? 'sun' : 'moon'} size={17} />
|
|
</button>
|
|
<button type="button" className="avatar">MS</button>
|
|
</nav>
|
|
|
|
<button type="button" className="mobile-menu icon-button" onClick={onMenuToggle} aria-label="Toggle navigation">
|
|
<Icon name="menu" />
|
|
</button>
|
|
</header>
|
|
)
|
|
}
|