@tollerud/ui 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md ADDED
@@ -0,0 +1,452 @@
1
+ ---
2
+ name: tollerud-ui
3
+ description: Use the @tollerud/ui design system correctly — components, props, Tailwind tokens, aesthetic rules, and known gotchas (Server Component imports, Button/Link composition). Trigger whenever a project imports from @tollerud/ui or @tollerud/footer, or when building UI that should match the Tollerud noir aesthetic.
4
+ ---
5
+
6
+ # @tollerud/ui — Design System Skill
7
+
8
+ Dark, monochrome + single yellow-accent design system ("noir" aesthetic). This skill documents the package's **actual current exports** (verified against `components/index.ts` in the source repo) — not aspirational docs. If you see a component referenced elsewhere that isn't listed below, it does not exist yet; don't import it.
9
+
10
+ ---
11
+
12
+ ## Install & setup
13
+
14
+ ```bash
15
+ npm install @tollerud/ui clsx tailwind-merge tailwindcss
16
+ # Optional — only if using NoirGlowBackground
17
+ npm install @paper-design/shaders-react
18
+ ```
19
+
20
+ Apply the Tailwind preset — without it, `text-tollerud-yellow`, `bg-tollerud-noir-900`, etc. won't resolve:
21
+
22
+ ```ts
23
+ // tailwind.config.ts
24
+ import type { Config } from 'tailwindcss'
25
+ import tollerudPreset from '@tollerud/ui/preset'
26
+
27
+ const config: Config = {
28
+ presets: [tollerudPreset],
29
+ content: ['./src/**/*.{ts,tsx}'],
30
+ }
31
+ export default config
32
+ ```
33
+
34
+ Import base styles/tokens from `@tollerud/ui/globals.css` (or `@tollerud/ui/tokens.css`) in your root layout / `globals.css`, alongside Tailwind's own layers:
35
+ ```css
36
+ @import "tailwindcss/preflight";
37
+ @import "tailwindcss/utilities";
38
+ @import "@tollerud/ui/globals.css";
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Critical gotchas (read before writing code)
44
+
45
+ ### 1. Server Components — just import normally (≥ 1.0.8)
46
+ `@tollerud/ui` ships as a single bundled file marked `'use client'`. As of **v1.0.8**, importing anything from it — components, hooks, or plain helpers like `buttonVariants` and `cn` — works fine from a Server Component file; the import itself doesn't force your file to become a Client Component, since you're just pulling in already-client-bundled code or plain functions.
47
+ - **If you're on `< 1.0.8`, upgrade first** — older versions crash on any import from a Server Component (the bundle wasn't marked `'use client'`, despite containing hook-based components).
48
+
49
+ ### 2. `<Button>` only renders a native `<button>` — use `asChild` for links (≥ 1.0.7)
50
+ `Button` extends `ButtonHTMLAttributes<HTMLButtonElement>` and has no `href`. **Never nest `<a>` inside `<button>` or vice versa** — it's invalid HTML and breaks accessibility. Two ways to style a `<Link>`/`<a>` like a Button:
51
+
52
+ ```tsx
53
+ // Option A — asChild (Radix Slot merges Button's classes/props onto the child)
54
+ import { Button } from '@tollerud/ui'
55
+ import Link from 'next/link'
56
+
57
+ <Button asChild variant="primary">
58
+ <Link href="/deploy">Deploy</Link>
59
+ </Button>
60
+ ```
61
+
62
+ ```tsx
63
+ // Option B — buttonVariants() when wrapping is awkward
64
+ import { buttonVariants } from '@tollerud/ui'
65
+ import Link from 'next/link'
66
+
67
+ <Link href="/deploy" className={buttonVariants({ variant: 'primary', size: 'md' })}>
68
+ Deploy
69
+ </Link>
70
+ ```
71
+
72
+ For a real `<button>` (form submit, logout, toggle, dialog/menu trigger), just use `<Button>` directly — no `asChild` needed.
73
+
74
+ ### 3. `cn` is exported — use it, don't reimplement
75
+ `@tollerud/ui` exports `cn` (clsx + tailwind-merge). Use it for conditional/merged class names instead of template strings or writing your own helper.
76
+
77
+ ---
78
+
79
+ ## Aesthetic rules (never violate)
80
+
81
+ - **Dark surfaces only.** Page background `#0A0A0A` / `bg-tollerud-noir-950`. Never white or light-gray backgrounds.
82
+ - **Yellow is for meaning, not decoration** — CTAs, focus rings, active states, key data points (`text-tollerud-yellow`, `#FFFF00`).
83
+ - **Never put yellow text on white** — contrast ratio is ~1.7:1, fails WCAG.
84
+ - **Borders over shadows.** Use `border-tollerud-noir-600` / `border-tollerud-noir-700` for separation; reserve shadows/glow for overlays (drawers, popovers, dialogs).
85
+ - **Strict monochrome + single accent.** No blues, greens, purples, or brand gradients — yellow is the only chromatic color (status colors like success/error/info exist as semantic exceptions inside `Badge`, `IncidentCard`, `Alert`, etc.).
86
+
87
+ ## Color tokens
88
+
89
+ | Token | Value | Use |
90
+ |---|---|---|
91
+ | `tollerud-yellow` | `#FFFF00` | Accent, CTA, focus ring, key data |
92
+ | `tollerud-yellow-warm` | `#E8D500` | Secondary yellow, gradients, warm states |
93
+ | `tollerud-noir-950` | `#0A0A0A` | Page background |
94
+ | `tollerud-noir-900` | `#111111` | Card / surface |
95
+ | `tollerud-noir-800` | `#1A1A1A` | Elevated surface |
96
+ | `tollerud-noir-700` | `#222222` | Hover states |
97
+ | `tollerud-noir-600` | `#333333` | Borders |
98
+ | `tollerud-text-primary` | `#F5F5F5` | Body text |
99
+ | `tollerud-text-secondary` | `#AAAAAA` | Secondary text / labels |
100
+ | `tollerud-text-muted` | `#666666` | Placeholders, hints |
101
+
102
+ Standard focus ring (apply to every interactive element): `focus-visible:outline-2 focus-visible:outline-tollerud-yellow focus-visible:outline-offset-2`
103
+
104
+ ---
105
+
106
+ ## Component catalog (verified against actual exports)
107
+
108
+ Import everything as named exports from `@tollerud/ui`.
109
+
110
+ ### Core / forms
111
+
112
+ ```tsx
113
+ import {
114
+ Button, buttonVariants, cn,
115
+ Card, Badge, StatusDot, Kbd,
116
+ Input, Textarea, Select, Checkbox, Switch, RadioGroup, Radio,
117
+ PasswordInput, Combobox, TagInput, Slider, FormRow,
118
+ Container, CodeBlock, StatCard, ActionRow, CommandMenu,
119
+ } from '@tollerud/ui'
120
+ ```
121
+
122
+ **Button** — `variant`: `primary` · `secondary` · `ghost` · `destructive` · `terminal`. `size`: `sm` · `md` · `lg`. `asChild?: boolean`.
123
+ ```tsx
124
+ <Button variant="primary" size="md">Deploy</Button>
125
+ <Button variant="destructive">Delete host</Button>
126
+ <Button variant="terminal" size="sm">start_building</Button>
127
+ ```
128
+
129
+ **Card** — `accent?: boolean`, `density?: 'comfortable' | 'compact'`. Plain `<div>` wrapper — safe to nest in `<Link>`.
130
+ ```tsx
131
+ <Card accent>Highlighted with yellow border</Card>
132
+ ```
133
+
134
+ **Badge** — `variant`: `default` · `accent` · `success` · `error` · `info` · `warning`.
135
+ ```tsx
136
+ <Badge variant="success">Online</Badge>
137
+ ```
138
+
139
+ **StatusDot** — `status`: `online` · `offline` · `warning` · `idle` (exported as `Status`); `label?`, `noPulse?`.
140
+ ```tsx
141
+ <StatusDot status="online" label="SSH Connected" />
142
+ ```
143
+
144
+ **Input / Textarea / Select / Checkbox / Switch / RadioGroup**
145
+ ```tsx
146
+ <Input label="Server Name" placeholder="e.g. emma.tollerud.no" error={errors.name} />
147
+ <Textarea label="Notes" rows={4} error={errors.notes} />
148
+ <Select label="Region" options={[{ value: 'eu', label: 'EU' }]} value={region} onChange={setRegion} />
149
+ <Checkbox label="Enable backups" checked={enabled} onChange={...} />
150
+ <Switch label="Dark mode" defaultChecked />
151
+ <RadioGroup label="Target" error={error}>
152
+ <Radio value="staging" label="Staging" name="target" />
153
+ <Radio value="production" label="Production" name="target" />
154
+ </RadioGroup>
155
+ ```
156
+ All form primitives require a `label` (renders an actual `<label>` for a11y) and accept `error?: string`.
157
+
158
+ **Kbd** — keyboard shortcut chip. `keys: string | string[]`, `size?: 'sm' | 'md'`.
159
+ ```tsx
160
+ <Kbd keys={["⌘", "⇧", "S"]} size="sm" />
161
+ ```
162
+
163
+ **ActionRow** / **CommandMenu** — Raycast-style command palette.
164
+ ```tsx
165
+ const [open, setOpen] = useState(false)
166
+ <CommandMenu
167
+ open={open}
168
+ onOpenChange={setOpen}
169
+ groups={[{ label: 'Servers', items: [
170
+ { id: 'emma', label: 'emma.tollerud.no', description: 'SSH · uptime 14d', onSelect: () => {} },
171
+ ]}]}
172
+ toggleShortcut="k" // built-in ⌘K / Ctrl+K listener
173
+ />
174
+ ```
175
+
176
+ **StatCard** — `label`, `value`, `change?: { value: string; direction: 'up' | 'down' }`, `accent?`.
177
+ ```tsx
178
+ <StatCard label="Active Sessions" value={42} change={{ value: '+12%', direction: 'up' }} />
179
+ ```
180
+
181
+ **CodeBlock** — `code?`, `promptPrefix?`, `showCopy?`. Renders a `<pre>`.
182
+ ```tsx
183
+ <CodeBlock promptPrefix showCopy code={`systemctl status tollerud-agent`} />
184
+ ```
185
+
186
+ **Container** — `as?: 'div' | 'section' | 'article' | 'main' | 'header' | 'footer'`, capped width + padding.
187
+
188
+ **PasswordInput** — same API as `Input` (label, error, id, …) plus built-in show/hide toggle.
189
+ ```tsx
190
+ <PasswordInput label="Password" placeholder="Enter password" error={errors.password} />
191
+ ```
192
+
193
+ **Combobox** — searchable single-select. `options: { value, label, disabled? }[]`, `value?`, `onChange?`, `placeholder?`, `filter?`, `label?`, `error?`.
194
+ ```tsx
195
+ <Combobox label="Connect to host" value={host} onChange={setHost} options={hostOptions} />
196
+ ```
197
+
198
+ **TagInput** — chip-style multi-value input. `value?: string[]`, `onChange?`, `max?`, `placeholder?`, `label?`, `error?`. Enter/comma to add, Backspace to remove last.
199
+ ```tsx
200
+ <TagInput label="Tags" value={tags} onChange={setTags} placeholder="Add tag…" max={10} />
201
+ ```
202
+
203
+ **Slider** — native range input styled with yellow thumb. `label?`, `showValue?`, `onChange?: (value: number) => void`, plus all native `<input type="range">` props.
204
+ ```tsx
205
+ <Slider label="Alert threshold" showValue value={threshold} onChange={setThreshold} min={0} max={100} />
206
+ ```
207
+
208
+ **FormRow** — accessible field wrapper. `label?`, `description?`, `error?`, `required?`, `htmlFor?`. Wires `aria-describedby` automatically.
209
+ ```tsx
210
+ <FormRow label="Hostname" htmlFor="hostname" description="Unique within your network." required error={errors.hostname}>
211
+ <Input id="hostname" placeholder="e.g. embla" />
212
+ </FormRow>
213
+ ```
214
+
215
+ ### Navigation & layout primitives
216
+
217
+ ```tsx
218
+ import {
219
+ Divider, Pill, Avatar, AvatarGroup,
220
+ Breadcrumb, Pagination, Segmented, Stepper,
221
+ Panel, Meter, Accordion, AccordionItem, AccordionTrigger, AccordionContent,
222
+ DatePicker, FileUpload, PricingCard,
223
+ } from '@tollerud/ui'
224
+ ```
225
+
226
+ **Divider** — `orientation?: 'horizontal' | 'vertical'`, `label?: ReactNode`.
227
+ ```tsx
228
+ <Divider />
229
+ <Divider label="or" />
230
+ <Divider orientation="vertical" className="h-6" />
231
+ ```
232
+
233
+ **Pill** — `variant?: 'outline' | 'solid' | 'accent'`.
234
+ ```tsx
235
+ <Pill variant="accent">production</Pill>
236
+ ```
237
+
238
+ **Avatar / AvatarGroup** — `src?`, `name?` (derives initials), `fallback?`, `size?: 'sm' | 'md' | 'lg'`. `AvatarGroup` takes `max?` and shows a +N chip.
239
+ ```tsx
240
+ <Avatar name="Mathias Tollerud" size="md" />
241
+ <AvatarGroup max={3}><Avatar name="Emma" /><Avatar name="Iris" /></AvatarGroup>
242
+ ```
243
+
244
+ **Breadcrumb** — `items: { label, href?, onClick? }[]`, `separator?`.
245
+ ```tsx
246
+ <Breadcrumb items={[{ label: 'Servers', href: '/servers' }, { label: 'Embla' }]} />
247
+ ```
248
+
249
+ **Pagination** — `page` (1-indexed), `pageCount`, `onChange`, `siblingCount?`.
250
+ ```tsx
251
+ <Pagination page={page} pageCount={20} onChange={setPage} />
252
+ ```
253
+
254
+ **Segmented** — `options: { value, label, disabled? }[]`, `value`, `onChange`, `size?: 'sm' | 'md'`.
255
+ ```tsx
256
+ <Segmented value={view} onChange={setView} options={[{ value: 'grid', label: 'Grid' }, { value: 'list', label: 'List' }]} />
257
+ ```
258
+
259
+ **Stepper** — `steps: { label, description? }[]`, `current` (0-indexed), `orientation?: 'horizontal' | 'vertical'`.
260
+ ```tsx
261
+ <Stepper steps={onboardingSteps} current={1} orientation="vertical" />
262
+ ```
263
+
264
+ **Panel** — `title?`, `description?`, `actions?: ReactNode` (renders in header), `children` (body with padding).
265
+ ```tsx
266
+ <Panel title="Overview" description="Live metrics" actions={<Button size="sm">Refresh</Button>}>…</Panel>
267
+ ```
268
+
269
+ **Meter** — `value`, `max?` (default 100), `label?`, `showValue?`, `tone?: 'default' | 'success' | 'warning' | 'error'`.
270
+ ```tsx
271
+ <Meter value={72} label="RAM" showValue tone="warning" />
272
+ ```
273
+
274
+ **Accordion** — compound. `multiple?: boolean`, `defaultOpen?: string | string[]`. Items use `value` prop to identify.
275
+ ```tsx
276
+ <Accordion defaultOpen="faq-1">
277
+ <AccordionItem value="faq-1">
278
+ <AccordionTrigger>What is Tia?</AccordionTrigger>
279
+ <AccordionContent>An infrastructure assistant for homelabs.</AccordionContent>
280
+ </AccordionItem>
281
+ </Accordion>
282
+ ```
283
+
284
+ **DatePicker** — `value?: Date | null`, `onChange?`, `label?`, `error?`, `placeholder?`, `formatDate?`.
285
+ ```tsx
286
+ <DatePicker label="Schedule deployment" value={date} onChange={setDate} />
287
+ ```
288
+
289
+ **FileUpload** — `accept?`, `multiple?`, `onFilesChange?`, `label?`, `description?`, `error?`.
290
+ ```tsx
291
+ <FileUpload label="Upload config" accept=".yaml,.json" onFilesChange={handleFiles} />
292
+ ```
293
+
294
+ **PricingCard** — `name`, `price`, `period?`, `description?`, `features?: ReactNode[]`, `ctaLabel?`, `onCtaClick?`, `featured?`, `badge?`.
295
+ ```tsx
296
+ <PricingCard name="Pro" price="$9" period="/month" features={['Unlimited servers']} featured badge="Most popular" />
297
+ ```
298
+
299
+ ### Overlays (Radix-based, all need `'use client'` boundary in the *consumer* component)
300
+
301
+ ```tsx
302
+ import {
303
+ Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, DialogClose,
304
+ Tooltip, TooltipTrigger, TooltipContent, TooltipProvider,
305
+ Tabs, TabsList, TabsTrigger, TabsContent,
306
+ DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel,
307
+ Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetClose,
308
+ Toaster,
309
+ } from '@tollerud/ui'
310
+ ```
311
+ - `Dialog` / `Sheet` / `DropdownMenu` follow the standard shadcn/Radix composition pattern — `Trigger` wraps the activating element with `asChild`. `Sheet` takes a `side?: 'left' | 'right'`.
312
+ - `Tooltip` requires a `<TooltipProvider>` ancestor.
313
+ - `Toaster` is the toast renderer (Sonner-based) — mount it once near the app root.
314
+
315
+ ### Empty states & loading
316
+
317
+ ```tsx
318
+ import { Empty, EmptyHeader, EmptyIcon, EmptyTitle, EmptyDescription, EmptyContent, Skeleton, Progress } from '@tollerud/ui'
319
+
320
+ <Empty>
321
+ <EmptyHeader>
322
+ <EmptyIcon>{/* icon */}</EmptyIcon>
323
+ <EmptyTitle>No hosts connected</EmptyTitle>
324
+ <EmptyDescription>Connect your first machine and Tia will start watching it.</EmptyDescription>
325
+ </EmptyHeader>
326
+ <EmptyContent><Button variant="primary" size="sm">Connect a host</Button></EmptyContent>
327
+ </Empty>
328
+ ```
329
+
330
+ ### Visual / decorative
331
+
332
+ ```tsx
333
+ import { GlowCard, NoirGlowBackground, BentoDashboard } from '@tollerud/ui'
334
+ ```
335
+ - **GlowCard** — `children`, `className?`, `glowColor?`, `intensity?: number`. Mouse-tracked glow card.
336
+ - **NoirGlowBackground** — animated WebGL shader background (needs `@paper-design/shaders-react`). `shape?: 'corners' | 'wave' | 'dots' | 'truchet' | 'ripple' | 'blob' | 'sphere'`, `intensity?: 'subtle' | 'medium' | 'loud'`, `speed?: 'still' | 'slow' | 'medium' | 'fast'`, `grain?: 'none' | 'soft' | 'high'`, `colors?: string[]`, `forceCssFallback?: boolean`.
337
+ - **BentoDashboard** — composed dashboard shell taking arrays of `HostCardProps`, `StatCardProps`, `ServiceHealthCardProps`, incidents, `BackupJob[]`.
338
+
339
+ ### Data & tables
340
+
341
+ ```tsx
342
+ import { DataTable } from '@tollerud/ui'
343
+
344
+ <DataTable
345
+ columns={[
346
+ { key: 'hostname', label: 'Host', sortable: true },
347
+ { key: 'status', label: 'Status', render: (_v, row) => <Badge variant={row.status === 'online' ? 'success' : 'error'}>{row.status}</Badge> },
348
+ ]}
349
+ data={hosts}
350
+ rowKey="id"
351
+ onRowClick={(row) => {}}
352
+ emptyMessage="No hosts found"
353
+ />
354
+ ```
355
+ Note the prop is `data`/`columns`/`label` (not `rows`/`header` — older docs may say otherwise).
356
+
357
+ ### Infra / homelab set
358
+
359
+ ```tsx
360
+ import {
361
+ HostCard, ServiceHealthCard, DockerStackCard, IncidentCard, ApprovalCard,
362
+ ActionDiff, LogViewer, AlertInbox, Timeline, RollbackPlan, BackupStatusPanel,
363
+ } from '@tollerud/ui'
364
+ import type { IncidentSeverity } from '@tollerud/ui'
365
+ ```
366
+
367
+ ```tsx
368
+ <HostCard hostname="emma" ip="10.0.10.10" status="online" cpu="23%" memory="6.2/16 GB" disk="45%" uptime="14d" containers={4} />
369
+ <ServiceHealthCard service="emma.tollerud.no" status="online" uptime="14d 3h" responseTime="23ms" />
370
+ <DockerStackCard name="hermes" services={[{ name: 'web', status: 'online' }]} composePath="compose.yml" />
371
+ <IncidentCard title="High CPU" severity="high" timestamp="2026-05-26 14:32" description="CPU at 92% for 5 min" service="emma" />
372
+ <ApprovalCard action="restart_container" description="Restart emma:hermes" state="pending" onApprove={() => {}} onReject={() => {}} />
373
+ <ActionDiff label="docker-compose.yml" lines={[{ text: 'image: nginx:1.27', type: 'add', newLine: 12 }]} />
374
+ <LogViewer lines={[{ text: 'Health check passed', level: 'info', timestamp: '14:32:01', source: 'hermes' }]} follow searchable showLineNumbers height="300px" />
375
+ <AlertInbox alerts={[{ id: '1', title: 'emma high CPU', severity: 'high', timestamp: '14:32', acknowledged: false }]} onAcknowledge={(id) => {}} />
376
+ <Timeline items={[{ id: '1', time: '14:32', title: 'Deploy started', status: 'online' }]} active loading={false} />
377
+ <RollbackPlan name="hermes-v2.1-rollback" steps={[{ id: '1', label: 'Stop container', status: 'success' }]} executing />
378
+ <BackupStatusPanel jobs={[{ name: 'nightly-db', status: 'online', lastRun: '02:00', nextRun: 'tomorrow 02:00', size: '4.2 GB' }]} totalSize="120 GB" />
379
+ ```
380
+
381
+ `IncidentSeverity` = `critical | high | medium | low | info` · `Status` (StatusDot/HostCard/etc) = `online | offline | warning | idle` · `LogLevel` = `debug | info | warn | error | trace` · `RollbackStepStatus` = `pending | running | success | failed | skipped` · `ApprovalState` = `pending | approved | rejected`.
382
+
383
+ ### Footer & branding
384
+
385
+ ```tsx
386
+ import { Footer } from '@tollerud/ui'
387
+ import logo from '@tollerud/ui/tollerud-logo.svg'
388
+
389
+ <Footer layout="responsive" accent labels={{ tollerudProject: 'A Tollerud Project' }} />
390
+ ```
391
+ The monogram must always sit left of the project name with `gap-2`. Never show the name without the monogram, or the monogram alone, in nav contexts.
392
+
393
+ ```tsx
394
+ <div className="flex items-center gap-2 shrink-0">
395
+ <img src={logo} alt="Tollerud" className="h-5 w-auto" />
396
+ <span className="font-semibold text-sm text-white">Project Name</span>
397
+ </div>
398
+ ```
399
+ Monogram sizing: top bar/sidebar expanded → `h-5`, collapsed → `h-6`, footer → `h-4` (handled automatically by `<Footer />`).
400
+
401
+ ---
402
+
403
+ ## Layout utility classes
404
+
405
+ ```html
406
+ <nav class="tollerud-glass fixed top-0 inset-x-0 z-50 h-14 flex items-center px-6">…</nav>
407
+ <section class="tollerud-grid-bg">…</section>
408
+ <h1 class="tollerud-display text-[70px]">Dark. Monochrome.</h1>
409
+ <h2 class="tollerud-display--secondary text-[40px]">Yellow where it counts</h2>
410
+ <div data-density="compact">…dense tables / forms…</div>
411
+ ```
412
+
413
+ Shadow scale: `--shadow-sm` `--shadow-md` `--shadow-lg` `--shadow-xl` `--shadow-glow`. Drawers/Sheets use `--shadow-xl`; popovers/tooltips `--shadow-lg`. Borders are the default separator — only reach for shadows on overlays.
414
+
415
+ ---
416
+
417
+ ## Copy & voice
418
+
419
+ - Action-first labels: "Deploy", "View Logs", "Restart" — not "Click here to initiate deployment"
420
+ - Terminal-style CTAs for technical actions: `❯ deploy --env production`, `$ init`
421
+ - Name the cause in errors: "Connection to emma.tollerud.no timed out" — not "Something went wrong"
422
+ - No exclamation marks or corporate filler ("Oops!", "Great!", "Please try again later")
423
+
424
+ ## Accessibility checklist
425
+
426
+ - Every interactive element gets a visible focus ring (see token section above, or class `.tollerud-focus-ring`)
427
+ - Icon-only buttons need `aria-label`
428
+ - Always pass `label` to `Input` / `Select` / `Textarea` / `Checkbox` / `Switch` / `Radio` — never rely on placeholder-as-label
429
+ - Error text uses `role="alert"` or `aria-live="polite"`
430
+ - Never convey state by color alone — pair `StatusDot` / `Badge` color with text/icon
431
+ - Respect `prefers-reduced-motion: reduce` — disable shimmer/pulse/shader animations
432
+
433
+ ## What NOT to do
434
+
435
+ | Don't | Why |
436
+ |---|---|
437
+ | Use light/white backgrounds | System is dark-only |
438
+ | Put yellow text on white | Fails contrast at ~1.7:1 |
439
+ | Recolor or add glow to the monogram | Yellow-on-dark branding is non-negotiable; glow is for interactive UI only |
440
+ | Introduce non-system chromatic colors (blue, green, purple) for decoration | Only the yellow accent + monochrome grays (status semantics are the lone exception) |
441
+ | Nest `<a>`/`<Link>` inside `<Button>` (or vice versa) | Invalid HTML — use `asChild` or `buttonVariants()` instead |
442
+ | Import a component name you saw in older docs without checking it exists | Some legacy docs list aspirational or since-shipped components. As of **1.0.9** all 19 previously "missing" components (`Divider`, `Pill`, `Avatar`, `AvatarGroup`, `Breadcrumb`, `Pagination`, `Segmented`, `Stepper`, `Panel`, `Meter`, `FormRow`, `Accordion`, `Slider`, `PasswordInput`, `Combobox`, `DatePicker`, `FileUpload`, `TagInput`, `PricingCard`) are exported. Components still **not** in the package: charts, marketing blocks (`HeroBlock`, `FeatureCard`, `CTABand`), `Drawer`, `EmptyState` (use `Empty`), `Toast` (use `Toaster` + `sonner`'s `toast()`) |
443
+
444
+ ---
445
+
446
+ ## Version notes
447
+
448
+ - **`asChild` / `buttonVariants` require `@tollerud/ui >= 1.0.7`**
449
+ - **Server Component import safety requires `@tollerud/ui >= 1.0.8`** (earlier versions crash when imported into a Server Component file — the bundle wasn't marked `'use client'`)
450
+ - **19 new components (`Divider`, `Pill`, `Avatar`/`AvatarGroup`, `Breadcrumb`, `Pagination`, `Segmented`, `Stepper`, `Panel`, `Meter`, `FormRow`, `Accordion`, `Slider`, `PasswordInput`, `Combobox`, `DatePicker`, `FileUpload`, `TagInput`, `PricingCard`) require `>= 1.0.9`**
451
+ - **`Combobox` + `DatePicker` close on window resize (≥ 1.1.0)** — earlier versions left the popover open and misaligned after viewport changes
452
+ - Always pin to the latest patch and check `CHANGELOG.md` in the design-system repo for breaking changes (e.g. the 1.0.5 yellow token rename: `tollerud-yellow-bright` → `tollerud-yellow`, old `tollerud-yellow` `#E8D500` → `tollerud-yellow-warm`)