@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,397 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@suphark/ui/components/ui/badge";
|
|
4
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "@suphark/ui/components/ui/card";
|
|
12
|
+
import {
|
|
13
|
+
Accessibility,
|
|
14
|
+
Check,
|
|
15
|
+
CheckCircle2,
|
|
16
|
+
Code2,
|
|
17
|
+
Copy,
|
|
18
|
+
MessageSquare,
|
|
19
|
+
RefreshCw,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import * as React from "react";
|
|
22
|
+
|
|
23
|
+
export function ShowcaseGuidelinesRecipes() {
|
|
24
|
+
const [copiedIndex, setCopiedIndex] = React.useState<number | null>(null);
|
|
25
|
+
|
|
26
|
+
const copyCode = (code: string, idx: number) => {
|
|
27
|
+
navigator.clipboard.writeText(code);
|
|
28
|
+
setCopiedIndex(idx);
|
|
29
|
+
setTimeout(() => setCopiedIndex(null), 2000);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const a11yRules = [
|
|
33
|
+
{
|
|
34
|
+
title: "Focus Visible Rings",
|
|
35
|
+
rule: "ทุก interactive element ต้องมี focus ring ชัดเจน",
|
|
36
|
+
detail:
|
|
37
|
+
"ใช้คลาส focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none เพื่อให้ผู้ใช้คีย์บอร์ดรับรู้ตำแหน่งปัจจุบันได้แม่นยำ",
|
|
38
|
+
tag: "Keyboard A11y",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: "Icon-Only Button Accessible Names",
|
|
42
|
+
rule: "ปุ่มที่มีเฉพาะไอคอนต้องมี aria-label เสมอ",
|
|
43
|
+
detail:
|
|
44
|
+
"<Button size='icon' aria-label='ลบแพ็กเกจ'><Trash2 /></Button> เพื่อให้ Screen Reader อ่านออกเสียงความหมายของปุ่มได้ถูกต้อง",
|
|
45
|
+
tag: "Screen Reader",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Dialog / Sheet Titles & Descriptions",
|
|
49
|
+
rule: "Modal ทุกตัวต้องมี DialogTitle และ DialogDescription",
|
|
50
|
+
detail:
|
|
51
|
+
"Base UI ต้องการ Title สำหรับ ARIA header ถ้าไม่ต้องการให้แสดงผลทางสายตาให้ใช้ className='sr-only'",
|
|
52
|
+
tag: "Dialog Semantics",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
title: "Error Recovery & Tone of Voice",
|
|
56
|
+
rule: "ข้อความ Error ต้องชี้แนะวิธีแก้ไข โดยไม่กล่าวโทษผู้ใช้",
|
|
57
|
+
detail:
|
|
58
|
+
"บอกสิ่งที่เกิดขึ้นและวิธีแก้ให้ตรงจุด เช่น 'ระบุงบประมาณเป็นตัวเลขมากกว่า 0 บาท' แทน 'คุณกรอกข้อมูลผิดพลาด'",
|
|
59
|
+
tag: "UX Copy",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Reduced Motion Respect",
|
|
63
|
+
rule: "เคารพการตั้งค่าลดการเคลื่อนไหวของผู้ใช้",
|
|
64
|
+
detail:
|
|
65
|
+
"ระบบ Tailwind v4 รองรับ motion-reduce อัตโนมัติ เลี่ยงการใส่แอนิเมชันที่ไม่จำเป็นในหน้างานทำงานข้อมูล",
|
|
66
|
+
tag: "Motion",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const uxWritingRules = [
|
|
71
|
+
{
|
|
72
|
+
bad: "Submit / ยืนยัน",
|
|
73
|
+
good: "Save changes / บันทึกการเปลี่ยนแปลง",
|
|
74
|
+
why: "ปุ่มควรระบุสิ่งที่จะเกิดขึ้นกับระบบโดยตรง (Active Voice)",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
bad: "Delete / ลบ",
|
|
78
|
+
good: "Delete package / ลบแพ็กเกจจัดซื้อ",
|
|
79
|
+
why: "ระบุเป้าหมายของ Action เพื่อป้องกันการกดลบผิดพลาด",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
bad: "คลิกที่ 'Publish' -> Toast 'บันทึกสำเร็จ'",
|
|
83
|
+
good: "คลิกที่ 'Publish' -> Toast 'Published package successfully'",
|
|
84
|
+
why: "รักษาความสอดคล้องของคำศัพท์จากปุ่มไปยังผลลัพธ์ (Action-to-Toast Consistency)",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
bad: "ไม่พบข้อมูล กรุณาลองใหม่อีกครั้ง",
|
|
88
|
+
good: "ยังไม่มีแพ็กเกจจัดซื้อในโครงการนี้ [สร้างแพ็กเกจใหม่]",
|
|
89
|
+
why: "Empty State คือคำเชิญชวนให้เริ่มต้นทำ Action ต่อไป ไม่ใช่ทางตัน",
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const recipe1 = `// 1. PageShell + Toolbar + DataTable Pattern
|
|
94
|
+
import { PageShell } from "@suphark/ui/components/shared/layout/page-shell";
|
|
95
|
+
import { AddNewButton } from "@suphark/ui/components/shared/button";
|
|
96
|
+
import { DataTable } from "@suphark/ui/components/shared/data-table/data-table";
|
|
97
|
+
|
|
98
|
+
export default function PackageListPage() {
|
|
99
|
+
return (
|
|
100
|
+
<PageShell
|
|
101
|
+
title="แพ็กเกจจัดซื้อ (Procurement Packages)"
|
|
102
|
+
description="จัดการและติดตามสถานะการเปิดซองราคาและจัดจ้างผู้รับเหมา"
|
|
103
|
+
actions={<AddNewButton label="สร้างแพ็กเกจจัดซื้อ" />}
|
|
104
|
+
>
|
|
105
|
+
<div className="flex flex-col gap-4">
|
|
106
|
+
{/* Toolbar Filters */}
|
|
107
|
+
<div className="flex items-center justify-between gap-3">
|
|
108
|
+
<PackageSearchFilter />
|
|
109
|
+
<PackageStatusFilter />
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Dynamic Table with Suspense Fallback */}
|
|
113
|
+
<PackageDataTable />
|
|
114
|
+
</div>
|
|
115
|
+
</PageShell>
|
|
116
|
+
);
|
|
117
|
+
}`;
|
|
118
|
+
|
|
119
|
+
const recipe2 = `// 2. FieldGroup + InputGroup + Server Action Validation
|
|
120
|
+
import { FieldGroup, Field, FieldLabel, FieldDescription, FieldError } from "@suphark/ui/components/ui/field";
|
|
121
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from "@suphark/ui/components/ui/input-group";
|
|
122
|
+
import { DollarSign } from "lucide-react";
|
|
123
|
+
|
|
124
|
+
export function BudgetField({ state }: { state?: ActionResponse }) {
|
|
125
|
+
const isInvalid = Boolean(state?.errors?.budget);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<FieldGroup>
|
|
129
|
+
<Field data-invalid={isInvalid ? true : undefined}>
|
|
130
|
+
<FieldLabel htmlFor="budget-input">งบประมาณกลาง (THB)</FieldLabel>
|
|
131
|
+
<InputGroup>
|
|
132
|
+
<InputGroupAddon>
|
|
133
|
+
<DollarSign className="size-4 text-muted-foreground" />
|
|
134
|
+
</InputGroupAddon>
|
|
135
|
+
<InputGroupInput
|
|
136
|
+
id="budget-input"
|
|
137
|
+
name="budget"
|
|
138
|
+
aria-invalid={isInvalid ? true : undefined}
|
|
139
|
+
placeholder="0.00"
|
|
140
|
+
/>
|
|
141
|
+
</InputGroup>
|
|
142
|
+
{isInvalid ? (
|
|
143
|
+
<FieldError>{state.errors.budget[0]}</FieldError>
|
|
144
|
+
) : (
|
|
145
|
+
<FieldDescription>ระบุจำนวนเงินสุทธิรวม VAT 7%</FieldDescription>
|
|
146
|
+
)}
|
|
147
|
+
</Field>
|
|
148
|
+
</FieldGroup>
|
|
149
|
+
);
|
|
150
|
+
}`;
|
|
151
|
+
|
|
152
|
+
const recipe3 = `// 3. Destructive Confirmation AlertDialog
|
|
153
|
+
import {
|
|
154
|
+
AlertDialog,
|
|
155
|
+
AlertDialogAction,
|
|
156
|
+
AlertDialogCancel,
|
|
157
|
+
AlertDialogContent,
|
|
158
|
+
AlertDialogDescription,
|
|
159
|
+
AlertDialogFooter,
|
|
160
|
+
AlertDialogHeader,
|
|
161
|
+
AlertDialogTitle,
|
|
162
|
+
AlertDialogTrigger,
|
|
163
|
+
} from "@suphark/ui/components/ui/alert-dialog";
|
|
164
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
165
|
+
|
|
166
|
+
export function DeletePackageDialog({ packageId }: { packageId: string }) {
|
|
167
|
+
return (
|
|
168
|
+
<AlertDialog>
|
|
169
|
+
<AlertDialogTrigger render={<Button variant="destructive" size="sm" />}>
|
|
170
|
+
ลบแพ็กเกจ
|
|
171
|
+
</AlertDialogTrigger>
|
|
172
|
+
<AlertDialogContent>
|
|
173
|
+
<AlertDialogHeader>
|
|
174
|
+
<AlertDialogTitle>ยืนยันการลบแพ็กเกจจัดซื้อ?</AlertDialogTitle>
|
|
175
|
+
<AlertDialogDescription>
|
|
176
|
+
การกระทำนี้จะลบใบเสนอราคาและเอกสารแนบทั้งหมดอย่างถาวร และไม่สามารถกู้คืนได้
|
|
177
|
+
</AlertDialogDescription>
|
|
178
|
+
</AlertDialogHeader>
|
|
179
|
+
<AlertDialogFooter>
|
|
180
|
+
<AlertDialogCancel>ยกเลิก</AlertDialogCancel>
|
|
181
|
+
<AlertDialogAction variant="destructive" onClick={() => handleDelete(packageId)}>
|
|
182
|
+
ยืนยันการลบแพ็กเกจ
|
|
183
|
+
</AlertDialogAction>
|
|
184
|
+
</AlertDialogFooter>
|
|
185
|
+
</AlertDialogContent>
|
|
186
|
+
</AlertDialog>
|
|
187
|
+
);
|
|
188
|
+
}`;
|
|
189
|
+
|
|
190
|
+
const recipe4 = `// 4. Server Action Trigger Button with Loading Spinner & Toast
|
|
191
|
+
"use client";
|
|
192
|
+
|
|
193
|
+
import { useTransition } from "react";
|
|
194
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
195
|
+
import { Spinner } from "@suphark/ui/components/ui/spinner";
|
|
196
|
+
import { toast } from "sonner";
|
|
197
|
+
import { approvePackageAction } from "../actions/approve-package";
|
|
198
|
+
|
|
199
|
+
export function ApprovePackageButton({ id }: { id: string }) {
|
|
200
|
+
const [isPending, startTransition] = useTransition();
|
|
201
|
+
|
|
202
|
+
const handleApprove = () => {
|
|
203
|
+
startTransition(async () => {
|
|
204
|
+
const res = await approvePackageAction(id);
|
|
205
|
+
if (res.success) {
|
|
206
|
+
toast.success("อนุมัติแพ็กเกจสำเร็จ", {
|
|
207
|
+
description: "ส่งการแจ้งเตือนไปยังผู้เสนอราคาเรียบร้อยแล้ว",
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
toast.error("การอนุมัติล้มเหลว", { description: res.error });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Button onClick={handleApprove} disabled={isPending}>
|
|
217
|
+
{isPending && <Spinner data-icon="inline-start" />}
|
|
218
|
+
{isPending ? "กำลังอนุมัติ..." : "อนุมัติแพ็กเกจ"}
|
|
219
|
+
</Button>
|
|
220
|
+
);
|
|
221
|
+
}`;
|
|
222
|
+
|
|
223
|
+
const recipes = [
|
|
224
|
+
{ title: "1. PageShell + Toolbar + Table", code: recipe1 },
|
|
225
|
+
{ title: "2. Unified FieldGroup & Validation", code: recipe2 },
|
|
226
|
+
{ title: "3. Destructive Confirmation AlertDialog", code: recipe3 },
|
|
227
|
+
{ title: "4. Server Action Button + Spinner + Toast", code: recipe4 },
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="flex flex-col gap-8">
|
|
232
|
+
{/* Section Header */}
|
|
233
|
+
<div className="flex flex-col gap-1">
|
|
234
|
+
<div className="flex items-center gap-2">
|
|
235
|
+
<Badge variant="outline">Rules & Recipes</Badge>
|
|
236
|
+
<span className="text-xs text-muted-foreground">
|
|
237
|
+
Web Interface Guidelines, UX Copy Standards & Production Code Recipes
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
<h2 className="text-xl font-bold tracking-tight text-foreground">
|
|
241
|
+
แนวปฏิบัติและแม่แบบโค้ดมาตรฐาน (Guidelines & Code Recipes)
|
|
242
|
+
</h2>
|
|
243
|
+
<p className="text-sm text-muted-foreground">
|
|
244
|
+
สรุปเกณฑ์การตรวจสอบ Accessibility, กฎการเขียน UX Copy สำหรับผู้ใช้,
|
|
245
|
+
และแม่แบบโค้ดที่สามารถนำไปใช้งานได้ทันที
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Web Interface Guidelines Checklist */}
|
|
250
|
+
<Card>
|
|
251
|
+
<CardHeader>
|
|
252
|
+
<div className="flex items-center justify-between">
|
|
253
|
+
<div>
|
|
254
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
255
|
+
<Accessibility className="size-4 text-primary" /> Web Interface Guidelines
|
|
256
|
+
(Mandatory Baseline)
|
|
257
|
+
</CardTitle>
|
|
258
|
+
<CardDescription className="text-xs">
|
|
259
|
+
เกณฑ์มาตรฐานสากลที่ทุกหน้าจอต้องผ่านการตรวจสอบ
|
|
260
|
+
</CardDescription>
|
|
261
|
+
</div>
|
|
262
|
+
<Badge variant="secondary">A11y Baseline</Badge>
|
|
263
|
+
</div>
|
|
264
|
+
</CardHeader>
|
|
265
|
+
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
266
|
+
{a11yRules.map((rule) => (
|
|
267
|
+
<div key={rule.title} className="flex flex-col gap-1.5 rounded-lg border bg-card p-3">
|
|
268
|
+
<div className="flex items-center justify-between">
|
|
269
|
+
<Badge variant="outline" className="text-xs">
|
|
270
|
+
{rule.tag}
|
|
271
|
+
</Badge>
|
|
272
|
+
<CheckCircle2 className="size-3.5 text-primary" />
|
|
273
|
+
</div>
|
|
274
|
+
<h4 className="font-semibold text-xs text-foreground pt-1">{rule.title}</h4>
|
|
275
|
+
<p className="text-xs text-primary/90 font-medium">{rule.rule}</p>
|
|
276
|
+
<p className="text-xs text-muted-foreground leading-normal">{rule.detail}</p>
|
|
277
|
+
</div>
|
|
278
|
+
))}
|
|
279
|
+
</CardContent>
|
|
280
|
+
</Card>
|
|
281
|
+
|
|
282
|
+
{/* UX Writing & Copy Standards */}
|
|
283
|
+
<Card>
|
|
284
|
+
<CardHeader>
|
|
285
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
286
|
+
<MessageSquare className="size-4 text-primary" /> Frontend Design: UX Writing & Action
|
|
287
|
+
Voice
|
|
288
|
+
</CardTitle>
|
|
289
|
+
<CardDescription className="text-xs">
|
|
290
|
+
การใช้ภาษาที่ชัดเจนและมุ่งเน้นสิ่งที่ผู้ใช้ควบคุมได้ (Active Voice & Cohesion)
|
|
291
|
+
</CardDescription>
|
|
292
|
+
</CardHeader>
|
|
293
|
+
<CardContent className="grid gap-3 md:grid-cols-2">
|
|
294
|
+
{uxWritingRules.map((item) => (
|
|
295
|
+
<div key={item.bad} className="flex flex-col gap-2 rounded-lg border p-3 bg-card">
|
|
296
|
+
<div className="flex flex-col gap-1 text-xs">
|
|
297
|
+
<div className="flex items-start gap-2 text-destructive">
|
|
298
|
+
<span className="font-bold">✕ หลีกเลี่ยง:</span>
|
|
299
|
+
<span className="line-through">{item.bad}</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="flex items-start gap-2 text-emerald-600 font-medium">
|
|
302
|
+
<span className="font-bold">✓ แนะนำ:</span>
|
|
303
|
+
<span>{item.good}</span>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
<p className="text-xs text-muted-foreground pt-1 border-t border-dashed">
|
|
307
|
+
{item.why}
|
|
308
|
+
</p>
|
|
309
|
+
</div>
|
|
310
|
+
))}
|
|
311
|
+
</CardContent>
|
|
312
|
+
</Card>
|
|
313
|
+
|
|
314
|
+
{/* Redesign Workflow Checklist */}
|
|
315
|
+
<Card>
|
|
316
|
+
<CardHeader>
|
|
317
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
318
|
+
<RefreshCw className="size-4 text-primary" /> Redesign Safety: Behavior Preservation
|
|
319
|
+
Checklist
|
|
320
|
+
</CardTitle>
|
|
321
|
+
<CardDescription className="text-xs">
|
|
322
|
+
เมื่อทำการ Redesign UI ต้องคงความสามารถของระบบเดิม 100%
|
|
323
|
+
</CardDescription>
|
|
324
|
+
</CardHeader>
|
|
325
|
+
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 text-xs">
|
|
326
|
+
<div className="flex flex-col gap-1 rounded-md border p-3 bg-card">
|
|
327
|
+
<span className="font-semibold text-foreground">1. Auth & Permissions</span>
|
|
328
|
+
<p className="text-muted-foreground">
|
|
329
|
+
คงการตรวจสิทธิ์ 7-action RBAC และ Server Action Guard เดิม
|
|
330
|
+
</p>
|
|
331
|
+
</div>
|
|
332
|
+
<div className="flex flex-col gap-1 rounded-md border p-3 bg-card">
|
|
333
|
+
<span className="font-semibold text-foreground">2. Data Contracts</span>
|
|
334
|
+
<p className="text-muted-foreground">
|
|
335
|
+
คง Form validation schema, ActionResponse<T> และ API types
|
|
336
|
+
</p>
|
|
337
|
+
</div>
|
|
338
|
+
<div className="flex flex-col gap-1 rounded-md border p-3 bg-card">
|
|
339
|
+
<span className="font-semibold text-foreground">3. URL & Search State</span>
|
|
340
|
+
<p className="text-muted-foreground">
|
|
341
|
+
คง Query params สำหรับ Search, Filter, Tab และ Pagination
|
|
342
|
+
</p>
|
|
343
|
+
</div>
|
|
344
|
+
<div className="flex flex-col gap-1 rounded-md border p-3 bg-card">
|
|
345
|
+
<span className="font-semibold text-foreground">4. Loading & Error States</span>
|
|
346
|
+
<p className="text-muted-foreground">
|
|
347
|
+
คง Skeletons, Suspense Boundaries, Error Boundaries, และ Empty views
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
</CardContent>
|
|
351
|
+
</Card>
|
|
352
|
+
|
|
353
|
+
{/* Code Recipes */}
|
|
354
|
+
<section className="flex flex-col gap-4">
|
|
355
|
+
<div className="flex items-center justify-between">
|
|
356
|
+
<div>
|
|
357
|
+
<h3 className="text-base font-semibold flex items-center gap-2">
|
|
358
|
+
<Code2 className="size-4 text-primary" /> Production Code Recipes
|
|
359
|
+
</h3>
|
|
360
|
+
<p className="text-xs text-muted-foreground">แม่แบบโค้ดที่พร้อมคัดลอกไปใช้งานในฟีเจอร์ต่าง ๆ</p>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<div className="grid gap-6 xl:grid-cols-2">
|
|
365
|
+
{recipes.map((recipe, idx) => (
|
|
366
|
+
<Card key={recipe.title} className="flex flex-col justify-between overflow-hidden">
|
|
367
|
+
<CardHeader className="py-3 px-4 bg-muted/40 border-b flex-row items-center justify-between">
|
|
368
|
+
<CardTitle className="text-xs font-semibold font-mono">{recipe.title}</CardTitle>
|
|
369
|
+
<Button
|
|
370
|
+
size="xs"
|
|
371
|
+
variant="outline"
|
|
372
|
+
className="gap-1.5"
|
|
373
|
+
onClick={() => copyCode(recipe.code, idx)}
|
|
374
|
+
>
|
|
375
|
+
{copiedIndex === idx ? (
|
|
376
|
+
<>
|
|
377
|
+
<Check className="size-3 text-success" /> คัดลอกแล้ว
|
|
378
|
+
</>
|
|
379
|
+
) : (
|
|
380
|
+
<>
|
|
381
|
+
<Copy className="size-3" /> คัดลอกโค้ด
|
|
382
|
+
</>
|
|
383
|
+
)}
|
|
384
|
+
</Button>
|
|
385
|
+
</CardHeader>
|
|
386
|
+
<CardContent className="p-0">
|
|
387
|
+
<pre className="max-h-[380px] overflow-x-auto p-3 text-xs font-mono leading-relaxed bg-muted/20">
|
|
388
|
+
<code>{recipe.code}</code>
|
|
389
|
+
</pre>
|
|
390
|
+
</CardContent>
|
|
391
|
+
</Card>
|
|
392
|
+
))}
|
|
393
|
+
</div>
|
|
394
|
+
</section>
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
}
|