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

+170
View File
@@ -0,0 +1,170 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { useState } from 'react'
import { describe, expect, it } from 'vitest'
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../src/components/ui/Dialog'
import { Sheet, SheetClose, SheetContent, SheetTitle } from '../src/components/ui/Sheet'
describe('Dialog (Overlay primitive)', () => {
it('renders a modal dialog with the panel focused when open', () => {
render(
<Dialog defaultOpen>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
</DialogContent>
</Dialog>,
)
const panel = screen.getByRole('dialog')
expect(panel).toHaveClass('dialog-panel')
expect(panel).toHaveAttribute('aria-modal', 'true')
expect(panel).toHaveFocus()
})
it('wires aria-labelledby only when a title is rendered', () => {
const { rerender } = render(
<Dialog defaultOpen>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
</DialogContent>
</Dialog>,
)
const panel = screen.getByRole('dialog')
const labelledBy = panel.getAttribute('aria-labelledby')
expect(labelledBy).toBeTruthy()
expect(document.getElementById(labelledBy!)).toHaveTextContent('Deploy')
// No DialogTitle → a dangling IDREF would announce nothing.
rerender(
<Dialog defaultOpen>
<DialogContent>
<div>body only</div>
</DialogContent>
</Dialog>,
)
expect(screen.getByRole('dialog')).not.toHaveAttribute('aria-labelledby')
})
it('wires aria-describedby only when a description is rendered', () => {
render(
<Dialog defaultOpen>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
<DialogDescription>To production</DialogDescription>
</DialogContent>
</Dialog>,
)
const describedBy = screen.getByRole('dialog').getAttribute('aria-describedby')
expect(describedBy).toBeTruthy()
expect(document.getElementById(describedBy!)).toHaveTextContent('To production')
})
it('restores focus to the trigger after closing', () => {
function Controlled() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
</DialogContent>
</Dialog>
</>
)
}
render(<Controlled />)
const trigger = screen.getByRole('button', { name: 'Open' })
// jsdom does not move focus on synthetic clicks — focus the trigger like a real user would.
trigger.focus()
fireEvent.click(trigger)
expect(screen.getByRole('dialog')).toHaveFocus()
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByRole('dialog')).toBeNull()
expect(trigger).toHaveFocus()
})
it('closes on Escape when open', () => {
render(
<Dialog defaultOpen>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
</DialogContent>
</Dialog>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByRole('dialog')).toBeNull()
})
it('closes on backdrop click', () => {
render(
<Dialog defaultOpen>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
</DialogContent>
</Dialog>,
)
fireEvent.click(screen.getByRole('dialog').parentElement!)
expect(screen.queryByRole('dialog')).toBeNull()
})
it('locks body scroll while open and restores it on close', () => {
const { rerender } = render(
<Dialog defaultOpen>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
</DialogContent>
</Dialog>,
)
expect(document.body.style.overflow).toBe('hidden')
rerender(
<Dialog open={false}>
<DialogContent>
<DialogTitle>Deploy</DialogTitle>
</DialogContent>
</Dialog>,
)
expect(document.body.style.overflow).not.toBe('hidden')
})
})
describe('Sheet (Overlay skin)', () => {
it('renders with sheet classes and its own close label', () => {
render(
<Sheet defaultOpen>
<SheetContent>
<SheetTitle>Filters</SheetTitle>
<SheetClose>Done</SheetClose>
</SheetContent>
</Sheet>,
)
const panel = screen.getByRole('dialog')
expect(panel).toHaveClass('sheet-panel')
expect(screen.getByRole('button', { name: 'Close sheet' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Done' }))
expect(screen.queryByRole('dialog')).toBeNull()
})
it('keeps Tab focus inside the panel (wrap from last to first)', () => {
render(
<Sheet defaultOpen>
<SheetContent>
<SheetTitle>Filters</SheetTitle>
<button>Only control</button>
</SheetContent>
</Sheet>,
)
const panel = screen.getByRole('dialog')
const closeBtn = screen.getByRole('button', { name: 'Close sheet' })
const last = screen.getByRole('button', { name: 'Only control' })
// Tab from the last control wraps to the first.
last.focus()
fireEvent.keyDown(panel, { key: 'Tab' })
expect(closeBtn).toHaveFocus()
// Shift+Tab from the first control wraps to the last.
fireEvent.keyDown(panel, { key: 'Tab', shiftKey: true })
expect(last).toHaveFocus()
})
})