@suphark/ui 0.1.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.
Files changed (147) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/components.json +21 -0
  5. package/package.json +108 -0
  6. package/scripts/audit-base-ui-props.mjs +179 -0
  7. package/scripts/audit-tailwind-typography.mjs +100 -0
  8. package/src/components/design-system/design-system-showcase.tsx +138 -0
  9. package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
  10. package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
  11. package/src/components/design-system/sections/showcase-forms.tsx +373 -0
  12. package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
  13. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
  14. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
  15. package/src/components/design-system/sections/showcase-overview.tsx +368 -0
  16. package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
  17. package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
  18. package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
  19. package/src/components/shared/button/add-new-button.tsx +19 -0
  20. package/src/components/shared/button/base/action-button.tsx +134 -0
  21. package/src/components/shared/button/base/link-button.tsx +130 -0
  22. package/src/components/shared/button/cancel-button.tsx +27 -0
  23. package/src/components/shared/button/confirm-button.tsx +26 -0
  24. package/src/components/shared/button/delete-button.tsx +36 -0
  25. package/src/components/shared/button/floating-save-button.tsx +89 -0
  26. package/src/components/shared/button/index.ts +13 -0
  27. package/src/components/shared/button/print-button.tsx +67 -0
  28. package/src/components/shared/button/refresh-button.tsx +63 -0
  29. package/src/components/shared/button/save-button.tsx +29 -0
  30. package/src/components/shared/button/submit-button.tsx +58 -0
  31. package/src/components/shared/client-redirect.tsx +11 -0
  32. package/src/components/shared/combobox.tsx +116 -0
  33. package/src/components/shared/data-list.tsx +65 -0
  34. package/src/components/shared/date/date-range-picker.tsx +106 -0
  35. package/src/components/shared/description-list.tsx +27 -0
  36. package/src/components/shared/empty-row.tsx +13 -0
  37. package/src/components/shared/empty-state.tsx +31 -0
  38. package/src/components/shared/env-snippet.tsx +62 -0
  39. package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
  40. package/src/components/shared/form/form-field-boolean.tsx +93 -0
  41. package/src/components/shared/form/form-field-color-select.tsx +86 -0
  42. package/src/components/shared/form/form-field-combobox.tsx +167 -0
  43. package/src/components/shared/form/form-field-date.tsx +55 -0
  44. package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
  45. package/src/components/shared/form/form-field-input.tsx +372 -0
  46. package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
  47. package/src/components/shared/form/form-field-radio-group.tsx +56 -0
  48. package/src/components/shared/form/form-field-select.tsx +312 -0
  49. package/src/components/shared/form/simple-select.tsx +152 -0
  50. package/src/components/shared/form-dialog.tsx +109 -0
  51. package/src/components/shared/form-message.tsx +30 -0
  52. package/src/components/shared/initials.tsx +33 -0
  53. package/src/components/shared/layout/error-state.tsx +160 -0
  54. package/src/components/shared/layout/page-shell.tsx +57 -0
  55. package/src/components/shared/link/transition-link.tsx +72 -0
  56. package/src/components/shared/multi-select-combobox.tsx +392 -0
  57. package/src/components/shared/name-id-fields.tsx +133 -0
  58. package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
  59. package/src/components/shared/notice.tsx +35 -0
  60. package/src/components/shared/number/formatted-number.tsx +133 -0
  61. package/src/components/shared/page-header.tsx +25 -0
  62. package/src/components/shared/page-skeleton.tsx +18 -0
  63. package/src/components/shared/row-actions.tsx +93 -0
  64. package/src/components/shared/row-menu.tsx +55 -0
  65. package/src/components/shared/secret-reveal.tsx +53 -0
  66. package/src/components/shared/section-card.tsx +44 -0
  67. package/src/components/shared/stat.tsx +26 -0
  68. package/src/components/shared/status-badge.tsx +47 -0
  69. package/src/components/shared/text/primary-secondary-text.tsx +186 -0
  70. package/src/components/shared/theme/index.ts +4 -0
  71. package/src/components/shared/theme/theme-menu-items.tsx +43 -0
  72. package/src/components/shared/theme/theme-provider.tsx +39 -0
  73. package/src/components/shared/theme/theme-switch.tsx +36 -0
  74. package/src/components/shared/theme/theme-toggle.tsx +76 -0
  75. package/src/components/shared/time/client-date-formatter.tsx +188 -0
  76. package/src/components/shared/ui-text.tsx +47 -0
  77. package/src/components/ui/__tests__/combobox.test.tsx +40 -0
  78. package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
  79. package/src/components/ui/__tests__/slider.test.tsx +18 -0
  80. package/src/components/ui/__tests__/tabs.test.tsx +33 -0
  81. package/src/components/ui/accordion.tsx +69 -0
  82. package/src/components/ui/alert-dialog.tsx +161 -0
  83. package/src/components/ui/alert.tsx +68 -0
  84. package/src/components/ui/aspect-ratio.tsx +22 -0
  85. package/src/components/ui/attachment.tsx +194 -0
  86. package/src/components/ui/avatar.tsx +92 -0
  87. package/src/components/ui/badge.tsx +48 -0
  88. package/src/components/ui/breadcrumb.tsx +102 -0
  89. package/src/components/ui/button-group.tsx +77 -0
  90. package/src/components/ui/button.tsx +57 -0
  91. package/src/components/ui/calendar.tsx +184 -0
  92. package/src/components/ui/card.tsx +87 -0
  93. package/src/components/ui/carousel.tsx +230 -0
  94. package/src/components/ui/chart.tsx +338 -0
  95. package/src/components/ui/checkbox.tsx +27 -0
  96. package/src/components/ui/collapsible.tsx +17 -0
  97. package/src/components/ui/combobox.tsx +272 -0
  98. package/src/components/ui/command.tsx +180 -0
  99. package/src/components/ui/context-menu.tsx +244 -0
  100. package/src/components/ui/date-picker.tsx +308 -0
  101. package/src/components/ui/dialog.tsx +137 -0
  102. package/src/components/ui/drawer.tsx +208 -0
  103. package/src/components/ui/dropdown-menu.tsx +257 -0
  104. package/src/components/ui/empty.tsx +93 -0
  105. package/src/components/ui/field.tsx +223 -0
  106. package/src/components/ui/form.tsx +161 -0
  107. package/src/components/ui/hover-card.tsx +46 -0
  108. package/src/components/ui/input-group.tsx +145 -0
  109. package/src/components/ui/input-otp.tsx +85 -0
  110. package/src/components/ui/input.tsx +19 -0
  111. package/src/components/ui/item.tsx +187 -0
  112. package/src/components/ui/kbd.tsx +26 -0
  113. package/src/components/ui/label.tsx +19 -0
  114. package/src/components/ui/menubar.tsx +266 -0
  115. package/src/components/ui/navigation-menu.tsx +160 -0
  116. package/src/components/ui/pagination.tsx +117 -0
  117. package/src/components/ui/popover.tsx +76 -0
  118. package/src/components/ui/progress.tsx +66 -0
  119. package/src/components/ui/radio-group.tsx +38 -0
  120. package/src/components/ui/relative-date.tsx +40 -0
  121. package/src/components/ui/resizable.tsx +41 -0
  122. package/src/components/ui/scroll-area.tsx +67 -0
  123. package/src/components/ui/searchable-select.tsx +124 -0
  124. package/src/components/ui/select.tsx +189 -0
  125. package/src/components/ui/separator.tsx +21 -0
  126. package/src/components/ui/sheet.tsx +124 -0
  127. package/src/components/ui/sidebar.tsx +691 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +55 -0
  130. package/src/components/ui/sonner.tsx +45 -0
  131. package/src/components/ui/spinner.tsx +16 -0
  132. package/src/components/ui/switch.tsx +32 -0
  133. package/src/components/ui/table.tsx +95 -0
  134. package/src/components/ui/tabs.tsx +79 -0
  135. package/src/components/ui/textarea.tsx +17 -0
  136. package/src/components/ui/toggle-group.tsx +86 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +54 -0
  139. package/src/hooks/use-copy-to-clipboard.ts +27 -0
  140. package/src/hooks/use-mobile.ts +21 -0
  141. package/src/index.ts +156 -0
  142. package/src/lib/formatters.ts +44 -0
  143. package/src/lib/permission.ts +75 -0
  144. package/src/lib/status-colors.ts +52 -0
  145. package/src/lib/utils.ts +6 -0
  146. package/src/next.ts +15 -0
  147. package/src/styles.css +214 -0
@@ -0,0 +1,273 @@
1
+ "use client";
2
+
3
+ import { Alert, AlertDescription, AlertTitle } from "@suphark/ui/components/ui/alert";
4
+ import { Badge } from "@suphark/ui/components/ui/badge";
5
+ import { Button } from "@suphark/ui/components/ui/button";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@suphark/ui/components/ui/card";
13
+ import { Field, FieldError, FieldGroup, FieldLabel } from "@suphark/ui/components/ui/field";
14
+ import { Input } from "@suphark/ui/components/ui/input";
15
+ import { Progress } from "@suphark/ui/components/ui/progress";
16
+ import {
17
+ AlertCircle,
18
+ Bell,
19
+ CheckCircle2,
20
+ Copy,
21
+ Info,
22
+ Loader2,
23
+ Send,
24
+ Trash2,
25
+ XCircle,
26
+ } from "lucide-react";
27
+ import * as React from "react";
28
+ import { toast } from "sonner";
29
+
30
+ export function ShowcaseFeedback() {
31
+ const [exportProgress, setExportProgress] = React.useState<number | null>(null);
32
+
33
+ const triggerExportSimulation = () => {
34
+ setExportProgress(10);
35
+ const interval = setInterval(() => {
36
+ setExportProgress((prev) => {
37
+ if (prev === null || prev >= 100) {
38
+ clearInterval(interval);
39
+ setTimeout(() => {
40
+ setExportProgress(null);
41
+ toast.success("ส่งออกเอกสารสำเร็จ (Export Complete)", {
42
+ description: "ไฟล์ BOQ_Package_2026.xlsx พร้อมดาวน์โหลดแล้ว",
43
+ });
44
+ }, 400);
45
+ return 100;
46
+ }
47
+ return prev + 30;
48
+ });
49
+ }, 300);
50
+ };
51
+
52
+ return (
53
+ <div className="flex flex-col gap-8">
54
+ {/* Section Header */}
55
+ <div className="flex flex-col gap-1">
56
+ <div className="flex items-center gap-2">
57
+ <Badge variant="outline">Feedback & Alerts</Badge>
58
+ <span className="text-xs text-muted-foreground">
59
+ Alerts, Sonner Toasts, Field Validation & Progress States
60
+ </span>
61
+ </div>
62
+ <h2 className="text-xl font-bold tracking-tight text-foreground">
63
+ ระบบการแจ้งเตือนและการตอบสนองต่อผู้ใช้ (Feedback System)
64
+ </h2>
65
+ <p className="text-sm text-muted-foreground">
66
+ การเลือกช่องทางแจ้งเตือนที่เหมาะสมตามระดับความสำคัญ: Inline Alert, Transient Sonner Toast,
67
+ FieldError, และ Progress
68
+ </p>
69
+ </div>
70
+
71
+ {/* Feedback Channel Guidance Matrix */}
72
+ <Card>
73
+ <CardHeader>
74
+ <CardTitle className="text-base">Feedback Channel Decision Matrix</CardTitle>
75
+ <CardDescription className="text-xs">
76
+ เกณฑ์การเลือกประเภท Feedback ตามบริบทของงานในแอป Suphark
77
+ </CardDescription>
78
+ </CardHeader>
79
+ <CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
80
+ <div className="flex flex-col gap-1.5 rounded-lg border bg-card p-3">
81
+ <Badge variant="outline" className="w-fit">
82
+ 1. Alert (Inline)
83
+ </Badge>
84
+ <h4 className="text-xs font-semibold pt-1">ข้อความในบริบทของหน้า</h4>
85
+ <p className="text-xs text-muted-foreground">
86
+ สำหรับคำเตือน, ข้อกำหนด หรือข้อผิดพลาดระดับทั้งหน้าหรือ Card ที่ต้องให้อ่านก่อนดำเนินการต่อ
87
+ </p>
88
+ </div>
89
+
90
+ <div className="flex flex-col gap-1.5 rounded-lg border bg-card p-3">
91
+ <Badge variant="outline" className="w-fit">
92
+ 2. Sonner (Toast)
93
+ </Badge>
94
+ <h4 className="text-xs font-semibold pt-1">ผลลัพธ์ Action ชั่วคราว</h4>
95
+ <p className="text-xs text-muted-foreground">
96
+ สำหรับแจ้งผลการกระทำสำเร็จสั้น ๆ เช่น "บันทึกแล้ว", "คัดลอกแล้ว", "ส่งอีเมลแล้ว"
97
+ </p>
98
+ </div>
99
+
100
+ <div className="flex flex-col gap-1.5 rounded-lg border bg-card p-3">
101
+ <Badge variant="outline" className="w-fit">
102
+ 3. FieldError
103
+ </Badge>
104
+ <h4 className="text-xs font-semibold pt-1">ข้อผิดพลาดของช่องกรอก</h4>
105
+ <p className="text-xs text-muted-foreground">
106
+ สำหรับ Validation ที่ผูกกับ input แต่ละช่องโดยตรง เพื่อชี้จุดที่ต้องแก้ไขให้ชัดเจน
107
+ </p>
108
+ </div>
109
+
110
+ <div className="flex flex-col gap-1.5 rounded-lg border bg-card p-3">
111
+ <Badge variant="outline" className="w-fit">
112
+ 4. Progress / Spinner
113
+ </Badge>
114
+ <h4 className="text-xs font-semibold pt-1">กระบวนการที่กำลังทำงาน</h4>
115
+ <p className="text-xs text-muted-foreground">
116
+ สำหรับการประมวลผลที่มีระยะเวลา เช่น การนำเข้าไฟล์ขนาดใหญ่ หรือการส่งออกรายงาน
117
+ </p>
118
+ </div>
119
+ </CardContent>
120
+ </Card>
121
+
122
+ {/* Inline Alerts */}
123
+ <section className="flex flex-col gap-4">
124
+ <h3 className="text-base font-semibold">1. Inline Alerts (Alert Primitive)</h3>
125
+ <div className="grid gap-4 md:grid-cols-2">
126
+ {/* Default Info Alert */}
127
+ <Alert>
128
+ <Info className="size-4" />
129
+ <AlertTitle>การเปิดซองราคาตามกำหนดเวลา</AlertTitle>
130
+ <AlertDescription>
131
+ ระบบจะทำการเปิดซองราคาพร้อมกันอัตโนมัติในวันที่ 25 ส.ค. 2569 เวลา 14:00 น.
132
+ โดยต้องมีกรรมการเข้าร่วมอย่างน้อย 3 ท่าน
133
+ </AlertDescription>
134
+ </Alert>
135
+
136
+ {/* Destructive Alert */}
137
+ <Alert variant="destructive">
138
+ <AlertCircle className="size-4" />
139
+ <AlertTitle>เอกสารหลักประกันหมดอายุ</AlertTitle>
140
+ <AlertDescription>
141
+ หนังสือค้ำประกันของบริษัท ABC Construction หมดอายุแล้ว กรุณาติดต่อขอเอกสารฉบับใหม่ก่อนดำเนินการทำสัญญา
142
+ </AlertDescription>
143
+ </Alert>
144
+ </div>
145
+ </section>
146
+
147
+ {/* Interactive Sonner Toasts */}
148
+ <Card>
149
+ <CardHeader>
150
+ <div className="flex items-center justify-between">
151
+ <div>
152
+ <CardTitle className="text-base flex items-center gap-2">
153
+ <Bell className="size-4 text-primary" /> 2. Interactive Sonner Toasts
154
+ </CardTitle>
155
+ <CardDescription className="text-xs">ทดสอบยิง Toast ในสถานการณ์ต่าง ๆ</CardDescription>
156
+ </div>
157
+ <Badge variant="secondary">sonner</Badge>
158
+ </div>
159
+ </CardHeader>
160
+ <CardContent>
161
+ <div className="flex flex-wrap items-center gap-3">
162
+ <Button
163
+ variant="outline"
164
+ size="sm"
165
+ onClick={() =>
166
+ toast.success("บันทึกการประเมินราคาสำเร็จ", {
167
+ description: "ข้อมูลถูกบันทึกและส่งแจ้งเตือนไปยังฝ่ายบัญชีแล้ว",
168
+ })
169
+ }
170
+ >
171
+ <CheckCircle2 className="size-3.5 text-success" /> Success Toast
172
+ </Button>
173
+
174
+ <Button
175
+ variant="outline"
176
+ size="sm"
177
+ onClick={() =>
178
+ toast.error("ไม่สามารถเชื่อมต่อฐานข้อมูลราคา", {
179
+ description: "โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองใหม่อีกครั้ง",
180
+ })
181
+ }
182
+ >
183
+ <XCircle className="size-3.5 text-destructive" /> Error Toast
184
+ </Button>
185
+
186
+ <Button
187
+ variant="outline"
188
+ size="sm"
189
+ onClick={() =>
190
+ toast.info("คัดลอกเลขที่สัญญาเรียบร้อย", {
191
+ description: "CTR-2026-00481 ถูกคัดลอกไปยังคลิปบอร์ด",
192
+ })
193
+ }
194
+ >
195
+ <Copy className="size-3.5 text-primary" /> Info / Copy Toast
196
+ </Button>
197
+
198
+ <Button
199
+ variant="outline"
200
+ size="sm"
201
+ onClick={() =>
202
+ toast("ลบรายการวัสดุชั่วคราว", {
203
+ description: "คุณสามารถกู้คืนรายการนี้ได้ภายใน 10 วินาที",
204
+ action: {
205
+ label: "เลิกทำ (Undo)",
206
+ onClick: () => toast.success("กู้คืนรายการเรียบร้อย"),
207
+ },
208
+ })
209
+ }
210
+ >
211
+ <Trash2 className="size-3.5 text-muted-foreground" /> Toast พร้อมปุ่ม Undo
212
+ </Button>
213
+ </div>
214
+ </CardContent>
215
+ </Card>
216
+
217
+ {/* Active Task Progress & Field Error */}
218
+ <div className="grid gap-6 lg:grid-cols-2">
219
+ {/* Active Task Progress Simulation */}
220
+ <Card>
221
+ <CardHeader className="pb-3">
222
+ <CardTitle className="text-base">3. Active Task Progress</CardTitle>
223
+ <CardDescription className="text-xs">
224
+ การแสดงผลขณะระบบกำลังทำงานแบบ Asynchronous
225
+ </CardDescription>
226
+ </CardHeader>
227
+ <CardContent className="flex flex-col gap-4">
228
+ {exportProgress !== null ? (
229
+ <div className="flex flex-col gap-2 rounded-md border p-3 bg-muted/20">
230
+ <div className="flex items-center justify-between text-xs">
231
+ <span className="flex items-center gap-2 font-medium">
232
+ <Loader2 className="size-3.5 animate-spin text-primary" />
233
+ กำลังประมวลผลตารางเปรียบเทียบ BOQ...
234
+ </span>
235
+ <span className="font-mono text-muted-foreground">{exportProgress}%</span>
236
+ </div>
237
+ <Progress value={exportProgress} />
238
+ </div>
239
+ ) : (
240
+ <Button
241
+ variant="outline"
242
+ size="sm"
243
+ onClick={triggerExportSimulation}
244
+ className="w-fit gap-2"
245
+ >
246
+ <Send className="size-3.5" /> ทดสอบจำลองการประมวลผล Asynchronous
247
+ </Button>
248
+ )}
249
+ </CardContent>
250
+ </Card>
251
+
252
+ {/* Input Validation State */}
253
+ <Card>
254
+ <CardHeader className="pb-3">
255
+ <CardTitle className="text-base">4. FieldError & Direct Validation</CardTitle>
256
+ <CardDescription className="text-xs">
257
+ การระบุข้อความช่วยเหลือและข้อผิดพลาดที่ตำแหน่ง Input
258
+ </CardDescription>
259
+ </CardHeader>
260
+ <CardContent>
261
+ <FieldGroup>
262
+ <Field data-invalid>
263
+ <FieldLabel htmlFor="fb-vat-input">เลขประจำตัวผู้เสียภาษี (Tax ID)</FieldLabel>
264
+ <Input id="fb-vat-input" defaultValue="12345" aria-invalid />
265
+ <FieldError>เลขประจำตัวผู้เสียภาษีต้องเป็นตัวเลข 13 หลัก</FieldError>
266
+ </Field>
267
+ </FieldGroup>
268
+ </CardContent>
269
+ </Card>
270
+ </div>
271
+ </div>
272
+ );
273
+ }
@@ -0,0 +1,373 @@
1
+ "use client";
2
+
3
+ import { Combobox } from "@suphark/ui/components/shared/combobox";
4
+ import { MultiSelectCombobox } from "@suphark/ui/components/shared/multi-select-combobox";
5
+ import { Badge } from "@suphark/ui/components/ui/badge";
6
+ import { Button } from "@suphark/ui/components/ui/button";
7
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "@suphark/ui/components/ui/card";
14
+ import { DatePicker } from "@suphark/ui/components/ui/date-picker";
15
+ import {
16
+ Field,
17
+ FieldDescription,
18
+ FieldError,
19
+ FieldGroup,
20
+ FieldLabel,
21
+ FieldLegend,
22
+ FieldSet,
23
+ } from "@suphark/ui/components/ui/field";
24
+ import { Input } from "@suphark/ui/components/ui/input";
25
+ import {
26
+ InputGroup,
27
+ InputGroupAddon,
28
+ InputGroupButton,
29
+ InputGroupInput,
30
+ InputGroupText,
31
+ InputGroupTextarea,
32
+ } from "@suphark/ui/components/ui/input-group";
33
+ import { InputOTP, InputOTPGroup, InputOTPSlot } from "@suphark/ui/components/ui/input-otp";
34
+ import { Label } from "@suphark/ui/components/ui/label";
35
+ import { RadioGroup, RadioGroupItem } from "@suphark/ui/components/ui/radio-group";
36
+ import {
37
+ Select,
38
+ SelectContent,
39
+ SelectGroup,
40
+ SelectItem,
41
+ SelectLabel,
42
+ SelectTrigger,
43
+ SelectValue,
44
+ } from "@suphark/ui/components/ui/select";
45
+ import { Separator } from "@suphark/ui/components/ui/separator";
46
+ import { Slider } from "@suphark/ui/components/ui/slider";
47
+ import { Switch } from "@suphark/ui/components/ui/switch";
48
+ import { ToggleGroup, ToggleGroupItem } from "@suphark/ui/components/ui/toggle-group";
49
+ import { Building2, DollarSign, FileText, Search } from "lucide-react";
50
+ import * as React from "react";
51
+
52
+ export function ShowcaseForms() {
53
+ const [hasErrors, setHasErrors] = React.useState(true);
54
+ const [selectedCombobox, setSelectedCombobox] = React.useState("th-cement");
55
+ const [selectedMulti, setSelectedMulti] = React.useState<string[]>(["concrete", "steel"]);
56
+ const [toggleVal, setToggleVal] = React.useState("grid");
57
+ const [sliderVal, setSliderVal] = React.useState([45]);
58
+ const [selectedDate, setSelectedDate] = React.useState<Date | null>(new Date());
59
+ const [otpVal, setOtpVal] = React.useState("2568");
60
+
61
+ const vendorOptions = [
62
+ { label: "Alpha Cement Co., Ltd.", value: "th-scg" },
63
+ { label: "Thai Concrete Industries", value: "th-cement" },
64
+ { label: "Boonthavorn Ceramic", value: "th-boonthavorn" },
65
+ { label: "Dynasty Ceramic PLC", value: "th-dynasty" },
66
+ { label: "TOA Paint Thailand", value: "th-toa" },
67
+ ];
68
+
69
+ const materialTags = [
70
+ { label: "งานคอนกรีตโครงสร้าง (Concrete)", value: "concrete" },
71
+ { label: "งานเหล็กรูปพรรณ (Structural Steel)", value: "steel" },
72
+ { label: "งานระบบไฟฟ้า (Electrical)", value: "electrical" },
73
+ { label: "งานสุขาภิบาล (Sanitary & Plumbing)", value: "plumbing" },
74
+ { label: "งานกระจกและอลูมิเนียม (Facade)", value: "facade" },
75
+ ];
76
+
77
+ return (
78
+ <div className="flex flex-col gap-8">
79
+ {/* Section Header */}
80
+ <div className="flex flex-col gap-1">
81
+ <div className="flex items-center gap-2">
82
+ <Badge variant="outline">Form System</Badge>
83
+ <span className="text-xs text-muted-foreground">
84
+ FieldGroup, Field, InputGroup, Combobox & Validation States
85
+ </span>
86
+ </div>
87
+ <h2 className="text-xl font-bold tracking-tight text-foreground">
88
+ ระบบฟอร์มและการจัดการข้อมูลนำเข้า (Forms & Inputs)
89
+ </h2>
90
+ <p className="text-sm text-muted-foreground">
91
+ มาตรฐานฟอร์มแบบ Unified <code>FieldGroup</code> + <code>Field</code>{" "}
92
+ พร้อมระบบตรวจสอบข้อผิดพลาดและ Accessibility
93
+ </p>
94
+ </div>
95
+
96
+ {/* Interactive Form Controls Showcase */}
97
+ <div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
98
+ {/* Left Column: Standard Field-Based Form */}
99
+ <Card>
100
+ <CardHeader>
101
+ <div className="flex items-center justify-between">
102
+ <div>
103
+ <CardTitle className="text-base">Field-Based Form Structure</CardTitle>
104
+ <CardDescription className="text-xs">
105
+ ใช้ <code>FieldGroup</code> และ <code>Field</code> เพื่อคุม Label, Description และ
106
+ Error
107
+ </CardDescription>
108
+ </div>
109
+ <Button
110
+ variant={hasErrors ? "destructive" : "outline"}
111
+ size="xs"
112
+ onClick={() => setHasErrors(!hasErrors)}
113
+ >
114
+ {hasErrors ? "ซ่อน Validation Error" : "แสดง Validation Error"}
115
+ </Button>
116
+ </div>
117
+ </CardHeader>
118
+ <CardContent>
119
+ <FieldGroup>
120
+ {/* Field with simple input */}
121
+ <Field>
122
+ <FieldLabel htmlFor="procurement-title">ชื่อแพ็กเกจจัดซื้อ (Package Title)</FieldLabel>
123
+ <Input
124
+ id="procurement-title"
125
+ placeholder="เช่น งานจัดซื้อโครงสร้างเหล็กอาคาร 1"
126
+ defaultValue="งานจัดซื้อโครงสร้างเหล็กและหลังคาอาคารหลัก"
127
+ />
128
+ <FieldDescription>ใช้ชื่อที่เป็นทางการและระบุอาคารให้ชัดเจน</FieldDescription>
129
+ </Field>
130
+
131
+ {/* Field with InputGroup (Search / Action) */}
132
+ <Field>
133
+ <FieldLabel htmlFor="vendor-tax-search">
134
+ ค้นหาผู้เสนอราคา / เลขประจำตัวผู้เสียภาษี
135
+ </FieldLabel>
136
+ <InputGroup>
137
+ <InputGroupAddon>
138
+ <Building2 className="size-4 text-muted-foreground" />
139
+ </InputGroupAddon>
140
+ <InputGroupInput
141
+ id="vendor-tax-search"
142
+ placeholder="พิมพ์ชื่อบริษัท หรือ Tax ID 13 หลัก..."
143
+ />
144
+ <InputGroupAddon align="inline-end">
145
+ <InputGroupButton aria-label="ค้นหา">
146
+ <Search />
147
+ </InputGroupButton>
148
+ </InputGroupAddon>
149
+ </InputGroup>
150
+ <FieldDescription>ระบบจะดึงประวัติการรับงานและคะแนนประเมินย้อนหลัง</FieldDescription>
151
+ </Field>
152
+
153
+ {/* Field with Select & SelectGroup */}
154
+ <Field>
155
+ <FieldLabel htmlFor="procurement-method">วิธีการจัดหา (Procurement Method)</FieldLabel>
156
+ <Select defaultValue="bidding">
157
+ <SelectTrigger id="procurement-method">
158
+ <SelectValue placeholder="เลือกวิธีการจัดหา" />
159
+ </SelectTrigger>
160
+ <SelectContent>
161
+ <SelectGroup>
162
+ <SelectLabel>การจัดหาแบบแข่งขัน</SelectLabel>
163
+ <SelectItem value="bidding">ประกวดราคาทั่วไป (Open Bidding)</SelectItem>
164
+ <SelectItem value="selective">คัดเลือกเฉพาะราย (Selective Bidding)</SelectItem>
165
+ </SelectGroup>
166
+ <SelectGroup>
167
+ <SelectLabel>การจัดหาแบบพิเศษ</SelectLabel>
168
+ <SelectItem value="direct">เจรจาตรง (Direct Negotiation)</SelectItem>
169
+ <SelectItem value="framework">สัญญาจะซื้อจะขาย (Framework Agreement)</SelectItem>
170
+ </SelectGroup>
171
+ </SelectContent>
172
+ </Select>
173
+ </Field>
174
+
175
+ {/* Field with Validation Error State */}
176
+ <Field data-invalid={hasErrors ? true : undefined}>
177
+ <FieldLabel htmlFor="tender-budget">
178
+ งบประมาณกลางที่ได้รับอนุมัติ (Approved Budget)
179
+ </FieldLabel>
180
+ <InputGroup>
181
+ <InputGroupAddon>
182
+ <DollarSign className="size-4 text-muted-foreground" />
183
+ </InputGroupAddon>
184
+ <InputGroupInput
185
+ id="tender-budget"
186
+ aria-invalid={hasErrors ? true : undefined}
187
+ placeholder="0.00"
188
+ defaultValue="0.00"
189
+ />
190
+ </InputGroup>
191
+ {hasErrors ? (
192
+ <FieldError>งบประมาณกลางต้องมากกว่า 0 บาท และตรงตามมติคณะกรรมการ</FieldError>
193
+ ) : (
194
+ <FieldDescription>ระบุจำนวนเงินสุทธิรวมภาษีมูลค่าเพิ่ม 7%</FieldDescription>
195
+ )}
196
+ </Field>
197
+
198
+ {/* Field with Textarea in InputGroup */}
199
+ <Field>
200
+ <FieldLabel htmlFor="tender-scope">
201
+ ขอบเขตงานและเงื่อนไขเทคนิค (Scope of Work)
202
+ </FieldLabel>
203
+ <InputGroup>
204
+ <InputGroupAddon align="block-start">
205
+ <InputGroupText>
206
+ <FileText className="size-4" />
207
+ Scope
208
+ </InputGroupText>
209
+ </InputGroupAddon>
210
+ <InputGroupTextarea
211
+ id="tender-scope"
212
+ rows={3}
213
+ placeholder="ระบุข้อกำหนดเฉพาะ เช่น มาตรฐาน มอก., ASTM หรือเงื่อนไขการทดสอบวัสดุ"
214
+ />
215
+ </InputGroup>
216
+ </Field>
217
+ </FieldGroup>
218
+ </CardContent>
219
+ </Card>
220
+
221
+ {/* Right Column: Advanced & Grouped Controls */}
222
+ <div className="flex flex-col gap-6">
223
+ {/* Combobox & MultiSelect */}
224
+ <Card>
225
+ <CardHeader className="pb-3">
226
+ <CardTitle className="text-base">Combobox & Multi-Select</CardTitle>
227
+ <CardDescription className="text-xs">
228
+ การเลือกข้อมูลจากรายการขนาดใหญ่และการแท็กหลายรายการ
229
+ </CardDescription>
230
+ </CardHeader>
231
+ <CardContent className="flex flex-col gap-4">
232
+ <Field>
233
+ <FieldLabel>ผู้ขายที่ได้รับเชิญหลัก (Primary Vendor)</FieldLabel>
234
+ <Combobox
235
+ options={vendorOptions}
236
+ value={selectedCombobox}
237
+ onSelect={setSelectedCombobox}
238
+ placeholder="เลือกผู้ขาย..."
239
+ searchPlaceholder="ค้นหารายชื่อผู้ขาย..."
240
+ />
241
+ </Field>
242
+
243
+ <Field>
244
+ <FieldLabel>หมวดหมู่วัสดุที่เกี่ยวข้อง (Material Categories)</FieldLabel>
245
+ <MultiSelectCombobox
246
+ options={materialTags}
247
+ value={selectedMulti}
248
+ onChange={setSelectedMulti}
249
+ placeholder="เลือกหมวดหมู่วัสดุ..."
250
+ />
251
+ </Field>
252
+ </CardContent>
253
+ </Card>
254
+
255
+ {/* Grouped FieldSet with Switches & Radios */}
256
+ <Card>
257
+ <CardHeader className="pb-3">
258
+ <CardTitle className="text-base">FieldSet & Grouped Controls</CardTitle>
259
+ <CardDescription className="text-xs">
260
+ ใช้ <code>FieldSet</code> และ <code>FieldLegend</code> รวมสวิตช์และเรดิโอที่เกี่ยวข้องกัน
261
+ </CardDescription>
262
+ </CardHeader>
263
+ <CardContent className="flex flex-col gap-6">
264
+ <FieldSet>
265
+ <FieldLegend>การควบคุมกระบวนการอนุมัติ (Approval Workflow)</FieldLegend>
266
+ <Field orientation="horizontal">
267
+ <Switch id="sw-multilevel" defaultChecked />
268
+ <FieldLabel htmlFor="sw-multilevel">
269
+ อนุมัติแบบลำดับชั้น (Multi-level Approval)
270
+ </FieldLabel>
271
+ </Field>
272
+ <Field orientation="horizontal">
273
+ <Switch id="sw-budget-lock" defaultChecked />
274
+ <FieldLabel htmlFor="sw-budget-lock">ล็อกงบประมาณทันทีเมื่อเปิดซองราคา</FieldLabel>
275
+ </Field>
276
+ <Field orientation="horizontal">
277
+ <Switch id="sw-vendor-notify" />
278
+ <FieldLabel htmlFor="sw-vendor-notify">ส่งอีเมลแจ้งเตือนผู้ขายอัตโนมัติ</FieldLabel>
279
+ </Field>
280
+ </FieldSet>
281
+
282
+ <Separator />
283
+
284
+ <FieldSet>
285
+ <FieldLegend>เกณฑ์การตัดสิน (Evaluation Method)</FieldLegend>
286
+ <RadioGroup defaultValue="price-tech" className="flex flex-col gap-2">
287
+ <div className="flex items-center space-x-2">
288
+ <RadioGroupItem value="price-tech" id="r-price-tech" />
289
+ <Label htmlFor="r-price-tech" className="text-xs font-normal">
290
+ คะแนนรวมราคาและเทคนิค (Quality-Cost Based)
291
+ </Label>
292
+ </div>
293
+ <div className="flex items-center space-x-2">
294
+ <RadioGroupItem value="lowest-price" id="r-lowest" />
295
+ <Label htmlFor="r-lowest" className="text-xs font-normal">
296
+ ราคาต่ำสุดที่ผ่านคุณสมบัติ (Lowest Responsive Bid)
297
+ </Label>
298
+ </div>
299
+ </RadioGroup>
300
+ </FieldSet>
301
+ </CardContent>
302
+ </Card>
303
+
304
+ {/* Specialized Controls (Date, OTP, Slider, ToggleGroup) */}
305
+ <Card>
306
+ <CardHeader className="pb-3">
307
+ <CardTitle className="text-base">Specialized Input Primitives</CardTitle>
308
+ <CardDescription className="text-xs">
309
+ Date Pickers, OTP, Sliders, และ ToggleGroup
310
+ </CardDescription>
311
+ </CardHeader>
312
+ <CardContent className="flex flex-col gap-5">
313
+ {/* DatePicker */}
314
+ <Field>
315
+ <FieldLabel>วันกำหนดส่งเอกสาร (Tender Deadline)</FieldLabel>
316
+ <DatePicker
317
+ value={selectedDate}
318
+ onChange={setSelectedDate}
319
+ placeholder="เลือกวันที่กำหนดส่ง..."
320
+ />
321
+ </Field>
322
+
323
+ {/* ToggleGroup */}
324
+ <Field>
325
+ <FieldLabel>รูปแบบมุมมองตาราง (View Layout)</FieldLabel>
326
+ <ToggleGroup
327
+ value={[toggleVal]}
328
+ onValueChange={(val) => {
329
+ if (val[0]) setToggleVal(val[0]);
330
+ }}
331
+ variant="outline"
332
+ >
333
+ <ToggleGroupItem value="grid">Grid View</ToggleGroupItem>
334
+ <ToggleGroupItem value="compact">Compact Table</ToggleGroupItem>
335
+ <ToggleGroupItem value="kanban">Kanban Board</ToggleGroupItem>
336
+ </ToggleGroup>
337
+ </Field>
338
+
339
+ {/* Slider */}
340
+ <Field>
341
+ <div className="flex items-center justify-between">
342
+ <FieldLabel>สัดส่วนคะแนนด้านราคา (Price Weight): {sliderVal[0]}%</FieldLabel>
343
+ <span className="text-xs text-muted-foreground">
344
+ เทคนิค: {100 - (sliderVal[0] ?? 0)}%
345
+ </span>
346
+ </div>
347
+ <Slider
348
+ value={sliderVal}
349
+ onValueChange={(val) => setSliderVal(Array.isArray(val) ? [...val] : [val])}
350
+ max={100}
351
+ step={5}
352
+ />
353
+ </Field>
354
+
355
+ {/* InputOTP */}
356
+ <Field>
357
+ <FieldLabel>รหัสยืนยันเปิดซองราคา (4-Digit Opening PIN)</FieldLabel>
358
+ <InputOTP maxLength={4} value={otpVal} onChange={setOtpVal}>
359
+ <InputOTPGroup>
360
+ <InputOTPSlot index={0} />
361
+ <InputOTPSlot index={1} />
362
+ <InputOTPSlot index={2} />
363
+ <InputOTPSlot index={3} />
364
+ </InputOTPGroup>
365
+ </InputOTP>
366
+ </Field>
367
+ </CardContent>
368
+ </Card>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ );
373
+ }