@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/components.json +21 -0
- package/package.json +108 -0
- package/scripts/audit-base-ui-props.mjs +179 -0
- package/scripts/audit-tailwind-typography.mjs +100 -0
- package/src/components/design-system/design-system-showcase.tsx +138 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
- package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
- package/src/components/design-system/sections/showcase-forms.tsx +373 -0
- package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
- package/src/components/design-system/sections/showcase-overview.tsx +368 -0
- package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
- package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
- package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
- package/src/components/shared/button/add-new-button.tsx +19 -0
- package/src/components/shared/button/base/action-button.tsx +134 -0
- package/src/components/shared/button/base/link-button.tsx +130 -0
- package/src/components/shared/button/cancel-button.tsx +27 -0
- package/src/components/shared/button/confirm-button.tsx +26 -0
- package/src/components/shared/button/delete-button.tsx +36 -0
- package/src/components/shared/button/floating-save-button.tsx +89 -0
- package/src/components/shared/button/index.ts +13 -0
- package/src/components/shared/button/print-button.tsx +67 -0
- package/src/components/shared/button/refresh-button.tsx +63 -0
- package/src/components/shared/button/save-button.tsx +29 -0
- package/src/components/shared/button/submit-button.tsx +58 -0
- package/src/components/shared/client-redirect.tsx +11 -0
- package/src/components/shared/combobox.tsx +116 -0
- package/src/components/shared/data-list.tsx +65 -0
- package/src/components/shared/date/date-range-picker.tsx +106 -0
- package/src/components/shared/description-list.tsx +27 -0
- package/src/components/shared/empty-row.tsx +13 -0
- package/src/components/shared/empty-state.tsx +31 -0
- package/src/components/shared/env-snippet.tsx +62 -0
- package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
- package/src/components/shared/form/form-field-boolean.tsx +93 -0
- package/src/components/shared/form/form-field-color-select.tsx +86 -0
- package/src/components/shared/form/form-field-combobox.tsx +167 -0
- package/src/components/shared/form/form-field-date.tsx +55 -0
- package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
- package/src/components/shared/form/form-field-input.tsx +372 -0
- package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
- package/src/components/shared/form/form-field-radio-group.tsx +56 -0
- package/src/components/shared/form/form-field-select.tsx +312 -0
- package/src/components/shared/form/simple-select.tsx +152 -0
- package/src/components/shared/form-dialog.tsx +109 -0
- package/src/components/shared/form-message.tsx +30 -0
- package/src/components/shared/initials.tsx +33 -0
- package/src/components/shared/layout/error-state.tsx +160 -0
- package/src/components/shared/layout/page-shell.tsx +57 -0
- package/src/components/shared/link/transition-link.tsx +72 -0
- package/src/components/shared/multi-select-combobox.tsx +392 -0
- package/src/components/shared/name-id-fields.tsx +133 -0
- package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
- package/src/components/shared/notice.tsx +35 -0
- package/src/components/shared/number/formatted-number.tsx +133 -0
- package/src/components/shared/page-header.tsx +25 -0
- package/src/components/shared/page-skeleton.tsx +18 -0
- package/src/components/shared/row-actions.tsx +93 -0
- package/src/components/shared/row-menu.tsx +55 -0
- package/src/components/shared/secret-reveal.tsx +53 -0
- package/src/components/shared/section-card.tsx +44 -0
- package/src/components/shared/stat.tsx +26 -0
- package/src/components/shared/status-badge.tsx +47 -0
- package/src/components/shared/text/primary-secondary-text.tsx +186 -0
- package/src/components/shared/theme/index.ts +4 -0
- package/src/components/shared/theme/theme-menu-items.tsx +43 -0
- package/src/components/shared/theme/theme-provider.tsx +39 -0
- package/src/components/shared/theme/theme-switch.tsx +36 -0
- package/src/components/shared/theme/theme-toggle.tsx +76 -0
- package/src/components/shared/time/client-date-formatter.tsx +188 -0
- package/src/components/shared/ui-text.tsx +47 -0
- package/src/components/ui/__tests__/combobox.test.tsx +40 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
- package/src/components/ui/__tests__/slider.test.tsx +18 -0
- package/src/components/ui/__tests__/tabs.test.tsx +33 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +161 -0
- package/src/components/ui/alert.tsx +68 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +194 -0
- package/src/components/ui/avatar.tsx +92 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button-group.tsx +77 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +184 -0
- package/src/components/ui/card.tsx +87 -0
- package/src/components/ui/carousel.tsx +230 -0
- package/src/components/ui/chart.tsx +338 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/combobox.tsx +272 -0
- package/src/components/ui/command.tsx +180 -0
- package/src/components/ui/context-menu.tsx +244 -0
- package/src/components/ui/date-picker.tsx +308 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +208 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +93 -0
- package/src/components/ui/field.tsx +223 -0
- package/src/components/ui/form.tsx +161 -0
- package/src/components/ui/hover-card.tsx +46 -0
- package/src/components/ui/input-group.tsx +145 -0
- package/src/components/ui/input-otp.tsx +85 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +187 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/menubar.tsx +266 -0
- package/src/components/ui/navigation-menu.tsx +160 -0
- package/src/components/ui/pagination.tsx +117 -0
- package/src/components/ui/popover.tsx +76 -0
- package/src/components/ui/progress.tsx +66 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/relative-date.tsx +40 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/scroll-area.tsx +67 -0
- package/src/components/ui/searchable-select.tsx +124 -0
- package/src/components/ui/select.tsx +189 -0
- package/src/components/ui/separator.tsx +21 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/sidebar.tsx +691 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +55 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +95 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +54 -0
- package/src/hooks/use-copy-to-clipboard.ts +27 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +156 -0
- package/src/lib/formatters.ts +44 -0
- package/src/lib/permission.ts +75 -0
- package/src/lib/status-colors.ts +52 -0
- package/src/lib/utils.ts +6 -0
- package/src/next.ts +15 -0
- 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
|
+
}
|