@tollerud/ui 1.1.1 → 1.1.2
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/AGENTS.md +472 -0
- package/SKILL.md +452 -0
- package/package.json +4 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# Tollerud Design System — AI Agent Guide
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working in projects that use `@tollerud/ui`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Set up the Claude Code skill (do this first)
|
|
8
|
+
|
|
9
|
+
This package ships a maintained [`SKILL.md`](https://github.com/Tollerud/ui/blob/main/SKILL.md) with the verified component catalog, prop signatures, aesthetic rules, and known gotchas (Server Component imports, `Button`/`Link` composition, etc.). If you're Claude Code (or another agent that supports project skills) and this project doesn't already have it:
|
|
10
|
+
|
|
11
|
+
1. Fetch the raw file from `https://raw.githubusercontent.com/Tollerud/ui/main/SKILL.md`
|
|
12
|
+
2. Write it verbatim to `.claude/skills/tollerud-ui/SKILL.md` in this project
|
|
13
|
+
3. Use it as your source of truth for `@tollerud/ui` going forward — it reflects the package's *actual current exports*, which is more reliable than this file's component list (kept here mainly for human readers and non-Claude tools)
|
|
14
|
+
|
|
15
|
+
Re-sync it whenever you bump the `@tollerud/ui` version and notice the local copy looks stale relative to `CHANGELOG.md`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @tollerud/ui clsx tailwind-merge tailwindcss
|
|
23
|
+
# Optional — only if using NoirGlowBackground
|
|
24
|
+
npm install @paper-design/shaders-react
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For the footer alone (no full design system dependency):
|
|
28
|
+
```bash
|
|
29
|
+
npm install @tollerud/footer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Tailwind Setup
|
|
33
|
+
|
|
34
|
+
The design system ships a Tailwind preset that provides all tokens. **Always apply it** — without it, `text-tollerud-yellow`, `bg-tollerud-noir-900`, etc. will not resolve.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// tailwind.config.ts
|
|
38
|
+
import type { Config } from 'tailwindcss'
|
|
39
|
+
import tollerudPreset from '@tollerud/ui/preset'
|
|
40
|
+
|
|
41
|
+
const config: Config = {
|
|
42
|
+
presets: [tollerudPreset],
|
|
43
|
+
content: ['./src/**/*.{ts,tsx}'],
|
|
44
|
+
}
|
|
45
|
+
export default config
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Import the CSS in your root layout or `globals.css`:
|
|
49
|
+
```css
|
|
50
|
+
@import "tailwindcss/preflight";
|
|
51
|
+
@import "tailwindcss/utilities";
|
|
52
|
+
```
|
|
53
|
+
And import the design system tokens/base styles from `@tollerud/ui/globals.css` or copy them locally.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Aesthetic Rules
|
|
58
|
+
|
|
59
|
+
**Never violate these:**
|
|
60
|
+
|
|
61
|
+
- Dark surfaces only. Background: `#0A0A0A` (`bg-tollerud-noir-950`). Never white or light gray backgrounds.
|
|
62
|
+
- Yellow accent (`#FFFF00`, `text-tollerud-yellow`) is for CTAs, focus rings, active states, and key data points — not decoration.
|
|
63
|
+
- Never put yellow text on white. The ratio is 1.7:1 — it fails contrast.
|
|
64
|
+
- Borders are decorative thin lines (`border-tollerud-noir-600` or `border-tollerud-noir-700`). Use them freely; reach for shadows only for overlays.
|
|
65
|
+
- Monochrome everywhere except the single yellow accent. No blues, no greens, no brand gradients.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Color Tokens
|
|
70
|
+
|
|
71
|
+
| Token | Value | Use |
|
|
72
|
+
|-------|-------|-----|
|
|
73
|
+
| `tollerud-yellow` | `#FFFF00` | Accent, CTA, focus, key data |
|
|
74
|
+
| `tollerud-yellow-warm` | `#E8D500` | Secondary yellow, gradients, warm states |
|
|
75
|
+
| `tollerud-noir-950` | `#0A0A0A` | Page background |
|
|
76
|
+
| `tollerud-noir-900` | `#111111` | Card / surface |
|
|
77
|
+
| `tollerud-noir-800` | `#1A1A1A` | Elevated surface |
|
|
78
|
+
| `tollerud-noir-700` | `#222222` | Hover states |
|
|
79
|
+
| `tollerud-noir-600` | `#333333` | Borders |
|
|
80
|
+
| `tollerud-text-primary` | `#F5F5F5` | Body text |
|
|
81
|
+
| `tollerud-text-secondary` | `#AAAAAA` | Secondary / labels |
|
|
82
|
+
| `tollerud-text-muted` | `#666666` | Placeholders, hints |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Components
|
|
87
|
+
|
|
88
|
+
> **Full, verified catalog with props lives in [SKILL.md](SKILL.md)** — that file is checked against the actual `components/index.ts` exports and is the source of truth. The list below is a quick-reference subset.
|
|
89
|
+
|
|
90
|
+
All components import from `@tollerud/ui`. Use named imports.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Core / forms
|
|
94
|
+
import { Button, buttonVariants, cn, Card, Badge, Input, StatusDot, Kbd } from '@tollerud/ui'
|
|
95
|
+
import { CommandMenu, ActionRow, DataTable, LogViewer, Timeline, CodeBlock, StatCard, Container } from '@tollerud/ui'
|
|
96
|
+
import { Checkbox, Switch, RadioGroup, Radio, Select, Textarea } from '@tollerud/ui'
|
|
97
|
+
import { PasswordInput, Combobox, TagInput, Slider, FormRow } from '@tollerud/ui'
|
|
98
|
+
// Primitives & navigation (added in 1.0.9)
|
|
99
|
+
import { Divider, Pill, Avatar, AvatarGroup } from '@tollerud/ui'
|
|
100
|
+
import { Breadcrumb, Pagination, Segmented, Stepper } from '@tollerud/ui'
|
|
101
|
+
import { Panel, Meter, PricingCard } from '@tollerud/ui'
|
|
102
|
+
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@tollerud/ui'
|
|
103
|
+
import { DatePicker, FileUpload } from '@tollerud/ui'
|
|
104
|
+
// Overlays & feedback
|
|
105
|
+
import { Empty, EmptyHeader, EmptyIcon, EmptyTitle, EmptyDescription, EmptyContent } from '@tollerud/ui'
|
|
106
|
+
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from '@tollerud/ui'
|
|
107
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@tollerud/ui'
|
|
108
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@tollerud/ui'
|
|
109
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@tollerud/ui'
|
|
110
|
+
import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from '@tollerud/ui'
|
|
111
|
+
import { Skeleton, Progress, Toaster, GlowCard, NoirGlowBackground, BentoDashboard, Alert } from '@tollerud/ui'
|
|
112
|
+
// Infra / homelab set
|
|
113
|
+
import { HostCard, ServiceHealthCard, DockerStackCard, IncidentCard } from '@tollerud/ui'
|
|
114
|
+
import { ApprovalCard, ActionDiff, AlertInbox, RollbackPlan, BackupStatusPanel } from '@tollerud/ui'
|
|
115
|
+
// Footer
|
|
116
|
+
import { Footer } from '@tollerud/ui' // or: import { Footer } from '@tollerud/footer'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Button
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<Button variant="primary" size="md">Deploy</Button>
|
|
123
|
+
<Button variant="secondary">Cancel</Button>
|
|
124
|
+
<Button variant="ghost" size="sm">More</Button>
|
|
125
|
+
<Button variant="destructive">Delete host</Button>
|
|
126
|
+
<Button variant="terminal" size="sm">start_building</Button>
|
|
127
|
+
|
|
128
|
+
// Styling a <Link> as a button — Button only renders a native <button>,
|
|
129
|
+
// so use asChild (Radix Slot) or buttonVariants() instead of nesting <a> in <button>
|
|
130
|
+
<Button asChild variant="primary"><Link href="/deploy">Deploy</Link></Button>
|
|
131
|
+
<Link href="/deploy" className={buttonVariants({ variant: 'primary' })}>Deploy</Link>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Variants: `primary` · `secondary` · `ghost` · `destructive` · `terminal`
|
|
135
|
+
Sizes: `sm` · `md` · `lg`
|
|
136
|
+
`asChild` and `buttonVariants` require `@tollerud/ui >= 1.0.7`.
|
|
137
|
+
|
|
138
|
+
### Card
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<Card>Content</Card>
|
|
142
|
+
<Card accent>Highlighted with yellow border</Card>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Badge
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<Badge>Default</Badge>
|
|
149
|
+
<Badge variant="accent">New</Badge>
|
|
150
|
+
<Badge variant="success">Online</Badge>
|
|
151
|
+
<Badge variant="error">Down</Badge>
|
|
152
|
+
<Badge variant="info">Info</Badge>
|
|
153
|
+
<Badge variant="warning">Degraded</Badge>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### StatusDot
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
<StatusDot status="online" label="SSH Connected" />
|
|
160
|
+
<StatusDot status="warning" label="CPU 87%" />
|
|
161
|
+
<StatusDot status="offline" label="Unreachable" />
|
|
162
|
+
<StatusDot status="idle" label="Idle" />
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Input / Textarea / Select / Checkbox / Switch / RadioGroup
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<Input label="Server Name" placeholder="e.g. emma.tollerud.no" error={errors.name} />
|
|
169
|
+
<Textarea label="Notes" rows={4} error={errors.notes} />
|
|
170
|
+
<Select label="Region" options={[{ value: 'eu', label: 'EU' }]} value={region} onChange={setRegion} />
|
|
171
|
+
<Checkbox label="Enable backups" checked={enabled} onChange={...} />
|
|
172
|
+
<Switch label="Dark mode" defaultChecked />
|
|
173
|
+
<RadioGroup label="Target" error={error}>
|
|
174
|
+
<Radio value="staging" label="Staging" name="target" />
|
|
175
|
+
<Radio value="production" label="Production" name="target" />
|
|
176
|
+
</RadioGroup>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Kbd — Keyboard shortcut chip
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
<Kbd keys="⌘K" />
|
|
183
|
+
<Kbd keys={["⌘", "⇧", "S"]} size="sm" />
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### CommandMenu — Raycast-style command palette
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
const [open, setOpen] = useState(false)
|
|
190
|
+
|
|
191
|
+
<Button onClick={() => setOpen(true)}>Open</Button>
|
|
192
|
+
<CommandMenu
|
|
193
|
+
open={open}
|
|
194
|
+
onOpenChange={setOpen}
|
|
195
|
+
groups={[
|
|
196
|
+
{
|
|
197
|
+
label: 'Servers',
|
|
198
|
+
items: [
|
|
199
|
+
{ id: 'emma', label: 'emma.tollerud.no', description: 'SSH · uptime 14d', onSelect: () => {} },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
]}
|
|
203
|
+
toggleShortcut="k"
|
|
204
|
+
/>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Built-in `⌘K` / `Ctrl+K` listener, arrow navigation, Esc to close, search across all groups.
|
|
208
|
+
|
|
209
|
+
### StatCard
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
<StatCard label="Active Sessions" value={42} change={{ value: "+12%", direction: "up" }} />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### CodeBlock
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
<CodeBlock promptPrefix showCopy code={`systemctl status tollerud-agent`} />
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### DataTable
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<DataTable
|
|
225
|
+
columns={[
|
|
226
|
+
{ key: 'hostname', label: 'Host', sortable: true },
|
|
227
|
+
{ key: 'status', label: 'Status', render: (_v, row) => <Badge variant={row.status === 'online' ? 'success' : 'error'}>{row.status}</Badge> },
|
|
228
|
+
]}
|
|
229
|
+
data={hosts}
|
|
230
|
+
rowKey="id"
|
|
231
|
+
onRowClick={(row) => {}}
|
|
232
|
+
emptyMessage="No hosts found"
|
|
233
|
+
/>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Empty (empty states)
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
<Empty>
|
|
240
|
+
<EmptyHeader>
|
|
241
|
+
<EmptyIcon>{/* icon */}</EmptyIcon>
|
|
242
|
+
<EmptyTitle>No hosts connected</EmptyTitle>
|
|
243
|
+
<EmptyDescription>Connect your first machine and Tia will start watching it.</EmptyDescription>
|
|
244
|
+
</EmptyHeader>
|
|
245
|
+
<EmptyContent><Button variant="primary" size="sm">Connect a host</Button></EmptyContent>
|
|
246
|
+
</Empty>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Infra / homelab components
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
<HostCard hostname="emma" ip="10.0.10.10" status="online" cpu="23%" memory="6.2/16 GB" disk="45%" uptime="14d" containers={4} />
|
|
253
|
+
<ServiceHealthCard service="emma.tollerud.no" status="online" uptime="14d 3h" responseTime="23ms" />
|
|
254
|
+
<IncidentCard title="High CPU" severity="high" timestamp="2026-05-26 14:32" description="CPU at 92% for 5 min" service="emma" />
|
|
255
|
+
<ApprovalCard action="restart_container" description="Restart emma:hermes" state="pending" onApprove={() => {}} onReject={() => {}} />
|
|
256
|
+
<LogViewer lines={[{ text: 'Health check passed', level: 'info', timestamp: '14:32:01', source: 'hermes' }]} follow searchable showLineNumbers height="300px" />
|
|
257
|
+
<AlertInbox alerts={[{ id: '1', title: 'emma high CPU', severity: 'high', timestamp: '14:32', acknowledged: false }]} onAcknowledge={(id) => {}} />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Severity scale: `critical` · `high` · `medium` · `low` · `info`
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Layout Patterns
|
|
266
|
+
|
|
267
|
+
### Navigation lockup
|
|
268
|
+
|
|
269
|
+
The monogram must always appear left of the project name with `gap-2`. Never show the name without the monogram or the monogram alone in a nav context.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import logo from '@tollerud/ui/tollerud-logo.svg'
|
|
273
|
+
|
|
274
|
+
// Top bar
|
|
275
|
+
<nav className="tollerud-glass fixed top-0 inset-x-0 z-50 h-14 flex items-center px-6 gap-6">
|
|
276
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
277
|
+
<img src={logo} alt="Tollerud" className="h-5 w-auto" />
|
|
278
|
+
<span className="font-semibold text-sm text-white">Project Name</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div className="flex items-center gap-4 ml-4">
|
|
281
|
+
<a href="/overview" className="text-sm text-tollerud-text-secondary hover:text-white transition-colors">Overview</a>
|
|
282
|
+
</div>
|
|
283
|
+
<div className="ml-auto flex items-center gap-3">
|
|
284
|
+
<Button variant="ghost" size="sm">Sign in</Button>
|
|
285
|
+
<Button variant="primary" size="sm">Get started</Button>
|
|
286
|
+
</div>
|
|
287
|
+
</nav>
|
|
288
|
+
<main className="pt-14">…</main>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Monogram sizing: top bar/sidebar expanded → `h-5`, sidebar collapsed → `h-6`, footer → `h-4` (handled automatically by `<Footer />`).
|
|
292
|
+
|
|
293
|
+
### Glass nav
|
|
294
|
+
|
|
295
|
+
```html
|
|
296
|
+
<nav class="tollerud-glass fixed top-0 left-0 right-0 z-50 h-16 flex items-center px-6">…</nav>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Grid background
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
<section class="tollerud-grid-bg">…</section>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Display headings
|
|
306
|
+
|
|
307
|
+
```html
|
|
308
|
+
<h1 class="tollerud-display text-[70px]">Dark. Monochrome.</h1>
|
|
309
|
+
<h2 class="tollerud-display--secondary text-[40px]">Yellow where it counts</h2>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Container
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
<Container>Content capped at 1100px with 24px padding</Container>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Density
|
|
319
|
+
|
|
320
|
+
Apply `data-density="compact"` to any container to tighten spacing for tables, forms, and panels inside it.
|
|
321
|
+
|
|
322
|
+
```html
|
|
323
|
+
<div data-density="compact">…dense tables / forms…</div>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Elevation
|
|
327
|
+
|
|
328
|
+
Use borders as the primary separation method. Only add shadows to lift overlays. Shadow scale: `--shadow-sm` `--shadow-md` `--shadow-lg` `--shadow-xl` `--shadow-glow`. Drawers use `--shadow-xl`; popovers `--shadow-lg`.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Copy & Voice
|
|
333
|
+
|
|
334
|
+
- Labels are short and action-first: "Deploy", "View Logs", "Restart" — not "Click here to initiate deployment"
|
|
335
|
+
- Terminal-style CTAs for technical actions: `❯ deploy --env production`, `$ init`
|
|
336
|
+
- Error messages name the cause: "Connection to emma.tollerud.no timed out" — not "Something went wrong"
|
|
337
|
+
- Avoid exclamation marks and corporate filler ("Oops!", "Great!", "Please try again later")
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Accessibility
|
|
342
|
+
|
|
343
|
+
- Every interactive element needs a visible focus ring: `focus-visible:outline-2 focus-visible:outline-tollerud-yellow focus-visible:outline-offset-2` (or `.tollerud-focus-ring`)
|
|
344
|
+
- Icon-only buttons must have `aria-label`
|
|
345
|
+
- Inputs must have `<label>` — always use the `label` prop on `Input`, `Select`, `Textarea`
|
|
346
|
+
- Error messages use `role="alert"` or `aria-live="polite"`
|
|
347
|
+
- Never convey information by color alone
|
|
348
|
+
- Respect `prefers-reduced-motion: reduce` — disable shimmer and animations
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## What NOT to do
|
|
353
|
+
|
|
354
|
+
| Don't | Why |
|
|
355
|
+
|-------|-----|
|
|
356
|
+
| Use light/white backgrounds | The system is dark-only |
|
|
357
|
+
| Put yellow text on white | Fails contrast at 1.7:1 |
|
|
358
|
+
| Recolor the monogram | Yellow on dark is non-negotiable |
|
|
359
|
+
| Use non-system colors (blue, green, purple) | Only yellow accent + monochrome grays |
|
|
360
|
+
| Add drop shadows or glows to the monogram | Glow is for interactive UI, not branding |
|
|
361
|
+
| Show the project name without the monogram | The lockup is the brand |
|
|
362
|
+
| Use verbose copy or exclamation marks | Violates voice guidelines |
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Updating the npm package (for agents working in this repo)
|
|
367
|
+
|
|
368
|
+
When asked to add components, fix bugs, or cut a release:
|
|
369
|
+
|
|
370
|
+
### 1. Build and typecheck before committing
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
npx tsc --noEmit -p tsconfig.build.json # must be clean
|
|
374
|
+
npx tsup # verify the bundle builds
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### 2. Every new component needs all four of these
|
|
378
|
+
|
|
379
|
+
| What | Where |
|
|
380
|
+
|------|-------|
|
|
381
|
+
| Component file | `components/ComponentName.tsx` |
|
|
382
|
+
| Named export + type export | `components/index.ts` |
|
|
383
|
+
| Registry entry | `registry.json` — add a `kebab-case` key with `name`, `description`, `files`, `dependencies`, `registryDependencies`, `type: "components:ui"` |
|
|
384
|
+
| Docs preview | `examples/docs-nextjs/components/ComponentPreviews.tsx` (export function) + `examples/docs-nextjs/app/components/page.tsx` (section entry + import) |
|
|
385
|
+
|
|
386
|
+
### 3. Version bump rules
|
|
387
|
+
|
|
388
|
+
| Change | Version bump |
|
|
389
|
+
|--------|-------------|
|
|
390
|
+
| New components, no breaking changes | minor (`1.x.0`) |
|
|
391
|
+
| Bug fixes only | patch (`1.0.x`) |
|
|
392
|
+
| Prop renames, removed exports, token renames | major (`x.0.0`) |
|
|
393
|
+
|
|
394
|
+
Edit `package.json` version, then update these to match:
|
|
395
|
+
- `COMPLETENESS_ROADMAP.md` — header line `### npm package (components/*.tsx) — vX.X.X`
|
|
396
|
+
- `registry.json` — top-level `"version"` field
|
|
397
|
+
|
|
398
|
+
### 4. Always update these files in the same commit
|
|
399
|
+
|
|
400
|
+
- `CHANGELOG.md` — add an entry at the top following the existing style (date · version · summary + bullet points)
|
|
401
|
+
- `COMPLETENESS_ROADMAP.md` — move completed items to the done list, strike through fixed quality items
|
|
402
|
+
- `SKILL.md` — add new components to the catalog, update version notes
|
|
403
|
+
- `AGENTS.md` (this file) — update the component import blocks if new exports were added
|
|
404
|
+
|
|
405
|
+
### 5. Commit and push
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
git add <changed files>
|
|
409
|
+
git commit -m "Brief description — vX.X.X"
|
|
410
|
+
git push origin main
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Fixing copy/paste component patterns (for agents working in consumer projects)
|
|
416
|
+
|
|
417
|
+
Older versions of projects that use `@tollerud/ui` sometimes copied component source files directly into the repo (e.g. `src/components/ui/Button.tsx` copied from the design system). These need to be replaced with package imports.
|
|
418
|
+
|
|
419
|
+
### How to detect it
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Find files that look like copied DS components (contain tollerud- tokens but aren't node_modules)
|
|
423
|
+
grep -rl "tollerud-yellow\|tollerud-noir\|tollerud-surface" src --include="*.tsx" --include="*.ts"
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Also check for a local `components/ui.ts` or `components/ui/index.ts` that re-exports from relative paths instead of `@tollerud/ui`.
|
|
427
|
+
|
|
428
|
+
### How to fix it
|
|
429
|
+
|
|
430
|
+
1. **Verify `@tollerud/ui` is installed** — check `package.json`. If not: `npm install @tollerud/ui clsx tailwind-merge`.
|
|
431
|
+
|
|
432
|
+
2. **Replace the local copy with a package import** — for each copied component:
|
|
433
|
+
```tsx
|
|
434
|
+
// Before (copied file)
|
|
435
|
+
import { Button } from '@/components/ui/Button'
|
|
436
|
+
|
|
437
|
+
// After
|
|
438
|
+
import { Button } from '@tollerud/ui'
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
3. **Delete the copied files** once all imports are updated and the project builds.
|
|
442
|
+
|
|
443
|
+
4. **Check for prop drift** — copied files may be outdated. Verify against `SKILL.md` (or `.claude/skills/tollerud-ui/SKILL.md`) that prop names haven't changed (e.g. `onValueChange` vs `onChange`, `label` vs `children` on form components).
|
|
444
|
+
|
|
445
|
+
5. **Check for inline token usage** — copied files sometimes hardcode hex values instead of using tokens. Replace any hardcoded `#FFFF00`, `#0A0A0A`, `#E8D500` etc. with `text-tollerud-yellow`, `bg-tollerud-noir-950`, `text-tollerud-yellow-warm`.
|
|
446
|
+
|
|
447
|
+
6. **Run typecheck** — `npx tsc --noEmit`. Prop signatures in the package may differ slightly from the copied version; fix any type errors before committing.
|
|
448
|
+
|
|
449
|
+
### Common copy/paste patterns to look for
|
|
450
|
+
|
|
451
|
+
| Pattern | Fix |
|
|
452
|
+
|---------|-----|
|
|
453
|
+
| `src/components/ui/Button.tsx` with `tollerud-btn` classes | Delete, import from `@tollerud/ui` |
|
|
454
|
+
| `lib/utils.ts` defining `cn()` manually | Replace with `import { cn } from '@tollerud/ui'` |
|
|
455
|
+
| `components/ui.ts` re-exporting from `'../../../components/Button'` | Replace all with `export * from '@tollerud/ui'` or direct named imports |
|
|
456
|
+
| Inline `bg-[#FFFF00]` or `text-[#0A0A0A]` | Replace with `bg-tollerud-yellow` / `text-tollerud-noir-950` |
|
|
457
|
+
| `import { toast } from 'sonner'` without a `<Toaster />` mount | Add `<Toaster />` near app root |
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Reference
|
|
462
|
+
|
|
463
|
+
| File | Contents |
|
|
464
|
+
|------|----------|
|
|
465
|
+
| [SKILL.md](SKILL.md) | **Verified** component catalog, props, gotchas — source of truth for what's actually shipped |
|
|
466
|
+
| [COMPONENTS.md](COMPONENTS.md) | Prop tables — includes both shipped and planned/roadmap components, check against SKILL.md before relying on an entry |
|
|
467
|
+
| [BRAND.md](BRAND.md) | Logo usage, nav lockup, sizing rules |
|
|
468
|
+
| [ACCESSIBILITY.md](ACCESSIBILITY.md) | Contrast ratios, focus, ARIA patterns |
|
|
469
|
+
| [VOICE.md](VOICE.md) | Copy tone, terminal-style CTAs, error messages |
|
|
470
|
+
| [KEYBOARD.md](KEYBOARD.md) | Keyboard contract for CommandMenu and navigation |
|
|
471
|
+
| [BACKGROUNDS.md](BACKGROUNDS.md) | NoirGlowBackground props and fallback rules |
|
|
472
|
+
| [GETTING_STARTED.md](GETTING_STARTED.md) | Install, Tailwind config, registry usage |
|
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`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollerud/ui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Tollerud User Interface — dark, monochrome + yellow accent. Noir aesthetic meets modern utility.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"tollerud-logo.svg",
|
|
47
47
|
"tollerud-avatar.svg",
|
|
48
48
|
"tollerud-avatar.png",
|
|
49
|
-
"tia-full-figure.svg"
|
|
49
|
+
"tia-full-figure.svg",
|
|
50
|
+
"AGENTS.md",
|
|
51
|
+
"SKILL.md"
|
|
50
52
|
],
|
|
51
53
|
"scripts": {
|
|
52
54
|
"build": "tsup",
|