@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 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 = "desktop",
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" : size === "tablet" ? "p-3" : "p-2.5";
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
  }) {