@suphark/ui 0.19.0 → 0.20.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 (72) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +14 -13
  3. package/scripts/audit.mjs +0 -0
  4. package/src/components/app-shell/app-shell.tsx +12 -0
  5. package/src/components/app-shell/index.ts +1 -0
  6. package/src/components/app-shell/mobile-tab-bar.tsx +104 -0
  7. package/src/components/shared/action-menu.tsx +155 -0
  8. package/src/components/shared/filter-bar.tsx +108 -0
  9. package/src/components/shared/month-stepper.tsx +124 -0
  10. package/src/components/shared/segmented-control.tsx +73 -0
  11. package/src/components/shared/settings-layout.tsx +170 -0
  12. package/src/components/shared/setup-checklist.tsx +104 -0
  13. package/src/components/shared/simple-dialog.tsx +172 -0
  14. package/src/components/shared/simple-pagination.tsx +109 -0
  15. package/src/components/shared/simple-table.tsx +93 -0
  16. package/src/components/shared/simple-tabs.tsx +118 -0
  17. package/src/components/shared/simple-tooltip.tsx +87 -0
  18. package/src/components/shared/ui-text.tsx +28 -0
  19. package/src/components/ui/accordion.tsx +2 -2
  20. package/src/components/ui/alert-dialog.tsx +4 -4
  21. package/src/components/ui/alert.tsx +2 -2
  22. package/src/components/ui/attachment.tsx +9 -9
  23. package/src/components/ui/avatar.tsx +4 -4
  24. package/src/components/ui/badge.tsx +1 -1
  25. package/src/components/ui/breadcrumb.tsx +1 -1
  26. package/src/components/ui/bubble.tsx +123 -0
  27. package/src/components/ui/button-group.tsx +2 -2
  28. package/src/components/ui/button.tsx +6 -6
  29. package/src/components/ui/calendar.tsx +9 -9
  30. package/src/components/ui/card.tsx +3 -3
  31. package/src/components/ui/chart.tsx +2 -2
  32. package/src/components/ui/checkbox.tsx +1 -1
  33. package/src/components/ui/combobox.tsx +6 -6
  34. package/src/components/ui/command.tsx +4 -4
  35. package/src/components/ui/context-menu.tsx +7 -7
  36. package/src/components/ui/dialog.tsx +4 -4
  37. package/src/components/ui/direction.tsx +6 -0
  38. package/src/components/ui/drawer.tsx +10 -10
  39. package/src/components/ui/dropdown-menu.tsx +8 -8
  40. package/src/components/ui/empty.tsx +4 -4
  41. package/src/components/ui/field.tsx +8 -8
  42. package/src/components/ui/hover-card.tsx +1 -2
  43. package/src/components/ui/input-group.tsx +3 -3
  44. package/src/components/ui/input-otp.tsx +1 -1
  45. package/src/components/ui/input.tsx +1 -1
  46. package/src/components/ui/item.tsx +4 -4
  47. package/src/components/ui/kbd.tsx +1 -1
  48. package/src/components/ui/label.tsx +1 -1
  49. package/src/components/ui/marker.tsx +67 -0
  50. package/src/components/ui/menubar.tsx +9 -9
  51. package/src/components/ui/message-scroller.tsx +129 -0
  52. package/src/components/ui/message.tsx +84 -0
  53. package/src/components/ui/native-select.tsx +1 -1
  54. package/src/components/ui/navigation-menu.tsx +6 -6
  55. package/src/components/ui/popover.tsx +1 -1
  56. package/src/components/ui/progress.tsx +2 -3
  57. package/src/components/ui/questionnaire.tsx +314 -0
  58. package/src/components/ui/radio-group.tsx +1 -2
  59. package/src/components/ui/resizable.tsx +1 -1
  60. package/src/components/ui/select.tsx +4 -4
  61. package/src/components/ui/separator.tsx +0 -1
  62. package/src/components/ui/sheet.tsx +3 -3
  63. package/src/components/ui/sidebar.tsx +14 -14
  64. package/src/components/ui/switch.tsx +2 -3
  65. package/src/components/ui/textarea.tsx +1 -1
  66. package/src/components/ui/toast.tsx +221 -0
  67. package/src/components/ui/toggle-group.tsx +2 -2
  68. package/src/components/ui/toggle.tsx +1 -1
  69. package/src/components/ui/tooltip.tsx +0 -1
  70. package/src/index.ts +41 -0
  71. package/src/lib/month.ts +60 -0
  72. package/src/styles.css +10 -0
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { Button } from "@suphark/ui/components/ui/button";
4
+ import { cn } from "@suphark/ui/lib/utils";
5
+ import { ChevronLeft, ChevronRight } from "lucide-react";
6
+ import { useUiText } from "./ui-text";
7
+
8
+ /** เลขหน้าที่แสดง: หน้าแรก/สุดท้ายเสมอ + รอบ ๆ หน้าปัจจุบัน, ช่องว่างเป็น "…" */
9
+ export function paginationRange(
10
+ page: number,
11
+ pageCount: number,
12
+ siblings = 1,
13
+ ): Array<number | "…"> {
14
+ // จำนวนช่องคงที่ (แรก + สุดท้าย + ปัจจุบัน + เพื่อนบ้าน + … สองฝั่ง) ปุ่มจะได้ไม่กระโดดเวลาเปลี่ยนหน้า
15
+ const slots = siblings * 2 + 5;
16
+ if (pageCount <= slots) return Array.from({ length: pageCount }, (_, i) => i + 1);
17
+ const left = Math.max(page - siblings, 1);
18
+ const right = Math.min(page + siblings, pageCount);
19
+ const leftGap = left > 2;
20
+ const rightGap = right < pageCount - 1;
21
+ const edge = siblings * 2 + 3;
22
+ const seq = (from: number, to: number) =>
23
+ Array.from({ length: to - from + 1 }, (_, i) => from + i);
24
+ if (!leftGap) return [...seq(1, edge), "…", pageCount];
25
+ if (!rightGap) return [1, "…", ...seq(pageCount - edge + 1, pageCount)];
26
+ return [1, "…", ...seq(left, right), "…", pageCount];
27
+ }
28
+
29
+ /**
30
+ * แบ่งหน้าสำเร็จรูป: ส่งหน้าปัจจุบัน + จำนวนหน้า แล้วได้ ‹ 1 … 4 5 6 … 20 › (แทน Pagination 7 ชิ้น + คำนวณช่วงเอง)
31
+ * เลขหน้าเริ่มที่ 1; หน้าเดียวหรือไม่มีข้อมูล = ไม่แสดงอะไร; มือถือซ่อนเลขเหลือ "หน้า x/y"
32
+ * ตาราง tanstack มี pagination ของ `@suphark/ui/data-table` อยู่แล้ว
33
+ *
34
+ * <SimplePagination page={page} pageCount={Math.ceil(total / 20)} onPageChange={setPage} />
35
+ */
36
+ export function SimplePagination({
37
+ page,
38
+ pageCount,
39
+ onPageChange,
40
+ siblings = 1,
41
+ className,
42
+ }: {
43
+ page: number;
44
+ pageCount: number;
45
+ onPageChange: (page: number) => void;
46
+ /** จำนวนหน้าข้าง ๆ หน้าปัจจุบันที่แสดง (ค่าเริ่มต้น 1) */
47
+ siblings?: number;
48
+ className?: string;
49
+ }) {
50
+ const text = useUiText().pagination;
51
+ if (pageCount <= 1) return null;
52
+ const go = (p: number) => onPageChange(Math.min(pageCount, Math.max(1, p)));
53
+ return (
54
+ <nav
55
+ aria-label={text?.label ?? "แบ่งหน้า"}
56
+ className={cn("flex items-center justify-center gap-1", className)}
57
+ >
58
+ <Button
59
+ type="button"
60
+ variant="ghost"
61
+ size="sm"
62
+ disabled={page <= 1}
63
+ onClick={() => go(page - 1)}
64
+ aria-label={text?.previous ?? "ก่อนหน้า"}
65
+ >
66
+ <ChevronLeft />
67
+ <span className="hidden sm:inline">{text?.previous ?? "ก่อนหน้า"}</span>
68
+ </Button>
69
+ <ul className="hidden items-center gap-1 sm:flex">
70
+ {paginationRange(page, pageCount, siblings).map((p, i) =>
71
+ p === "…" ? (
72
+ // biome-ignore lint/suspicious/noArrayIndexKey: "…" มีได้สองตัว ตำแหน่งคือ key
73
+ <li key={`gap-${i}`} aria-hidden className="px-1 text-muted-foreground">
74
+ …
75
+ </li>
76
+ ) : (
77
+ <li key={p}>
78
+ <Button
79
+ type="button"
80
+ size="icon-sm"
81
+ variant={p === page ? "outline" : "ghost"}
82
+ aria-current={p === page ? "page" : undefined}
83
+ aria-label={`${text?.page ?? "หน้า"} ${p}`}
84
+ onClick={() => go(p)}
85
+ className="tabular-nums"
86
+ >
87
+ {p}
88
+ </Button>
89
+ </li>
90
+ ),
91
+ )}
92
+ </ul>
93
+ <span className="px-2 text-muted-foreground text-sm tabular-nums sm:hidden">
94
+ {page} / {pageCount}
95
+ </span>
96
+ <Button
97
+ type="button"
98
+ variant="ghost"
99
+ size="sm"
100
+ disabled={page >= pageCount}
101
+ onClick={() => go(page + 1)}
102
+ aria-label={text?.next ?? "ถัดไป"}
103
+ >
104
+ <span className="hidden sm:inline">{text?.next ?? "ถัดไป"}</span>
105
+ <ChevronRight />
106
+ </Button>
107
+ </nav>
108
+ );
109
+ }
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow,
10
+ } from "@suphark/ui/components/ui/table";
11
+ import { cn } from "@suphark/ui/lib/utils";
12
+ import type { ReactNode } from "react";
13
+ import { EmptyRow } from "./empty-row";
14
+
15
+ export interface SimpleTableColumn<T> {
16
+ /** key ของคอลัมน์ — ถ้าตรงกับ field ของแถวและไม่ส่ง cell จะแสดงค่านั้นตรง ๆ */
17
+ key: string;
18
+ header: ReactNode;
19
+ cell?: (row: T, index: number) => ReactNode;
20
+ align?: "left" | "center" | "right";
21
+ /** class ของทั้งหัวและเซลล์ เช่น "w-32" หรือ "hidden md:table-cell" (ซ่อนบนมือถือ) */
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * ตารางสำเร็จรูปจาก columns + rows (แทน Table/TableHeader/TableRow/TableHead/TableBody/TableCell + map สองชั้น)
27
+ * สำหรับรายการเล็ก–กลางที่ไม่ต้อง sort/filter/เลือกหลายแถว — ต้องการพวกนั้นใช้ `@suphark/ui/data-table`
28
+ * ไม่มีแถว = แสดง `empty` ในแถวเดียวเต็มความกว้าง; ตัวเลขใช้ `align: "right"` (tabular-nums ให้เอง)
29
+ *
30
+ * <SimpleTable rows={members} getRowKey={(m) => m.id} empty="ยังไม่มีสมาชิก"
31
+ * columns={[{ key: "name", header: "ชื่อ" },
32
+ * { key: "role", header: "บทบาท", cell: (m) => <StatusBadge>{m.role}</StatusBadge> },
33
+ * { key: "actions", header: "", align: "right", cell: (m) => <ActionMenu items={…} /> }]} />
34
+ */
35
+ export function SimpleTable<T>({
36
+ columns,
37
+ rows,
38
+ getRowKey,
39
+ empty = "ไม่มีข้อมูล",
40
+ onRowClick,
41
+ caption,
42
+ className,
43
+ }: {
44
+ columns: ReadonlyArray<SimpleTableColumn<T>>;
45
+ rows: ReadonlyArray<T>;
46
+ getRowKey: (row: T, index: number) => string | number;
47
+ empty?: ReactNode;
48
+ /** คลิกทั้งแถว (เช่นเปิดรายละเอียด) — ปุ่มในแถวควร stopPropagation เอง */
49
+ onRowClick?: (row: T) => void;
50
+ /** คำอธิบายตารางสำหรับ screen reader (sr-only) */
51
+ caption?: string;
52
+ className?: string;
53
+ }) {
54
+ const alignClass = (a?: "left" | "center" | "right") =>
55
+ a === "right" ? "text-right tabular-nums" : a === "center" ? "text-center" : undefined;
56
+ return (
57
+ <div className={cn("overflow-hidden rounded-xl border bg-card", className)}>
58
+ <Table>
59
+ {caption && <caption className="sr-only">{caption}</caption>}
60
+ <TableHeader>
61
+ <TableRow>
62
+ {columns.map((c) => (
63
+ <TableHead key={c.key} className={cn(alignClass(c.align), c.className)}>
64
+ {c.header}
65
+ </TableHead>
66
+ ))}
67
+ </TableRow>
68
+ </TableHeader>
69
+ <TableBody>
70
+ {rows.length === 0 ? (
71
+ <EmptyRow colSpan={columns.length}>{empty}</EmptyRow>
72
+ ) : (
73
+ rows.map((row, i) => (
74
+ <TableRow
75
+ key={getRowKey(row, i)}
76
+ onClick={onRowClick ? () => onRowClick(row) : undefined}
77
+ className={cn(onRowClick && "cursor-pointer")}
78
+ >
79
+ {columns.map((c) => (
80
+ <TableCell key={c.key} className={cn(alignClass(c.align), c.className)}>
81
+ {c.cell
82
+ ? c.cell(row, i)
83
+ : String((row as Record<string, unknown>)[c.key] ?? "")}
84
+ </TableCell>
85
+ ))}
86
+ </TableRow>
87
+ ))
88
+ )}
89
+ </TableBody>
90
+ </Table>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ import {
4
+ Accordion,
5
+ AccordionContent,
6
+ AccordionItem,
7
+ AccordionTrigger,
8
+ } from "@suphark/ui/components/ui/accordion";
9
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@suphark/ui/components/ui/tabs";
10
+ import { cn } from "@suphark/ui/lib/utils";
11
+ import type { ReactNode } from "react";
12
+
13
+ export interface SimpleTabItem {
14
+ value: string;
15
+ label: ReactNode;
16
+ content: ReactNode;
17
+ /** ท้ายชื่อแท็บ เช่น `<CountBadge value={3} />` */
18
+ badge?: ReactNode;
19
+ icon?: ReactNode;
20
+ disabled?: boolean;
21
+ }
22
+
23
+ /**
24
+ * แท็บสำเร็จรูปจาก array เดียว (แทน Tabs/TabsList/TabsTrigger/TabsContent 4 ชิ้นต่อแท็บ)
25
+ * แท็บที่เป็นหน้าแยก (เปลี่ยน URL) ใช้ `SectionNav`/`TopTabs` แทน — ตัวนี้สลับเนื้อหาในหน้าเดียว
26
+ *
27
+ * <SimpleTabs items={[{ value: "info", label: "ข้อมูล", content: <Info /> },
28
+ * { value: "log", label: "ประวัติ", badge: <CountBadge value={3} />, content: <Log /> }]} />
29
+ */
30
+ export function SimpleTabs({
31
+ items,
32
+ value,
33
+ onValueChange,
34
+ initialValue,
35
+ variant = "default",
36
+ fullWidth = false,
37
+ className,
38
+ listClassName,
39
+ }: {
40
+ items: ReadonlyArray<SimpleTabItem>;
41
+ value?: string;
42
+ onValueChange?: (value: string) => void;
43
+ /** แท็บเริ่มต้นแบบ uncontrolled (ค่าเริ่มต้น: แท็บแรก) */
44
+ initialValue?: string;
45
+ /** default = พื้นเทาแบบปุ่ม · line = เส้นใต้ (หัวการ์ด/หน้า) */
46
+ variant?: "default" | "line";
47
+ /** แท็บแบ่งเต็มความกว้าง (มือถือ/กล่องแคบ) */
48
+ fullWidth?: boolean;
49
+ className?: string;
50
+ listClassName?: string;
51
+ }) {
52
+ return (
53
+ <Tabs
54
+ value={value}
55
+ // base-ui-ok: ค่าเริ่มต้นใช้ครั้งเดียวตอน mount (uncontrolled) — ส่ง value ถ้าจะคุมเอง
56
+ defaultValue={value === undefined ? (initialValue ?? items[0]?.value) : undefined}
57
+ onValueChange={(v) => onValueChange?.(String(v))}
58
+ className={className}
59
+ >
60
+ <TabsList
61
+ variant={variant}
62
+ className={cn(fullWidth && "w-full", "max-w-full overflow-x-auto", listClassName)}
63
+ >
64
+ {items.map((item) => (
65
+ <TabsTrigger key={item.value} value={item.value} disabled={item.disabled}>
66
+ {item.icon}
67
+ {item.label}
68
+ {item.badge}
69
+ </TabsTrigger>
70
+ ))}
71
+ </TabsList>
72
+ {items.map((item) => (
73
+ <TabsContent key={item.value} value={item.value}>
74
+ {item.content}
75
+ </TabsContent>
76
+ ))}
77
+ </Tabs>
78
+ );
79
+ }
80
+
81
+ export interface SimpleAccordionItem {
82
+ value: string;
83
+ title: ReactNode;
84
+ content: ReactNode;
85
+ disabled?: boolean;
86
+ }
87
+
88
+ /**
89
+ * หัวข้อพับเก็บได้จาก array เดียว (FAQ, รายละเอียดเพิ่มเติม, กลุ่มการตั้งค่า)
90
+ * `multiple` = เปิดหลายข้อพร้อมกันได้; `initialOpen` = ข้อที่เปิดไว้ตอนแรก
91
+ */
92
+ export function SimpleAccordion({
93
+ items,
94
+ multiple = false,
95
+ initialOpen,
96
+ className,
97
+ }: {
98
+ items: ReadonlyArray<SimpleAccordionItem>;
99
+ multiple?: boolean;
100
+ initialOpen?: ReadonlyArray<string>;
101
+ className?: string;
102
+ }) {
103
+ return (
104
+ <Accordion
105
+ multiple={multiple}
106
+ // base-ui-ok: ค่าเริ่มต้นใช้ครั้งเดียวตอน mount
107
+ defaultValue={initialOpen ? [...initialOpen] : undefined}
108
+ className={className}
109
+ >
110
+ {items.map((item) => (
111
+ <AccordionItem key={item.value} value={item.value} disabled={item.disabled}>
112
+ <AccordionTrigger>{item.title}</AccordionTrigger>
113
+ <AccordionContent>{item.content}</AccordionContent>
114
+ </AccordionItem>
115
+ ))}
116
+ </Accordion>
117
+ );
118
+ }
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import {
4
+ Popover,
5
+ PopoverContent,
6
+ PopoverDescription,
7
+ PopoverHeader,
8
+ PopoverTitle,
9
+ PopoverTrigger,
10
+ } from "@suphark/ui/components/ui/popover";
11
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@suphark/ui/components/ui/tooltip";
12
+ import { cn } from "@suphark/ui/lib/utils";
13
+ import type { ReactElement, ReactNode } from "react";
14
+
15
+ type Side = "top" | "bottom" | "left" | "right";
16
+
17
+ /**
18
+ * tooltip สำเร็จรูป: ห่อ element เดียว + ข้อความ (แทน Tooltip/TooltipTrigger/TooltipContent 3 ชิ้น)
19
+ * `content` ว่าง = ไม่มี tooltip (ไม่ต้องเขียนเงื่อนไขเอง) · ปุ่มไอคอนอย่างเดียวยังต้องมี aria-label ของตัวเอง
20
+ * คำอธิบายยาวหลังไอคอน (i) ใช้ `InfoHint`
21
+ *
22
+ * <SimpleTooltip content="คัดลอกลิงก์"><Button size="icon" aria-label="คัดลอกลิงก์"><Copy /></Button></SimpleTooltip>
23
+ */
24
+ export function SimpleTooltip({
25
+ content,
26
+ children,
27
+ side = "top",
28
+ className,
29
+ }: {
30
+ content: ReactNode;
31
+ /** element ที่ชี้แล้วโชว์ tooltip (ต้องรับ ref/props ได้ เช่น Button) */
32
+ children: ReactElement;
33
+ side?: Side;
34
+ className?: string;
35
+ }) {
36
+ if (content === null || content === undefined || content === "") return children;
37
+ return (
38
+ <Tooltip>
39
+ <TooltipTrigger render={children} />
40
+ <TooltipContent side={side} className={cn("max-w-xs text-pretty", className)}>
41
+ {content}
42
+ </TooltipContent>
43
+ </Tooltip>
44
+ );
45
+ }
46
+
47
+ /**
48
+ * popover สำเร็จรูป: trigger + หัวข้อ/คำอธิบาย (ไม่บังคับ) + เนื้อหา ในคอมโพเนนต์เดียว
49
+ * (แทน Popover/PopoverTrigger/PopoverContent/PopoverHeader/PopoverTitle/PopoverDescription)
50
+ * ใช้กับตัวกรองเล็ก ๆ, รายละเอียดสั้น, ฟอร์มบรรทัดเดียว — เนื้อหายาวใช้ `SimpleSheet`/`SimpleDialog`
51
+ */
52
+ export function SimplePopover({
53
+ trigger,
54
+ title,
55
+ description,
56
+ children,
57
+ open,
58
+ onOpenChange,
59
+ side = "bottom",
60
+ align = "center",
61
+ className,
62
+ }: {
63
+ trigger: ReactElement;
64
+ title?: ReactNode;
65
+ description?: ReactNode;
66
+ children?: ReactNode;
67
+ open?: boolean;
68
+ onOpenChange?: (open: boolean) => void;
69
+ side?: Side;
70
+ align?: "start" | "center" | "end";
71
+ className?: string;
72
+ }) {
73
+ return (
74
+ <Popover open={open} onOpenChange={onOpenChange}>
75
+ <PopoverTrigger render={trigger} />
76
+ <PopoverContent side={side} align={align} className={className}>
77
+ {(title || description) && (
78
+ <PopoverHeader>
79
+ {title && <PopoverTitle>{title}</PopoverTitle>}
80
+ {description && <PopoverDescription>{description}</PopoverDescription>}
81
+ </PopoverHeader>
82
+ )}
83
+ {children}
84
+ </PopoverContent>
85
+ </Popover>
86
+ );
87
+ }
@@ -56,6 +56,20 @@ export interface UiText {
56
56
  /** ConfirmDialog `requireText`: คำนำหน้า/ต่อท้ายช่องพิมพ์ยืนยัน (optional — "พิมพ์" … "เพื่อยืนยัน") */
57
57
  confirmTypeToConfirm?: string;
58
58
  confirmTypeToConfirmSuffix?: string;
59
+ /** MonthStepper (optional) */
60
+ month?: Partial<{ previous: string; next: string; current: string }>;
61
+ /** SetupChecklist: ป้ายขั้นเสริม + ความคืบหน้า ({done}/{total}) + ปุ่มของขั้นที่ทำแล้ว (optional) */
62
+ setup?: Partial<{ optional: string; progress: string; view: string }>;
63
+ /** FilterBar: ปุ่มล้างตัวกรอง (optional) */
64
+ filters?: Partial<{ clear: string; label: string }>;
65
+ /** SettingsLayout: ปุ่มย้อนกลับบนมือถือ + ไม่พบหัวข้อ (optional) */
66
+ settings?: Partial<{ back: string; empty: string; label: string }>;
67
+ /** MobileTabBar: aria-label ของแถบ (optional) */
68
+ mobileTabs?: string;
69
+ /** SimpleDialog/SimpleSheet: ปุ่มปิด (sr-only) (optional — "ปิด") */
70
+ close?: string;
71
+ /** SimplePagination (optional) */
72
+ pagination?: Partial<{ previous: string; next: string; label: string; page: string }>;
59
73
  }
60
74
 
61
75
  export const UI_TEXT_TH: UiText = {
@@ -93,6 +107,13 @@ export const UI_TEXT_TH: UiText = {
93
107
  language: { label: "ภาษา" },
94
108
  confirmTypeToConfirm: "พิมพ์",
95
109
  confirmTypeToConfirmSuffix: "เพื่อยืนยัน",
110
+ month: { previous: "เดือนก่อน", next: "เดือนถัดไป", current: "กลับไปเดือนนี้" },
111
+ setup: { optional: "ไม่บังคับ", progress: "ทำแล้ว {done} จาก {total} ขั้น", view: "ดู" },
112
+ filters: { clear: "ล้างตัวกรอง", label: "ตัวกรอง" },
113
+ settings: { back: "กลับ", empty: "ไม่พบหัวข้อที่ตรงกับคำค้น", label: "หัวข้อการตั้งค่า" },
114
+ mobileTabs: "เมนูหลัก",
115
+ close: "ปิด",
116
+ pagination: { previous: "ก่อนหน้า", next: "ถัดไป", label: "แบ่งหน้า", page: "หน้า" },
96
117
  };
97
118
 
98
119
  export const UI_TEXT_EN: UiText = {
@@ -130,6 +151,13 @@ export const UI_TEXT_EN: UiText = {
130
151
  language: { label: "Language" },
131
152
  confirmTypeToConfirm: "Type",
132
153
  confirmTypeToConfirmSuffix: "to confirm",
154
+ month: { previous: "Previous month", next: "Next month", current: "Back to this month" },
155
+ setup: { optional: "Optional", progress: "{done} of {total} steps done", view: "View" },
156
+ filters: { clear: "Clear filters", label: "Filters" },
157
+ settings: { back: "Back", empty: "No settings match your search", label: "Settings sections" },
158
+ mobileTabs: "Main menu",
159
+ close: "Close",
160
+ pagination: { previous: "Previous", next: "Next", label: "Pagination", page: "Page" },
133
161
  };
134
162
 
135
163
  const UiTextContext = createContext<UiText>(UI_TEXT_TH);
@@ -28,7 +28,7 @@ function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.
28
28
  <AccordionPrimitive.Trigger
29
29
  data-slot="accordion-trigger"
30
30
  className={cn(
31
- "group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left font-medium text-sm outline-none transition-all hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
31
+ "group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
32
32
  className,
33
33
  )}
34
34
  {...props}
@@ -51,7 +51,7 @@ function AccordionContent({ className, children, ...props }: AccordionPrimitive.
51
51
  return (
52
52
  <AccordionPrimitive.Panel
53
53
  data-slot="accordion-content"
54
- className="overflow-hidden text-sm data-closed:animate-accordion-up data-open:animate-accordion-down"
54
+ className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
55
55
  {...props}
56
56
  >
57
57
  <div
@@ -22,7 +22,7 @@ function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdr
22
22
  <AlertDialogPrimitive.Backdrop
23
23
  data-slot="alert-dialog-overlay"
24
24
  className={cn(
25
- "data-open:fade-in-0 data-closed:fade-out-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 data-closed:animate-out data-open:animate-in supports-backdrop-filter:backdrop-blur-xs",
25
+ "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
26
26
  className,
27
27
  )}
28
28
  {...props}
@@ -44,7 +44,7 @@ function AlertDialogContent({
44
44
  data-slot="alert-dialog-content"
45
45
  data-size={size}
46
46
  className={cn(
47
- "group/alert-dialog-content data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground outline-none ring-1 ring-foreground/10 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-closed:animate-out data-open:animate-in data-[size=default]:sm:max-w-sm",
47
+ "group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
48
48
  className,
49
49
  )}
50
50
  {...props}
@@ -100,7 +100,7 @@ function AlertDialogTitle({
100
100
  <AlertDialogPrimitive.Title
101
101
  data-slot="alert-dialog-title"
102
102
  className={cn(
103
- "font-medium text-base sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
103
+ "text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
104
104
  className,
105
105
  )}
106
106
  {...props}
@@ -116,7 +116,7 @@ function AlertDialogDescription({
116
116
  <AlertDialogPrimitive.Description
117
117
  data-slot="alert-dialog-description"
118
118
  className={cn(
119
- "text-balance text-muted-foreground text-sm md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
119
+ "text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
120
120
  className,
121
121
  )}
122
122
  {...props}
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import type * as React from "react";
4
4
 
5
5
  const alertVariants = cva(
6
- "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 has-data-[slot=alert-action]:pr-18 *:[svg:not([class*='size-'])]:size-4 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current",
6
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
7
7
  {
8
8
  variants: {
9
9
  variant: {
@@ -51,7 +51,7 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
51
51
  <div
52
52
  data-slot="alert-description"
53
53
  className={cn(
54
- "text-balance text-muted-foreground text-sm md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
54
+ "text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
55
55
  className,
56
56
  )}
57
57
  {...props}
@@ -6,14 +6,14 @@ import { cva, type VariantProps } from "class-variance-authority";
6
6
  import type * as React from "react";
7
7
 
8
8
  const attachmentVariants = cva(
9
- "group/attachment relative flex w-fit min-w-0 max-w-full shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
9
+ "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
10
10
  {
11
11
  variants: {
12
12
  size: {
13
13
  default:
14
- "gap-2 text-sm has-data-[slot=attachment-media]:p-2 has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2",
15
- sm: "gap-2.5 text-xs has-data-[slot=attachment-media]:p-1.5 has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5",
16
- xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-media]:p-1 has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1",
14
+ "gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2",
15
+ sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5",
16
+ xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1",
17
17
  },
18
18
  orientation: {
19
19
  horizontal: "min-w-40 items-center",
@@ -46,7 +46,7 @@ function Attachment({
46
46
  }
47
47
 
48
48
  const attachmentMediaVariants = cva(
49
- "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none",
49
+ "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
50
50
  {
51
51
  variants: {
52
52
  variant: {
@@ -81,7 +81,7 @@ function AttachmentContent({ className, ...props }: React.ComponentProps<"div">)
81
81
  <div
82
82
  data-slot="attachment-content"
83
83
  className={cn(
84
- "min-w-0 max-w-full flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
84
+ "max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
85
85
  className,
86
86
  )}
87
87
  {...props}
@@ -94,7 +94,7 @@ function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">)
94
94
  <span
95
95
  data-slot="attachment-title"
96
96
  className={cn(
97
- "group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer block min-w-0 max-w-full truncate font-medium",
97
+ "block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer",
98
98
  className,
99
99
  )}
100
100
  {...props}
@@ -107,7 +107,7 @@ function AttachmentDescription({ className, ...props }: React.ComponentProps<"sp
107
107
  <span
108
108
  data-slot="attachment-description"
109
109
  className={cn(
110
- "mt-0.5 block min-w-0 truncate text-muted-foreground text-xs group-data-[state=error]/attachment:text-destructive/80",
110
+ "mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80",
111
111
  "max-w-full",
112
112
  className,
113
113
  )}
@@ -173,7 +173,7 @@ function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
173
173
  <div
174
174
  data-slot="attachment-group"
175
175
  className={cn(
176
- "scroll-fade-x scrollbar-none flex min-w-0 snap-x snap-mandatory scroll-px-1 gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
176
+ "flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
177
177
  className,
178
178
  )}
179
179
  {...props}
@@ -16,7 +16,7 @@ function Avatar({
16
16
  data-slot="avatar"
17
17
  data-size={size}
18
18
  className={cn(
19
- "group/avatar relative flex size-8 shrink-0 select-none rounded-full after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
19
+ "group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
20
20
  className,
21
21
  )}
22
22
  {...props}
@@ -39,7 +39,7 @@ function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props)
39
39
  <AvatarPrimitive.Fallback
40
40
  data-slot="avatar-fallback"
41
41
  className={cn(
42
- "flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground text-sm group-data-[size=sm]/avatar:text-xs",
42
+ "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
43
43
  className,
44
44
  )}
45
45
  {...props}
@@ -52,7 +52,7 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
52
52
  <span
53
53
  data-slot="avatar-badge"
54
54
  className={cn(
55
- "absolute right-0 bottom-0 z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background",
55
+ "absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
56
56
  "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
57
57
  "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
58
58
  "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
@@ -81,7 +81,7 @@ function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">)
81
81
  <div
82
82
  data-slot="avatar-group-count"
83
83
  className={cn(
84
- "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground text-sm ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
84
+ "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
85
85
  className,
86
86
  )}
87
87
  {...props}
@@ -4,7 +4,7 @@ import { cn } from "@suphark/ui/lib/utils";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
 
6
6
  const badgeVariants = cva(
7
- "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-4xl border border-transparent px-2 py-0.5 font-medium text-xs transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
7
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
8
8
  {
9
9
  variants: {
10
10
  variant: {