@suphark/ui 0.27.0 → 0.28.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 (48) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/src/components/app-shell/app-shell.tsx +11 -2
  5. package/src/components/app-shell/organization-switcher.tsx +1 -1
  6. package/src/components/app-shell/user-menu.tsx +1 -1
  7. package/src/components/data-table/data-table-column-header.tsx +1 -1
  8. package/src/components/data-table/data-table-faceted-filter.tsx +1 -1
  9. package/src/components/data-table/data-table-pagination.tsx +9 -4
  10. package/src/components/data-table/data-table-row-actions.tsx +24 -12
  11. package/src/components/data-table/data-table-view-option.tsx +1 -1
  12. package/src/components/data-table/data-table.tsx +3 -1
  13. package/src/components/design-system/sections/hero-specimen.tsx +1 -1
  14. package/src/components/design-system/sections/showcase-data-display.tsx +1 -1
  15. package/src/components/design-system/sections/showcase-feedback.tsx +1 -1
  16. package/src/components/design-system/sections/showcase-forms.tsx +1 -1
  17. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +1 -1
  18. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +1 -1
  19. package/src/components/design-system/sections/showcase-overview.tsx +1 -1
  20. package/src/components/design-system/sections/showcase-primitives.tsx +1 -1
  21. package/src/components/pwa/pwa-install-card.tsx +1 -1
  22. package/src/components/shared/action-form.tsx +1 -1
  23. package/src/components/shared/activity-feed.tsx +114 -0
  24. package/src/components/shared/button/base/action-button.tsx +8 -3
  25. package/src/components/shared/button/base/link-button.tsx +3 -1
  26. package/src/components/shared/button/button.tsx +8 -1
  27. package/src/components/shared/button/copy-button.tsx +1 -1
  28. package/src/components/shared/button/floating-save-button.tsx +1 -1
  29. package/src/components/shared/combobox.tsx +1 -1
  30. package/src/components/shared/date/date-range-picker.tsx +1 -1
  31. package/src/components/shared/dropzone.tsx +1 -1
  32. package/src/components/shared/env-snippet.tsx +1 -1
  33. package/src/components/shared/filter-bar.tsx +1 -1
  34. package/src/components/shared/form/date-time-picker.tsx +1 -1
  35. package/src/components/shared/form/form-field-combobox.tsx +1 -1
  36. package/src/components/shared/form/form-field-select.tsx +1 -1
  37. package/src/components/shared/form-dialog.tsx +1 -1
  38. package/src/components/shared/inline-edit.tsx +175 -0
  39. package/src/components/shared/language/language-switch.tsx +1 -1
  40. package/src/components/shared/layout/error-state.tsx +2 -1
  41. package/src/components/shared/multi-select-combobox.tsx +1 -1
  42. package/src/components/shared/selection-bar.tsx +94 -0
  43. package/src/components/shared/selection-button.tsx +1 -1
  44. package/src/components/shared/simple-table.tsx +51 -2
  45. package/src/components/shared/simple-tabs.tsx +54 -13
  46. package/src/components/shared/step-form.tsx +1 -1
  47. package/src/components/shared/ui-text.tsx +47 -0
  48. package/src/index.ts +11 -0
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { cn } from "@suphark/ui/lib/utils";
5
5
  import { Check, Loader2, Save, X } from "lucide-react";
6
6
  import { useEffect, useRef, useState } from "react";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Command,
7
7
  CommandEmpty,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Badge } from "@suphark/ui/components/ui/badge";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import { Calendar } from "@suphark/ui/components/ui/calendar";
6
6
  import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
7
7
  import { Separator } from "@suphark/ui/components/ui/separator";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { formatBytes } from "@suphark/ui/lib/formatters";
5
5
  import { cn } from "@suphark/ui/lib/utils";
6
6
  import { UploadCloud } from "lucide-react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { copyText, selectContents } from "@suphark/ui/lib/copy-text";
5
5
  import { CheckIcon, CopyIcon } from "lucide-react";
6
6
  import { useRef, useState } from "react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { NativeSelect, NativeSelectOption } from "@suphark/ui/components/ui/native-select";
5
5
  import { cn } from "@suphark/ui/lib/utils";
6
6
  import { XIcon } from "lucide-react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { Calendar } from "@suphark/ui/components/ui/calendar";
5
5
  import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
6
6
  import { useMounted } from "@suphark/ui/hooks/use-mounted";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Command,
7
7
  CommandEmpty,
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { MultiSelectCombobox } from "@suphark/ui/components/shared/multi-select-combobox";
4
5
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
5
- import { Button } from "@suphark/ui/components/ui/button";
6
6
  import {
7
7
  Command,
8
8
  CommandEmpty,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SecretReveal } from "@suphark/ui/components/shared/secret-reveal";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Dialog,
7
7
  DialogContent,
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
+ import { useUiText } from "@suphark/ui/components/shared/ui-text";
5
+ import { Input } from "@suphark/ui/components/ui/input";
6
+ import { errorMessage, isNavigationError } from "@suphark/ui/lib/action-result";
7
+ import { cn } from "@suphark/ui/lib/utils";
8
+ import { CheckIcon, Loader2Icon, PencilIcon, XIcon } from "lucide-react";
9
+ import { type ReactNode, useRef, useState } from "react";
10
+ import { toast } from "sonner";
11
+
12
+ export interface InlineEditProps {
13
+ value: string;
14
+ /** บันทึกค่าใหม่ — throw หรือคืน `{ ok: false, error }` = ล้มเหลว (คงโหมดแก้ไขไว้ให้ลองใหม่) */
15
+ onSave: (value: string) => void | Promise<unknown>;
16
+ /** ป้ายสำหรับ screen reader ของปุ่มแก้ไข/ช่องกรอก เช่น "ชื่อโปรเจกต์" */
17
+ label: string;
18
+ placeholder?: string;
19
+ /** แสดงแทนค่าว่าง (ค่าเริ่มต้น "—") */
20
+ emptyLabel?: ReactNode;
21
+ /** ตรวจก่อนบันทึก — คืนข้อความ = ไม่ผ่าน */
22
+ validate?: (value: string) => string | null | undefined;
23
+ /** ค่าเดิมกับค่าใหม่เท่ากัน = ปิดโหมดแก้ไขโดยไม่เรียก `onSave` (ค่าเริ่มต้น true) */
24
+ skipWhenUnchanged?: boolean;
25
+ disabled?: boolean;
26
+ /** sm = ใช้ในแถวตาราง */
27
+ size?: "sm" | "default";
28
+ className?: string;
29
+ }
30
+
31
+ /**
32
+ * แก้ค่าในที่ — คลิกข้อความ (หรือปุ่มดินสอ) → ช่องกรอก → Enter/✓ บันทึก, Esc/✕ ยกเลิก
33
+ * ใช้กับค่าสั้น ๆ ที่ไม่คุ้มเปิด dialog (ชื่อโปรเจกต์, ชื่อเล่นของคีย์, หมายเหตุหนึ่งบรรทัด)
34
+ * ล้มเหลว = ขึ้น toast และคงโหมดแก้ไขไว้ให้ลองใหม่ · ค่ายาว/หลายช่องใช้ `FormDialog`
35
+ *
36
+ * <InlineEdit label="ชื่อโปรเจกต์" value={p.name} onSave={(name) => renameProject(p.id, name)} />
37
+ */
38
+ export function InlineEdit({
39
+ value,
40
+ onSave,
41
+ label,
42
+ placeholder,
43
+ emptyLabel = "—",
44
+ validate,
45
+ skipWhenUnchanged = true,
46
+ disabled = false,
47
+ size = "default",
48
+ className,
49
+ }: InlineEditProps) {
50
+ const text = useUiText();
51
+ const [editing, setEditing] = useState(false);
52
+ const [draft, setDraft] = useState(value);
53
+ const [pending, setPending] = useState(false);
54
+ const inputRef = useRef<HTMLInputElement>(null);
55
+ // เลือกข้อความทั้งหมดครั้งแรกที่เข้าโหมดแก้ไข — พิมพ์ทับได้ทันที (โฟกัสรอบหลังไม่เลือกซ้ำ)
56
+ const selectedOnOpen = useRef(false);
57
+
58
+ function start() {
59
+ setDraft(value);
60
+ selectedOnOpen.current = false;
61
+ setEditing(true);
62
+ }
63
+
64
+ async function commit() {
65
+ const next = draft.trim();
66
+ if (skipWhenUnchanged && next === value.trim()) {
67
+ setEditing(false);
68
+ return;
69
+ }
70
+ const invalid = validate?.(next);
71
+ if (invalid) {
72
+ toast.error(invalid);
73
+ inputRef.current?.focus();
74
+ return;
75
+ }
76
+ setPending(true);
77
+ try {
78
+ const result = await onSave(next);
79
+ // server action ที่คืน { ok: false, error } ถือว่าล้มเหลวเหมือน throw
80
+ if (result && typeof result === "object" && "ok" in result && result.ok === false) {
81
+ const message = "error" in result ? String(result.error) : undefined;
82
+ toast.error(message || (text.actionFailed ?? "ดำเนินการไม่สำเร็จ"));
83
+ return;
84
+ }
85
+ setEditing(false);
86
+ } catch (err) {
87
+ // action ที่ redirect() = สำเร็จ (Next นำทางให้แล้ว)
88
+ if (isNavigationError(err)) {
89
+ setEditing(false);
90
+ return;
91
+ }
92
+ toast.error(errorMessage(err, text.actionFailed ?? "ดำเนินการไม่สำเร็จ"));
93
+ } finally {
94
+ setPending(false);
95
+ }
96
+ }
97
+
98
+ const compact = size === "sm";
99
+
100
+ if (!editing) {
101
+ return (
102
+ <span
103
+ data-slot="inline-edit"
104
+ className={cn("group/inline inline-flex min-w-0 items-center gap-1", className)}
105
+ >
106
+ <span
107
+ className={cn("truncate", compact ? "text-sm" : "", !value && "text-muted-foreground")}
108
+ >
109
+ {value || emptyLabel}
110
+ </span>
111
+ <Button
112
+ variant="ghost"
113
+ size={compact ? "icon-xs" : "icon-sm"}
114
+ aria-label={`${text.edit ?? "แก้ไข"}: ${label}`}
115
+ disabled={disabled}
116
+ onClick={start}
117
+ className="opacity-0 transition-opacity focus-visible:opacity-100 group-hover/inline:opacity-100"
118
+ >
119
+ <PencilIcon />
120
+ </Button>
121
+ </span>
122
+ );
123
+ }
124
+
125
+ return (
126
+ <span
127
+ data-slot="inline-edit"
128
+ className={cn("inline-flex min-w-0 items-center gap-1", className)}
129
+ >
130
+ <Input
131
+ ref={inputRef}
132
+ // biome-ignore lint/a11y/noAutofocus: เพิ่งกดปุ่มแก้ไข — โฟกัสต่อเนื่องคือสิ่งที่ผู้ใช้คาดหวัง
133
+ autoFocus
134
+ aria-label={label}
135
+ value={draft}
136
+ placeholder={placeholder}
137
+ disabled={pending}
138
+ className={cn("min-w-0", compact && "h-7")}
139
+ onFocus={(e) => {
140
+ if (selectedOnOpen.current) return;
141
+ selectedOnOpen.current = true;
142
+ e.currentTarget.select();
143
+ }}
144
+ onChange={(e) => setDraft(e.target.value)}
145
+ onKeyDown={(e) => {
146
+ if (e.key === "Enter") {
147
+ e.preventDefault();
148
+ void commit();
149
+ } else if (e.key === "Escape") {
150
+ e.preventDefault();
151
+ setEditing(false);
152
+ }
153
+ }}
154
+ />
155
+ <Button
156
+ variant="ghost"
157
+ size={compact ? "icon-xs" : "icon-sm"}
158
+ aria-label={text.save}
159
+ disabled={pending}
160
+ onClick={() => void commit()}
161
+ >
162
+ {pending ? <Loader2Icon className="animate-spin" /> : <CheckIcon />}
163
+ </Button>
164
+ <Button
165
+ variant="ghost"
166
+ size={compact ? "icon-xs" : "icon-sm"}
167
+ aria-label={text.cancel}
168
+ disabled={pending}
169
+ onClick={() => setEditing(false)}
170
+ >
171
+ <XIcon />
172
+ </Button>
173
+ </span>
174
+ );
175
+ }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { useUiText } from "@suphark/ui/components/shared/ui-text";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  DropdownMenu,
7
7
  DropdownMenuContent,
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { Button, buttonVariants } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
+ import { buttonVariants } from "@suphark/ui/components/ui/button";
4
5
  import { Card, CardContent, CardHeader, CardTitle } from "@suphark/ui/components/ui/card";
5
6
  import { cn } from "@suphark/ui/lib/utils";
6
7
  import { AlertTriangle, Home, LogIn, RotateCcw } from "lucide-react";
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
4
5
  import { Badge } from "@suphark/ui/components/ui/badge";
5
- import { Button } from "@suphark/ui/components/ui/button";
6
6
  import {
7
7
  Command,
8
8
  CommandEmpty,
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
+ import { useUiText } from "@suphark/ui/components/shared/ui-text";
5
+ import { cn } from "@suphark/ui/lib/utils";
6
+ import { XIcon } from "lucide-react";
7
+ import type { ReactNode } from "react";
8
+
9
+ export interface SelectionAction {
10
+ label: string;
11
+ onClick: () => void;
12
+ icon?: ReactNode;
13
+ destructive?: boolean;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export interface SelectionBarProps {
18
+ /** จำนวนที่เลือก — 0 = ไม่แสดงอะไรเลย */
19
+ count: number;
20
+ /** ล้างการเลือก (เช่น `table.resetRowSelection`) */
21
+ onClear: () => void;
22
+ /** ปุ่มคำสั่งหมู่ — ใส่เป็น array หรือจะส่ง `children` เป็น element เองก็ได้ */
23
+ actions?: ReadonlyArray<SelectionAction>;
24
+ children?: ReactNode;
25
+ /** ข้อความนับ ใช้ `{count}` เช่น "เลือก {count} รายการ" (ไม่ส่ง = `UiText.selection.selected`) */
26
+ label?: string;
27
+ /** floating = แถบลอยกลางล่างจอ (ค่าเริ่มต้น) · inline = แถบในหน้า เหนือตาราง */
28
+ variant?: "floating" | "inline";
29
+ className?: string;
30
+ }
31
+
32
+ /**
33
+ * แถบคำสั่งหมู่ที่โผล่เมื่อเลือกหลายแถว — "เลือก N รายการ" + ปุ่มคำสั่ง + ปุ่มล้าง
34
+ * ใช้คู่ `DataTable` (`table.getFilteredSelectedRowModel().rows`) หรือ `SimpleTable` ที่แอปคุม state เอง
35
+ * คำสั่งที่ย้อนกลับไม่ได้ให้ส่ง element ของ `ConfirmActionButton`/`DeleteManyButton` ผ่าน `children`
36
+ *
37
+ * <SelectionBar count={selected.length} onClear={() => table.resetRowSelection()}
38
+ * actions={[{ label: "ส่งออก", onClick: exportRows }, { label: "ลบ", destructive: true, onClick: removeRows }]} />
39
+ */
40
+ export function SelectionBar({
41
+ count,
42
+ onClear,
43
+ actions,
44
+ children,
45
+ label,
46
+ variant = "floating",
47
+ className,
48
+ }: SelectionBarProps) {
49
+ const text = useUiText();
50
+ if (count <= 0) return null;
51
+
52
+ const template = label ?? text.selection?.selected ?? "เลือก {count} รายการ";
53
+ const message = template.replace("{count}", String(count));
54
+
55
+ return (
56
+ <div
57
+ data-slot="selection-bar"
58
+ // อ่านออกเสียงจำนวนที่เลือกให้ screen reader โดยไม่ขัดจังหวะ
59
+ aria-live="polite"
60
+ className={cn(
61
+ "flex items-center gap-2 rounded-xl border bg-card px-3 py-2 shadow-lg",
62
+ variant === "floating" &&
63
+ "-translate-x-1/2 fixed bottom-[calc(1rem+env(safe-area-inset-bottom))] left-1/2 z-40 max-w-[calc(100vw-2rem)]",
64
+ className,
65
+ )}
66
+ >
67
+ <span className="shrink-0 font-medium text-sm tabular-nums">{message}</span>
68
+ <div className="flex min-w-0 flex-wrap items-center gap-1.5">
69
+ {actions?.map((action) => (
70
+ <Button
71
+ key={action.label}
72
+ size="sm"
73
+ variant={action.destructive ? "destructive" : "outline"}
74
+ disabled={action.disabled}
75
+ onClick={action.onClick}
76
+ >
77
+ {action.icon}
78
+ {action.label}
79
+ </Button>
80
+ ))}
81
+ {children}
82
+ </div>
83
+ <Button
84
+ variant="ghost"
85
+ size="icon-sm"
86
+ aria-label={text.selection?.clear ?? "ล้างการเลือก"}
87
+ onClick={onClear}
88
+ className="ml-auto shrink-0"
89
+ >
90
+ <XIcon />
91
+ </Button>
92
+ </div>
93
+ );
94
+ }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { cn } from "@suphark/ui/lib/utils";
5
5
  import type { ComponentProps } from "react";
6
6
 
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { Checkbox } from "@suphark/ui/components/ui/checkbox";
3
4
  import {
4
5
  Table,
5
6
  TableBody,
@@ -38,6 +39,10 @@ export function SimpleTable<T>({
38
39
  getRowKey,
39
40
  empty = "ไม่มีข้อมูล",
40
41
  onRowClick,
42
+ selected,
43
+ onSelectedChange,
44
+ selectLabel = "เลือกแถว",
45
+ selectAllLabel = "เลือกทั้งหมด",
41
46
  caption,
42
47
  className,
43
48
  }: {
@@ -45,6 +50,13 @@ export function SimpleTable<T>({
45
50
  rows: ReadonlyArray<T>;
46
51
  getRowKey: (row: T, index: number) => string | number;
47
52
  empty?: ReactNode;
53
+ /** คีย์ของแถวที่เลือก — ส่งคู่กับ `onSelectedChange` เพื่อเปิดช่องติ๊กหน้าแถว (ใช้คู่ `SelectionBar`) */
54
+ selected?: ReadonlyArray<string | number>;
55
+ onSelectedChange?: (selected: Array<string | number>) => void;
56
+ /** aria-label ของช่องติ๊กแต่ละแถว (ค่าเริ่มต้น "เลือกแถว") */
57
+ selectLabel?: string;
58
+ /** aria-label ของช่องติ๊กบนหัวตาราง (ค่าเริ่มต้น "เลือกทั้งหมด") */
59
+ selectAllLabel?: string;
48
60
  /** คลิกทั้งแถว (เช่นเปิดรายละเอียด) — ปุ่มในแถวควร stopPropagation เอง */
49
61
  onRowClick?: (row: T) => void;
50
62
  /** คำอธิบายตารางสำหรับ screen reader (sr-only) */
@@ -53,12 +65,37 @@ export function SimpleTable<T>({
53
65
  }) {
54
66
  const alignClass = (a?: "left" | "center" | "right") =>
55
67
  a === "right" ? "text-right tabular-nums" : a === "center" ? "text-center" : undefined;
68
+
69
+ const selectable = Boolean(onSelectedChange);
70
+ const selectedKeys = new Set(selected ?? []);
71
+ const allKeys = rows.map((row, i) => getRowKey(row, i));
72
+ const allSelected = allKeys.length > 0 && allKeys.every((k) => selectedKeys.has(k));
73
+ const someSelected = !allSelected && allKeys.some((k) => selectedKeys.has(k));
74
+
75
+ function toggleRow(key: string | number, checked: boolean) {
76
+ const next = new Set(selectedKeys);
77
+ if (checked) next.add(key);
78
+ else next.delete(key);
79
+ onSelectedChange?.([...next]);
80
+ }
56
81
  return (
57
82
  <div className={cn("overflow-hidden rounded-xl border bg-card", className)}>
58
83
  <Table>
59
84
  {caption && <caption className="sr-only">{caption}</caption>}
60
85
  <TableHeader>
61
86
  <TableRow>
87
+ {selectable && (
88
+ <TableHead className="w-10">
89
+ <Checkbox
90
+ aria-label={selectAllLabel}
91
+ checked={allSelected}
92
+ indeterminate={someSelected}
93
+ // ครึ่งหนึ่ง: ซ่อนเครื่องหมายถูก แสดงขีดแทน (ไฟล์ shadcn ไม่มีไอคอนขีด)
94
+ className="data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground data-indeterminate:before:absolute data-indeterminate:before:h-0.5 data-indeterminate:before:w-2 data-indeterminate:before:rounded-full data-indeterminate:before:bg-current [&[data-indeterminate]_svg]:hidden"
95
+ onCheckedChange={(checked) => onSelectedChange?.(checked ? allKeys : [])}
96
+ />
97
+ </TableHead>
98
+ )}
62
99
  {columns.map((c) => (
63
100
  <TableHead key={c.key} className={cn(alignClass(c.align), c.className)}>
64
101
  {c.header}
@@ -68,14 +105,26 @@ export function SimpleTable<T>({
68
105
  </TableHeader>
69
106
  <TableBody>
70
107
  {rows.length === 0 ? (
71
- <EmptyRow colSpan={columns.length}>{empty}</EmptyRow>
108
+ <EmptyRow colSpan={columns.length + (selectable ? 1 : 0)}>{empty}</EmptyRow>
72
109
  ) : (
73
110
  rows.map((row, i) => (
74
111
  <TableRow
75
112
  key={getRowKey(row, i)}
76
113
  onClick={onRowClick ? () => onRowClick(row) : undefined}
77
- className={cn(onRowClick && "cursor-pointer")}
114
+ data-selected={selectedKeys.has(getRowKey(row, i)) || undefined}
115
+ className={cn(onRowClick && "cursor-pointer", "data-selected:bg-muted/50")}
78
116
  >
117
+ {selectable && (
118
+ <TableCell className="w-10">
119
+ <Checkbox
120
+ aria-label={selectLabel}
121
+ checked={selectedKeys.has(getRowKey(row, i))}
122
+ onCheckedChange={(checked) => toggleRow(getRowKey(row, i), checked === true)}
123
+ // คลิกช่องติ๊กไม่ให้ไปโดน onRowClick ของทั้งแถว
124
+ onClick={(event) => event.stopPropagation()}
125
+ />
126
+ </TableCell>
127
+ )}
79
128
  {columns.map((c) => (
80
129
  <TableCell key={c.key} className={cn(alignClass(c.align), c.className)}>
81
130
  {c.cell
@@ -8,7 +8,7 @@ import {
8
8
  } from "@suphark/ui/components/ui/accordion";
9
9
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@suphark/ui/components/ui/tabs";
10
10
  import { cn } from "@suphark/ui/lib/utils";
11
- import type { ReactNode } from "react";
11
+ import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
12
12
 
13
13
  export interface SimpleTabItem {
14
14
  value: string;
@@ -49,6 +49,26 @@ export function SimpleTabs({
49
49
  className?: string;
50
50
  listClassName?: string;
51
51
  }) {
52
+ const listRef = useRef<HTMLDivElement>(null);
53
+ const [edges, setEdges] = useState({ start: false, end: false });
54
+
55
+ const updateEdges = useCallback(() => {
56
+ const el = listRef.current;
57
+ if (!el) return;
58
+ const max = el.scrollWidth - el.clientWidth;
59
+ // 4px กันเศษจากการปัดทศนิยม
60
+ setEdges({ start: el.scrollLeft > 4, end: el.scrollLeft < max - 4 });
61
+ }, []);
62
+
63
+ useEffect(() => {
64
+ updateEdges();
65
+ const el = listRef.current;
66
+ if (!el || typeof ResizeObserver === "undefined") return;
67
+ const observer = new ResizeObserver(updateEdges);
68
+ observer.observe(el);
69
+ return () => observer.disconnect();
70
+ }, [updateEdges]);
71
+
52
72
  return (
53
73
  <Tabs
54
74
  value={value}
@@ -57,18 +77,39 @@ export function SimpleTabs({
57
77
  onValueChange={(v) => onValueChange?.(String(v))}
58
78
  className={className}
59
79
  >
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>
80
+ {/* แท็บเยอะเกินความกว้าง = เลื่อนแนวนอนได้ + เงาบอกว่ายังมีต่อ (ซ่อนแถบเลื่อน) */}
81
+ <div className="relative">
82
+ <TabsList
83
+ ref={listRef}
84
+ onScroll={updateEdges}
85
+ variant={variant}
86
+ className={cn(
87
+ fullWidth && "w-full",
88
+ "no-scrollbar max-w-full overflow-x-auto scroll-smooth",
89
+ listClassName,
90
+ )}
91
+ >
92
+ {items.map((item) => (
93
+ <TabsTrigger key={item.value} value={item.value} disabled={item.disabled}>
94
+ {item.icon}
95
+ {item.label}
96
+ {item.badge}
97
+ </TabsTrigger>
98
+ ))}
99
+ </TabsList>
100
+ {edges.start && (
101
+ <span
102
+ aria-hidden="true"
103
+ className="pointer-events-none absolute inset-y-0 left-0 w-6 rounded-l-lg bg-gradient-to-r from-background to-transparent"
104
+ />
105
+ )}
106
+ {edges.end && (
107
+ <span
108
+ aria-hidden="true"
109
+ className="pointer-events-none absolute inset-y-0 right-0 w-6 rounded-r-lg bg-gradient-to-l from-background to-transparent"
110
+ />
111
+ )}
112
+ </div>
72
113
  {items.map((item) => (
73
114
  <TabsContent key={item.value} value={item.value}>
74
115
  {item.content}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { Spinner } from "@suphark/ui/components/ui/spinner";
5
5
  import { errorMessage, failedResult, isNavigationError } from "@suphark/ui/lib/action-result";
6
6
  import { cn } from "@suphark/ui/lib/utils";