@suphark/ui 0.3.1 → 0.3.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
4
 
5
+ ## [0.3.3] — 2026-09-19
6
+
7
+ ### Fixed
8
+
9
+ - `FormDialog`: จำกัดความสูงไว้ในจอ (`max-h-[calc(100dvh-2rem)]`) header/footer ตรึง เฉพาะเนื้อฟอร์มเลื่อน — ฟอร์มยาวไม่ล้นในหน้าต่างเตี้ย (รายงานจาก Pi Auth หน้าลงทะเบียนแอป); `contentClassName` ยังใช้กำหนดความกว้างได้เหมือนเดิม
10
+
11
+ ### Added
12
+
13
+ - export type `DateRange` (react-day-picker) จาก barrel — แอปใช้กับ `DateRangePicker` ได้โดยไม่ต้องติดตั้ง react-day-picker
14
+
15
+ ### Changed (showcase เท่านั้น — ไม่กระทบ API)
16
+
17
+ - หน้า **/components** แบบ ui.shadcn.com/docs/components: 74 หน้า (ทะเบียน `apps/showcase/src/docs/registry.ts` + demo ต่อไฟล์ใน `src/docs/demos/`) แต่ละหน้ามีตัวอย่างจริง/โค้ด (อ่าน source ของ demo ตอน build)/import line/หมายเหตุ/ลิงก์ shadcn+source, เมนูซ้ายตามหมวด, prev/next
18
+
19
+ - `DesignSystemShowcase` รับ prop `links?: { shell }` และตัดหัวใหญ่/badge ออก (แอปเป็นคนมีหัวเดียว); แท็บป้ายสั้น (จอแคบเลื่อนแนวนอน 1 แถว — แก้แท็บทับหัวเรื่องเพราะ `group-data-horizontal/tabs:h-8`); hero ไม่มีกรอบ + `HeroSpecimen` (component จริง + สวิตช์ --brand); CardTitle/CardDescription/หัว section ใหญ่ขึ้น 1 ขั้น; h2 ภาษาไทยอย่างเดียว; ปุ่ม "ตัวอย่างหน้าจอ" ชี้ไป `/shell`
20
+ - ช่องค้นหาในหน้า Overview ค้นได้จริง: `search-index.ts` (26 หัวข้อ) → สลับแท็บ + เลื่อนไป anchor (`id` บนทุก section หลัก) + empty state
21
+
22
+ ## [0.3.2] — 2026-09-18
23
+
24
+ ### Fixed
25
+
26
+ - `copyText`/`selectContents` export จาก barrel หลักด้วย (แอปใช้ทำปุ่มคัดลอกเองได้)
27
+ - `EnvSnippet`: ค่ายาว (client secret) ล้นออกนอก dialog — ตัดบรรทัดในกรอบ (`whitespace-pre-wrap break-all` + `min-w-0`) แทน scroll แนวนอน (รายงานจากหน้า Register app ของ Pi Auth)
28
+ - `EnvSnippet` / `SecretReveal`: ปุ่มคัดลอกเงียบเมื่อ `navigator.clipboard` ถูกบล็อก (webview/embedded browser เช่น browser ใน Claude desktop) — เพิ่ม `copyText()` (`@suphark/ui/lib/copy-text`) ที่ fallback ไป `execCommand("copy")`; ถ้ายังไม่ได้ EnvSnippet จะเลือกข้อความให้และบอกให้กด Ctrl+C (`UiText.copyFailed` optional)
29
+
5
30
  ## [0.3.1] — 2026-09-18
6
31
 
7
32
  ### Changed
package/README.md CHANGED
@@ -4,6 +4,8 @@ Design system กลางของ Suphark apps (Pi Auth, DueBell, Pi PDF, supp
4
4
 
5
5
  แพ็กเกจส่ง **source (.ts/.tsx) ไม่ build** — แอป Next.js transpile เองผ่าน `transpilePackages`
6
6
 
7
+ ดูตัวอย่างสด: **https://ui.suphark.com** — `/components` (ตัวอย่าง + โค้ดทุก component), `/design-system` (กติกา), `/shell` (AppShell), `/brand`
8
+
7
9
  ## ติดตั้ง
8
10
 
9
11
  ```bash
@@ -118,6 +120,17 @@ export default async function Layout({ children }) {
118
120
  - `<UserMenu>` children คือรายการเมนู: `ThemeMenuItems` มี `DropdownMenuGroup` + หัว "ธีม" ในตัวแล้ว (ปรับข้อความด้วย `label`/`labels`) ไม่ต้องห่อเพิ่ม; รายการอื่นใส่ `DropdownMenuItem`/`DropdownMenuGroup` เอง
119
121
  - ตัวอย่างรันได้ที่ `apps/showcase/src/app/shell`
120
122
 
123
+ ### Override contract — แอปปรับได้แค่นี้
124
+
125
+ | ทำได้ | ทำไม่ได้ |
126
+ |---|---|
127
+ | `--brand` / `--brand-foreground` (inline บน wrapper หรือ `data-brand`) | แก้ semantic token (`--primary`, `--background`, `--muted`, `--destructive` …) ตรง ๆ |
128
+ | `--font-sans` / `--font-mono` ใน `@theme` ของแอป | แก้ `--sidebar-*`, `--chart-*` |
129
+ | `--radius` (ค่าเดียว radius ทั้งหมดคำนวณจากตัวนี้) | เปลี่ยนวิธีสลับธีม (ต้องใช้ `ThemeProvider` ของแพ็กเกจ) |
130
+ | `className` เพื่อจัด layout | `className` เปลี่ยนสี/ขนาดตัวอักษรของ primitive |
131
+
132
+ กติกาฉบับเต็มสำหรับ agent/CLAUDE.md ของแอป: [`docs/agent-rules.md`](../../docs/agent-rules.md)
133
+
121
134
  ### สีแบรนด์ต่อ tenant/แอป (`--brand`)
122
135
 
123
136
  ตั้ง `--brand` / `--brand-foreground` inline บน wrapper (หรือใส่ `data-brand`) แล้ว `primary`/`ring`/`sidebar-primary` ข้างในตามไปด้วย — ค่าเริ่มต้นไม่ตั้ง `--brand` (primary = slate) ดูตัวอย่างที่ `apps/showcase/src/app/brand`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/ui",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import { Badge } from "@suphark/ui/components/ui/badge";
4
3
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@suphark/ui/components/ui/tabs";
5
4
  import {
6
5
  Boxes,
@@ -22,54 +21,55 @@ import { ShowcaseOverlaysNavigation } from "./sections/showcase-overlays-navigat
22
21
  import { ShowcaseOverview } from "./sections/showcase-overview";
23
22
  import { ShowcasePrimitives } from "./sections/showcase-primitives";
24
23
 
25
- export function DesignSystemShowcase() {
24
+ export function DesignSystemShowcase({
25
+ links,
26
+ }: {
27
+ /** ลิงก์ไปหน้าตัวอย่างของแอป showcase (เช่น /shell) — ไม่ส่ง = ซ่อนปุ่ม */
28
+ links?: { shell?: string };
29
+ } = {}) {
26
30
  const [activeTab, setActiveTab] = React.useState("overview");
27
31
  const [searchQuery, setSearchQuery] = React.useState("");
28
32
 
33
+ /** สลับแท็บแล้วเลื่อนไปหัวข้อ — เลื่อนใน effect หลังแท็บใหม่ render แล้ว (ตำแหน่งจึงถูก) ชดเชย header ที่ sticky */
34
+ const [pendingAnchor, setPendingAnchor] = React.useState<string | null>(null);
35
+ const jumpTo = React.useCallback((tab: string, anchor?: string) => {
36
+ setActiveTab(tab);
37
+ setPendingAnchor(anchor ?? "__top__");
38
+ }, []);
39
+ React.useEffect(() => {
40
+ if (!pendingAnchor) return;
41
+ // หน่วงเล็กน้อยให้ Tabs โฟกัส trigger ใหม่เสร็จก่อน (การโฟกัสเลื่อนหน้าไปที่แถบแท็บ) แล้วค่อยเลื่อนไปหัวข้อ
42
+ const timer = window.setTimeout(() => {
43
+ if (pendingAnchor === "__top__") {
44
+ window.scrollTo({ top: 0, behavior: "smooth" });
45
+ setPendingAnchor(null);
46
+ return;
47
+ }
48
+ const el = document.getElementById(pendingAnchor);
49
+ if (!el) {
50
+ setPendingAnchor(null);
51
+ return;
52
+ }
53
+ el.style.scrollMarginTop = "88px"; // ชดเชย header ที่ sticky
54
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
55
+ setPendingAnchor(null);
56
+ }, 80);
57
+ return () => window.clearTimeout(timer);
58
+ }, [pendingAnchor]);
59
+
29
60
  const tabItems = [
30
- { value: "overview", label: "ภาพรวม (Overview)", icon: LayoutGrid },
31
- { value: "foundations", label: "Foundations & Tokens", icon: Palette },
32
- { value: "primitives", label: "Core Primitives", icon: Boxes },
33
- { value: "forms", label: "Forms & Inputs", icon: Layers },
34
- { value: "data-display", label: "Data Display & States", icon: Zap },
35
- { value: "overlays", label: "Overlays & Navigation", icon: Compass },
36
- { value: "feedback", label: "Feedback & Alerts", icon: ShieldAlert },
37
- { value: "guidelines-recipes", label: "Rules & Code Recipes", icon: FileCode2 },
61
+ { value: "overview", label: "ภาพรวม", icon: LayoutGrid },
62
+ { value: "foundations", label: "Tokens", icon: Palette },
63
+ { value: "primitives", label: "Primitives", icon: Boxes },
64
+ { value: "forms", label: "Forms", icon: Layers },
65
+ { value: "data-display", label: "Data", icon: Zap },
66
+ { value: "overlays", label: "Overlays", icon: Compass },
67
+ { value: "feedback", label: "Feedback", icon: ShieldAlert },
68
+ { value: "guidelines-recipes", label: "Rules", icon: FileCode2 },
38
69
  ];
39
70
 
40
71
  return (
41
- <div className="flex flex-col gap-6 p-4 md:p-8 max-w-[1600px] mx-auto min-h-screen">
42
- {/* Top Header */}
43
- <header className="flex flex-col gap-2">
44
- <div className="flex flex-wrap items-center justify-between gap-4">
45
- <div className="flex items-center gap-2.5">
46
- <div className="flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground font-bold shadow-xs">
47
- <Boxes className="size-5" />
48
- </div>
49
- <div>
50
- <h1 className="text-xl md:text-2xl font-bold tracking-tight text-foreground">
51
- @suphark/ui Design System & Showcase
52
- </h1>
53
- <p className="text-xs text-muted-foreground">
54
- Living Component Showcase, Foundations, Patterns & Web Interface Guidelines
55
- </p>
56
- </div>
57
- </div>
58
-
59
- <div className="flex flex-wrap items-center gap-2">
60
- <Badge variant="secondary" className="text-xs font-normal">
61
- shadcn/ui base-nova
62
- </Badge>
63
- <Badge variant="outline" className="text-xs font-normal">
64
- Tailwind v4
65
- </Badge>
66
- <Badge variant="outline" className="text-xs font-normal">
67
- Base UI
68
- </Badge>
69
- </div>
70
- </div>
71
- </header>
72
-
72
+ <div className="mx-auto flex min-h-screen max-w-[1600px] flex-col gap-6 p-4 md:p-8">
73
73
  {/* Main Tab Navigation */}
74
74
  <Tabs
75
75
  value={activeTab}
@@ -78,17 +78,18 @@ export function DesignSystemShowcase() {
78
78
  }}
79
79
  className="flex flex-col gap-6"
80
80
  >
81
- <div className="overflow-x-auto pb-1">
82
- <TabsList className="h-auto w-full min-w-max justify-start gap-1 p-1 bg-muted/60">
81
+ {/* แถบแท็บ: จอแคบเลื่อนแนวนอน 1 แถว (ขอบไล่จางบอกว่าเลื่อนได้) · จอกว้าง wrap ได้ */}
82
+ <div className="-mx-4 overflow-x-auto px-4 no-scrollbar md:mx-0 md:overflow-visible md:px-0 max-md:[mask-image:linear-gradient(to_right,transparent,black_16px,black_calc(100%-16px),transparent)]">
83
+ <TabsList className="h-auto w-max flex-nowrap justify-start gap-1 bg-muted/60 p-1 group-data-horizontal/tabs:h-auto md:w-full md:flex-wrap">
83
84
  {tabItems.map((tab) => {
84
85
  const Icon = tab.icon;
85
86
  return (
86
87
  <TabsTrigger
87
88
  key={tab.value}
88
89
  value={tab.value}
89
- className="gap-2 py-2 px-3 text-xs data-[state=active]:bg-card data-[state=active]:shadow-xs"
90
+ className="gap-2 px-3 py-2 text-sm data-[state=active]:bg-card data-[state=active]:shadow-xs"
90
91
  >
91
- <Icon className="size-3.5" />
92
+ <Icon className="size-4" />
92
93
  {tab.label}
93
94
  </TabsTrigger>
94
95
  );
@@ -99,6 +100,8 @@ export function DesignSystemShowcase() {
99
100
  {/* Tab Contents */}
100
101
  <TabsContent value="overview" className="mt-0 outline-none">
101
102
  <ShowcaseOverview
103
+ links={links}
104
+ onJump={jumpTo}
102
105
  onSelectTab={setActiveTab}
103
106
  searchQuery={searchQuery}
104
107
  onSearchChange={setSearchQuery}
@@ -0,0 +1,219 @@
1
+ /** ดัชนีค้นหาของ showcase — หัวข้อ → แท็บ + anchor (id ของ section) + คำค้นเพิ่ม */
2
+ export interface SearchEntry {
3
+ title: string;
4
+ tab: string;
5
+ anchor?: string;
6
+ keywords: string[];
7
+ }
8
+
9
+ export const SEARCH_INDEX: SearchEntry[] = [
10
+ {
11
+ title: "Semantic color tokens",
12
+ tab: "foundations",
13
+ anchor: "tokens-color",
14
+ keywords: ["สี", "token", "background", "primary", "destructive", "success", "warning", "muted"],
15
+ },
16
+ {
17
+ title: "Sidebar & chart tokens",
18
+ tab: "foundations",
19
+ anchor: "tokens-sidebar-chart",
20
+ keywords: ["sidebar", "chart", "กราฟ", "สี"],
21
+ },
22
+ {
23
+ title: "Typography scale",
24
+ tab: "foundations",
25
+ anchor: "tokens-typography",
26
+ keywords: ["ฟอนต์", "font", "text-xs", "text-sm", "ขนาดตัวอักษร", "noto sans thai"],
27
+ },
28
+ {
29
+ title: "Spacing, radius & keyboard",
30
+ tab: "foundations",
31
+ anchor: "tokens-spacing",
32
+ keywords: ["gap", "spacing", "radius", "ระยะห่าง", "layout", "keyboard", "focus"],
33
+ },
34
+ {
35
+ title: "Button variants & sizes",
36
+ tab: "primitives",
37
+ anchor: "primitives-button",
38
+ keywords: ["ปุ่ม", "button", "variant", "icon button", "loading", "spinner"],
39
+ },
40
+ {
41
+ title: "Standard action buttons (Save / Cancel / Delete)",
42
+ tab: "primitives",
43
+ anchor: "primitives-shared-buttons",
44
+ keywords: ["savebutton", "cancelbutton", "deletebutton", "addnewbutton", "บันทึก", "ยกเลิก", "ลบ"],
45
+ },
46
+ {
47
+ title: "Badge & StatusBadge",
48
+ tab: "primitives",
49
+ anchor: "primitives-badge",
50
+ keywords: ["badge", "status", "สถานะ", "ป้าย", "tone"],
51
+ },
52
+ {
53
+ title: "Card, Avatar & Initials",
54
+ tab: "primitives",
55
+ anchor: "primitives-card",
56
+ keywords: ["card", "avatar", "initials", "การ์ด", "รูปโปรไฟล์"],
57
+ },
58
+ {
59
+ title: "Accordion & Collapsible",
60
+ tab: "primitives",
61
+ anchor: "primitives-accordion",
62
+ keywords: ["accordion", "collapsible", "พับ", "กาง"],
63
+ },
64
+ {
65
+ title: "Form fields (FieldGroup, Field, InputGroup)",
66
+ tab: "forms",
67
+ anchor: "forms-fields",
68
+ keywords: [
69
+ "form",
70
+ "ฟอร์ม",
71
+ "input",
72
+ "field",
73
+ "label",
74
+ "validation",
75
+ "error",
76
+ "select",
77
+ "combobox",
78
+ "checkbox",
79
+ "radio",
80
+ "switch",
81
+ "date",
82
+ "otp",
83
+ "slider",
84
+ "textarea",
85
+ "react-hook-form",
86
+ ],
87
+ },
88
+ {
89
+ title: "Table with loading / empty states",
90
+ tab: "data-display",
91
+ anchor: "data-triple-state",
92
+ keywords: ["table", "ตาราง", "skeleton", "empty", "loading", "ว่าง"],
93
+ },
94
+ {
95
+ title: "DataTable (@suphark/ui/data-table)",
96
+ tab: "data-display",
97
+ anchor: "data-table",
98
+ keywords: ["datatable", "tanstack", "filter", "pagination", "sort", "ตาราง", "ค้นหา"],
99
+ },
100
+ {
101
+ title: "Item & Attachment",
102
+ tab: "data-display",
103
+ anchor: "data-item-attachment",
104
+ keywords: ["item", "attachment", "ไฟล์แนบ", "list"],
105
+ },
106
+ {
107
+ title: "Number & date formatters",
108
+ tab: "data-display",
109
+ anchor: "data-formatters",
110
+ keywords: ["formattednumber", "relativedate", "วันที่", "ตัวเลข", "บาท", "currency"],
111
+ },
112
+ {
113
+ title: "Dialog, AlertDialog, Sheet & Drawer",
114
+ tab: "overlays",
115
+ anchor: "overlays-dialog",
116
+ keywords: [
117
+ "dialog",
118
+ "modal",
119
+ "sheet",
120
+ "drawer",
121
+ "alertdialog",
122
+ "ยืนยัน",
123
+ "กล่องโต้ตอบ",
124
+ "formdialog",
125
+ ],
126
+ },
127
+ {
128
+ title: "Popover, Tooltip, HoverCard & menus",
129
+ tab: "overlays",
130
+ anchor: "overlays-popover",
131
+ keywords: ["popover", "tooltip", "hovercard", "menubar", "contextmenu", "dropdown", "เมนู"],
132
+ },
133
+ {
134
+ title: "Breadcrumb, Pagination, Tabs & Resizable",
135
+ tab: "overlays",
136
+ anchor: "overlays-navigation",
137
+ keywords: ["breadcrumb", "pagination", "tabs", "resizable", "navigation", "นำทาง"],
138
+ },
139
+ {
140
+ title: "Choosing a feedback channel",
141
+ tab: "feedback",
142
+ anchor: "feedback-matrix",
143
+ keywords: ["feedback", "แจ้งเตือน", "alert", "toast", "inline"],
144
+ },
145
+ {
146
+ title: "Inline Alert",
147
+ tab: "feedback",
148
+ anchor: "feedback-alert",
149
+ keywords: ["alert", "notice", "คำเตือน"],
150
+ },
151
+ {
152
+ title: "Toast (sonner)",
153
+ tab: "feedback",
154
+ anchor: "feedback-toast",
155
+ keywords: ["toast", "sonner", "undo", "แจ้งเตือน"],
156
+ },
157
+ {
158
+ title: "Progress & field errors",
159
+ tab: "feedback",
160
+ anchor: "feedback-progress",
161
+ keywords: ["progress", "spinner", "fielderror", "error", "กำลังทำงาน"],
162
+ },
163
+ {
164
+ title: "Web Interface Guidelines (a11y)",
165
+ tab: "guidelines-recipes",
166
+ anchor: "rules-a11y",
167
+ keywords: [
168
+ "a11y",
169
+ "accessibility",
170
+ "focus",
171
+ "aria",
172
+ "screen reader",
173
+ "keyboard",
174
+ "reduced motion",
175
+ ],
176
+ },
177
+ {
178
+ title: "UX writing",
179
+ tab: "guidelines-recipes",
180
+ anchor: "rules-ux-writing",
181
+ keywords: ["copy", "ux writing", "ข้อความ", "active voice", "empty state"],
182
+ },
183
+ {
184
+ title: "Redesign safety workflow",
185
+ tab: "guidelines-recipes",
186
+ anchor: "rules-redesign",
187
+ keywords: ["redesign", "workflow", "checklist", "ปรับปรุง"],
188
+ },
189
+ {
190
+ title: "Code recipes",
191
+ tab: "guidelines-recipes",
192
+ anchor: "rules-recipes",
193
+ keywords: ["recipe", "code", "pageshell", "datatable", "fieldgroup", "ตัวอย่างโค้ด"],
194
+ },
195
+ {
196
+ title: "AppShell (sidebar + header)",
197
+ tab: "guidelines-recipes",
198
+ anchor: "rules-recipes",
199
+ keywords: ["appshell", "sidebar", "shell", "layout", "เมนูข้าง", "usermenu"],
200
+ },
201
+ ];
202
+
203
+ /** ค้นแบบ contains ไม่สนตัวพิมพ์ ทั้ง title และ keywords — คืนเรียงตามคะแนน (ตรง title มาก่อน) */
204
+ export function searchIndex(query: string, limit = 8): SearchEntry[] {
205
+ const q = query.trim().toLowerCase();
206
+ if (!q) return [];
207
+ return SEARCH_INDEX.map((e) => {
208
+ const t = e.title.toLowerCase();
209
+ let score = 0;
210
+ if (t.startsWith(q)) score = 3;
211
+ else if (t.includes(q)) score = 2;
212
+ else if (e.keywords.some((k) => k.toLowerCase().includes(q))) score = 1;
213
+ return { e, score };
214
+ })
215
+ .filter((x) => x.score > 0)
216
+ .sort((a, b) => b.score - a.score)
217
+ .slice(0, limit)
218
+ .map((x) => x.e);
219
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ import { Initials } from "@suphark/ui/components/shared/initials";
4
+ import { StatusBadge } from "@suphark/ui/components/shared/status-badge";
5
+ import { Button } from "@suphark/ui/components/ui/button";
6
+ import { Checkbox } from "@suphark/ui/components/ui/checkbox";
7
+ import {
8
+ InputGroup,
9
+ InputGroupAddon,
10
+ InputGroupInput,
11
+ } from "@suphark/ui/components/ui/input-group";
12
+ import { Label } from "@suphark/ui/components/ui/label";
13
+ import { Progress } from "@suphark/ui/components/ui/progress";
14
+ import { Switch } from "@suphark/ui/components/ui/switch";
15
+ import { cn } from "@suphark/ui/lib/utils";
16
+ import { Search } from "lucide-react";
17
+ import { type CSSProperties, useId, useState } from "react";
18
+
19
+ /** แบรนด์ตัวอย่าง — ตัวแรกไม่ตั้ง --brand = slate ของแพ็กเกจ */
20
+ const BRANDS = [
21
+ { id: "default", label: "ค่าเริ่มต้น", swatch: "oklch(0.208 0.042 265.755)" },
22
+ { id: "a", label: "Tenant A", brand: "oklch(0.55 0.2 25)", foreground: "oklch(0.98 0 0)" },
23
+ { id: "b", label: "Tenant B", brand: "oklch(0.5 0.09 200)", foreground: "oklch(0.985 0 0)" },
24
+ ] as const;
25
+
26
+ /**
27
+ * แถบตัวอย่างสดใต้หัวเรื่อง — component จริงของแพ็กเกจในบรรทัดเดียว + สวิตช์สีแบรนด์
28
+ * วิทยานิพนธ์ของหน้านี้คือ "UI ชุดเดียวทุกแอป" จึงให้เห็นระบบทำงานก่อนอ่านคำอธิบาย
29
+ */
30
+ export function HeroSpecimen() {
31
+ const [brandId, setBrandId] = useState<(typeof BRANDS)[number]["id"]>("default");
32
+ const [enabled, setEnabled] = useState(true);
33
+ const switchId = useId();
34
+ const checkboxId = useId();
35
+ const brand = BRANDS.find((b) => b.id === brandId) ?? BRANDS[0];
36
+ const style =
37
+ "brand" in brand
38
+ ? ({ "--brand": brand.brand, "--brand-foreground": brand.foreground } as CSSProperties)
39
+ : undefined;
40
+
41
+ return (
42
+ <div className="flex flex-col gap-3">
43
+ {/* สวิตช์แบรนด์ — radio group ที่ตั้ง --brand บนกล่องด้านล่างสด ๆ */}
44
+ <div
45
+ role="radiogroup"
46
+ aria-label="ดูตัวอย่างด้วยสีแบรนด์"
47
+ className="flex flex-wrap items-center gap-1.5"
48
+ >
49
+ <span className="mr-1 text-muted-foreground text-xs">ดูด้วยสี</span>
50
+ {BRANDS.map((b) => {
51
+ const active = b.id === brandId;
52
+ return (
53
+ <button
54
+ key={b.id}
55
+ type="button"
56
+ role="radio"
57
+ aria-checked={active}
58
+ onClick={() => setBrandId(b.id)}
59
+ className={cn(
60
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors",
61
+ active
62
+ ? "border-foreground/30 bg-foreground/5 text-foreground"
63
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
64
+ )}
65
+ >
66
+ <span
67
+ aria-hidden="true"
68
+ className="size-2.5 rounded-full border border-black/10"
69
+ style={{ backgroundColor: "brand" in b ? b.brand : b.swatch }}
70
+ />
71
+ {b.label}
72
+ </button>
73
+ );
74
+ })}
75
+ </div>
76
+
77
+ {/* specimen: ทุกอย่างในกล่องนี้รับ --brand จาก style ด้านบน */}
78
+ <div
79
+ style={style}
80
+ className="flex flex-wrap items-center gap-x-5 gap-y-3 rounded-xl border bg-card/60 px-4 py-3"
81
+ >
82
+ <Button size="sm">บันทึก</Button>
83
+ <Button size="sm" variant="outline">
84
+ ยกเลิก
85
+ </Button>
86
+ <StatusBadge tone="success">อนุมัติแล้ว</StatusBadge>
87
+ <div className="flex items-center gap-2">
88
+ <Switch id={switchId} checked={enabled} onCheckedChange={setEnabled} />
89
+ <Label htmlFor={switchId} className="text-sm">
90
+ {enabled ? "แจ้งเตือนเปิด" : "แจ้งเตือนปิด"}
91
+ </Label>
92
+ </div>
93
+ <div className="flex items-center gap-2">
94
+ <Checkbox id={checkboxId} defaultChecked />
95
+ <Label htmlFor={checkboxId} className="text-sm">
96
+ รวม VAT
97
+ </Label>
98
+ </div>
99
+ <InputGroup className="h-8 w-44">
100
+ <InputGroupAddon>
101
+ <Search aria-hidden="true" />
102
+ </InputGroupAddon>
103
+ <InputGroupInput placeholder="focus ดู ring" aria-label="ตัวอย่าง input" />
104
+ </InputGroup>
105
+ <div className="flex w-28 flex-col gap-1">
106
+ <Progress value={enabled ? 72 : 30} aria-label="ความคืบหน้า" />
107
+ </div>
108
+ <Initials name="สมชาย ใจดี" size="sm" />
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -170,12 +170,12 @@ export function ShowcaseDataDisplay() {
170
170
  <div className="flex flex-col gap-1">
171
171
  <div className="flex items-center gap-2">
172
172
  <Badge variant="outline">Data Presentation</Badge>
173
- <span className="text-xs text-muted-foreground">
173
+ <span className="text-muted-foreground text-sm">
174
174
  Tables, Triple States, Items, Attachments & Formatters
175
175
  </span>
176
176
  </div>
177
- <h2 className="text-xl font-bold tracking-tight text-foreground">
178
- การแสดงผลชุดข้อมูลและสถานะการทำงาน (Data Display & States)
177
+ <h2 className="font-bold text-2xl text-foreground tracking-tight">
178
+ การแสดงผลชุดข้อมูลและสถานะการทำงาน
179
179
  </h2>
180
180
  <p className="text-sm text-muted-foreground">
181
181
  ตารางข้อมูลที่มีความหนาแน่นสูง (Data-Dense Tables), การสลับ 3 สถานะ (Ready / Loading / Empty),
@@ -184,12 +184,12 @@ export function ShowcaseDataDisplay() {
184
184
  </div>
185
185
 
186
186
  {/* Triple State Demonstration */}
187
- <Card>
187
+ <Card id="data-triple-state">
188
188
  <CardHeader>
189
189
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
190
190
  <div>
191
- <CardTitle className="text-base">Data Table & Triple State Lifecycle</CardTitle>
192
- <CardDescription className="text-xs">
191
+ <CardTitle className="text-lg">Data Table & Triple State Lifecycle</CardTitle>
192
+ <CardDescription>
193
193
  ทดสอบสลับระหว่าง Ready State, Geometry-Accurate Skeleton, และ Actionable Empty State
194
194
  </CardDescription>
195
195
  </div>
@@ -350,10 +350,10 @@ export function ShowcaseDataDisplay() {
350
350
  </Card>
351
351
 
352
352
  {/* DataTable (@suphark/ui/data-table) */}
353
- <Card>
353
+ <Card id="data-table">
354
354
  <CardHeader>
355
- <CardTitle className="text-base">DataTable (tanstack) — @suphark/ui/data-table</CardTitle>
356
- <CardDescription className="text-xs">
355
+ <CardTitle className="text-lg">DataTable (tanstack) — @suphark/ui/data-table</CardTitle>
356
+ <CardDescription>
357
357
  ตารางมาตรฐานพร้อม global search, faceted filter, เรียงคอลัมน์, ซ่อน/แสดงคอลัมน์ และ pagination
358
358
  — import แยกจาก <code>@suphark/ui/data-table</code> (ต้องมี peer{" "}
359
359
  <code>@tanstack/react-table</code>)
@@ -382,12 +382,12 @@ export function ShowcaseDataDisplay() {
382
382
  </Card>
383
383
 
384
384
  {/* Item & Attachment Primitives */}
385
- <div className="grid gap-6 lg:grid-cols-2">
385
+ <div id="data-item-attachment" className="grid gap-6 lg:grid-cols-2">
386
386
  {/* Item Primitive */}
387
387
  <Card>
388
388
  <CardHeader className="pb-3">
389
- <CardTitle className="text-base">Item & ItemGroup Primitive</CardTitle>
390
- <CardDescription className="text-xs">
389
+ <CardTitle className="text-lg">Item & ItemGroup Primitive</CardTitle>
390
+ <CardDescription>
391
391
  คอมโพเนนต์สำหรับแสดงแถวรายการที่มี Media, Badge, และ Inline Actions
392
392
  </CardDescription>
393
393
  </CardHeader>
@@ -435,8 +435,8 @@ export function ShowcaseDataDisplay() {
435
435
  {/* Attachment Primitive */}
436
436
  <Card>
437
437
  <CardHeader className="pb-3">
438
- <CardTitle className="text-base">Attachment Primitive</CardTitle>
439
- <CardDescription className="text-xs">
438
+ <CardTitle className="text-lg">Attachment Primitive</CardTitle>
439
+ <CardDescription>
440
440
  การ์ดไฟล์แนบพร้อมสถานะการอัปโหลด (Done / Uploading / Error)
441
441
  </CardDescription>
442
442
  </CardHeader>
@@ -477,12 +477,12 @@ export function ShowcaseDataDisplay() {
477
477
  </div>
478
478
 
479
479
  {/* Shared Formatters & Benchmark Analysis */}
480
- <div className="grid gap-6 lg:grid-cols-2">
480
+ <div id="data-formatters" className="grid gap-6 lg:grid-cols-2">
481
481
  {/* Value Formatters */}
482
482
  <Card>
483
483
  <CardHeader className="pb-3">
484
- <CardTitle className="text-base">Shared Value Formatters</CardTitle>
485
- <CardDescription className="text-xs">
484
+ <CardTitle className="text-lg">Shared Value Formatters</CardTitle>
485
+ <CardDescription>
486
486
  Formatters เพื่อป้องกัน SSR Hydration Mismatch และคุม Locale ภาษาไทย
487
487
  </CardDescription>
488
488
  </CardHeader>