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:
developer committed 2026-09-13 00:22:16 +00:00
1 parent c0a94d3b6d
commit f6e81afe5e
48 files changed
+3299 -696

No files matched your search

+9 -101
View File
@@ -1,109 +1,15 @@
import { useState, useMemo } from 'react'
import {
Search,
Sun,
Moon,
Copy,
Check,
Eye,
ArrowRight,
ArrowLeft,
Menu,
Layers,
Sparkles,
Terminal,
AlertTriangle,
AlertCircle,
Info,
CheckCircle2,
X,
Plus,
Minus,
Trash2,
ExternalLink,
ChevronDown,
ChevronRight,
ChevronLeft,
ChevronUp,
Filter,
SlidersHorizontal,
Code2,
Cpu,
Database,
User,
Shield,
Folder,
FileText,
Calendar,
Clock,
RefreshCw,
Download,
Upload,
Settings,
HelpCircle,
type LucideIcon,
} from 'lucide-react'
import { useTimeout } from '../utils/useTimeout'
import { Icon } from './Icon'
import { iconRegistry, type IconRegistryEntry } from './icon-registry'
import { Kbd } from './ui/Kbd'
import { Badge } from './ui/Badge'
type IconItem = {
name: string
pascalName: string
category: 'Navigation' | 'Interface' | 'System' | 'Status'
component: LucideIcon
}
type IconItem = IconRegistryEntry
const ICONS: IconItem[] = [
// Navigation
{ name: 'arrow-right', pascalName: 'ArrowRight', category: 'Navigation', component: ArrowRight },
{ name: 'arrow-left', pascalName: 'ArrowLeft', category: 'Navigation', component: ArrowLeft },
{ name: 'chevron-right', pascalName: 'ChevronRight', category: 'Navigation', component: ChevronRight },
{ name: 'chevron-left', pascalName: 'ChevronLeft', category: 'Navigation', component: ChevronLeft },
{ name: 'chevron-down', pascalName: 'ChevronDown', category: 'Navigation', component: ChevronDown },
{ name: 'chevron-up', pascalName: 'ChevronUp', category: 'Navigation', component: ChevronUp },
{ name: 'external-link', pascalName: 'ExternalLink', category: 'Navigation', component: ExternalLink },
{ name: 'menu', pascalName: 'Menu', category: 'Navigation', component: Menu },
// System & AI
{ name: 'sparkles', pascalName: 'Sparkles', category: 'System', component: Sparkles },
{ name: 'terminal', pascalName: 'Terminal', category: 'System', component: Terminal },
{ name: 'code', pascalName: 'Code2', category: 'System', component: Code2 },
{ name: 'cpu', pascalName: 'Cpu', category: 'System', component: Cpu },
{ name: 'layers', pascalName: 'Layers', category: 'System', component: Layers },
{ name: 'sun', pascalName: 'Sun', category: 'System', component: Sun },
{ name: 'moon', pascalName: 'Moon', category: 'System', component: Moon },
{ name: 'database', pascalName: 'Database', category: 'System', component: Database },
// Interface & Actions
{ name: 'search', pascalName: 'Search', category: 'Interface', component: Search },
{ name: 'copy', pascalName: 'Copy', category: 'Interface', component: Copy },
{ name: 'settings', pascalName: 'Settings', category: 'Interface', component: Settings },
{ name: 'sliders', pascalName: 'SlidersHorizontal', category: 'Interface', component: SlidersHorizontal },
{ name: 'filter', pascalName: 'Filter', category: 'Interface', component: Filter },
{ name: 'plus', pascalName: 'Plus', category: 'Interface', component: Plus },
{ name: 'minus', pascalName: 'Minus', category: 'Interface', component: Minus },
{ name: 'trash', pascalName: 'Trash2', category: 'Interface', component: Trash2 },
{ name: 'refresh', pascalName: 'RefreshCw', category: 'Interface', component: RefreshCw },
{ name: 'download', pascalName: 'Download', category: 'Interface', component: Download },
{ name: 'upload', pascalName: 'Upload', category: 'Interface', component: Upload },
{ name: 'folder', pascalName: 'Folder', category: 'Interface', component: Folder },
{ name: 'file', pascalName: 'FileText', category: 'Interface', component: FileText },
{ name: 'user', pascalName: 'User', category: 'Interface', component: User },
{ name: 'calendar', pascalName: 'Calendar', category: 'Interface', component: Calendar },
{ name: 'clock', pascalName: 'Clock', category: 'Interface', component: Clock },
// Status & Feedback
{ name: 'check', pascalName: 'Check', category: 'Status', component: Check },
{ name: 'check-circle', pascalName: 'CheckCircle2', category: 'Status', component: CheckCircle2 },
{ name: 'warning', pascalName: 'AlertTriangle', category: 'Status', component: AlertTriangle },
{ name: 'alert', pascalName: 'AlertCircle', category: 'Status', component: AlertCircle },
{ name: 'info', pascalName: 'Info', category: 'Status', component: Info },
{ name: 'close', pascalName: 'X', category: 'Status', component: X },
{ name: 'shield', pascalName: 'Shield', category: 'Status', component: Shield },
{ name: 'help', pascalName: 'HelpCircle', category: 'Status', component: HelpCircle },
{ name: 'eye', pascalName: 'Eye', category: 'Status', component: Eye },
]
/* The curated showcase set — components come straight from the shared
registry in Icon.tsx, so there is no second icon list to drift. */
const ICONS: IconItem[] = iconRegistry
const CATEGORIES = ['All', 'Navigation', 'Interface', 'System', 'Status'] as const
type Category = (typeof CATEGORIES)[number]
@@ -113,6 +19,7 @@ export function IconMatrix() {
const [search, setSearch] = useState('')
const [size, setSize] = useState<number>(20)
const [copiedName, setCopiedName] = useState<string | null>(null)
const { schedule } = useTimeout()
const filteredIcons = useMemo(() => {
return ICONS.filter(icon => {
@@ -128,7 +35,7 @@ export function IconMatrix() {
const code = `<${icon.pascalName} size={${size}} />`
navigator.clipboard?.writeText(code)
setCopiedName(icon.name)
setTimeout(() => setCopiedName(null), 2000)
schedule(() => setCopiedName(null), 2000)
}
return (
@@ -140,6 +47,7 @@ export function IconMatrix() {
<input
type="text"
placeholder="Search icons (e.g. arrow, check, sparkles)..."
aria-label="Search icons"
value={search}
onChange={e => setSearch(e.target.value)}
className="icon-search-input"