@suphark/ui 0.4.1 → 0.4.3
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 +23 -0
- package/README.md +18 -1
- package/package.json +1 -1
- package/src/components/shared/button/confirm-action-button.tsx +73 -0
- package/src/components/shared/confirm-dialog.tsx +83 -0
- package/src/components/shared/row-actions.tsx +92 -65
- package/src/components/shared/ui-text.tsx +8 -0
- package/src/hooks/use-confirm.tsx +63 -0
- package/src/index.ts +6 -0
- package/src/next.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.4.3] — 2026-09-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`useConfirm()`** (barrel) — `window.confirm` แบบ promise บน ConfirmDialog สำหรับโค้ดฝั่ง client ที่ไม่ใช่ server action (ยกมาจาก DueBell ที่ย้าย window.confirm 8 จุดด้วย hook นี้): `const { confirm, confirmDialog } = useConfirm(); if (!(await confirm({ description, destructive: true }))) return;` แล้ว render `{confirmDialog}`; เรียกซ้ำขณะเปิดอยู่ → คำขอเดิม resolve(false)
|
|
10
|
+
- `ConfirmSpec` object รับ `destructive?` (RowActions/ConfirmActionButton ใช้เป็นตัวทับค่าเดิมของรายการ/variant)
|
|
11
|
+
- README: ตาราง "ยืนยันก่อนทำ — ห้ามใช้ window.confirm" (เลือก RowActions.confirm / ConfirmActionButton / useConfirm / ConfirmDialog) และหมายเหตุ Turbopack ค้าง `Package path ./styles.css is exported … but no valid target file` หลังอัปเดตแพ็กเกจ = cache ของ dev server ให้ลบ `.next`
|
|
12
|
+
|
|
13
|
+
## [0.4.2] — 2026-09-19
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`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()`
|
|
18
|
+
- **`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)
|
|
19
|
+
- `UiText.confirm` / `UiText.confirmTitle` (optional มีค่าเริ่มต้นไทย/อังกฤษ) — แอปที่ส่ง UiText เองไม่ต้องเพิ่ม
|
|
20
|
+
- export type `RowActionItem` (รายการที่ไม่ใช่ separator)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **`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 เหมือนเดิม — โค้ดเรียกเดิมไม่ต้องแก้
|
|
25
|
+
- ไม่มี `window.confirm` เหลือในแพ็กเกจ
|
|
26
|
+
- showcase: หน้า `/components/confirm-dialog` (ConfirmDialog + ConfirmActionButton) และ `/components/row-actions` ใช้ confirm แบบ object
|
|
27
|
+
|
|
5
28
|
## [0.4.1] — 2026-09-19
|
|
6
29
|
|
|
7
30
|
### Added
|
package/README.md
CHANGED
|
@@ -51,11 +51,15 @@ import { ThemeProvider, Toaster } from "@suphark/ui";
|
|
|
51
51
|
|
|
52
52
|
แอปจะ type-check source ของแพ็กเกจด้วย — แพ็กเกจผ่าน `strict` + `noUncheckedIndexedAccess` + `verbatimModuleSyntax` อยู่แล้ว
|
|
53
53
|
|
|
54
|
+
### หลังอัปเดตเวอร์ชันแพ็กเกจ
|
|
55
|
+
|
|
56
|
+
ถ้า Turbopack ค้าง `Package path ./styles.css is exported … but no valid target file` (หรือ resolve entry อื่นไม่ได้) หลัง `pnpm install` เวอร์ชันใหม่ — เป็น cache ของ dev server ไม่ใช่ปัญหาแพ็กเกจ: หยุด dev server แล้วลบ `.next` ของแอป
|
|
57
|
+
|
|
54
58
|
## การ import
|
|
55
59
|
|
|
56
60
|
```ts
|
|
57
61
|
// ทุกอย่างที่ไม่พึ่ง Next — primitives, shared, theme, hooks, cn
|
|
58
|
-
import { Button, Card, FormFieldInput, StatusBadge, ThemeToggle, cn } from "@suphark/ui";
|
|
62
|
+
import { Button, Card, FormFieldInput, StatusBadge, ThemeToggle, ConfirmDialog, useConfirm, cn } from "@suphark/ui";
|
|
59
63
|
|
|
60
64
|
// ของที่ใช้ next/navigation หรือ next/link + โครงแอป
|
|
61
65
|
import { AppShell, UserMenu, filterNav, FormDialog, RowActions, LinkButton, RefreshButton, PageShell } from "@suphark/ui/next";
|
|
@@ -140,6 +144,19 @@ export default async function Layout({ children }) {
|
|
|
140
144
|
<div style={{ "--brand": tenant.color, "--brand-foreground": "oklch(0.985 0 0)" } as React.CSSProperties}>…</div>
|
|
141
145
|
```
|
|
142
146
|
|
|
147
|
+
### ยืนยันก่อนทำ — ห้ามใช้ `window.confirm`
|
|
148
|
+
|
|
149
|
+
webview/automation บางตัวปิด `window.confirm` ทิ้ง (กดแล้วไม่เกิดอะไร) และไม่เข้า design system — ใช้ 3 ตัวนี้แทน
|
|
150
|
+
|
|
151
|
+
| สถานการณ์ | ใช้ |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| รายการในเมนู … ท้ายแถว | `RowActions` / `RowMenu` prop `confirm` (string หรือ `{ title, description, confirmLabel }`) |
|
|
154
|
+
| ปุ่มเดี่ยวเรียก server action | `ConfirmActionButton` (`@suphark/ui/next`) — `confirm` + `action` + `data` |
|
|
155
|
+
| โค้ด client อื่น ๆ (fetch/mutation เอง) | `const { confirm, confirmDialog } = useConfirm(); if (!(await confirm({ description, destructive: true }))) return;` แล้ว render `{confirmDialog}` |
|
|
156
|
+
| คุม state เอง | `ConfirmDialog` controlled |
|
|
157
|
+
|
|
158
|
+
ข้อความค่าเริ่มต้น (หัว/ปุ่ม) มาจาก `UiText.confirmTitle` / `UiText.confirm`
|
|
159
|
+
|
|
143
160
|
### สิทธิ์ (ActionButton/LinkButton `permission` prop)
|
|
144
161
|
|
|
145
162
|
แพ็กเกจไม่รู้จักระบบสิทธิ์ของแอป — ครอบด้วย `UiPermissionProvider` แล้วส่งฟังก์ชัน `check(requirement, options)` ของแอปเข้ามา ถ้าไม่ครอบ = อนุญาตทุกอย่าง
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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 เอง",
|
|
@@ -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 = spec.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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
/** ปุ่มยืนยันสีแดง (RowActions ใช้ `destructive` ของรายการเป็นค่าเริ่มต้นอยู่แล้ว) */
|
|
26
|
+
destructive?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** แปลง ConfirmSpec เป็น object เสมอ */
|
|
30
|
+
export function normalizeConfirm(spec: ConfirmSpec): Exclude<ConfirmSpec, string> {
|
|
31
|
+
return typeof spec === "string" ? { description: spec } : spec;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* กล่องยืนยันมาตรฐานของแพ็กเกจ (AlertDialog) — ใช้แทน `window.confirm` ทุกที่
|
|
36
|
+
* (webview/automation บางตัวปิด window.confirm ทิ้ง กดแล้วไม่เกิดอะไร และไม่เข้า design system)
|
|
37
|
+
* controlled: `open`/`onOpenChange`; `pending` = กำลังรัน action → ปุ่มยืนยันหมุน ปิดไม่ได้ชั่วคราว
|
|
38
|
+
*/
|
|
39
|
+
export function ConfirmDialog({
|
|
40
|
+
open,
|
|
41
|
+
onOpenChange,
|
|
42
|
+
title,
|
|
43
|
+
description,
|
|
44
|
+
confirmLabel,
|
|
45
|
+
cancelLabel,
|
|
46
|
+
destructive = false,
|
|
47
|
+
pending = false,
|
|
48
|
+
onConfirm,
|
|
49
|
+
}: {
|
|
50
|
+
open: boolean;
|
|
51
|
+
onOpenChange: (open: boolean) => void;
|
|
52
|
+
title?: ReactNode;
|
|
53
|
+
description: ReactNode;
|
|
54
|
+
confirmLabel?: string;
|
|
55
|
+
cancelLabel?: string;
|
|
56
|
+
/** ปุ่มยืนยันสีแดง (ลบ/เพิกถอน) */
|
|
57
|
+
destructive?: boolean;
|
|
58
|
+
pending?: boolean;
|
|
59
|
+
onConfirm: () => void;
|
|
60
|
+
}) {
|
|
61
|
+
const text = useUiText();
|
|
62
|
+
return (
|
|
63
|
+
<AlertDialog open={open} onOpenChange={(o) => !pending && onOpenChange(o)}>
|
|
64
|
+
<AlertDialogContent>
|
|
65
|
+
<AlertDialogHeader>
|
|
66
|
+
<AlertDialogTitle>{title ?? text.confirmTitle ?? "ยืนยันการทำรายการ"}</AlertDialogTitle>
|
|
67
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
68
|
+
</AlertDialogHeader>
|
|
69
|
+
<AlertDialogFooter>
|
|
70
|
+
<AlertDialogCancel disabled={pending}>{cancelLabel ?? text.cancel}</AlertDialogCancel>
|
|
71
|
+
<AlertDialogAction
|
|
72
|
+
variant={destructive ? "destructive" : "default"}
|
|
73
|
+
disabled={pending}
|
|
74
|
+
onClick={onConfirm}
|
|
75
|
+
>
|
|
76
|
+
{pending && <Loader2 className="animate-spin" aria-hidden="true" />}
|
|
77
|
+
{confirmLabel ?? text.confirm ?? "ยืนยัน"}
|
|
78
|
+
</AlertDialogAction>
|
|
79
|
+
</AlertDialogFooter>
|
|
80
|
+
</AlertDialogContent>
|
|
81
|
+
</AlertDialog>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -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={spec.destructive ?? confirming.destructive}
|
|
111
|
+
onConfirm={() => {
|
|
112
|
+
const it = confirming;
|
|
113
|
+
setConfirming(null);
|
|
114
|
+
run(it);
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</>
|
|
92
119
|
);
|
|
93
120
|
}
|
|
@@ -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);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ConfirmDialog,
|
|
5
|
+
type ConfirmSpec,
|
|
6
|
+
normalizeConfirm,
|
|
7
|
+
} from "@suphark/ui/components/shared/confirm-dialog";
|
|
8
|
+
import { type ReactNode, useCallback, useState } from "react";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `window.confirm` แบบ promise บน ConfirmDialog — สำหรับโค้ดฝั่ง client ที่ไม่ใช่ server action
|
|
12
|
+
* (เช่น กดลบแล้วเรียก fetch/mutation เอง) — ยกมาจาก DueBell
|
|
13
|
+
*
|
|
14
|
+
* const { confirm, confirmDialog } = useConfirm();
|
|
15
|
+
* async function remove() {
|
|
16
|
+
* if (!(await confirm({ description: "ลบรายการนี้?", destructive: true }))) return;
|
|
17
|
+
* …
|
|
18
|
+
* }
|
|
19
|
+
* return <>{confirmDialog}<Button onClick={remove}>ลบ</Button></>;
|
|
20
|
+
*
|
|
21
|
+
* ต้อง render `confirmDialog` ในต้นไม้ของ component ที่เรียก hook (ครั้งเดียวต่อ hook)
|
|
22
|
+
* เรียก `confirm` ซ้ำขณะกล่องเปิดอยู่ → คำขอเดิม resolve(false) แล้วแสดงคำขอใหม่แทน
|
|
23
|
+
*/
|
|
24
|
+
export function useConfirm(): {
|
|
25
|
+
confirm: (spec: ConfirmSpec) => Promise<boolean>;
|
|
26
|
+
confirmDialog: ReactNode;
|
|
27
|
+
} {
|
|
28
|
+
const [request, setRequest] = useState<{
|
|
29
|
+
options: Exclude<ConfirmSpec, string>;
|
|
30
|
+
resolve: (ok: boolean) => void;
|
|
31
|
+
} | null>(null);
|
|
32
|
+
|
|
33
|
+
const confirm = useCallback(
|
|
34
|
+
(spec: ConfirmSpec) =>
|
|
35
|
+
new Promise<boolean>((resolve) => {
|
|
36
|
+
setRequest((prev) => {
|
|
37
|
+
prev?.resolve(false);
|
|
38
|
+
return { options: normalizeConfirm(spec), resolve };
|
|
39
|
+
});
|
|
40
|
+
}),
|
|
41
|
+
[],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function settle(ok: boolean) {
|
|
45
|
+
request?.resolve(ok);
|
|
46
|
+
setRequest(null);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const confirmDialog = request ? (
|
|
50
|
+
<ConfirmDialog
|
|
51
|
+
open
|
|
52
|
+
onOpenChange={(open) => !open && settle(false)}
|
|
53
|
+
title={request.options.title}
|
|
54
|
+
description={request.options.description}
|
|
55
|
+
confirmLabel={request.options.confirmLabel}
|
|
56
|
+
cancelLabel={request.options.cancelLabel}
|
|
57
|
+
destructive={request.options.destructive}
|
|
58
|
+
onConfirm={() => settle(true)}
|
|
59
|
+
/>
|
|
60
|
+
) : null;
|
|
61
|
+
|
|
62
|
+
return { confirm, confirmDialog };
|
|
63
|
+
}
|
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";
|
|
@@ -137,6 +142,7 @@ export * from "./components/ui/toggle";
|
|
|
137
142
|
export * from "./components/ui/toggle-group";
|
|
138
143
|
export * from "./components/ui/tooltip";
|
|
139
144
|
// ---------- lib / hooks ----------
|
|
145
|
+
export { useConfirm } from "./hooks/use-confirm";
|
|
140
146
|
export { useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
141
147
|
export { useIsMobile } from "./hooks/use-mobile";
|
|
142
148
|
export { copyText, selectContents } from "./lib/copy-text";
|
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,
|