@sarunyu/system-one 3.0.3 → 4.0.0

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/llms.txt CHANGED
@@ -1,6 +1,27 @@
1
- # @sarunyu/system-one
1
+ # @sarunyu/system-one — AI usage guide
2
2
 
3
- React component library. Tailwind CSS v4 + CSS custom properties. 13 production-ready components. Built for AI-powered generation tools (v0, Lovable, Figma Make).
3
+ React component library. Tailwind CSS v4 + CSS custom properties. 17 components.
4
+ Built for AI-powered UI generation (v0, Lovable, Figma Make, Cursor).
5
+
6
+ **This file is the contract.** Read it top-to-bottom before generating any screen
7
+ that uses this library. The rules are non-negotiable.
8
+
9
+ ---
10
+
11
+ ## The 3 rules
12
+
13
+ 1. **Use library components for every element it provides.** Never recreate
14
+ Button, Input, Tag, Dropdown, Card, Tab, Checkbox, Radio, DateInput, TimeInput,
15
+ Table, SearchInput, TextArea, Chip as raw HTML.
16
+ 2. **Use design-token classes for color and typography.** Never `text-blue-600`,
17
+ `bg-gray-100`, `text-[#3b82f6]`. The token table below is exhaustive — if a
18
+ color you need is not in it, use `text-foreground` / `bg-card`.
19
+ 3. **Layout is free.** Build page structure with plain `<div>` + Tailwind
20
+ (flex, grid, container, max-w-*, gap-*, p-*, mx-auto). The library does
21
+ NOT ship layout primitives. Do not import `Page`, `Section`, `Stack`,
22
+ `CardGrid`, `Toolbar` — they don't exist.
23
+
24
+ ---
4
25
 
5
26
  ## Install
6
27
 
@@ -8,82 +29,199 @@ React component library. Tailwind CSS v4 + CSS custom properties. 13 production-
8
29
  npm install @sarunyu/system-one
9
30
  ```
10
31
 
11
- ## Setup by Platform
32
+ Peer dep: `react >= 18`.
12
33
 
13
- ### v0 / Next.js (App Router)
34
+ ## Setup
14
35
 
15
36
  ```tsx
16
- // app/layout.tsx
17
- import "@sarunyu/system-one/styles.css"
37
+ // Next.js App Router: app/layout.tsx
38
+ // Next.js Pages: pages/_app.tsx
39
+ // Vite / Figma Make / Lovable: src/main.tsx
40
+ import "@sarunyu/system-one/styles.css";
18
41
  ```
19
42
 
20
- Components are pre-annotated with `"use client"`. Safe to import anywhere — Server Components, Client Components, pages.
43
+ No provider, no wrapper. Components ship with `"use client"`.
21
44
 
22
- ### Next.js (Pages Router)
45
+ ## Dark mode
23
46
 
24
- ```tsx
25
- // pages/_app.tsx
26
- import "@sarunyu/system-one/styles.css"
27
- ```
28
-
29
- ### Figma Make / Lovable / Vite
47
+ Add `.dark` to any ancestor. All components and tokens adapt.
30
48
 
31
49
  ```tsx
32
- // src/main.tsx
33
- import "@sarunyu/system-one/styles.css"
50
+ <html className={isDark ? "dark" : ""}>
34
51
  ```
35
52
 
36
- ## Import
53
+ ---
54
+
55
+ ## Available imports
37
56
 
38
57
  ```tsx
39
58
  import {
40
- Button, Input, TextArea, SearchInput,
59
+ // Actions
60
+ Button,
61
+ // Form
62
+ Input, TextArea, SearchInput,
41
63
  Dropdown, DropdownMultiple, OptionList,
64
+ Checkbox, Radio,
65
+ DateInput, TimeInput,
66
+ // Display
42
67
  Tag, StatusTag, Chip,
43
68
  Tab, TabGroup,
44
69
  Card,
45
- DateInput, TimeInput,
46
- cn
47
- } from "@sarunyu/system-one"
70
+ Table, TableRow, TableHeaderCell, TableCell,
71
+ // Utility
72
+ cn,
73
+ } from "@sarunyu/system-one";
48
74
  ```
49
75
 
50
76
  ---
51
77
 
78
+ ## Token reference (memorize this)
79
+
80
+ All values are CSS custom properties → exposed as Tailwind utilities.
81
+ Use the Tailwind class. Never hard-code colors.
82
+
83
+ Every class below is **Figma-aligned**: Light mode mirrors Color.Light (System One),
84
+ dark mode mirrors Color.Dark (System One). Status colors: `danger`=Red.500,
85
+ `warning`=Yellow.500, `success`=Green.500, `info`=Blue.500.
86
+
87
+ ### Text color
88
+
89
+ | Use for | Class | Alt (Figma semantic) |
90
+ |---|---|---|
91
+ | Body text, headings | `text-foreground` | `text-default` |
92
+ | Secondary body | `text-muted-foreground` | `text-default-secondary` |
93
+ | Tertiary / placeholder | `text-default-tertiary` / `text-default-placeholder` | |
94
+ | Disabled text | `text-disabled` | `text-default-disabled` |
95
+ | Inverse (on dark surfaces) | `text-default-inverse` | |
96
+ | Subtle captions | `text-caption` | |
97
+ | Brand / link / active | `text-primary-action` | `text-brand` / `text-brand-link` |
98
+ | Destructive / error | `text-destructive` | `text-danger` |
99
+ | Warning (yellow) | `text-warning` | |
100
+ | Success (green) | `text-success` | |
101
+ | Info (blue) | `text-info` | |
102
+ | On primary-action surfaces | `text-on-primary-action` | `text-on-brand` |
103
+
104
+ ### Background
105
+
106
+ | Use for | Class | Alt (Figma semantic) |
107
+ |---|---|---|
108
+ | Page | `bg-background` | `bg-default` |
109
+ | Card / popover surface | `bg-card` | `bg-default` |
110
+ | Secondary surface (sidebar, subtle panel) | `bg-default-secondary` | |
111
+ | Tertiary surface (nested card) | `bg-default-tertiary` | |
112
+ | Neutral fill (inputs, chips) | `bg-muted` / `bg-input-background` | |
113
+ | Hover on neutral | `bg-hover-bg` | `bg-default-hover` |
114
+ | Disabled fill | `bg-disabled-bg` | `bg-default-disabled` |
115
+ | Brand action (primary buttons) | `bg-primary-action` + `hover:bg-primary-action-hover` + `active:bg-primary-action-active` | `bg-brand` / `bg-brand-hover` / `bg-brand-pressed` |
116
+ | Tinted primary surface | `bg-primary-action-light` / `bg-primary-action-muted` | `bg-brand-light` / `bg-brand-muted` |
117
+ | Selection / active row | `bg-selected-bg` / `bg-selected-light-bg` | |
118
+ | Destructive fill | `bg-destructive` | `bg-danger` |
119
+ | Destructive soft surface | `bg-error-bg` | `bg-danger-light` / `bg-danger-soft` |
120
+ | Warning fill / soft | `bg-warning` / `bg-warning-light` | |
121
+ | Success fill / soft | `bg-success` / `bg-success-bg` | `bg-success-light` |
122
+ | Info fill / soft | `bg-info` / `bg-info-light` | |
123
+
124
+ ### Border
125
+
126
+ | Use for | Class | Alt (Figma semantic) |
127
+ |---|---|---|
128
+ | Default border | `border-border` | `border-default` |
129
+ | Strong border | `border-border-muted` | `border-strong` |
130
+ | Subtle divider | `border-divider` | |
131
+ | Disabled border | `border-border-disabled` | |
132
+ | Brand border | `border-brand` | |
133
+ | Status borders | `border-danger` / `border-warning` / `border-success` / `border-info` | |
134
+
135
+ ### Icon color (when setting color on an SVG)
136
+
137
+ `text-icon-default` · `text-icon-default-secondary` · `text-icon-default-tertiary` ·
138
+ `text-icon-default-disabled` · `text-icon-brand` · `text-icon-on-brand` ·
139
+ `text-icon-danger` · `text-icon-warning` · `text-icon-success` · `text-icon-info`
140
+
141
+ ### Visual scale — notification dots, badge counts, data viz
142
+
143
+ For accent colors used in badges, counts, or charts (not for semantic status),
144
+ use the `visual-*` scale. Available hues: `neutral`, `purple`, `blue`, `cyan`,
145
+ `pink`, `red`, `orange`. Each hue has 7 steps:
146
+
147
+ `bg-visual-red-light` · `bg-visual-red-soft` · `bg-visual-red-low` · `bg-visual-red` · `bg-visual-red-high` · `bg-visual-red-vivid` · `bg-visual-red-deep`
148
+
149
+ Same pattern for `text-visual-*` and `border-visual-*`. Prefer `visual-*` over
150
+ `bg-red-500` for notification counts — visual tokens adapt in dark mode, hard
151
+ palette classes do not.
152
+
153
+ ### Radius
154
+
155
+ `rounded-sm` (2px) · `rounded` (4px, default `--radius`) · `rounded-md` (6px) · `rounded-lg` (8px) · `rounded-xl` (12px) · `rounded-2xl` (16px) · `rounded-3xl` (24px) · `rounded-full`
156
+
157
+ ### Shadow
158
+
159
+ `shadow-xs` · `shadow-sm` · `shadow-md` · `shadow-lg` · `shadow-card` (event/content cards) · `shadow-popover` (dropdowns, menus)
160
+
161
+ ### Typography
162
+
163
+ Pre-styled HTML headings: `<h1>` (24px) · `<h2>` (20px) · `<h3>` (18px) · `<h4>` (16px). Do NOT add `text-*` / `font-*` to override.
164
+
165
+ Body text inherits `--foreground`. Font family is `--font-sans` (Noto Sans Thai by default — override via `--font-sans` to change globally).
166
+
167
+ ### Spacing / sizing
168
+
169
+ Standard Tailwind scale (4px units). Fine-grained: `p-0.5` (2px), `p-1.5` (6px), `p-2.5` (10px), `p-3.5` (14px), `p-13` (56px).
170
+
171
+ ---
172
+
52
173
  ## Components
53
174
 
175
+ Every example below assumes state is managed by the caller via `useState`.
176
+
54
177
  ### Button
55
178
 
56
- Variants: `primary` | `outline` | `plain` | `outline-black` | `plain-black`
57
- Label sizes: `xs` | `sm` | `md` | `lg` | `xl`
58
- Icon-only sizes: `icon-xs` | `icon-sm` | `icon-md` | `icon-lg` | `icon-xl`
179
+ Actions only. Never `<button>` with utility classes.
59
180
 
60
181
  ```tsx
182
+ type ButtonVariant = "primary" | "outline" | "plain" | "outline-black" | "plain-black";
183
+ type ButtonLabelSize = "xs" | "sm" | "md" | "lg" | "xl";
184
+ type ButtonIconSize = "icon-xs" | "icon-sm" | "icon-md" | "icon-lg" | "icon-xl";
185
+
61
186
  <Button variant="primary" size="md">Submit</Button>
62
- <Button variant="outline" size="md" onClick={fn}>Cancel</Button>
187
+ <Button variant="outline" size="md" onClick={save}>Save draft</Button>
63
188
  <Button variant="plain" size="sm">Learn more</Button>
64
- <Button variant="primary" size="md" leftIcon={<Icon />}>Add item</Button>
65
- <Button variant="outline" size="md" rightIcon={<Icon />}>Continue</Button>
189
+ <Button variant="primary" size="lg" leftIcon={<PlusIcon />}>Add item</Button>
66
190
  <Button size="icon-md" aria-label="Settings"><GearIcon /></Button>
191
+ <Button variant="primary" size="md" disabled>Can't submit</Button>
67
192
  ```
68
193
 
69
- Props: `variant`, `size`, `leftIcon`, `rightIcon`, `disabled`, `onClick`, `className`, `children`
194
+ **Semantics** `primary` = main CTA (one per context). `outline` = secondary action.
195
+ `plain` = tertiary / low-priority. `outline-black` / `plain-black` = same as outline/plain
196
+ but in neutral tone (use when brand color shouldn't compete, e.g. toolbar actions).
197
+
198
+ **Sizing** — `md` desktop default. `xl` mobile default. `xs`/`sm` for dense toolbars. Icon-only sizes match label sizes 1:1.
199
+
200
+ Props: `variant`, `size`, `leftIcon`, `rightIcon`, `disabled`, `onClick`, `className`, plus all native `<button>` props.
70
201
 
71
202
  ---
72
203
 
73
204
  ### Input
74
205
 
75
- Floating-label text input with validation states.
206
+ Floating-label text input. Never `<input>` with utility classes.
76
207
 
77
208
  ```tsx
78
209
  <Input placeholder="Email" value={email} onChange={setEmail} />
79
- <Input placeholder="Amount" unit="THB" />
80
- <Input placeholder="Password" type="password" />
81
- <Input placeholder="Bio" showCount maxCount={160} />
82
- <Input forceState="error" errorMessage="This field is required" placeholder="Email" />
83
- <Input forceState="disabled" placeholder="Read only" />
210
+ <Input placeholder="Password" type="password" value={pw} onChange={setPw} />
211
+ <Input placeholder="Amount" unit="THB" value={amount} onChange={setAmount} />
212
+ <Input placeholder="Bio" showCount maxCount={160} value={bio} onChange={setBio} />
213
+ <Input placeholder="Email" required />
214
+ <Input placeholder="Email" forceState="error" errorMessage="Invalid email" value={email} onChange={setEmail} />
215
+ <Input placeholder="Email" forceState="disabled" value="locked@example.com" />
216
+ <Input placeholder="Search" helperText="Press Enter to search" value={q} onChange={setQ} />
84
217
  ```
85
218
 
86
- Props: `placeholder`, `value`, `onChange`, `type`, `unit`, `showCount`, `maxCount`, `forceState` (`"default"` | `"focus"` | `"error"` | `"disabled"`), `errorMessage`, `required`, `className`
219
+ **Important** `onChange` receives `(value: string)`, **not** an event. The placeholder
220
+ IS the label (it floats up when filled — don't add a separate `<label>`).
221
+
222
+ Props: `placeholder`, `value`, `onChange(value)`, `type`, `unit`, `showCount`, `maxCount`,
223
+ `forceState` (`"default"` | `"focus"` | `"error"` | `"disabled"`), `errorMessage`, `helperText`,
224
+ `required`, `rightIcon`, `className`.
87
225
 
88
226
  ---
89
227
 
@@ -93,24 +231,25 @@ Multi-line input. API mirrors Input.
93
231
 
94
232
  ```tsx
95
233
  <TextArea placeholder="Description" value={text} onChange={setText} />
96
- <TextArea placeholder="Tweet" showCount maxCount={280} />
97
- <TextArea forceState="error" errorMessage="Required" placeholder="Notes" />
234
+ <TextArea placeholder="Tweet" showCount maxCount={280} value={post} onChange={setPost} />
235
+ <TextArea placeholder="Feedback" forceState="error" errorMessage="Required" />
98
236
  ```
99
237
 
100
- Props: `placeholder`, `value`, `onChange`, `showCount`, `maxCount`, `forceState`, `errorMessage`, `required`, `className`
238
+ Props: `placeholder`, `value`, `onChange(value)`, `showCount`, `maxCount`, `forceState`,
239
+ `errorMessage`, `helperText`, `required`, `className`.
101
240
 
102
241
  ---
103
242
 
104
243
  ### SearchInput
105
244
 
106
- Search field with icon and clear button.
245
+ Search field with icon + clear button.
107
246
 
108
247
  ```tsx
109
- <SearchInput placeholder="Search events..." value={q} onChange={setQ} />
110
- <SearchInput size="sm" placeholder="Filter by name..." />
248
+ <SearchInput placeholder="Search events" value={q} onChange={setQ} />
249
+ <SearchInput size="sm" placeholder="Filter…" value={q} onChange={setQ} />
111
250
  ```
112
251
 
113
- Props: `placeholder`, `value`, `onChange`, `size` (`"lg"` | `"sm"`), `className`
252
+ Props: `placeholder`, `value`, `onChange(value)`, `size` (`"lg"` default | `"sm"`), `className`.
114
253
 
115
254
  ---
116
255
 
@@ -131,9 +270,8 @@ Single-select dropdown.
131
270
  />
132
271
  ```
133
272
 
134
- Props: `placeholder`, `options` (`Array<{ label: string, value: string, disabled?: boolean }>`), `value`, `onChange`, `disabled`, `className`
135
-
136
- ---
273
+ Props: `placeholder`, `options: { label, value, disabled? }[]`, `value`, `onChange(value)`,
274
+ `disabled`, `className`.
137
275
 
138
276
  ### DropdownMultiple
139
277
 
@@ -142,115 +280,130 @@ Multi-select dropdown with checkboxes.
142
280
  ```tsx
143
281
  <DropdownMultiple
144
282
  placeholder="Select tags"
145
- options={options}
283
+ options={tagOptions}
146
284
  values={selected}
147
285
  onChange={setSelected}
148
286
  />
149
287
  ```
150
288
 
151
- Props: `placeholder`, `options`, `values`, `onChange`, `disabled`, `className`
289
+ Props: `placeholder`, `options`, `values: string[]`, `onChange(values)`, `disabled`, `className`.
152
290
 
153
291
  ---
154
292
 
155
- ### Tag
293
+ ### Checkbox
294
+
295
+ ```tsx
296
+ <Checkbox checked={agreed} onCheckedChange={setAgreed} label="I agree to the terms" />
297
+ <Checkbox checked="indeterminate" onCheckedChange={setAll} label="Select all" />
298
+ <Checkbox checked={sub} onCheckedChange={setSub} label="Subscribe" disabled />
299
+ ```
300
+
301
+ `checked` is `boolean | "indeterminate"`. Always provide `label` — don't wrap Checkbox in a `<label>`.
302
+
303
+ ### Radio
304
+
305
+ Render a group — use `name` to bind siblings.
306
+
307
+ ```tsx
308
+ <div className="flex flex-col gap-2">
309
+ <Radio name="plan" value="free" checked={plan === "free"} onChange={setPlan} label="Free" />
310
+ <Radio name="plan" value="pro" checked={plan === "pro"} onChange={setPlan} label="Pro" />
311
+ </div>
312
+ ```
156
313
 
157
- Compact colored label. Use for categories, statuses, filters.
314
+ ---
158
315
 
159
- Variants: `blue` | `green` | `yellow` | `red` | `gray` | `lime`
160
- Sizes: `large` (default) | `small`
316
+ ### Tag
161
317
 
162
- Color semantics: `green` → positive/active, `red` → error/danger, `yellow` → warning/pending, `blue` → informational, `gray` → neutral/inactive
318
+ Compact categorical label.
163
319
 
164
320
  ```tsx
321
+ type TagVariant = "blue" | "green" | "yellow" | "red" | "gray" | "lime";
322
+ // green=positive, red=danger, yellow=warning, blue=info, gray=neutral
323
+
165
324
  <Tag text="Active" variant="green" />
166
325
  <Tag text="Draft" variant="yellow" />
167
- <Tag text="Error" variant="red" size="small" />
168
- <Tag text="Filter" close onClose={fn} />
326
+ <Tag text="Filter: Design" close onClose={remove} />
327
+ <Tag text="Small" variant="blue" size="small" />
169
328
  ```
170
329
 
171
- Props: `text`, `variant`, `size`, `close`, `onClose`, `className`
172
-
173
- ---
330
+ Props: `text`, `variant`, `size` (`"large"` default | `"small"`), `close`, `onClose`, `className`.
174
331
 
175
332
  ### StatusTag
176
333
 
177
- Process-flow state indicator with colored dot.
178
-
179
- Types: `stop` | `success` | `hold` | `processing` | `error`
334
+ Workflow/process state with colored dot. Use this (not Tag) for statuses.
180
335
 
181
336
  ```tsx
337
+ type StatusTagType = "stop" | "success" | "hold" | "processing" | "error";
338
+
182
339
  <StatusTag type="success" />
183
- <StatusTag type="processing" text="In progress" />
184
- <StatusTag type="hold" />
340
+ <StatusTag type="processing" text="Uploading…" />
185
341
  <StatusTag type="error" />
186
342
  ```
187
343
 
188
- Props: `type`, `text`, `className`
189
-
190
- Use `StatusTag` for workflow states. Use `Tag` for categorical labels.
191
-
192
- ---
344
+ **When to pick which** — workflow state (order flow, job status) → StatusTag.
345
+ Category / label / filter pill → Tag.
193
346
 
194
347
  ### Chip
195
348
 
196
- Toggleable filter/selection chip. Always use in groups of 2+.
197
-
198
- Types: `single` (default) | `multiple`
199
- Sizes: `large` | `medium` | `small`
349
+ Toggleable filter selection. Always use in groups of 2+.
200
350
 
201
351
  ```tsx
202
- {/* Single-select group */}
203
- <div className="flex gap-2">
204
- <Chip label="All" selected={filter === "all"} onClick={() => setFilter("all")} />
205
- <Chip label="Active" selected={filter === "active"} onClick={() => setFilter("active")} />
206
- <Chip label="Archived" selected={filter === "archived"} onClick={() => setFilter("archived")} />
352
+ // Single-select filter bar
353
+ <div className="flex flex-wrap gap-2">
354
+ {["all", "active", "archived"].map(v => (
355
+ <Chip key={v} label={v} selected={filter === v} onClick={() => setFilter(v)} />
356
+ ))}
207
357
  </div>
208
358
 
209
- {/* Multi-select */}
210
- <Chip label="Design" type="multiple" selected={tags.includes("design")} onClick={() => toggle("design")} />
359
+ // Multi-select tag picker
360
+ <div className="flex flex-wrap gap-2">
361
+ {tags.map(t => (
362
+ <Chip key={t} label={t} type="multiple" size="medium"
363
+ selected={picked.has(t)} onClick={() => toggle(t)} />
364
+ ))}
365
+ </div>
211
366
  ```
212
367
 
213
- Props: `label`, `selected`, `onClick`, `type`, `size`, `disabled`, `className`
368
+ Props: `label`, `selected`, `onClick`, `type` (`"single"` default | `"multiple"`),
369
+ `size` (`"large"` | `"medium"` | `"small"`), `disabled`, `className`.
214
370
 
215
371
  ---
216
372
 
217
373
  ### TabGroup
218
374
 
219
- Tabbed navigation. Always use `TabGroup`, not bare `Tab`.
220
-
221
- Sizes: `lg` | `md` (default) | `sm`
375
+ Tabbed navigation. Always use `TabGroup` (never bare `<Tab>`).
222
376
 
223
377
  ```tsx
224
378
  <TabGroup
225
379
  items={[
226
380
  { id: "overview", title: "Overview" },
227
- { id: "details", title: "Details", icon: true },
228
- { id: "history", title: "History", notification: 3 },
381
+ { id: "details", title: "Details", icon: true },
382
+ { id: "history", title: "History", notification: 3 },
229
383
  { id: "settings", title: "Settings", disabled: true },
230
384
  ]}
231
- activeId={activeTab}
232
- onChange={setActiveTab}
385
+ activeId={active}
386
+ onChange={setActive}
233
387
  size="md"
234
388
  />
235
389
  ```
236
390
 
237
- Props: `items` (`Array<{ id: string, title: string, icon?: boolean, notification?: number, disabled?: boolean }>`), `activeId`, `onChange`, `size`, `className`
391
+ Props: `items`, `activeId`, `onChange(id)`, `size` (`"lg"` | `"md"` default | `"sm"`), `className`.
392
+
393
+ All tabs in one group must share the same size.
238
394
 
239
395
  ---
240
396
 
241
397
  ### Card
242
398
 
243
- Responsive event/content card.
244
-
245
- Variants: `desktop` (308px wide) | `tablet` (224px wide) | `mobile` (163px wide)
246
- tagStatus: `not-registered` | `registered` | `full`
399
+ Event/content card. Self-contained — don't wrap in another card.
247
400
 
248
401
  ```tsx
249
402
  <Card
250
403
  variant="desktop"
251
404
  title="Annual Conference 2024"
252
405
  date="Jun 23, 2024"
253
- time="08:30 - 12:00"
406
+ time="08:30 12:00"
254
407
  location="Main Hall, Floor 7"
255
408
  count="150/200"
256
409
  tagStatus="registered"
@@ -258,253 +411,396 @@ tagStatus: `not-registered` | `registered` | `full`
258
411
  />
259
412
  ```
260
413
 
261
- Props: `variant`, `title`, `date`, `time`, `location`, `count`, `tagStatus`, `image`, `className`
262
-
263
- Match variant to viewport: `desktop` on wide layouts, `tablet` / `mobile` on narrow.
264
-
265
- ---
414
+ Variants set width: `desktop` (308px) · `tablet` (224px) · `mobile` (163px).
415
+ Pick based on viewport breakpoint, not aesthetic.
266
416
 
267
- ### DateInput
417
+ `tagStatus`: `"not-registered"` | `"registered"` | `"full"`.
268
418
 
269
- Calendar date picker.
419
+ ### Table
270
420
 
271
- Modes: `single` | `range` | `multiple`
272
- Variants: `popover` (default) | `modal`
421
+ Data tables. Compose with TableRow + TableHeaderCell + TableCell.
273
422
 
274
423
  ```tsx
275
- <DateInput placeholder="Select date" mode="single" value={date} onChange={setDate} />
276
- <DateInput mode="range" value={range} onChange={setRange} />
277
- <DateInput mode="multiple" value={dates} onChange={setDates} />
424
+ <Table>
425
+ <TableRow header>
426
+ <TableHeaderCell>Name</TableHeaderCell>
427
+ <TableHeaderCell>Role</TableHeaderCell>
428
+ <TableHeaderCell>Status</TableHeaderCell>
429
+ </TableRow>
430
+ {users.map(u => (
431
+ <TableRow key={u.id}>
432
+ <TableCell>{u.name}</TableCell>
433
+ <TableCell>{u.role}</TableCell>
434
+ <TableCell><StatusTag type={u.status} /></TableCell>
435
+ </TableRow>
436
+ ))}
437
+ </Table>
278
438
  ```
279
439
 
280
- Props: `placeholder`, `mode`, `value`, `onChange`, `variant`, `disabled`, `className`
440
+ Align numeric columns `right`, status center. Don't stuff StatusTag inside the Table header.
281
441
 
282
442
  ---
283
443
 
284
- ### TimeInput
285
-
286
- 24-hour time picker.
287
-
288
- Modes: `single` | `range`
444
+ ### DateInput / TimeInput
289
445
 
290
446
  ```tsx
447
+ <DateInput placeholder="Select date" mode="single" value={date} onChange={setDate} />
448
+ <DateInput mode="range" value={range} onChange={setRange} />
449
+ <DateInput mode="multiple" value={dates} onChange={setDates} />
450
+
291
451
  <TimeInput placeholder="Start time" value={time} onChange={setTime} />
292
452
  <TimeInput mode="range" value={timeRange} onChange={setTimeRange} />
293
453
  ```
294
454
 
295
- Props: `placeholder`, `mode`, `value`, `onChange`, `disabled`, `className`
296
-
297
- ---
455
+ Always format `"DD MMM YYYY"` for display (system format). Don't mix Thai month names with C.E. year.
298
456
 
299
457
  ### OptionList
300
458
 
301
- Scrollable option list for custom dropdowns.
459
+ Raw option rows. Use when you need a custom dropdown or a sidebar menu — otherwise use `Dropdown` / `DropdownMultiple`.
302
460
 
303
461
  ```tsx
304
- {/* Single-select */}
305
462
  <OptionList
306
463
  options={[{ label: "Item A", value: "a" }, { label: "Item B", value: "b" }]}
307
464
  selectedValue={value}
308
465
  onSelect={setValue}
309
466
  />
310
-
311
- {/* Multi-select */}
312
- <OptionList
313
- options={options}
314
- selectedValues={values}
315
- onToggle={toggleValue}
316
- />
317
467
  ```
318
468
 
319
- Props: `options` (`Array<{ label: string, value: string, disabled?: boolean }>`), `selectedValue`, `onSelect`, `selectedValues`, `onToggle`, `className`
320
-
321
469
  ---
322
470
 
323
- ## Design Tokens
471
+ ## Layout — you design it
324
472
 
325
- All tokens are CSS custom properties. Override after the stylesheet import:
473
+ The library ships zero layout components. Compose page structure with plain
474
+ Tailwind. Example scaffolds:
326
475
 
327
- ```css
328
- :root {
329
- --primary-action: #7c3aed; /* brand color */
330
- --primary-action-hover: #6d28d9;
331
- --primary-action-active: #5b21b6;
332
- --font-sans: "Inter", sans-serif; /* override library default (Noto Sans Thai) */
333
- --radius: 8px; /* border radius base */
334
- }
476
+ ```tsx
477
+ // Centered content page
478
+ <main className="mx-auto w-full max-w-[1200px] px-6 md:px-8 py-10 flex flex-col gap-12">
479
+ <header className="flex items-start justify-between gap-4">
480
+ <div className="flex flex-col gap-2">
481
+ <h1>Dashboard</h1>
482
+ <p className="text-muted-foreground">Overview of your activity</p>
483
+ </div>
484
+ <Button variant="primary" size="md">New item</Button>
485
+ </header>
486
+ <section className="flex flex-col gap-6">{/* ... */}</section>
487
+ </main>
335
488
  ```
336
489
 
337
- Key tokens:
338
- - `--primary-action` — brand color (primary buttons, links, active states)
339
- - `--background` / `--foreground` — page surface and text
340
- - `--border` — default border color
341
- - `--muted-foreground` — placeholder and secondary text
342
- - `--destructive` — error/danger red
343
- - `--success` — success green
344
- - `--font-sans` — font family (default: Noto Sans Thai)
345
- - `--radius` — base border radius (default: 4px)
346
-
347
- ---
348
-
349
- ## Dark Mode
350
-
351
- Add `.dark` to `<html>` or any ancestor:
352
-
353
490
  ```tsx
354
- // Next.js
355
- <html className={isDark ? "dark" : ""}>
356
-
357
- // Vite
358
- document.documentElement.classList.toggle("dark", isDark)
491
+ // Sidebar + content
492
+ <div className="flex min-h-screen">
493
+ <aside className="w-64 shrink-0 border-r border-divider bg-card p-6">{/* nav */}</aside>
494
+ <main className="flex-1 p-8">{/* content */}</main>
495
+ </div>
359
496
  ```
360
497
 
361
- All components adapt automatically.
362
-
363
- ---
364
-
365
- ## TypeScript
366
-
367
- ```ts
368
- import type { ButtonVariant, ButtonSize, TagVariant, ChipSize } from "@sarunyu/system-one"
498
+ ```tsx
499
+ // Responsive card grid
500
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
501
+ {events.map(e => <Card key={e.id} {...e} />)}
502
+ </div>
369
503
  ```
370
504
 
371
- ---
372
-
373
- ## Design Guidance
374
-
375
- **You design the layout. The library provides the components.** Compose, arrange, and style page structure freely using Tailwind — the library does not dictate layout. Focus instead on using the components below correctly (right variants, right semantics, right props) so the visual language stays consistent with the design system.
376
-
377
- ### Anchors the library provides
378
-
379
- These are all you need to keep a page visually coherent with the system:
380
-
381
- - **Typography** — `<h1>`–`<h4>` are pre-styled (size, weight, line-height). Use them as-is; do not add `text-*` or `font-*` classes to override.
382
- - **Text color** — body text inherits from `--foreground`. Secondary text: `text-muted-foreground`. Don't hard-code hex colors.
383
- - **Surfaces** — use `bg-background` / `bg-card` / `bg-muted` so dark mode works automatically.
384
- - **Brand color** — reference via `--primary-action`, or Tailwind `text-primary-action` / `bg-primary-action`.
385
- - **Spacing** — Tailwind spacing scale (4px units). Pick whatever gaps/paddings look right; the design system has no prescribed rhythm.
386
- - **Radius** — `rounded-md` (6px), `rounded-lg` (8px), `rounded-xl` (12px), `rounded-full`.
387
-
388
- ### Component usage — the only hard rules
389
-
390
- Use the library's components for these elements. Do **not** recreate them with raw HTML or Tailwind:
391
-
392
- - Buttons → `<Button>` (never `<button>` with utility classes)
393
- - Text inputs / textareas / search → `<Input>`, `<TextArea>`, `<SearchInput>`
394
- - Single / multi select → `<Dropdown>`, `<DropdownMultiple>`, `<OptionList>`
395
- - Date / time pickers → `<DateInput>`, `<TimeInput>`
396
- - Labels / statuses / filters → `<Tag>`, `<StatusTag>`, `<Chip>`
397
- - Tabs → `<TabGroup>` (never `<Tab>` alone)
398
- - Event/content cards → `<Card>`
399
-
400
- Pick the correct variant / prop for the semantic role (e.g. `StatusTag type="success"` for success, `Button variant="primary"` once per context). The per-component rules below in "Design Rules" are the full reference.
401
-
402
- ### Layout freedom
403
-
404
- Structure the page however you like — any combination of flex, grid, max-width containers, custom spacing, hero compositions, sidebars, split layouts, dashboards, etc. Use aesthetic judgement: generous padding, clear hierarchy, balanced whitespace. The only layout constraint is that content shouldn't sit flush against viewport edges or adjacent blocks without breathing room.
405
-
406
- ### Optional: layout helper components
407
-
408
- The library also ships a small set of layout primitives (`Page`, `PageHeader`, `Section`, `Toolbar`, `CardGrid`, `Stack`) that some users prefer for rapid scaffolding. They are entirely optional — skip them and design your own layout if that produces a better result. If you do use them, they apply sensible defaults (centered container, 48px section gap, responsive card grid). Full API is in the TypeScript types.
505
+ **Rules for layout:**
506
+ - Use `bg-background` / `bg-card` — never hard-code colors.
507
+ - Leave breathing room: `gap-4` inner clusters, `gap-6`–`gap-12` between sections, `py-10`+ page padding.
508
+ - Max-width the content column (`max-w-[640px]` forms, `max-w-[1200px]` dashboards).
509
+ - Use `<h1>`…`<h4>` directly. They are pre-styled.
409
510
 
410
511
  ---
411
512
 
412
- ## Notes
513
+ ## DO / DON'T
413
514
 
414
- - No provider or wrapper needed — just import the stylesheet and use components
415
- - The library ships pre-built CSS — no Tailwind config required in the consuming project
416
- - If the project already uses Tailwind and you see font differences, override `--font-sans` in your CSS
417
- - CSS custom properties can be referenced in Tailwind classes: `bg-[var(--primary-action)]`
515
+ | DO | DON'T |
516
+ |---|---|
517
+ | `<Button variant="primary" size="md">Save</Button>` | `<button className="bg-blue-600 text-white px-4 py-2 rounded">Save</button>` |
518
+ | `<Input placeholder="Email" value={e} onChange={setE} />` | `<label>Email</label><input className="border…" />` |
519
+ | `<Tag text="Active" variant="green" />` | `<span className="bg-green-100 text-green-800 px-2 py-1 rounded">Active</span>` |
520
+ | `text-muted-foreground` | `text-gray-500` |
521
+ | `bg-primary-action` | `bg-blue-600` / `bg-[#3b82f6]` |
522
+ | `<h1>Title</h1>` | `<h1 className="text-3xl font-bold">Title</h1>` |
523
+ | `<div className="flex flex-col gap-6">` | `import { Stack } from "@sarunyu/system-one"` |
524
+ | One `variant="primary"` per context | Two primary buttons side-by-side |
525
+ | `onChange={setValue}` (value, not event) | `onChange={e => setValue(e.target.value)}` |
418
526
 
419
527
  ---
420
528
 
421
- ## Component Usage Guide
422
-
423
- ### Button
424
-
425
- Buttons are interactive elements that allow users to click or tap to perform actions — such as submitting a form, saving data, navigating to another page, or opening a popup.
426
-
427
- Types: `primary` — main CTA, use once per context. `outline` — secondary action, supports primary without affecting main flow. `plain` — no background or border, low-priority actions. `secondary` (outline-black) — same as outline but black, use when blue is not appropriate.
428
-
429
- Sizes: `base` recommended for Desktop. `xl` recommended for Mobile.
430
-
431
- States: Default, Hover, Press, Disabled.
432
-
433
- ### Input
434
-
435
- Floating Label Input is a single-line or multi-line field with a label that floats inside.
529
+ ## Full page recipes
436
530
 
437
- Types: `Input` — single-line, general text/numeric. `TextArea` — multi-line, for comments or addresses. `Dropdown` — predefined list selection, displays as OptionList.
531
+ ### Login form
438
532
 
439
- States: Default, Focus, Error, Disabled.
440
-
441
- ### Tab
533
+ ```tsx
534
+ import { Button, Input, Checkbox } from "@sarunyu/system-one";
535
+ import { useState } from "react";
536
+
537
+ export function LoginPage() {
538
+ const [email, setEmail] = useState("");
539
+ const [pw, setPw] = useState("");
540
+ const [remember, setRmb] = useState(false);
541
+
542
+ return (
543
+ <main className="min-h-screen grid place-items-center bg-background p-6">
544
+ <div className="w-full max-w-[400px] flex flex-col gap-6 bg-card p-8 rounded-xl shadow-card">
545
+ <div className="flex flex-col gap-1">
546
+ <h1>Welcome back</h1>
547
+ <p className="text-muted-foreground">Sign in to continue</p>
548
+ </div>
549
+ <div className="flex flex-col gap-4">
550
+ <Input placeholder="Email" type="email" value={email} onChange={setEmail} required />
551
+ <Input placeholder="Password" type="password" value={pw} onChange={setPw} required />
552
+ <Checkbox checked={remember} onCheckedChange={setRmb} label="Remember me" />
553
+ </div>
554
+ <Button variant="primary" size="lg" className="w-full">Sign in</Button>
555
+ <p className="text-center text-sm text-muted-foreground">
556
+ No account? <a className="text-primary-action">Sign up</a>
557
+ </p>
558
+ </div>
559
+ </main>
560
+ );
561
+ }
562
+ ```
442
563
 
443
- Tabs allow users to switch between different content sections within the same page.
564
+ ### Event list page
444
565
 
445
- Types: Default (text only), Icon (with icon), Notification (with badge).
566
+ ```tsx
567
+ import { Button, SearchInput, Chip, Card, TabGroup } from "@sarunyu/system-one";
568
+ import { useState } from "react";
569
+
570
+ export function EventsPage() {
571
+ const [tab, setTab] = useState("upcoming");
572
+ const [query, setQuery] = useState("");
573
+ const [filter, setFilter] = useState("all");
574
+
575
+ return (
576
+ <main className="mx-auto w-full max-w-[1200px] px-6 md:px-8 py-10 flex flex-col gap-8">
577
+ <header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
578
+ <div className="flex flex-col gap-2">
579
+ <h1>Events</h1>
580
+ <p className="text-muted-foreground">Browse and register</p>
581
+ </div>
582
+ <Button variant="primary" size="md" leftIcon={<PlusIcon />}>New event</Button>
583
+ </header>
584
+
585
+ <TabGroup
586
+ items={[
587
+ { id: "upcoming", title: "Upcoming" },
588
+ { id: "past", title: "Past", notification: 12 },
589
+ ]}
590
+ activeId={tab}
591
+ onChange={setTab}
592
+ />
593
+
594
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
595
+ <SearchInput placeholder="Search events…" value={query} onChange={setQuery} />
596
+ <div className="flex flex-wrap gap-2">
597
+ {["all", "conference", "workshop", "meetup"].map(v => (
598
+ <Chip key={v} label={v} selected={filter === v} onClick={() => setFilter(v)} />
599
+ ))}
600
+ </div>
601
+ </div>
602
+
603
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
604
+ {events.map(e => <Card key={e.id} variant="desktop" {...e} />)}
605
+ </div>
606
+ </main>
607
+ );
608
+ }
609
+ ```
446
610
 
447
- States: Default, Active (selected), Disabled.
611
+ ### Settings form
448
612
 
449
- Sizes: `lg` (default), `md`, `sm`. Always use the same size within one tab group.
613
+ ```tsx
614
+ import { Input, TextArea, Dropdown, DropdownMultiple, DateInput, Button } from "@sarunyu/system-one";
615
+
616
+ export function SettingsPage() {
617
+ return (
618
+ <main className="mx-auto w-full max-w-[640px] px-6 py-10 flex flex-col gap-8">
619
+ <header className="flex flex-col gap-2">
620
+ <h1>Account settings</h1>
621
+ <p className="text-muted-foreground">Manage your profile and preferences</p>
622
+ </header>
623
+
624
+ <section className="flex flex-col gap-6">
625
+ <h2>Profile</h2>
626
+ <Input placeholder="Full name" value={name} onChange={setName} required />
627
+ <Input placeholder="Email" value={email} onChange={setEmail} required />
628
+ <TextArea placeholder="Bio" value={bio} onChange={setBio} showCount maxCount={160} />
629
+ </section>
630
+
631
+ <section className="flex flex-col gap-6">
632
+ <h2>Preferences</h2>
633
+ <Dropdown
634
+ placeholder="Timezone"
635
+ options={timezones}
636
+ value={tz}
637
+ onChange={setTz}
638
+ />
639
+ <DropdownMultiple
640
+ placeholder="Interests"
641
+ options={interests}
642
+ values={picked}
643
+ onChange={setPicked}
644
+ />
645
+ <DateInput placeholder="Birthday" mode="single" value={dob} onChange={setDob} />
646
+ </section>
647
+
648
+ <div className="flex justify-end gap-3">
649
+ <Button variant="outline" size="md">Cancel</Button>
650
+ <Button variant="primary" size="md">Save changes</Button>
651
+ </div>
652
+ </main>
653
+ );
654
+ }
655
+ ```
450
656
 
451
- ### Tag
657
+ ### Data table page
452
658
 
453
- Tags display categories, types, or short metadata. Status Tags communicate process state.
659
+ ```tsx
660
+ import {
661
+ SearchInput, Button, Table, TableRow, TableHeaderCell, TableCell, StatusTag,
662
+ } from "@sarunyu/system-one";
663
+
664
+ export function OrdersPage() {
665
+ return (
666
+ <main className="mx-auto w-full max-w-[1200px] px-6 py-10 flex flex-col gap-6">
667
+ <header className="flex items-center justify-between">
668
+ <h1>Orders</h1>
669
+ <Button variant="primary" size="md">Export</Button>
670
+ </header>
671
+ <div className="flex items-center gap-3">
672
+ <SearchInput placeholder="Search orders…" value={q} onChange={setQ} />
673
+ </div>
674
+ <Table>
675
+ <TableRow header>
676
+ <TableHeaderCell>Order</TableHeaderCell>
677
+ <TableHeaderCell>Customer</TableHeaderCell>
678
+ <TableHeaderCell>Amount</TableHeaderCell>
679
+ <TableHeaderCell>Status</TableHeaderCell>
680
+ </TableRow>
681
+ {orders.map(o => (
682
+ <TableRow key={o.id}>
683
+ <TableCell>#{o.id}</TableCell>
684
+ <TableCell>{o.customer}</TableCell>
685
+ <TableCell>{o.amount}</TableCell>
686
+ <TableCell><StatusTag type={o.status} /></TableCell>
687
+ </TableRow>
688
+ ))}
689
+ </Table>
690
+ </main>
691
+ );
692
+ }
693
+ ```
454
694
 
455
- Tag types: Default (text only), Icon (with icon), Remove (dismissible).
456
- Tag states: Default, Hover/Press, Disabled.
457
- Tag sizes: Large, Small.
695
+ ---
458
696
 
459
- Status Tag variants: `success` — process completed. `processing` — in progress. `hold` — temporarily paused. `stop` — stopped. `error` — failed.
697
+ ## Theming
460
698
 
461
- ### Chip
699
+ Override tokens after importing the stylesheet. **You must override both
700
+ `:root` (light mode) and `.dark` (dark mode) — they are independent.**
462
701
 
463
- Chips are toggleable filter/selection elements. Always use in groups of 2+.
702
+ The hex values below are **placeholders for your brand palette** (shown in
703
+ violet for contrast). They are NOT part of System One — the library's own
704
+ defaults live in `tokens/color.json` (P1 blue). Replace with your own values.
464
705
 
465
- Types: `single` — one selection at a time. `multiple` — multiple selections simultaneously.
706
+ ```css
707
+ /* Light mode — replace with YOUR brand palette */
708
+ :root {
709
+ --primary-action: #7c3aed; /* your brand 600 */
710
+ --primary-action-hover: #6d28d9; /* your brand 700 */
711
+ --primary-action-active: #5b21b6; /* your brand 800 */
712
+ /* -light / -muted derive from --primary-action via color-mix(), so they
713
+ cascade automatically in :root. No need to override them here. */
466
714
 
467
- States: Default, Active, Disabled (Default), Disabled (Active).
715
+ --font-sans: "Inter", sans-serif; /* replace default Noto Sans Thai */
716
+ }
468
717
 
469
- Sizes: `large`, `medium`, `small`.
718
+ /* Dark mode — override separately. The library's .dark block hard-codes
719
+ these to a blue palette, so setting only :root leaves dark mode unchanged. */
720
+ .dark {
721
+ --primary-action: #a78bfa; /* your brand 400 */
722
+ --primary-action-hover: #c4b5fd; /* your brand 300 */
723
+ --primary-action-active: #8b5cf6; /* your brand 500 */
724
+ --primary-action-light: color-mix(in srgb, #a78bfa 10%, transparent);
725
+ --primary-action-muted: color-mix(in srgb, #a78bfa 15%, transparent);
726
+ }
727
+ ```
470
728
 
471
- ---
729
+ Every component that uses `--primary-action` will pick up your brand color
730
+ in both modes.
472
731
 
473
- ## Design Rules
732
+ **Do not try to override `--radius`.** It is a legacy var kept for
733
+ compatibility and is not referenced by any Tailwind utility. To change
734
+ corner rounding per element, apply a different utility (`rounded-lg`
735
+ → `rounded-xl`). To change it globally, fork the library.
474
736
 
475
- ### Button Rules
737
+ ### Example: Wealth theme (Midnight Blue + Burnt Sienna)
476
738
 
477
- - Maximum width is 343px (`max-w-[343px]`). Never remove or override. Do not detach the component.
478
- - Do not manually recreate or modify padding (`py-[10px] px-[16px]`) or border-radius (`rounded-[8px]`).
479
- - Use Primary only once per context. Never place two Primary buttons side-by-side. Use Outline/Secondary for supporting actions.
480
- - Hug width is correct for short labels only. For long labels, use Fill width or set explicit max-w-[343px].
739
+ The design system ships with ramps for alternate brands. To switch the
740
+ whole UI to the Wealth palette, override the brand semantic tokens:
481
741
 
482
- ### Input Rules
742
+ ```css
743
+ :root {
744
+ /* Primary brand → Midnight Blue */
745
+ --bg-brand-primary: var(--fill-midnight-blue-500);
746
+ --bg-brand-hover: var(--fill-midnight-blue-600);
747
+ --bg-brand-pressed: var(--fill-midnight-blue-700);
748
+ --bg-brand-primary-light: var(--fill-midnight-blue-50);
749
+ --text-brand-primary: var(--fill-midnight-blue-500);
750
+ --text-brand-link-primary: var(--fill-midnight-blue-500);
751
+ --border-brand-primary: var(--fill-midnight-blue-500);
752
+
753
+ /* Secondary brand → Burnt Sienna */
754
+ --bg-brand-secondary: var(--fill-burnt-sienna-500);
755
+ --bg-brand-secondary-light: var(--fill-burnt-sienna-50);
756
+ --text-brand-secondary: var(--fill-burnt-sienna-500);
757
+ --border-brand-secondary: var(--fill-burnt-sienna-500);
758
+
759
+ /* Keep the legacy --primary-action alias in sync so existing components track */
760
+ --primary-action: var(--bg-brand-primary);
761
+ --primary-action-hover: var(--bg-brand-hover);
762
+ --primary-action-active: var(--bg-brand-pressed);
763
+ }
483
764
 
484
- - Never override input border or background colors manually — use built-in state variants (Default, Focus, Error, Disabled).
485
- - Do not recreate input styles manually or adjust gap/height outside the component's tokens.
486
- - Use Dropdown Tags only for multi-select. For single-select, use standard Dropdown.
487
- - Keep labelText short enough to fit on one line. Long labels break the floating label layout.
488
- - Do not show Helper Text and Error Message simultaneously. Error state replaces helper text.
489
- - Use Option List only when there are multiple selectable values.
490
- - Always follow the system date format (DD MMM YYYY). Do not mix Thai month names with C.E. year or reorder day/month/year.
765
+ .dark {
766
+ --bg-brand-primary: var(--fill-midnight-blue-400);
767
+ --bg-brand-hover: var(--fill-midnight-blue-300);
768
+ --bg-brand-pressed: var(--fill-midnight-blue-500);
769
+ --bg-brand-primary-light: color-mix(in srgb, var(--fill-midnight-blue-400) 15%, transparent);
770
+ --text-brand-primary: var(--fill-midnight-blue-300);
771
+ --text-brand-link-primary: var(--fill-midnight-blue-300);
772
+ --border-brand-primary: var(--fill-midnight-blue-400);
773
+
774
+ --bg-brand-secondary: var(--fill-burnt-sienna-400);
775
+ --bg-brand-secondary-light: color-mix(in srgb, var(--fill-burnt-sienna-400) 15%, transparent);
776
+ --text-brand-secondary: var(--fill-burnt-sienna-300);
777
+ --border-brand-secondary: var(--fill-burnt-sienna-400);
778
+
779
+ --primary-action: var(--bg-brand-primary);
780
+ --primary-action-hover: var(--bg-brand-hover);
781
+ --primary-action-active: var(--bg-brand-pressed);
782
+ }
783
+ ```
491
784
 
492
- ### Tab Rules
785
+ Override primitives (`--fill-*`) only if you want to change the ramp itself
786
+ across all consumers. Override semantics (`--bg-brand-*`, `--text-brand-*`)
787
+ to switch which ramp the brand uses without touching anything else.
493
788
 
494
- - Do not override padding, gap, border-radius, element order, or colors inside the tab bar.
495
- - All tabs in one group must use the same size prop. Never mix sizes.
496
- - Do not add extra gap or margin between tabs — use built-in spacing tokens only.
789
+ ## If you need a component the library doesn't provide
497
790
 
498
- ### Tag Rules
791
+ Build it with tokens, not hard-coded colors. Example — a custom badge:
499
792
 
500
- - Do not override height, padding, or layout of Tag or StatusTag.
501
- - All tags in the same context must use the same size.
502
- - Do not reorder internal elements (icon, label, badge) inside Tag or StatusTag.
503
- - Always use the correct StatusTag variant for its semantic meaning. Never override the color.
793
+ ```tsx
794
+ <span className="inline-flex items-center gap-1 rounded-full bg-primary-action-light text-primary-action px-2 py-0.5 text-xs">
795
+ {text}
796
+ </span>
797
+ ```
504
798
 
505
- ### Chip Rules
799
+ Rules for custom components:
800
+ - Colors → always from the token table above.
801
+ - Radius → one of `rounded-sm`/`rounded-md`/`rounded-lg`/`rounded-xl`/`rounded-full`.
802
+ - Shadow → `shadow-card` for resting surfaces, `shadow-popover` for floating.
803
+ - Border → `border-border` (or `border-divider` for light separators).
804
+ - Typography → use `<h1>`…`<h4>` and body text inherits from `--foreground`.
506
805
 
507
- - Keep chip labels short and single-purpose. Long labels cause overflow.
508
- - Do not override padding, gap, radius, height, element order, or colors.
509
- - All chips in the same group must share the same size and spacing.
510
- - Use Chip only for groups with 2+ options. For single options, use a toggle or checkbox instead.
806
+ If the thing you're building is conceptually a button/input/tag/chip/tab/card/table **do not build a custom one. Use the library's component.** The only reason to build custom is when the library truly doesn't cover the concept (e.g. a bespoke hero section, a custom chart, a breadcrumb).