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
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user