@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,138 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@suphark/ui/components/ui/badge";
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@suphark/ui/components/ui/tabs";
5
+ import {
6
+ Boxes,
7
+ Compass,
8
+ FileCode2,
9
+ Layers,
10
+ LayoutGrid,
11
+ Palette,
12
+ ShieldAlert,
13
+ Zap,
14
+ } from "lucide-react";
15
+ import * as React from "react";
16
+ import { ShowcaseDataDisplay } from "./sections/showcase-data-display";
17
+ import { ShowcaseFeedback } from "./sections/showcase-feedback";
18
+ import { ShowcaseForms } from "./sections/showcase-forms";
19
+ import { ShowcaseFoundations } from "./sections/showcase-foundations";
20
+ import { ShowcaseGuidelinesRecipes } from "./sections/showcase-guidelines-recipes";
21
+ import { ShowcaseOverlaysNavigation } from "./sections/showcase-overlays-navigation";
22
+ import { ShowcaseOverview } from "./sections/showcase-overview";
23
+ import { ShowcasePrimitives } from "./sections/showcase-primitives";
24
+
25
+ export function DesignSystemShowcase() {
26
+ const [activeTab, setActiveTab] = React.useState("overview");
27
+ const [searchQuery, setSearchQuery] = React.useState("");
28
+
29
+ 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 },
38
+ ];
39
+
40
+ 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
+
73
+ {/* Main Tab Navigation */}
74
+ <Tabs
75
+ value={activeTab}
76
+ onValueChange={(val) => {
77
+ if (val !== null) setActiveTab(val);
78
+ }}
79
+ className="flex flex-col gap-6"
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">
83
+ {tabItems.map((tab) => {
84
+ const Icon = tab.icon;
85
+ return (
86
+ <TabsTrigger
87
+ key={tab.value}
88
+ value={tab.value}
89
+ className="gap-2 py-2 px-3 text-xs data-[state=active]:bg-card data-[state=active]:shadow-xs"
90
+ >
91
+ <Icon className="size-3.5" />
92
+ {tab.label}
93
+ </TabsTrigger>
94
+ );
95
+ })}
96
+ </TabsList>
97
+ </div>
98
+
99
+ {/* Tab Contents */}
100
+ <TabsContent value="overview" className="mt-0 outline-none">
101
+ <ShowcaseOverview
102
+ onSelectTab={setActiveTab}
103
+ searchQuery={searchQuery}
104
+ onSearchChange={setSearchQuery}
105
+ />
106
+ </TabsContent>
107
+
108
+ <TabsContent value="foundations" className="mt-0 outline-none">
109
+ <ShowcaseFoundations />
110
+ </TabsContent>
111
+
112
+ <TabsContent value="primitives" className="mt-0 outline-none">
113
+ <ShowcasePrimitives />
114
+ </TabsContent>
115
+
116
+ <TabsContent value="forms" className="mt-0 outline-none">
117
+ <ShowcaseForms />
118
+ </TabsContent>
119
+
120
+ <TabsContent value="data-display" className="mt-0 outline-none">
121
+ <ShowcaseDataDisplay />
122
+ </TabsContent>
123
+
124
+ <TabsContent value="overlays" className="mt-0 outline-none">
125
+ <ShowcaseOverlaysNavigation />
126
+ </TabsContent>
127
+
128
+ <TabsContent value="feedback" className="mt-0 outline-none">
129
+ <ShowcaseFeedback />
130
+ </TabsContent>
131
+
132
+ <TabsContent value="guidelines-recipes" className="mt-0 outline-none">
133
+ <ShowcaseGuidelinesRecipes />
134
+ </TabsContent>
135
+ </Tabs>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,454 @@
1
+ "use client";
2
+
3
+ import { FormattedNumber } from "@suphark/ui/components/shared/number/formatted-number";
4
+ import { PrimarySecondaryText } from "@suphark/ui/components/shared/text/primary-secondary-text";
5
+ import {
6
+ Attachment,
7
+ AttachmentAction,
8
+ AttachmentActions,
9
+ AttachmentContent,
10
+ AttachmentDescription,
11
+ AttachmentGroup,
12
+ AttachmentMedia,
13
+ AttachmentTitle,
14
+ } from "@suphark/ui/components/ui/attachment";
15
+ import { Badge } from "@suphark/ui/components/ui/badge";
16
+ import { Button } from "@suphark/ui/components/ui/button";
17
+ import {
18
+ Card,
19
+ CardContent,
20
+ CardDescription,
21
+ CardHeader,
22
+ CardTitle,
23
+ } from "@suphark/ui/components/ui/card";
24
+ import {
25
+ DropdownMenu,
26
+ DropdownMenuContent,
27
+ DropdownMenuGroup,
28
+ DropdownMenuItem,
29
+ DropdownMenuLabel,
30
+ DropdownMenuSeparator,
31
+ DropdownMenuTrigger,
32
+ } from "@suphark/ui/components/ui/dropdown-menu";
33
+ import {
34
+ Empty,
35
+ EmptyContent,
36
+ EmptyDescription,
37
+ EmptyHeader,
38
+ EmptyMedia,
39
+ EmptyTitle,
40
+ } from "@suphark/ui/components/ui/empty";
41
+ import {
42
+ Item,
43
+ ItemActions,
44
+ ItemContent,
45
+ ItemDescription,
46
+ ItemGroup,
47
+ ItemMedia,
48
+ ItemTitle,
49
+ } from "@suphark/ui/components/ui/item";
50
+ import { RelativeDate } from "@suphark/ui/components/ui/relative-date";
51
+ import { Skeleton } from "@suphark/ui/components/ui/skeleton";
52
+ import {
53
+ Table,
54
+ TableBody,
55
+ TableCell,
56
+ TableHead,
57
+ TableHeader,
58
+ TableRow,
59
+ } from "@suphark/ui/components/ui/table";
60
+ import {
61
+ AlertTriangle,
62
+ Download,
63
+ Eye,
64
+ FileCheck,
65
+ FileSpreadsheet,
66
+ FileText,
67
+ MoreHorizontal,
68
+ Package,
69
+ Plus,
70
+ Trash2,
71
+ } from "lucide-react";
72
+ import * as React from "react";
73
+
74
+ export function ShowcaseDataDisplay() {
75
+ const [dataState, setDataState] = React.useState<"ready" | "loading" | "empty">("ready");
76
+
77
+ const tableRows = [
78
+ {
79
+ code: "PKG-2026-041",
80
+ name: "งานจัดซื้อคอนกรีตผสมเสร็จ ผสมสารกันซึม",
81
+ category: "Material / Structural",
82
+ vendor: "Alpha Cement Co., Ltd.",
83
+ quantity: "1,200 m³",
84
+ unitRate: 2450.0,
85
+ totalAmount: 2940000.0,
86
+ status: "Approved",
87
+ badgeVariant: "default",
88
+ date: new Date(Date.now() - 1000 * 60 * 60 * 2),
89
+ },
90
+ {
91
+ code: "PKG-2026-042",
92
+ name: "งานจัดจ้างติดตั้งระบบแจ้งเหตุเพลิงไหม้ (Fire Alarm)",
93
+ category: "Subcontract / MEP",
94
+ vendor: "Beta Fire Engineering Ltd.",
95
+ quantity: "1 Lump Sum",
96
+ unitRate: 4850000.0,
97
+ totalAmount: 4850000.0,
98
+ status: "Under Review",
99
+ badgeVariant: "secondary",
100
+ date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
101
+ },
102
+ {
103
+ code: "PKG-2026-043",
104
+ name: "งานจัดซื้อกระจกลามิเนตกันเสียง Facade",
105
+ category: "Material / Architectural",
106
+ vendor: "Gamma Glass Architectural",
107
+ quantity: "850 m²",
108
+ unitRate: 3200.0,
109
+ totalAmount: 2720000.0,
110
+ status: "Draft",
111
+ badgeVariant: "outline",
112
+ date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),
113
+ },
114
+ {
115
+ code: "PKG-2026-044",
116
+ name: "งานจัดจ้างเจาะเสาเข็ม Bored Pile Dia 800mm",
117
+ category: "Subcontract / Foundation",
118
+ vendor: "Delta Piling Co., Ltd.",
119
+ quantity: "140 Piles",
120
+ unitRate: 18500.0,
121
+ totalAmount: 2590000.0,
122
+ status: "Approved",
123
+ badgeVariant: "default",
124
+ date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 8),
125
+ },
126
+ ];
127
+
128
+ return (
129
+ <div className="flex flex-col gap-8">
130
+ {/* Section Header */}
131
+ <div className="flex flex-col gap-1">
132
+ <div className="flex items-center gap-2">
133
+ <Badge variant="outline">Data Presentation</Badge>
134
+ <span className="text-xs text-muted-foreground">
135
+ Tables, Triple States, Items, Attachments & Formatters
136
+ </span>
137
+ </div>
138
+ <h2 className="text-xl font-bold tracking-tight text-foreground">
139
+ การแสดงผลชุดข้อมูลและสถานะการทำงาน (Data Display & States)
140
+ </h2>
141
+ <p className="text-sm text-muted-foreground">
142
+ ตารางข้อมูลที่มีความหนาแน่นสูง (Data-Dense Tables), การสลับ 3 สถานะ (Ready / Loading / Empty),
143
+ และตัวจัดรูปแบบตัวเลข-วันที่
144
+ </p>
145
+ </div>
146
+
147
+ {/* Triple State Demonstration */}
148
+ <Card>
149
+ <CardHeader>
150
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
151
+ <div>
152
+ <CardTitle className="text-base">Data Table & Triple State Lifecycle</CardTitle>
153
+ <CardDescription className="text-xs">
154
+ ทดสอบสลับระหว่าง Ready State, Geometry-Accurate Skeleton, และ Actionable Empty State
155
+ </CardDescription>
156
+ </div>
157
+ <div className="flex items-center gap-1 rounded-lg border bg-muted/40 p-1">
158
+ <Button
159
+ variant={dataState === "ready" ? "default" : "ghost"}
160
+ size="xs"
161
+ onClick={() => setDataState("ready")}
162
+ >
163
+ 1. Ready Table
164
+ </Button>
165
+ <Button
166
+ variant={dataState === "loading" ? "default" : "ghost"}
167
+ size="xs"
168
+ onClick={() => setDataState("loading")}
169
+ >
170
+ 2. Loading Skeleton
171
+ </Button>
172
+ <Button
173
+ variant={dataState === "empty" ? "default" : "ghost"}
174
+ size="xs"
175
+ onClick={() => setDataState("empty")}
176
+ >
177
+ 3. Empty State
178
+ </Button>
179
+ </div>
180
+ </div>
181
+ </CardHeader>
182
+ <CardContent>
183
+ {dataState === "ready" && (
184
+ <div className="rounded-md border bg-card overflow-hidden">
185
+ <Table>
186
+ <TableHeader>
187
+ <TableRow>
188
+ <TableHead className="w-[140px]">รหัส / Package</TableHead>
189
+ <TableHead>รายการและหมวดหมู่งาน</TableHead>
190
+ <TableHead>คู่ค้าที่เสนอราคา</TableHead>
191
+ <TableHead className="text-center">สถานะ</TableHead>
192
+ <TableHead className="text-right">จำนวน</TableHead>
193
+ <TableHead className="text-right">ราคาต่อหน่วย</TableHead>
194
+ <TableHead className="text-right font-semibold">มูลค่ารวม (THB)</TableHead>
195
+ <TableHead className="text-center w-[70px]">จัดการ</TableHead>
196
+ </TableRow>
197
+ </TableHeader>
198
+ <TableBody>
199
+ {tableRows.map((row) => (
200
+ <TableRow key={row.code} className="hover:bg-muted/40 transition-colors">
201
+ <TableCell className="font-mono text-xs font-medium text-foreground">
202
+ {row.code}
203
+ </TableCell>
204
+ <TableCell>
205
+ <PrimarySecondaryText
206
+ primary={row.name}
207
+ secondary={row.category}
208
+ primaryClassName="font-medium text-xs text-foreground"
209
+ secondaryClassName="text-xs text-muted-foreground"
210
+ />
211
+ </TableCell>
212
+ <TableCell>
213
+ <div className="flex flex-col gap-0.5">
214
+ <span className="text-xs font-medium">{row.vendor}</span>
215
+ <RelativeDate
216
+ date={row.date}
217
+ className="text-xs text-muted-foreground flex items-center gap-1"
218
+ />
219
+ </div>
220
+ </TableCell>
221
+ <TableCell className="text-center">
222
+ <Badge
223
+ variant={row.badgeVariant as any}
224
+ className={
225
+ row.status === "Approved" ? "bg-success text-success-foreground" : ""
226
+ }
227
+ >
228
+ {row.status}
229
+ </Badge>
230
+ </TableCell>
231
+ <TableCell className="text-right text-xs font-mono">{row.quantity}</TableCell>
232
+ <TableCell className="text-right text-xs font-mono">
233
+ <FormattedNumber value={row.unitRate} />
234
+ </TableCell>
235
+ <TableCell className="text-right text-xs font-mono font-semibold text-foreground">
236
+ <FormattedNumber value={row.totalAmount} />
237
+ </TableCell>
238
+ <TableCell className="text-center">
239
+ <DropdownMenu>
240
+ <DropdownMenuTrigger
241
+ render={
242
+ <Button
243
+ variant="ghost"
244
+ size="icon"
245
+ className="size-7"
246
+ aria-label="จัดการรายการ"
247
+ />
248
+ }
249
+ >
250
+ <MoreHorizontal className="size-4" />
251
+ </DropdownMenuTrigger>
252
+ <DropdownMenuContent align="end" className="w-44">
253
+ <DropdownMenuGroup>
254
+ <DropdownMenuLabel className="text-xs">
255
+ ตัวเลือกรายการ
256
+ </DropdownMenuLabel>
257
+ <DropdownMenuItem className="text-xs gap-2">
258
+ <Eye className="size-3.5" /> ดูรายละเอียด
259
+ </DropdownMenuItem>
260
+ <DropdownMenuItem className="text-xs gap-2">
261
+ <FileSpreadsheet className="size-3.5" /> ส่งออก BOQ
262
+ </DropdownMenuItem>
263
+ </DropdownMenuGroup>
264
+ <DropdownMenuSeparator />
265
+ <DropdownMenuItem className="text-xs gap-2 text-destructive">
266
+ <Trash2 className="size-3.5" /> ลบแพ็กเกจ
267
+ </DropdownMenuItem>
268
+ </DropdownMenuContent>
269
+ </DropdownMenu>
270
+ </TableCell>
271
+ </TableRow>
272
+ ))}
273
+ </TableBody>
274
+ </Table>
275
+ </div>
276
+ )}
277
+
278
+ {dataState === "loading" && (
279
+ <div className="flex flex-col gap-3 rounded-md border p-4 bg-card">
280
+ <div className="flex items-center justify-between">
281
+ <Skeleton className="h-6 w-48" />
282
+ <Skeleton className="h-6 w-24" />
283
+ </div>
284
+ <Skeleton className="h-8 w-full" />
285
+ <Skeleton className="h-10 w-full" />
286
+ <Skeleton className="h-10 w-full" />
287
+ <Skeleton className="h-10 w-full" />
288
+ <Skeleton className="h-10 w-full" />
289
+ </div>
290
+ )}
291
+
292
+ {dataState === "empty" && (
293
+ <Empty className="border rounded-lg bg-card py-10">
294
+ <EmptyHeader>
295
+ <EmptyMedia variant="icon">
296
+ <Package className="size-6 text-muted-foreground" />
297
+ </EmptyMedia>
298
+ <EmptyTitle>ไม่พบรายการแพ็กเกจจัดซื้อ</EmptyTitle>
299
+ <EmptyDescription>
300
+ ยังไม่มีการสร้างแพ็กเกจในงวดนี้ หรือคำค้นหาไม่ตรงกับเงื่อนไขการค้นหา
301
+ </EmptyDescription>
302
+ </EmptyHeader>
303
+ <EmptyContent>
304
+ <Button className="gap-2" size="sm">
305
+ <Plus className="size-4" /> สร้างแพ็กเกจจัดซื้อใหม่
306
+ </Button>
307
+ </EmptyContent>
308
+ </Empty>
309
+ )}
310
+ </CardContent>
311
+ </Card>
312
+
313
+ {/* Item & Attachment Primitives */}
314
+ <div className="grid gap-6 lg:grid-cols-2">
315
+ {/* Item Primitive */}
316
+ <Card>
317
+ <CardHeader className="pb-3">
318
+ <CardTitle className="text-base">Item & ItemGroup Primitive</CardTitle>
319
+ <CardDescription className="text-xs">
320
+ คอมโพเนนต์สำหรับแสดงแถวรายการที่มี Media, Badge, และ Inline Actions
321
+ </CardDescription>
322
+ </CardHeader>
323
+ <CardContent>
324
+ <ItemGroup className="gap-2">
325
+ <Item variant="outline" className="p-3">
326
+ <ItemMedia variant="icon">
327
+ <FileCheck className="size-4 text-primary" />
328
+ </ItemMedia>
329
+ <ItemContent>
330
+ <ItemTitle className="text-xs">
331
+ ใบรับรองคุณภาพ มอก. 20-2558 (Alpha Concrete)
332
+ </ItemTitle>
333
+ <ItemDescription className="text-xs">
334
+ รับรองมาตรฐานคอนกรีตผสมเสร็จความแข็งแรงสูง • มีผลบังคับใช้ถึง 31 ธ.ค. 2570
335
+ </ItemDescription>
336
+ </ItemContent>
337
+ <ItemActions>
338
+ <Button variant="outline" size="xs">
339
+ <Download className="size-3" />
340
+ </Button>
341
+ </ItemActions>
342
+ </Item>
343
+
344
+ <Item variant="outline" className="p-3">
345
+ <ItemMedia variant="icon">
346
+ <AlertTriangle className="size-4 text-amber-500" />
347
+ </ItemMedia>
348
+ <ItemContent>
349
+ <ItemTitle className="text-xs">หนังสือค้ำประกันผลงาน (Performance Bond)</ItemTitle>
350
+ <ItemDescription className="text-xs">
351
+ อยู่ระหว่างรอหนังสือค้ำประกันฉบับจริงจากธนาคารกสิกรไทย
352
+ </ItemDescription>
353
+ </ItemContent>
354
+ <ItemActions>
355
+ <Badge variant="outline" className="text-xs">
356
+ Pending
357
+ </Badge>
358
+ </ItemActions>
359
+ </Item>
360
+ </ItemGroup>
361
+ </CardContent>
362
+ </Card>
363
+
364
+ {/* Attachment Primitive */}
365
+ <Card>
366
+ <CardHeader className="pb-3">
367
+ <CardTitle className="text-base">Attachment Primitive</CardTitle>
368
+ <CardDescription className="text-xs">
369
+ การ์ดไฟล์แนบพร้อมสถานะการอัปโหลด (Done / Uploading / Error)
370
+ </CardDescription>
371
+ </CardHeader>
372
+ <CardContent className="flex flex-col gap-3">
373
+ <AttachmentGroup>
374
+ <Attachment state="done" size="default">
375
+ <AttachmentMedia variant="icon">
376
+ <FileSpreadsheet className="size-4 text-emerald-600" />
377
+ </AttachmentMedia>
378
+ <AttachmentContent>
379
+ <AttachmentTitle>BOQ_Facade_Rev2.xlsx</AttachmentTitle>
380
+ <AttachmentDescription>2.4 MB • Alpha Approved</AttachmentDescription>
381
+ </AttachmentContent>
382
+ <AttachmentActions>
383
+ <AttachmentAction size="icon-xs" aria-label="ดาวน์โหลดไฟล์">
384
+ <Download className="size-3" />
385
+ </AttachmentAction>
386
+ </AttachmentActions>
387
+ </Attachment>
388
+
389
+ <Attachment state="done" size="default">
390
+ <AttachmentMedia variant="icon">
391
+ <FileText className="size-4 text-primary" />
392
+ </AttachmentMedia>
393
+ <AttachmentContent>
394
+ <AttachmentTitle>Tender_Terms_2026.pdf</AttachmentTitle>
395
+ <AttachmentDescription>1.1 MB • Signed</AttachmentDescription>
396
+ </AttachmentContent>
397
+ <AttachmentActions>
398
+ <AttachmentAction size="icon-xs" aria-label="ดาวน์โหลดไฟล์">
399
+ <Download className="size-3" />
400
+ </AttachmentAction>
401
+ </AttachmentActions>
402
+ </Attachment>
403
+ </AttachmentGroup>
404
+ </CardContent>
405
+ </Card>
406
+ </div>
407
+
408
+ {/* Shared Formatters & Benchmark Analysis */}
409
+ <div className="grid gap-6 lg:grid-cols-2">
410
+ {/* Value Formatters */}
411
+ <Card>
412
+ <CardHeader className="pb-3">
413
+ <CardTitle className="text-base">Shared Value Formatters</CardTitle>
414
+ <CardDescription className="text-xs">
415
+ Formatters เพื่อป้องกัน SSR Hydration Mismatch และคุม Locale ภาษาไทย
416
+ </CardDescription>
417
+ </CardHeader>
418
+ <CardContent className="flex flex-col gap-3">
419
+ <div className="flex items-center justify-between rounded-md border p-3 bg-card">
420
+ <div className="flex flex-col">
421
+ <span className="text-xs font-semibold">FormattedNumber (Currency)</span>
422
+ <span className="text-xs text-muted-foreground">ปัดทศนิยม 2 ตำแหน่ง พร้อมลูกน้ำ</span>
423
+ </div>
424
+ <span className="font-mono text-sm font-bold text-foreground">
425
+ ฿ <FormattedNumber value={18450750.5} />
426
+ </span>
427
+ </div>
428
+
429
+ <div className="flex items-center justify-between rounded-md border p-3 bg-card">
430
+ <div className="flex flex-col">
431
+ <span className="text-xs font-semibold">PrimarySecondaryText</span>
432
+ <span className="text-xs text-muted-foreground">ข้อความหลัก/รองแบบสองชั้น</span>
433
+ </div>
434
+ <PrimarySecondaryText
435
+ primary="นายตัวอย่าง ใจดี"
436
+ secondary="Chief Procurement Officer"
437
+ primaryClassName="font-medium text-xs"
438
+ secondaryClassName="text-xs text-muted-foreground"
439
+ />
440
+ </div>
441
+
442
+ <div className="flex items-center justify-between rounded-md border p-3 bg-card">
443
+ <div className="flex flex-col">
444
+ <span className="text-xs font-semibold">RelativeDate (Thai relative)</span>
445
+ <span className="text-xs text-muted-foreground">แสดงเวลาแบบสัมพัทธ์ภาษาไทย</span>
446
+ </div>
447
+ <RelativeDate date={new Date(Date.now() - 1000 * 60 * 30)} />
448
+ </div>
449
+ </CardContent>
450
+ </Card>
451
+ </div>
452
+ </div>
453
+ );
454
+ }