@suphark/ui 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +13 -1
- package/package.json +1 -1
- package/src/components/app-shell/app-shell.tsx +121 -2
- package/src/components/app-shell/header-crumb.tsx +82 -0
- package/src/components/app-shell/index.ts +3 -0
- package/src/components/app-shell/sub-nav.tsx +147 -0
- package/src/components/app-shell/top-tabs.tsx +68 -0
- package/src/components/design-system/design-system-showcase.tsx +49 -46
- package/src/components/design-system/search-index.ts +219 -0
- package/src/components/design-system/sections/hero-specimen.tsx +112 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +17 -17
- package/src/components/design-system/sections/showcase-feedback.tsx +14 -20
- package/src/components/design-system/sections/showcase-forms.tsx +12 -16
- package/src/components/design-system/sections/showcase-foundations.tsx +12 -18
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +10 -14
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +16 -20
- package/src/components/design-system/sections/showcase-overview.tsx +95 -27
- package/src/components/design-system/sections/showcase-primitives.tsx +18 -22
- package/src/components/shared/form-dialog.tsx +9 -3
- package/src/index.ts +2 -0
- package/src/styles.css +4 -0
|
@@ -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: "ภาพรวม
|
|
31
|
-
{ value: "foundations", label: "
|
|
32
|
-
{ value: "primitives", label: "
|
|
33
|
-
{ value: "forms", label: "Forms
|
|
34
|
-
{ value: "data-display", label: "Data
|
|
35
|
-
{ value: "overlays", label: "Overlays
|
|
36
|
-
{ value: "feedback", label: "Feedback
|
|
37
|
-
{ value: "guidelines-recipes", label: "Rules
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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="
|
|
178
|
-
การแสดงผลชุดข้อมูลและสถานะการทำงาน
|
|
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-
|
|
192
|
-
<CardDescription
|
|
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-
|
|
356
|
-
<CardDescription
|
|
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-
|
|
390
|
-
<CardDescription
|
|
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-
|
|
439
|
-
<CardDescription
|
|
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-
|
|
485
|
-
<CardDescription
|
|
484
|
+
<CardTitle className="text-lg">Shared Value Formatters</CardTitle>
|
|
485
|
+
<CardDescription>
|
|
486
486
|
Formatters เพื่อป้องกัน SSR Hydration Mismatch และคุม Locale ภาษาไทย
|
|
487
487
|
</CardDescription>
|
|
488
488
|
</CardHeader>
|
|
@@ -55,12 +55,12 @@ export function ShowcaseFeedback() {
|
|
|
55
55
|
<div className="flex flex-col gap-1">
|
|
56
56
|
<div className="flex items-center gap-2">
|
|
57
57
|
<Badge variant="outline">Feedback & Alerts</Badge>
|
|
58
|
-
<span className="text-
|
|
58
|
+
<span className="text-muted-foreground text-sm">
|
|
59
59
|
Alerts, Sonner Toasts, Field Validation & Progress States
|
|
60
60
|
</span>
|
|
61
61
|
</div>
|
|
62
|
-
<h2 className="
|
|
63
|
-
ระบบการแจ้งเตือนและการตอบสนองต่อผู้ใช้
|
|
62
|
+
<h2 className="font-bold text-2xl text-foreground tracking-tight">
|
|
63
|
+
ระบบการแจ้งเตือนและการตอบสนองต่อผู้ใช้
|
|
64
64
|
</h2>
|
|
65
65
|
<p className="text-sm text-muted-foreground">
|
|
66
66
|
การเลือกช่องทางแจ้งเตือนที่เหมาะสมตามระดับความสำคัญ: Inline Alert, Transient Sonner Toast,
|
|
@@ -69,12 +69,10 @@ export function ShowcaseFeedback() {
|
|
|
69
69
|
</div>
|
|
70
70
|
|
|
71
71
|
{/* Feedback Channel Guidance Matrix */}
|
|
72
|
-
<Card>
|
|
72
|
+
<Card id="feedback-matrix">
|
|
73
73
|
<CardHeader>
|
|
74
|
-
<CardTitle className="text-
|
|
75
|
-
<CardDescription
|
|
76
|
-
เกณฑ์การเลือกประเภท Feedback ตามบริบทของงานในแอป Suphark
|
|
77
|
-
</CardDescription>
|
|
74
|
+
<CardTitle className="text-lg">Feedback Channel Decision Matrix</CardTitle>
|
|
75
|
+
<CardDescription>เกณฑ์การเลือกประเภท Feedback ตามบริบทของงานในแอป Suphark</CardDescription>
|
|
78
76
|
</CardHeader>
|
|
79
77
|
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
80
78
|
<div className="flex flex-col gap-1.5 rounded-lg border bg-card p-3">
|
|
@@ -120,7 +118,7 @@ export function ShowcaseFeedback() {
|
|
|
120
118
|
</Card>
|
|
121
119
|
|
|
122
120
|
{/* Inline Alerts */}
|
|
123
|
-
<section className="flex flex-col gap-4">
|
|
121
|
+
<section id="feedback-alert" className="flex flex-col gap-4">
|
|
124
122
|
<h3 className="text-base font-semibold">1. Inline Alerts (Alert Primitive)</h3>
|
|
125
123
|
<div className="grid gap-4 md:grid-cols-2">
|
|
126
124
|
{/* Default Info Alert */}
|
|
@@ -145,14 +143,14 @@ export function ShowcaseFeedback() {
|
|
|
145
143
|
</section>
|
|
146
144
|
|
|
147
145
|
{/* Interactive Sonner Toasts */}
|
|
148
|
-
<Card>
|
|
146
|
+
<Card id="feedback-toast">
|
|
149
147
|
<CardHeader>
|
|
150
148
|
<div className="flex items-center justify-between">
|
|
151
149
|
<div>
|
|
152
150
|
<CardTitle className="text-base flex items-center gap-2">
|
|
153
151
|
<Bell className="size-4 text-primary" /> 2. Interactive Sonner Toasts
|
|
154
152
|
</CardTitle>
|
|
155
|
-
<CardDescription
|
|
153
|
+
<CardDescription>ทดสอบยิง Toast ในสถานการณ์ต่าง ๆ</CardDescription>
|
|
156
154
|
</div>
|
|
157
155
|
<Badge variant="secondary">sonner</Badge>
|
|
158
156
|
</div>
|
|
@@ -215,14 +213,12 @@ export function ShowcaseFeedback() {
|
|
|
215
213
|
</Card>
|
|
216
214
|
|
|
217
215
|
{/* Active Task Progress & Field Error */}
|
|
218
|
-
<div className="grid gap-6 lg:grid-cols-2">
|
|
216
|
+
<div id="feedback-progress" className="grid gap-6 lg:grid-cols-2">
|
|
219
217
|
{/* Active Task Progress Simulation */}
|
|
220
218
|
<Card>
|
|
221
219
|
<CardHeader className="pb-3">
|
|
222
|
-
<CardTitle className="text-
|
|
223
|
-
<CardDescription
|
|
224
|
-
การแสดงผลขณะระบบกำลังทำงานแบบ Asynchronous
|
|
225
|
-
</CardDescription>
|
|
220
|
+
<CardTitle className="text-lg">3. Active Task Progress</CardTitle>
|
|
221
|
+
<CardDescription>การแสดงผลขณะระบบกำลังทำงานแบบ Asynchronous</CardDescription>
|
|
226
222
|
</CardHeader>
|
|
227
223
|
<CardContent className="flex flex-col gap-4">
|
|
228
224
|
{exportProgress !== null ? (
|
|
@@ -252,10 +248,8 @@ export function ShowcaseFeedback() {
|
|
|
252
248
|
{/* Input Validation State */}
|
|
253
249
|
<Card>
|
|
254
250
|
<CardHeader className="pb-3">
|
|
255
|
-
<CardTitle className="text-
|
|
256
|
-
<CardDescription
|
|
257
|
-
การระบุข้อความช่วยเหลือและข้อผิดพลาดที่ตำแหน่ง Input
|
|
258
|
-
</CardDescription>
|
|
251
|
+
<CardTitle className="text-lg">4. FieldError & Direct Validation</CardTitle>
|
|
252
|
+
<CardDescription>การระบุข้อความช่วยเหลือและข้อผิดพลาดที่ตำแหน่ง Input</CardDescription>
|
|
259
253
|
</CardHeader>
|
|
260
254
|
<CardContent>
|
|
261
255
|
<FieldGroup>
|