@sarunyu/system-one 3.0.3 → 4.0.1
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 +102 -0
- package/README.md +75 -39
- package/dist/index.cjs +1103 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1105 -254
- package/dist/index.js.map +1 -1
- package/dist/src/components/card.d.ts +34 -11
- package/dist/src/components/card.d.ts.map +1 -1
- package/dist/src/components/checkbox.d.ts +28 -0
- package/dist/src/components/checkbox.d.ts.map +1 -0
- package/dist/src/components/radio.d.ts +27 -0
- package/dist/src/components/radio.d.ts.map +1 -0
- package/dist/src/components/table.d.ts +50 -0
- package/dist/src/components/table.d.ts.map +1 -0
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/style.css +1 -1
- package/llms.txt +565 -269
- package/package.json +4 -2
- package/dist/src/components/layout.d.ts +0 -50
- package/dist/src/components/layout.d.ts.map +0 -1
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.
|
|
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
|
-
|
|
32
|
+
Peer dep: `react >= 18`.
|
|
12
33
|
|
|
13
|
-
|
|
34
|
+
## Setup
|
|
14
35
|
|
|
15
36
|
```tsx
|
|
16
|
-
// app/layout.tsx
|
|
17
|
-
|
|
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
|
|
43
|
+
No provider, no wrapper. Components ship with `"use client"`.
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
## Dark mode
|
|
23
46
|
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
import "@sarunyu/system-one/styles.css"
|
|
50
|
+
<html className={isDark ? "dark" : ""}>
|
|
34
51
|
```
|
|
35
52
|
|
|
36
|
-
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Available imports
|
|
37
56
|
|
|
38
57
|
```tsx
|
|
39
58
|
import {
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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={
|
|
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="
|
|
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
|
-
|
|
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
|
|
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="
|
|
80
|
-
<Input placeholder="
|
|
81
|
-
<Input placeholder="Bio" showCount maxCount={160} />
|
|
82
|
-
<Input
|
|
83
|
-
<Input forceState="
|
|
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
|
-
|
|
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"
|
|
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`,
|
|
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
|
|
245
|
+
Search field with icon + clear button.
|
|
107
246
|
|
|
108
247
|
```tsx
|
|
109
|
-
<SearchInput placeholder="Search events
|
|
110
|
-
<SearchInput size="sm" placeholder="Filter
|
|
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
|
|
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={
|
|
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
|
-
###
|
|
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
|
-
|
|
314
|
+
---
|
|
158
315
|
|
|
159
|
-
|
|
160
|
-
Sizes: `large` (default) | `small`
|
|
316
|
+
### Tag
|
|
161
317
|
|
|
162
|
-
|
|
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="
|
|
168
|
-
<Tag text="
|
|
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
|
|
172
|
-
|
|
173
|
-
---
|
|
330
|
+
Props: `text`, `variant`, `size` (`"large"` default | `"small"`), `close`, `onClose`, `className`.
|
|
174
331
|
|
|
175
332
|
### StatusTag
|
|
176
333
|
|
|
177
|
-
|
|
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="
|
|
184
|
-
<StatusTag type="hold" />
|
|
340
|
+
<StatusTag type="processing" text="Uploading…" />
|
|
185
341
|
<StatusTag type="error" />
|
|
186
342
|
```
|
|
187
343
|
|
|
188
|
-
|
|
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
|
|
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
|
-
|
|
203
|
-
<div className="flex gap-2">
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
<
|
|
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
|
|
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
|
|
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",
|
|
228
|
-
{ id: "history",
|
|
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={
|
|
232
|
-
onChange={
|
|
385
|
+
activeId={active}
|
|
386
|
+
onChange={setActive}
|
|
233
387
|
size="md"
|
|
234
388
|
/>
|
|
235
389
|
```
|
|
236
390
|
|
|
237
|
-
Props: `items` (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
417
|
+
`tagStatus`: `"not-registered"` | `"registered"` | `"full"`.
|
|
268
418
|
|
|
269
|
-
|
|
419
|
+
### Table
|
|
270
420
|
|
|
271
|
-
|
|
272
|
-
Variants: `popover` (default) | `modal`
|
|
421
|
+
Data tables. Compose with TableRow + TableHeaderCell + TableCell.
|
|
273
422
|
|
|
274
423
|
```tsx
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
471
|
+
## Layout — you design it
|
|
324
472
|
|
|
325
|
-
|
|
473
|
+
The library ships zero layout components. Compose page structure with plain
|
|
474
|
+
Tailwind. Example scaffolds:
|
|
326
475
|
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
//
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
##
|
|
513
|
+
## DO / DON'T
|
|
413
514
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
531
|
+
### Login form
|
|
438
532
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
564
|
+
### Event list page
|
|
444
565
|
|
|
445
|
-
|
|
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
|
-
|
|
611
|
+
### Settings form
|
|
448
612
|
|
|
449
|
-
|
|
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
|
-
###
|
|
657
|
+
### Data table page
|
|
452
658
|
|
|
453
|
-
|
|
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
|
-
|
|
456
|
-
Tag states: Default, Hover/Press, Disabled.
|
|
457
|
-
Tag sizes: Large, Small.
|
|
695
|
+
---
|
|
458
696
|
|
|
459
|
-
|
|
697
|
+
## Theming
|
|
460
698
|
|
|
461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
+
--font-sans: "Inter", sans-serif; /* replace default Noto Sans Thai */
|
|
716
|
+
}
|
|
468
717
|
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
737
|
+
### Example: Wealth theme (Midnight Blue + Burnt Sienna)
|
|
476
738
|
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
-
|
|
486
|
-
-
|
|
487
|
-
-
|
|
488
|
-
-
|
|
489
|
-
-
|
|
490
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
791
|
+
Build it with tokens, not hard-coded colors. Example — a custom badge:
|
|
499
792
|
|
|
500
|
-
|
|
501
|
-
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|