@suphark/ui 0.4.0 → 0.4.2
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 +30 -0
- package/package.json +1 -1
- package/src/components/app-shell/header-crumb.tsx +2 -2
- package/src/components/app-shell/user-menu.tsx +3 -4
- package/src/components/shared/button/confirm-action-button.tsx +73 -0
- package/src/components/shared/combobox.tsx +4 -7
- package/src/components/shared/confirm-dialog.tsx +81 -0
- package/src/components/shared/form/form-field-combobox.tsx +3 -2
- package/src/components/shared/form/form-field-select.tsx +3 -2
- package/src/components/shared/multi-select-combobox.tsx +3 -4
- package/src/components/shared/row-actions.tsx +92 -65
- package/src/components/shared/switcher-chevron.tsx +19 -0
- package/src/components/shared/ui-text.tsx +8 -0
- package/src/index.ts +6 -0
- package/src/next.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.4.2] — 2026-09-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`ConfirmDialog`** (barrel) — กล่องยืนยันมาตรฐานบน `AlertDialog` ของแพ็กเกจ ใช้แทน `window.confirm` (webview/automation บางตัวปิด window.confirm ทิ้ง กดแล้วไม่เกิดอะไร และไม่เข้า design system): controlled `open`/`onOpenChange`, `title?` (ค่าเริ่มต้น `UiText.confirmTitle`), `description`, `confirmLabel?` (ค่าเริ่มต้น `UiText.confirm`), `cancelLabel?`, `destructive` (ปุ่มยืนยันสีแดง), `pending` (ปุ่มหมุน + ปิดไม่ได้ชั่วคราว), `onConfirm`; พร้อม type `ConfirmSpec` (= `string | { title?, description, confirmLabel?, cancelLabel? }`) และ `normalizeConfirm()`
|
|
10
|
+
- **`ConfirmActionButton`** (`@suphark/ui/next`) — ปุ่มเดี่ยว + ConfirmDialog + server action ในตัว (`confirm`, `action`, `data`, `onDone?` + props ของ ActionButton เช่น `variant`/`icon`/`permission`) รอ action เสร็จแล้ว `router.refresh()` — ใช้แทน `<form action>` + window.confirm ที่ Pi Auth มี (ชื่อไม่ใช้ `ConfirmButton` เพราะมีปุ่มธรรมดาชื่อนั้นอยู่แล้วใน standard buttons)
|
|
11
|
+
- `UiText.confirm` / `UiText.confirmTitle` (optional มีค่าเริ่มต้นไทย/อังกฤษ) — แอปที่ส่ง UiText เองไม่ต้องเพิ่ม
|
|
12
|
+
- export type `RowActionItem` (รายการที่ไม่ใช่ separator)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`RowActions` / `RowMenu` prop `confirm`** เปิด `ConfirmDialog` แทน `window.confirm` (ขอจาก Pi Auth ผ่าน Pi Mail) — รับ `ConfirmSpec`: string เดิมยังใช้ได้ (= description, title/ปุ่มค่าเริ่มต้น) หรือ object กำหนด `title`/`confirmLabel`/`cancelLabel`; ปุ่มยืนยันสีแดงเมื่อ `destructive`; dropdown ปิดก่อน dialog เปิด และ dialog render นอก DropdownMenu (กัน focus trap ของ Base UI ชนกัน); กดยืนยันแล้วค่อยรัน `href`/`onSelect`/`action` ใน transition เหมือนเดิม — โค้ดเรียกเดิมไม่ต้องแก้
|
|
17
|
+
- ไม่มี `window.confirm` เหลือในแพ็กเกจ
|
|
18
|
+
- showcase: หน้า `/components/confirm-dialog` (ConfirmDialog + ConfirmActionButton) และ `/components/row-actions` ใช้ confirm แบบ object
|
|
19
|
+
|
|
20
|
+
## [0.4.1] — 2026-09-19
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`SwitcherChevron`** (barrel) — ลูกศร ⇅ มาตรฐานท้าย trigger แบบ switcher/select: `ChevronsUpDown` ขนาด `size-3` (12px) `shrink-0 text-muted-foreground opacity-70` aria-hidden (เล็กบางแบบ Clerk) รับ `className` ทับได้ — ใช้ตัวเดียวทั้งแพ็กเกจ/แอป (`OrganizationSwitcher` ใน @suphark/auth-ui จะย้ายมาใช้ด้วย)
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `HeaderCrumb`: ลูกศรใหญ่ 24px ในแอปปลายทาง (Pi Auth) — เดิมใช้ `size-3.5!` ซึ่ง Tailwind ของแอปที่ scan ผ่าน `@source` ไม่ generate class แบบ `!` → svg ไม่มีขนาดจึงตกไป 24px ของ lucide; เปลี่ยนเป็น `SwitcherChevron` (ไม่ใช้ `!`)
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- ลูกศรของ `UserMenu`, `Combobox`, `MultiSelectCombobox`, `FormFieldCombobox`, `FormFieldSelect` เปลี่ยนมาใช้ `SwitcherChevron` (เดิม h-4 w-4 opacity-50 ปนกันหลายขนาด) — ทุกจุดได้ 12px จางเท่ากัน; ไอคอน sort ใน `DataTableColumnHeader` ไม่ใช่ switcher คงเดิม; `components/ui/searchable-select.tsx` เป็นไฟล์ shadcn ไม่แตะ
|
|
33
|
+
- นโยบาย: หลีกเลี่ยง `!` บน utility ในแพ็กเกจถ้าไม่จำเป็น (เหลือเฉพาะ `group-data-[collapsible=icon]:p-0!` ใน `AppBrand` ซึ่ง `sidebar.tsx` ของ shadcn generate class เดียวกันอยู่แล้ว)
|
|
34
|
+
|
|
5
35
|
## [0.4.0] — 2026-09-19
|
|
6
36
|
|
|
7
37
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
|
|
3
4
|
import { cn } from "@suphark/ui/lib/utils";
|
|
4
|
-
import { ChevronsUpDown } from "lucide-react";
|
|
5
5
|
import { type ComponentProps, Fragment, type ReactNode } from "react";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -50,7 +50,7 @@ export function HeaderCrumb({
|
|
|
50
50
|
</span>
|
|
51
51
|
)}
|
|
52
52
|
{children}
|
|
53
|
-
{chevron && <
|
|
53
|
+
{chevron && <SwitcherChevron />}
|
|
54
54
|
</button>
|
|
55
55
|
);
|
|
56
56
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Initials } from "@suphark/ui/components/shared/initials";
|
|
4
|
+
import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
|
|
4
5
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
5
6
|
import {
|
|
6
7
|
DropdownMenu,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
DropdownMenuTrigger,
|
|
13
14
|
} from "@suphark/ui/components/ui/dropdown-menu";
|
|
14
15
|
import { cn } from "@suphark/ui/lib/utils";
|
|
15
|
-
import {
|
|
16
|
+
import { LogOut } from "lucide-react";
|
|
16
17
|
import { type ReactNode, useTransition } from "react";
|
|
17
18
|
|
|
18
19
|
export interface UserMenuUser {
|
|
@@ -82,10 +83,8 @@ export function UserMenu({
|
|
|
82
83
|
<span className="truncate text-muted-foreground text-xs">{description}</span>
|
|
83
84
|
)}
|
|
84
85
|
</span>
|
|
85
|
-
<
|
|
86
|
-
aria-hidden="true"
|
|
86
|
+
<SwitcherChevron
|
|
87
87
|
className={cn(
|
|
88
|
-
"text-muted-foreground",
|
|
89
88
|
variant === "sidebar" ? "group-data-[collapsible=icon]:hidden" : "hidden xl:block",
|
|
90
89
|
)}
|
|
91
90
|
/>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ActionButton,
|
|
5
|
+
type ActionButtonProps,
|
|
6
|
+
} from "@suphark/ui/components/shared/button/base/action-button";
|
|
7
|
+
import {
|
|
8
|
+
ConfirmDialog,
|
|
9
|
+
type ConfirmSpec,
|
|
10
|
+
normalizeConfirm,
|
|
11
|
+
} from "@suphark/ui/components/shared/confirm-dialog";
|
|
12
|
+
import { useRouter } from "next/navigation";
|
|
13
|
+
import { useState, useTransition } from "react";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ปุ่มเดี่ยวที่ถามยืนยัน (ConfirmDialog) ก่อนเรียก server action — ใช้แทน `<form action>` + window.confirm
|
|
17
|
+
* `confirm` เหมือน `RowAction.confirm`; `action` + `data` เหมือน RowAction; refresh หน้าเมื่อเสร็จ
|
|
18
|
+
* ต้องการ next/navigation → export จาก `@suphark/ui/next`
|
|
19
|
+
*/
|
|
20
|
+
export function ConfirmActionButton({
|
|
21
|
+
confirm,
|
|
22
|
+
action,
|
|
23
|
+
data,
|
|
24
|
+
onDone,
|
|
25
|
+
variant,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: Omit<ActionButtonProps, "onClick" | "isLoading" | "type"> & {
|
|
29
|
+
confirm: ConfirmSpec;
|
|
30
|
+
action: (formData: FormData) => Promise<unknown>;
|
|
31
|
+
data?: Record<string, string>;
|
|
32
|
+
/** เรียกหลัง action สำเร็จ (ก่อน refresh) เช่น toast */
|
|
33
|
+
onDone?: () => void;
|
|
34
|
+
}) {
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const [pending, start] = useTransition();
|
|
38
|
+
const spec = normalizeConfirm(confirm);
|
|
39
|
+
const destructive = variant === "destructive";
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<ActionButton
|
|
43
|
+
type="button"
|
|
44
|
+
variant={variant}
|
|
45
|
+
isLoading={pending}
|
|
46
|
+
onClick={() => setOpen(true)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</ActionButton>
|
|
51
|
+
<ConfirmDialog
|
|
52
|
+
open={open}
|
|
53
|
+
onOpenChange={setOpen}
|
|
54
|
+
title={spec.title}
|
|
55
|
+
description={spec.description}
|
|
56
|
+
confirmLabel={spec.confirmLabel}
|
|
57
|
+
cancelLabel={spec.cancelLabel}
|
|
58
|
+
destructive={destructive}
|
|
59
|
+
pending={pending}
|
|
60
|
+
onConfirm={() => {
|
|
61
|
+
const fd = new FormData();
|
|
62
|
+
for (const [k, v] of Object.entries(data ?? {})) fd.set(k, v);
|
|
63
|
+
start(async () => {
|
|
64
|
+
await action(fd);
|
|
65
|
+
onDone?.();
|
|
66
|
+
setOpen(false);
|
|
67
|
+
router.refresh();
|
|
68
|
+
});
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
|
|
3
4
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
5
|
import {
|
|
5
6
|
Command,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
} from "@suphark/ui/components/ui/command";
|
|
12
13
|
import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
|
|
13
14
|
import { cn } from "@suphark/ui/lib/utils";
|
|
14
|
-
import { Check
|
|
15
|
+
import { Check } from "lucide-react";
|
|
15
16
|
import * as React from "react";
|
|
16
17
|
|
|
17
18
|
interface ComboboxProps {
|
|
@@ -75,12 +76,8 @@ export function Combobox({
|
|
|
75
76
|
>
|
|
76
77
|
{value ? options.find((option) => option.value === value)?.label : placeholder}
|
|
77
78
|
</span>
|
|
78
|
-
<
|
|
79
|
-
className={cn(
|
|
80
|
-
"ml-2 h-4 w-4 shrink-0 opacity-50 print:hidden",
|
|
81
|
-
wrapText && "self-start",
|
|
82
|
-
disabled && "hidden",
|
|
83
|
-
)}
|
|
79
|
+
<SwitcherChevron
|
|
80
|
+
className={cn("ml-2 print:hidden", wrapText && "self-start", disabled && "hidden")}
|
|
84
81
|
/>
|
|
85
82
|
</PopoverTrigger>
|
|
86
83
|
<PopoverContent className="w-[--anchor-width] p-0">
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogAction,
|
|
6
|
+
AlertDialogCancel,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
} from "@suphark/ui/components/ui/alert-dialog";
|
|
13
|
+
import { Loader2 } from "lucide-react";
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
import { useUiText } from "./ui-text";
|
|
16
|
+
|
|
17
|
+
/** สเปกการยืนยัน — string = description อย่างเดียว (title/ปุ่มใช้ค่าเริ่มต้นจาก UiText) */
|
|
18
|
+
export type ConfirmSpec =
|
|
19
|
+
| string
|
|
20
|
+
| {
|
|
21
|
+
title?: ReactNode;
|
|
22
|
+
description: ReactNode;
|
|
23
|
+
confirmLabel?: string;
|
|
24
|
+
cancelLabel?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** แปลง ConfirmSpec เป็น object เสมอ */
|
|
28
|
+
export function normalizeConfirm(spec: ConfirmSpec): Exclude<ConfirmSpec, string> {
|
|
29
|
+
return typeof spec === "string" ? { description: spec } : spec;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* กล่องยืนยันมาตรฐานของแพ็กเกจ (AlertDialog) — ใช้แทน `window.confirm` ทุกที่
|
|
34
|
+
* (webview/automation บางตัวปิด window.confirm ทิ้ง กดแล้วไม่เกิดอะไร และไม่เข้า design system)
|
|
35
|
+
* controlled: `open`/`onOpenChange`; `pending` = กำลังรัน action → ปุ่มยืนยันหมุน ปิดไม่ได้ชั่วคราว
|
|
36
|
+
*/
|
|
37
|
+
export function ConfirmDialog({
|
|
38
|
+
open,
|
|
39
|
+
onOpenChange,
|
|
40
|
+
title,
|
|
41
|
+
description,
|
|
42
|
+
confirmLabel,
|
|
43
|
+
cancelLabel,
|
|
44
|
+
destructive = false,
|
|
45
|
+
pending = false,
|
|
46
|
+
onConfirm,
|
|
47
|
+
}: {
|
|
48
|
+
open: boolean;
|
|
49
|
+
onOpenChange: (open: boolean) => void;
|
|
50
|
+
title?: ReactNode;
|
|
51
|
+
description: ReactNode;
|
|
52
|
+
confirmLabel?: string;
|
|
53
|
+
cancelLabel?: string;
|
|
54
|
+
/** ปุ่มยืนยันสีแดง (ลบ/เพิกถอน) */
|
|
55
|
+
destructive?: boolean;
|
|
56
|
+
pending?: boolean;
|
|
57
|
+
onConfirm: () => void;
|
|
58
|
+
}) {
|
|
59
|
+
const text = useUiText();
|
|
60
|
+
return (
|
|
61
|
+
<AlertDialog open={open} onOpenChange={(o) => !pending && onOpenChange(o)}>
|
|
62
|
+
<AlertDialogContent>
|
|
63
|
+
<AlertDialogHeader>
|
|
64
|
+
<AlertDialogTitle>{title ?? text.confirmTitle ?? "ยืนยันการทำรายการ"}</AlertDialogTitle>
|
|
65
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
66
|
+
</AlertDialogHeader>
|
|
67
|
+
<AlertDialogFooter>
|
|
68
|
+
<AlertDialogCancel disabled={pending}>{cancelLabel ?? text.cancel}</AlertDialogCancel>
|
|
69
|
+
<AlertDialogAction
|
|
70
|
+
variant={destructive ? "destructive" : "default"}
|
|
71
|
+
disabled={pending}
|
|
72
|
+
onClick={onConfirm}
|
|
73
|
+
>
|
|
74
|
+
{pending && <Loader2 className="animate-spin" aria-hidden="true" />}
|
|
75
|
+
{confirmLabel ?? text.confirm ?? "ยืนยัน"}
|
|
76
|
+
</AlertDialogAction>
|
|
77
|
+
</AlertDialogFooter>
|
|
78
|
+
</AlertDialogContent>
|
|
79
|
+
</AlertDialog>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
|
|
3
4
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
5
|
import {
|
|
5
6
|
Command,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
} from "@suphark/ui/components/ui/form";
|
|
19
20
|
import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
|
|
20
21
|
import { cn } from "@suphark/ui/lib/utils";
|
|
21
|
-
import { Check
|
|
22
|
+
import { Check } from "lucide-react";
|
|
22
23
|
import * as React from "react";
|
|
23
24
|
import { useEffect, useState, useTransition } from "react";
|
|
24
25
|
import type { FieldValues, Path } from "react-hook-form";
|
|
@@ -119,7 +120,7 @@ export function FormFieldCombobox<T extends FieldValues>({
|
|
|
119
120
|
? "Loading..."
|
|
120
121
|
: placeholder}
|
|
121
122
|
</span>
|
|
122
|
-
<
|
|
123
|
+
<SwitcherChevron className="ml-2" />
|
|
123
124
|
</PopoverTrigger>
|
|
124
125
|
<PopoverContent className="w-[--anchor-width] p-0">
|
|
125
126
|
<Command>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { MultiSelectCombobox } from "@suphark/ui/components/shared/multi-select-combobox";
|
|
4
|
+
import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
|
|
4
5
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
5
6
|
import {
|
|
6
7
|
Command,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
SelectValue,
|
|
27
28
|
} from "@suphark/ui/components/ui/select";
|
|
28
29
|
import { cn } from "@suphark/ui/lib/utils";
|
|
29
|
-
import { Check
|
|
30
|
+
import { Check } from "lucide-react";
|
|
30
31
|
import * as React from "react";
|
|
31
32
|
import type { ControllerRenderProps, FieldValues, Path, PathValue } from "react-hook-form";
|
|
32
33
|
|
|
@@ -126,7 +127,7 @@ function ComboboxContent<T extends FieldValues>({
|
|
|
126
127
|
disabled={disabled}
|
|
127
128
|
>
|
|
128
129
|
<span className="truncate">{selectedLabel}</span>
|
|
129
|
-
<
|
|
130
|
+
<SwitcherChevron className="ml-2" />
|
|
130
131
|
</Button>
|
|
131
132
|
</PopoverTrigger>
|
|
132
133
|
<PopoverContent className="w-[--anchor-width] p-0">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
|
|
3
4
|
import { Badge } from "@suphark/ui/components/ui/badge";
|
|
4
5
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
5
6
|
import {
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
} from "@suphark/ui/components/ui/command";
|
|
14
15
|
import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
|
|
15
16
|
import { cn } from "@suphark/ui/lib/utils";
|
|
16
|
-
import { Check,
|
|
17
|
+
import { Check, Plus, X } from "lucide-react";
|
|
17
18
|
import * as React from "react";
|
|
18
19
|
|
|
19
20
|
export interface MultiSelectOption {
|
|
@@ -259,9 +260,7 @@ export function MultiSelectCombobox({
|
|
|
259
260
|
<X className="size-3.5" />
|
|
260
261
|
</span>
|
|
261
262
|
)}
|
|
262
|
-
<
|
|
263
|
-
className={cn("size-4 shrink-0 opacity-50 print:hidden", disabled && "hidden")}
|
|
264
|
-
/>
|
|
263
|
+
<SwitcherChevron className={cn("print:hidden", disabled && "hidden")} />
|
|
265
264
|
</div>
|
|
266
265
|
</PopoverTrigger>
|
|
267
266
|
<PopoverContent className="w-[--anchor-width] min-w-[220px] p-0" align="start">
|
|
@@ -11,83 +11,110 @@ import {
|
|
|
11
11
|
import { MoreHorizontalIcon } from "lucide-react";
|
|
12
12
|
import { useRouter } from "next/navigation";
|
|
13
13
|
import { useState, useTransition } from "react";
|
|
14
|
+
import { ConfirmDialog, type ConfirmSpec, normalizeConfirm } from "./confirm-dialog";
|
|
14
15
|
import { useUiText } from "./ui-text";
|
|
15
16
|
|
|
16
|
-
export type
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
export type RowActionItem = {
|
|
18
|
+
type?: "item";
|
|
19
|
+
label: string;
|
|
20
|
+
href?: string;
|
|
21
|
+
onSelect?: () => void;
|
|
22
|
+
destructive?: boolean;
|
|
23
|
+
/** ถามยืนยันก่อน (ConfirmDialog ของแพ็กเกจ — ไม่ใช่ window.confirm): string = ข้อความ หรือ object กำหนด title/ปุ่ม */
|
|
24
|
+
confirm?: ConfirmSpec;
|
|
25
|
+
action?: (formData: FormData) => Promise<unknown>;
|
|
26
|
+
data?: Record<string, string>;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RowAction = RowActionItem | { type: "separator" };
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* เมนู "…" ท้ายแถว — รวมการกระทำรองไว้ที่เดียวแทนปุ่ม/ฟอร์มหลายอันในแถว
|
|
32
34
|
* รายการเรียก server action ได้โดยตรง (`action` + `data`) หรือไปหน้าอื่น (`href`) หรือเปิด dialog (`onSelect`)
|
|
33
|
-
* `confirm` =
|
|
35
|
+
* `confirm` = ถามยืนยันก่อนด้วย ConfirmDialog (ใช้กับสิ่งที่ย้อนกลับไม่ได้) — dialog render นอก DropdownMenu กัน focus trap ชนกัน
|
|
34
36
|
*/
|
|
35
37
|
export function RowActions({ items, label }: { items: RowAction[]; label?: string }) {
|
|
36
38
|
const router = useRouter();
|
|
37
39
|
const text = useUiText();
|
|
38
40
|
const [pending, start] = useTransition();
|
|
39
41
|
const [open, setOpen] = useState(false);
|
|
42
|
+
const [confirming, setConfirming] = useState<RowActionItem | null>(null);
|
|
43
|
+
|
|
44
|
+
function run(it: RowActionItem) {
|
|
45
|
+
if (it.href) {
|
|
46
|
+
router.push(it.href);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (it.onSelect) {
|
|
50
|
+
it.onSelect();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (it.action) {
|
|
54
|
+
const fd = new FormData();
|
|
55
|
+
for (const [k, v] of Object.entries(it.data ?? {})) fd.set(k, v);
|
|
56
|
+
const act = it.action;
|
|
57
|
+
start(async () => {
|
|
58
|
+
await act(fd);
|
|
59
|
+
router.refresh();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const spec = confirming?.confirm ? normalizeConfirm(confirming.confirm) : null;
|
|
40
65
|
return (
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
it
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
66
|
+
<>
|
|
67
|
+
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
68
|
+
<DropdownMenuTrigger
|
|
69
|
+
render={
|
|
70
|
+
<Button
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="icon-sm"
|
|
73
|
+
aria-label={label ?? text.actions}
|
|
74
|
+
disabled={pending}
|
|
75
|
+
/>
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
<MoreHorizontalIcon />
|
|
79
|
+
</DropdownMenuTrigger>
|
|
80
|
+
<DropdownMenuContent align="end" className="min-w-52">
|
|
81
|
+
{items.map((it, i) =>
|
|
82
|
+
it.type === "separator" ? (
|
|
83
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: separator ไม่มี identity
|
|
84
|
+
<DropdownMenuSeparator key={`sep-${i}`} />
|
|
85
|
+
) : (
|
|
86
|
+
<DropdownMenuItem
|
|
87
|
+
key={it.label}
|
|
88
|
+
variant={it.destructive ? "destructive" : "default"}
|
|
89
|
+
disabled={it.disabled}
|
|
90
|
+
onClick={() => {
|
|
91
|
+
setOpen(false);
|
|
92
|
+
if (it.confirm) setConfirming(it);
|
|
93
|
+
else run(it);
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{it.label}
|
|
97
|
+
</DropdownMenuItem>
|
|
98
|
+
),
|
|
99
|
+
)}
|
|
100
|
+
</DropdownMenuContent>
|
|
101
|
+
</DropdownMenu>
|
|
102
|
+
{spec && confirming && (
|
|
103
|
+
<ConfirmDialog
|
|
104
|
+
open
|
|
105
|
+
onOpenChange={(o) => !o && setConfirming(null)}
|
|
106
|
+
title={spec.title}
|
|
107
|
+
description={spec.description}
|
|
108
|
+
confirmLabel={spec.confirmLabel}
|
|
109
|
+
cancelLabel={spec.cancelLabel}
|
|
110
|
+
destructive={confirming.destructive}
|
|
111
|
+
onConfirm={() => {
|
|
112
|
+
const it = confirming;
|
|
113
|
+
setConfirming(null);
|
|
114
|
+
run(it);
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</>
|
|
92
119
|
);
|
|
93
120
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
2
|
+
import { ChevronsUpDown } from "lucide-react";
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ลูกศร ⇅ มาตรฐานท้าย trigger แบบ switcher/select (HeaderCrumb, UserMenu, Combobox, OrganizationSwitcher …)
|
|
7
|
+
* เล็กบางแบบ Clerk: 12px จาง — ใช้ตัวเดียวทั้งแพ็กเกจเพื่อให้ขนาดเท่ากันทุกจุด
|
|
8
|
+
* ไม่ใช้ `!` บน utility (แอปที่ scan ผ่าน @source อาจไม่ generate class นั้น); ปุ่มที่มี
|
|
9
|
+
* `[&_svg:not([class*='size-'])]:size-4` จะไม่ทับเพราะ class มี `size-3` อยู่แล้ว
|
|
10
|
+
*/
|
|
11
|
+
export function SwitcherChevron({ className, ...props }: ComponentProps<typeof ChevronsUpDown>) {
|
|
12
|
+
return (
|
|
13
|
+
<ChevronsUpDown
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
className={cn("size-3 shrink-0 text-muted-foreground opacity-70", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -14,6 +14,10 @@ export interface UiText {
|
|
|
14
14
|
success: string;
|
|
15
15
|
/** EnvSnippet: คัดลอกอัตโนมัติไม่ได้ (optional — มีค่าเริ่มต้นในตัว) */
|
|
16
16
|
copyFailed?: string;
|
|
17
|
+
/** ConfirmDialog: ปุ่มยืนยัน (optional — ค่าเริ่มต้น "ยืนยัน") */
|
|
18
|
+
confirm?: string;
|
|
19
|
+
/** ConfirmDialog: หัวกล่อง (optional — ค่าเริ่มต้น "ยืนยันการทำรายการ") */
|
|
20
|
+
confirmTitle?: string;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export const UI_TEXT_TH: UiText = {
|
|
@@ -26,6 +30,8 @@ export const UI_TEXT_TH: UiText = {
|
|
|
26
30
|
showOnce: "แสดงครั้งเดียว — คัดลอกเก็บไว้ทันที",
|
|
27
31
|
success: "สำเร็จ",
|
|
28
32
|
copyFailed: "คัดลอกอัตโนมัติไม่ได้ — เลือกข้อความให้แล้ว กด Ctrl+C / ⌘C",
|
|
33
|
+
confirm: "ยืนยัน",
|
|
34
|
+
confirmTitle: "ยืนยันการทำรายการ",
|
|
29
35
|
};
|
|
30
36
|
|
|
31
37
|
export const UI_TEXT_EN: UiText = {
|
|
@@ -38,6 +44,8 @@ export const UI_TEXT_EN: UiText = {
|
|
|
38
44
|
showOnce: "Shown once — copy it now",
|
|
39
45
|
success: "Done",
|
|
40
46
|
copyFailed: "Automatic copy is blocked — text selected, press Ctrl+C / ⌘C",
|
|
47
|
+
confirm: "Confirm",
|
|
48
|
+
confirmTitle: "Please confirm",
|
|
41
49
|
};
|
|
42
50
|
|
|
43
51
|
const UiTextContext = createContext<UiText>(UI_TEXT_TH);
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,11 @@ export { toast } from "sonner";
|
|
|
16
16
|
export * from "./components/shared/button";
|
|
17
17
|
export { ClientRedirect } from "./components/shared/client-redirect";
|
|
18
18
|
export { Combobox } from "./components/shared/combobox";
|
|
19
|
+
export {
|
|
20
|
+
ConfirmDialog,
|
|
21
|
+
type ConfirmSpec,
|
|
22
|
+
normalizeConfirm,
|
|
23
|
+
} from "./components/shared/confirm-dialog";
|
|
19
24
|
export { DataList, DataRow } from "./components/shared/data-list";
|
|
20
25
|
export { DateRangePicker } from "./components/shared/date/date-range-picker";
|
|
21
26
|
export { DescriptionList } from "./components/shared/description-list";
|
|
@@ -63,6 +68,7 @@ export { SecretReveal } from "./components/shared/secret-reveal";
|
|
|
63
68
|
export { SectionCard } from "./components/shared/section-card";
|
|
64
69
|
export { Stat } from "./components/shared/stat";
|
|
65
70
|
export { StatusBadge, statusTone, type Tone } from "./components/shared/status-badge";
|
|
71
|
+
export { SwitcherChevron } from "./components/shared/switcher-chevron";
|
|
66
72
|
export {
|
|
67
73
|
PrimarySecondaryText,
|
|
68
74
|
type PrimarySecondaryTextProps,
|
package/src/next.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// ---------- app shell (sidebar + header + เนื้อหา) ----------
|
|
7
7
|
export * from "./components/app-shell";
|
|
8
8
|
export { LinkButton, type LinkButtonProps } from "./components/shared/button/base/link-button";
|
|
9
|
+
export { ConfirmActionButton } from "./components/shared/button/confirm-action-button";
|
|
9
10
|
export { RefreshButton } from "./components/shared/button/refresh-button";
|
|
10
11
|
export { FormDialog } from "./components/shared/form-dialog";
|
|
11
12
|
export { PageShell } from "./components/shared/layout/page-shell";
|
|
@@ -14,7 +15,7 @@ export {
|
|
|
14
15
|
PageBreadcrumb,
|
|
15
16
|
type PageBreadcrumbItem,
|
|
16
17
|
} from "./components/shared/navigation/page-breadcrumb";
|
|
17
|
-
export { type RowAction, RowActions } from "./components/shared/row-actions";
|
|
18
|
+
export { type RowAction, type RowActionItem, RowActions } from "./components/shared/row-actions";
|
|
18
19
|
export { RowMenu, type RowMenuDialog, type RowMenuItem } from "./components/shared/row-menu";
|
|
19
20
|
export {
|
|
20
21
|
TABLE_PARAM,
|