@sarunyu/system-one 4.5.1 → 4.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +262 -0
- package/dist/index.cjs +20 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -18
- package/dist/index.js.map +1 -1
- package/dist/src/components/card.d.ts +1 -1
- package/dist/src/components/card.d.ts.map +1 -1
- package/package.json +5 -1
package/DESIGN.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# DESIGN.md — @sarunyu/system-one
|
|
2
|
+
|
|
3
|
+
Visual identity guide for AI coding tools (Figma Make, Lovable, v0, Claude).
|
|
4
|
+
Read this before generating any UI. For component API and token class names, read `AGENTS.md` and `llms.txt`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
**Personality:** Corporate — structured, trustworthy, data-dense, professional.
|
|
11
|
+
**Feel:** Clean surfaces, strong hierarchy, controlled whitespace. Never playful, never decorative.
|
|
12
|
+
**Target context:** Enterprise web applications — dashboards, data tables, form-heavy workflows, financial UIs.
|
|
13
|
+
|
|
14
|
+
Design decisions lean toward clarity over expression. Every element earns its space.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Colors
|
|
19
|
+
|
|
20
|
+
### Brand palette
|
|
21
|
+
|
|
22
|
+
| Role | Light mode | Dark mode | Tailwind class |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| Primary action (buttons, links, active) | `#0a6ee7` | `#4792ed` | `bg-primary-action` / `text-primary-action` |
|
|
25
|
+
| Primary hover | `#095ec4` | `#91bef4` | `bg-primary-action-hover` |
|
|
26
|
+
| Primary active/pressed | `#074ea4` | — | `bg-primary-action-active` |
|
|
27
|
+
| Primary light tint | `#dae9fb` | — | `bg-primary-action-light` |
|
|
28
|
+
| Secondary accent (highlights, badges) | `#eb6101` | — | use `bg-visual-orange` |
|
|
29
|
+
|
|
30
|
+
### Surfaces
|
|
31
|
+
|
|
32
|
+
| Role | Value | Tailwind class |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Page background | `#ffffff` (light) / `#0f172a` (dark) | `bg-background` |
|
|
35
|
+
| Card / panel | `#ffffff` (light) / `#1e293b` (dark) | `bg-card` |
|
|
36
|
+
| Subtle secondary surface | `#f9fafb` | `bg-default-secondary` |
|
|
37
|
+
| Input background | `#f9fafb` | `bg-muted` |
|
|
38
|
+
| Hover state | `#f3f4f6` | `bg-hover-bg` |
|
|
39
|
+
| Disabled | `#f3f4f6` | `bg-disabled-bg` |
|
|
40
|
+
|
|
41
|
+
### Status colors
|
|
42
|
+
|
|
43
|
+
| Status | Fill | Light surface | Tailwind class |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| Danger / error | `#fb2c36` | `#fef2f2` | `bg-destructive` / `bg-error-bg` |
|
|
46
|
+
| Warning | `#f0b100` | `#fefce8` | `bg-warning` / `bg-warning-light` |
|
|
47
|
+
| Success | `#00c951` | `#f0fdf4` | `bg-success` / `bg-success-bg` |
|
|
48
|
+
| Info | blue-500 | blue-50 | `bg-info` / `bg-info-light` |
|
|
49
|
+
|
|
50
|
+
### Borders
|
|
51
|
+
|
|
52
|
+
Default border: `1px solid` — `border-border` (`#e5e7eb` light).
|
|
53
|
+
Dividers between sections: `border-divider` (lighter than border).
|
|
54
|
+
Never hard-code border colors.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Typography
|
|
59
|
+
|
|
60
|
+
**Font family:** `Noto Sans Thai`, sans-serif — loaded via Google Fonts (wght 400 / 500 / 600).
|
|
61
|
+
Override globally: `--font-sans: "Your Font", sans-serif;`
|
|
62
|
+
|
|
63
|
+
### Heading scale (pre-styled — do NOT add Tailwind text-* overrides)
|
|
64
|
+
|
|
65
|
+
| Tag | Size | Weight | Use for |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `<h1>` | 24px | 600 | Page title |
|
|
68
|
+
| `<h2>` | 20px | 600 | Section title |
|
|
69
|
+
| `<h3>` | 18px | 600 | Card / panel heading |
|
|
70
|
+
| `<h4>` | 16px | 500 | Sub-section, field group label |
|
|
71
|
+
|
|
72
|
+
### Body scale
|
|
73
|
+
|
|
74
|
+
| Class | Size | Use for |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| (default, no class) | 16px | Body text, form values |
|
|
77
|
+
| `text-sm` | 14px | Secondary labels, table cells, helper text |
|
|
78
|
+
| `text-xs` | 12px | Captions, timestamps, fine print |
|
|
79
|
+
|
|
80
|
+
### Color rules
|
|
81
|
+
|
|
82
|
+
- Body / headings: `text-foreground`
|
|
83
|
+
- Secondary labels: `text-muted-foreground`
|
|
84
|
+
- Disabled: `text-disabled`
|
|
85
|
+
- Brand / link: `text-primary-action`
|
|
86
|
+
- Error: `text-destructive`
|
|
87
|
+
|
|
88
|
+
Never use `text-gray-*`, `text-blue-*`, or hex colors for text.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Spacing
|
|
93
|
+
|
|
94
|
+
**Base unit:** 4px. All spacing values are multiples of 4px.
|
|
95
|
+
|
|
96
|
+
### Layout spacing
|
|
97
|
+
|
|
98
|
+
| Context | Value | Tailwind |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| Page horizontal padding | 24px (desktop) / 16px (mobile) | `px-6 md:px-8` |
|
|
101
|
+
| Page vertical padding | 40px | `py-10` |
|
|
102
|
+
| Between major page sections | 48px | `gap-12` |
|
|
103
|
+
| Between related content blocks | 24px | `gap-6` |
|
|
104
|
+
| Between items in a cluster | 16px | `gap-4` |
|
|
105
|
+
| Inside a card / panel | 24px | `p-6` |
|
|
106
|
+
| Dense toolbar / header row | 12–16px | `gap-3` or `gap-4` |
|
|
107
|
+
|
|
108
|
+
### Content width
|
|
109
|
+
|
|
110
|
+
| Layout | Max-width |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Dashboard / wide content | `max-w-[1200px]` |
|
|
113
|
+
| Standard content page | `max-w-[960px]` |
|
|
114
|
+
| Form / narrow content | `max-w-[640px]` |
|
|
115
|
+
| Dialog / modal body | `max-w-[480px]` |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Border radius
|
|
120
|
+
|
|
121
|
+
Default: **4px** (`rounded`). Corporate contexts use tight radius — avoid large curves.
|
|
122
|
+
|
|
123
|
+
| Usage | Value | Tailwind |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| Default (inputs, cards, buttons) | 4px | `rounded` |
|
|
126
|
+
| Tags, badges, chips | 6px | `rounded-md` |
|
|
127
|
+
| Pills | 999px | `rounded-full` |
|
|
128
|
+
| Modals, large panels | 8px | `rounded-lg` |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Elevation / shadow
|
|
133
|
+
|
|
134
|
+
Use sparingly. Corporate UIs prefer border separation over heavy elevation.
|
|
135
|
+
|
|
136
|
+
| Level | Usage | Tailwind |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| None | Flat cards separated by border | — |
|
|
139
|
+
| Subtle | Floating cards, content panels | `shadow-sm` |
|
|
140
|
+
| Card | Event cards, interactive tiles | `shadow-card` |
|
|
141
|
+
| Popover | Dropdowns, menus, tooltips | `shadow-popover` |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Layout patterns
|
|
146
|
+
|
|
147
|
+
### Standard dashboard page
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<main className="mx-auto w-full max-w-[1200px] px-6 md:px-8 py-10 flex flex-col gap-12">
|
|
151
|
+
<header className="flex items-start justify-between gap-4">
|
|
152
|
+
<div className="flex flex-col gap-1">
|
|
153
|
+
<h1>Page Title</h1>
|
|
154
|
+
<p className="text-sm text-muted-foreground">Supporting description</p>
|
|
155
|
+
</div>
|
|
156
|
+
<Button variant="primary" size="md">Primary action</Button>
|
|
157
|
+
</header>
|
|
158
|
+
|
|
159
|
+
<section className="flex flex-col gap-6">
|
|
160
|
+
<h2>Section</h2>
|
|
161
|
+
{/* content */}
|
|
162
|
+
</section>
|
|
163
|
+
</main>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Sidebar + content
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<div className="flex min-h-screen bg-background">
|
|
170
|
+
<aside className="w-64 shrink-0 border-r border-divider bg-card p-6 flex flex-col gap-2">
|
|
171
|
+
{/* navigation items */}
|
|
172
|
+
</aside>
|
|
173
|
+
<main className="flex-1 px-8 py-10 flex flex-col gap-12">
|
|
174
|
+
{/* page content */}
|
|
175
|
+
</main>
|
|
176
|
+
</div>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Card grid (data tiles)
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
183
|
+
{items.map(item => (
|
|
184
|
+
<Card key={item.id} title={item.title} description={item.description} />
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Form page
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
<main className="mx-auto w-full max-w-[640px] px-6 py-10 flex flex-col gap-8">
|
|
193
|
+
<h1>Form Title</h1>
|
|
194
|
+
<section className="flex flex-col gap-4">
|
|
195
|
+
<Input placeholder="Field label" value={v} onChange={setV} />
|
|
196
|
+
<Input placeholder="Another field" value={v2} onChange={setV2} />
|
|
197
|
+
<Dropdown placeholder="Select option" options={opts} value={sel} onChange={setSel} />
|
|
198
|
+
</section>
|
|
199
|
+
<div className="flex justify-end gap-3">
|
|
200
|
+
<Button variant="outline" size="md">Cancel</Button>
|
|
201
|
+
<Button variant="primary" size="md">Submit</Button>
|
|
202
|
+
</div>
|
|
203
|
+
</main>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Data table page
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
<main className="mx-auto w-full max-w-[1200px] px-6 md:px-8 py-10 flex flex-col gap-6">
|
|
210
|
+
<header className="flex items-center justify-between gap-4">
|
|
211
|
+
<h1>Table Title</h1>
|
|
212
|
+
<div className="flex items-center gap-3">
|
|
213
|
+
<SearchInput placeholder="Search..." value={q} onChange={setQ} />
|
|
214
|
+
<Button variant="primary" size="md">Add new</Button>
|
|
215
|
+
</div>
|
|
216
|
+
</header>
|
|
217
|
+
<Table>
|
|
218
|
+
<TableRow header>
|
|
219
|
+
<TableHeaderCell>Column A</TableHeaderCell>
|
|
220
|
+
<TableHeaderCell>Column B</TableHeaderCell>
|
|
221
|
+
</TableRow>
|
|
222
|
+
{rows.map(row => (
|
|
223
|
+
<TableRow key={row.id}>
|
|
224
|
+
<TableCell>{row.a}</TableCell>
|
|
225
|
+
<TableCell>{row.b}</TableCell>
|
|
226
|
+
</TableRow>
|
|
227
|
+
))}
|
|
228
|
+
</Table>
|
|
229
|
+
</main>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Visual do / don't
|
|
235
|
+
|
|
236
|
+
| Do | Don't |
|
|
237
|
+
|---|---|
|
|
238
|
+
| Use `bg-background`, `bg-card` for surfaces | Hard-code `#ffffff` or `bg-white` |
|
|
239
|
+
| Use `text-foreground` for body text | Use `text-gray-900` or `text-black` |
|
|
240
|
+
| Use `border-border` for dividers | Use `border-gray-200` |
|
|
241
|
+
| Use `rounded` (4px) by default | Use `rounded-xl` or `rounded-2xl` on data elements |
|
|
242
|
+
| Keep whitespace controlled: `gap-4`–`gap-6` inside sections | Over-pad with `gap-10`+ inside cards |
|
|
243
|
+
| One `variant="primary"` Button per context | Two or more primary buttons side by side |
|
|
244
|
+
| Separate sections with `gap-12` or a `border-divider` line | Use heavy drop shadows to separate sections |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Component library
|
|
249
|
+
|
|
250
|
+
This design system ships 24 pre-built components. **Always use them — never recreate with raw HTML.**
|
|
251
|
+
|
|
252
|
+
Key components: `Button`, `Input`, `TextArea`, `SearchInput`, `Dropdown`, `DropdownMultiple`, `Checkbox`, `Radio`, `Toggle`, `DateInput`, `TimeInput`, `Tag`, `StatusTag`, `Chip`, `TabGroup`, `Card`, `Table` + `TableRow` + `TableHeaderCell` + `TableCell`, `Modal`, `BottomSheet`, `Alert`, `Toast`, `ToastStack`, `Notification`, `Badge`.
|
|
253
|
+
|
|
254
|
+
For props, variants, and usage rules → read `AGENTS.md` and `llms.txt` in this package.
|
|
255
|
+
|
|
256
|
+
### Setup (required)
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
import "@sarunyu/system-one/styles.css";
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
If the screen looks unstyled, this import is missing.
|
package/dist/index.cjs
CHANGED
|
@@ -382,6 +382,22 @@ const Alert = React.forwardRef(function Alert2({ status = "normal", message, mul
|
|
|
382
382
|
);
|
|
383
383
|
});
|
|
384
384
|
Alert.displayName = "Alert";
|
|
385
|
+
const MOBILE_BREAKPOINT = 768;
|
|
386
|
+
function useIsMobile() {
|
|
387
|
+
const [isMobile, setIsMobile] = React__namespace.useState(
|
|
388
|
+
void 0
|
|
389
|
+
);
|
|
390
|
+
React__namespace.useEffect(() => {
|
|
391
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
392
|
+
const onChange = () => {
|
|
393
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
394
|
+
};
|
|
395
|
+
mql.addEventListener("change", onChange);
|
|
396
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
397
|
+
return () => mql.removeEventListener("change", onChange);
|
|
398
|
+
}, []);
|
|
399
|
+
return !!isMobile;
|
|
400
|
+
}
|
|
385
401
|
function BannerMedia({ src, alt }) {
|
|
386
402
|
if (!src) {
|
|
387
403
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", className: "absolute inset-0 bg-muted" });
|
|
@@ -489,7 +505,7 @@ const tagConfig = {
|
|
|
489
505
|
};
|
|
490
506
|
const Card = React.forwardRef(function Card2({
|
|
491
507
|
variant = "default",
|
|
492
|
-
size
|
|
508
|
+
size: sizeProp,
|
|
493
509
|
children,
|
|
494
510
|
title,
|
|
495
511
|
locked = true,
|
|
@@ -513,9 +529,11 @@ const Card = React.forwardRef(function Card2({
|
|
|
513
529
|
// live
|
|
514
530
|
duration
|
|
515
531
|
}, ref) {
|
|
532
|
+
const isMobile = useIsMobile();
|
|
533
|
+
const size = sizeProp ?? (isMobile ? "mobile" : "desktop");
|
|
516
534
|
const bannerSrc = image ?? "";
|
|
517
535
|
if (variant === "default") {
|
|
518
|
-
const shellPadding = size === "desktop" ? "p-4" :
|
|
536
|
+
const shellPadding = size === "desktop" ? "p-4" : "p-3";
|
|
519
537
|
const shellRadius = size === "mobile" ? "rounded-[6px]" : "rounded-[8px]";
|
|
520
538
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
521
539
|
"div",
|
|
@@ -1400,22 +1418,6 @@ const Chip = React.forwardRef(function Chip2({
|
|
|
1400
1418
|
);
|
|
1401
1419
|
});
|
|
1402
1420
|
Chip.displayName = "Chip";
|
|
1403
|
-
const MOBILE_BREAKPOINT = 768;
|
|
1404
|
-
function useIsMobile() {
|
|
1405
|
-
const [isMobile, setIsMobile] = React__namespace.useState(
|
|
1406
|
-
void 0
|
|
1407
|
-
);
|
|
1408
|
-
React__namespace.useEffect(() => {
|
|
1409
|
-
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
1410
|
-
const onChange = () => {
|
|
1411
|
-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
1412
|
-
};
|
|
1413
|
-
mql.addEventListener("change", onChange);
|
|
1414
|
-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
1415
|
-
return () => mql.removeEventListener("change", onChange);
|
|
1416
|
-
}, []);
|
|
1417
|
-
return !!isMobile;
|
|
1418
|
-
}
|
|
1419
1421
|
function Drawer({
|
|
1420
1422
|
...props
|
|
1421
1423
|
}) {
|