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
@@ -0,0 +1,95 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const GUARD = join(process.cwd(), 'scripts', 'check-tokens.mjs')
|
||||
|
||||
function runIn(files) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'token-guard-'))
|
||||
mkdirSync(join(dir, 'src'))
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
writeFileSync(join(dir, 'src', name), content)
|
||||
}
|
||||
try {
|
||||
execFileSync(process.execPath, [GUARD, join(dir, 'src')], { encoding: 'utf8' })
|
||||
return { code: 0, output: '' }
|
||||
} catch (err) {
|
||||
return { code: err.status, output: err.stdout + String(err.stderr ?? '') }
|
||||
}
|
||||
}
|
||||
|
||||
const TOKENS = ':root { --primary: lab(50% 0 0); --space-2: 0.5rem; }\n'
|
||||
|
||||
describe('check-tokens guard', () => {
|
||||
it('passes a clean file', () => {
|
||||
const result = runIn({
|
||||
'tokens.css': TOKENS,
|
||||
'a.tsx': `export const A = () => <div style={{ color: 'var(--primary)', padding: 'var(--space-2)' }} />\n`,
|
||||
})
|
||||
expect(result.code).toBe(0)
|
||||
})
|
||||
|
||||
it('flags hex, named colors and hsl()/oklch() inside style={{ }}', () => {
|
||||
const result = runIn({
|
||||
'tokens.css': TOKENS,
|
||||
'a.tsx': `export const A = () => (
|
||||
<div style={{
|
||||
color: '#fff',
|
||||
background: 'oklch(70% 0.1 200)',
|
||||
borderColor: 'red',
|
||||
}} />
|
||||
)\n`,
|
||||
})
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.output).toMatch(/hex color #fff/)
|
||||
expect(result.output).toMatch(/oklch\(\) color/)
|
||||
expect(result.output).toMatch(/named color "red"/)
|
||||
})
|
||||
|
||||
it('allows color-mix over tokens and neutral rgba shines', () => {
|
||||
const result = runIn({
|
||||
'tokens.css': TOKENS,
|
||||
'a.tsx': `export const A = () => (
|
||||
<div style={{
|
||||
background: 'color-mix(in srgb, var(--primary) 12%, transparent)',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.2)',
|
||||
}} />
|
||||
)\n`,
|
||||
})
|
||||
expect(result.code).toBe(0)
|
||||
})
|
||||
|
||||
it('reads values split across lines in css', () => {
|
||||
const result = runIn({
|
||||
'tokens.css': TOKENS,
|
||||
'b.css': `.b {\n background:\n linear-gradient(\n to right,\n crimson,\n navy\n );\n}\n`,
|
||||
})
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.output).toMatch(/named color "crimson"/)
|
||||
})
|
||||
|
||||
it('flags px font sizes in css and tsx', () => {
|
||||
const result = runIn({
|
||||
'tokens.css': TOKENS,
|
||||
'b.css': `.b { font-size: 14px; }\n`,
|
||||
'a.tsx': `export const A = () => <div style={{ fontSize: '14px' }} />\n`,
|
||||
})
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.output.match(/px font size/g)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('fails on var(--x) that no css file defines', () => {
|
||||
const result = runIn({
|
||||
'tokens.css': TOKENS,
|
||||
'a.tsx': `export const A = () => <div style={{ color: 'var(--priamry)' }} />\n`,
|
||||
})
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.output).toMatch(/var\(--priamry\) is never defined/)
|
||||
})
|
||||
|
||||
it('keeps the real repo clean', () => {
|
||||
expect(() => execFileSync(process.execPath, [GUARD], { stdio: 'pipe' })).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Icon, type IconName } from '../src/components/Icon'
|
||||
import { iconMap, iconRegistry } from '../src/components/icon-registry'
|
||||
|
||||
describe('icon registry (single source of truth)', () => {
|
||||
it('has unique names', () => {
|
||||
const names = iconRegistry.map(entry => entry.name)
|
||||
expect(new Set(names).size).toBe(names.length)
|
||||
})
|
||||
|
||||
it('maps every name (including aliases) to a real registry component', () => {
|
||||
const components = new Set(iconRegistry.map(entry => entry.component))
|
||||
for (const [name, component] of Object.entries(iconMap)) {
|
||||
expect(components.has(component), `icon "${name}" has no canonical twin in the registry`).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves every registry entry through iconMap', () => {
|
||||
for (const entry of iconRegistry) {
|
||||
expect(iconMap[entry.name]).toBe(entry.component)
|
||||
}
|
||||
})
|
||||
|
||||
it('renders a registry icon by name', () => {
|
||||
const { container } = render(<Icon name="sparkles" data-testid="sparkles" />)
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('throws on unknown names in dev instead of silently rendering a wrong glyph', () => {
|
||||
expect(() => render(<Icon name={'definitely-not-an-icon' as IconName} />)).toThrow(/Unknown icon name/)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
@@ -0,0 +1,50 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { useTimeout } from '../src/utils/useTimeout'
|
||||
|
||||
describe('useTimeout', () => {
|
||||
it('fires the callback after the delay', () => {
|
||||
vi.useFakeTimers()
|
||||
const { result } = renderHook(() => useTimeout())
|
||||
const fn = vi.fn()
|
||||
act(() => {
|
||||
result.current.schedule(fn, 1000)
|
||||
})
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000)
|
||||
})
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('clear() cancels pending timers', () => {
|
||||
vi.useFakeTimers()
|
||||
const { result } = renderHook(() => useTimeout())
|
||||
const fn = vi.fn()
|
||||
act(() => {
|
||||
result.current.schedule(fn, 1000)
|
||||
result.current.clear()
|
||||
})
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5000)
|
||||
})
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('clears pending timers on unmount (no setState-after-unmount leaks)', () => {
|
||||
vi.useFakeTimers()
|
||||
const { result, unmount } = renderHook(() => useTimeout())
|
||||
const fn = vi.fn()
|
||||
act(() => {
|
||||
result.current.schedule(fn, 1000)
|
||||
})
|
||||
unmount()
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5000)
|
||||
})
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user