Template
Harden agent guardrails and fix audited bugs
Gate hardening:
- strict + noUncheckedIndexedAccess in tsconfig.app.json; fix resulting errors
- check-tokens.mjs: scan inline style={{ }} in ts/tsx, ban named colors and
rgb()/hsl()/oklch()/lab(), read multi-line values, fail on undefined var(--x)
- vitest (24 tests: overlay, icon-registry, useTimeout, token guard) wired into
npm run check; GitHub Actions CI workflow
Single-source refactors:
- new Overlay primitive owns focus trap/Escape/scroll-lock/focus-restore and
conditional aria-labelledby/aria-describedby; Dialog + Sheet are thin skins
(overlayBehavior.ts exports trapTabKey/useOverlayLifecycle for SearchPalette)
- icon-registry.ts is the one icon map: strict IconName, aliases typed, dev
throws on unknown names instead of silently rendering <Layers/>; IconMatrix
consumes the shared registry (41 curated)
- useTimeout hook replaces fire-and-forget setTimeout in DeploymentModal,
SettingsPanel, IconMatrix, CodeSample (timers die on close/unmount)
Bug fixes:
- SearchPalette: role=dialog/aria-modal, focus trap + restore, combobox/listbox
semantics, labelled input, selection clamped to results
- theme: pre-paint script in index.html (localStorage -> prefers-color-scheme ->
dark), useTheme no longer writes DOM during state init
- StatCard: direction (arrow) decoupled from trend (sentiment); latency drop
renders green "↓ -4.2ms" instead of "↑ -4.2ms"
- sidebar collapse button wired (aria-pressed + collapsed rail CSS); pattern
routing keys on patternId from library.ts, not English labels
- remove 25 dead legacy tokens (--panel*, --line*, --text-soft, --critical,
--pink, --blue, ...); migrate --bg/--text/--positive/--warning usages
- title "Northstar Design System", real README, AGENTS.md structure/theme/
overlay/registry/test docs updated; dev error sink (#dev-errors +
ErrorBoundary) so agents can read runtime errors via inspect
This commit is contained in:
1 parent
c0a94d3b6d
commit
f6e81afe5e
48 files changed
+3299
-696
No files matched your search
+54
-38
@@ -1,45 +1,56 @@
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export type Theme = 'dark' | 'light'
|
||||
|
||||
/**
|
||||
* Read the theme the pre-paint script in index.html already applied to
|
||||
* <html> — that keeps first paint flash-free and honors both the saved
|
||||
* preference and the OS `prefers-color-scheme` when nothing is saved.
|
||||
* This hook never touches the DOM during render/state-initialization.
|
||||
*/
|
||||
function getInitialTheme(): Theme {
|
||||
const attr = document.documentElement.getAttribute('data-theme')
|
||||
return attr === 'light' ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
/** App theme is a data attribute + class on <html>; tokens.css defines both palettes. */
|
||||
function applyTheme(theme: Theme) {
|
||||
const root = document.documentElement
|
||||
root.dataset.theme = theme
|
||||
root.classList.toggle('dark', theme === 'dark')
|
||||
root.classList.toggle('light', theme === 'light')
|
||||
}
|
||||
|
||||
function useTheme() {
|
||||
const [theme, setThemeState] = useState<Theme>(getInitialTheme)
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme)
|
||||
}, [theme])
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const next: Theme = theme === 'dark' ? 'light' : 'dark'
|
||||
setThemeState(next)
|
||||
persistTheme(next)
|
||||
}, [theme])
|
||||
|
||||
return { theme, toggle }
|
||||
}
|
||||
|
||||
function persistTheme(theme: Theme) {
|
||||
try {
|
||||
localStorage.setItem('ns-theme', theme)
|
||||
} catch {
|
||||
/* private mode — theme just won't persist */
|
||||
}
|
||||
}
|
||||
|
||||
type TopbarProps = {
|
||||
onOpenSearch: () => void
|
||||
onMenuToggle: () => void
|
||||
}
|
||||
|
||||
/** App theme is a data attribute on <html>; tokens.css defines both palettes. */
|
||||
function useTheme() {
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
|
||||
const saved = typeof window !== 'undefined' ? localStorage.getItem('ns-theme') : null
|
||||
const initial = saved === 'light' ? 'light' : 'dark'
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.dataset.theme = initial
|
||||
if (initial === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
document.documentElement.classList.remove('light')
|
||||
} else {
|
||||
document.documentElement.classList.add('light')
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
return initial
|
||||
})
|
||||
|
||||
const toggle = () => {
|
||||
const next = theme === 'dark' ? 'light' : 'dark'
|
||||
setTheme(next)
|
||||
document.documentElement.dataset.theme = next
|
||||
if (next === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
document.documentElement.classList.remove('light')
|
||||
} else {
|
||||
document.documentElement.classList.add('light')
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
localStorage.setItem('ns-theme', next)
|
||||
}
|
||||
return { theme, toggle }
|
||||
}
|
||||
|
||||
export function Topbar({ onOpenSearch, onMenuToggle }: TopbarProps) {
|
||||
const { theme, toggle } = useTheme()
|
||||
|
||||
@@ -67,13 +78,18 @@ export function Topbar({ onOpenSearch, onMenuToggle }: TopbarProps) {
|
||||
<nav className="top-links">
|
||||
<a href="#docs">Docs</a>
|
||||
<a href="#github">GitHub ↗</a>
|
||||
<button className="icon-button" onClick={toggle} aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={toggle}
|
||||
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
|
||||
>
|
||||
<Icon name={theme === 'dark' ? 'sun' : 'moon'} size={17} />
|
||||
</button>
|
||||
<button className="avatar">MS</button>
|
||||
<button type="button" className="avatar">MS</button>
|
||||
</nav>
|
||||
|
||||
<button className="mobile-menu icon-button" onClick={onMenuToggle} aria-label="Toggle navigation">
|
||||
<button type="button" className="mobile-menu icon-button" onClick={onMenuToggle} aria-label="Toggle navigation">
|
||||
<Icon name="menu" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
Reference in new issue
Block a user