@suphark/ui 0.27.1 → 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.
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/components/data-table/data-table-pagination.tsx +9 -4
- package/src/components/data-table/data-table-row-actions.tsx +14 -9
- package/src/components/data-table/data-table.tsx +3 -1
- package/src/components/shared/activity-feed.tsx +114 -0
- package/src/components/shared/inline-edit.tsx +175 -0
- package/src/components/shared/selection-bar.tsx +94 -0
- package/src/components/shared/simple-table.tsx +51 -2
- package/src/components/shared/simple-tabs.tsx +54 -13
- package/src/components/shared/ui-text.tsx +43 -0
- package/src/index.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.28.0] — 2026-10-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`ActivityFeed`** — เส้นเวลา "ใครทำอะไรเมื่อไหร่" จาก array เดียว (audit log ของ Pi Auth, ประวัติงานของ Pi Done, timeline ของเอกสาร): avatar ของผู้ทำหรือไอคอน+โทนสำหรับเหตุการณ์ของระบบ, เวลาสัมพัทธ์ที่คำนวณฝั่ง client หลัง mount (ไม่ทำ hydration mismatch) และช่องใส่ปุ่ม/ป้ายท้ายแถว
|
|
10
|
+
- **`InlineEdit`** — แก้ค่าในที่: คลิกดินสอ → ช่องกรอก (เลือกข้อความเดิมไว้ให้พิมพ์ทับได้ทันที) → Enter/✓ บันทึก, Esc/✕ ยกเลิก พร้อม `validate`, สถานะกำลังบันทึก และ toast เมื่อล้มเหลว (ถือ `{ ok: false, error }` เป็นล้มเหลว, `redirect()` เป็นสำเร็จ) — ใช้กับค่าสั้น ๆ ที่ไม่คุ้มเปิด `FormDialog`
|
|
11
|
+
- **`SelectionBar`** — แถบคำสั่งหมู่ "เลือก N รายการ" + ปุ่มคำสั่ง + ปุ่มล้าง (`floating` กลางล่างจอ หรือ `inline` ในหน้า) ใช้คู่ `DataTable` หรือ `SimpleTable`; ข้อความจาก `UiText.selection`
|
|
12
|
+
- **`SimpleTable` เลือกหลายแถวได้** — `selected` + `onSelectedChange` เปิดช่องติ๊กหน้าแถวและหัวตาราง (เลือกบางส่วนแสดงเป็นขีด), แถวที่เลือกมีพื้นจาง; คลิกช่องติ๊กไม่ไปโดน `onRowClick`
|
|
13
|
+
- `SimpleTabs`: แท็บล้นความกว้างแล้วเลื่อนแนวนอนได้ (ซ่อนแถบเลื่อน) + เงาขอบบอกว่ายังมีแท็บต่อ
|
|
14
|
+
- `UiText` คีย์ใหม่: `edit`, `selection.{selected,clear}`, `table.{rowsPerPage,pageOf,rowsSelected,noResults,deleteTitle,deleteDescription,deleting,deleted,delete}`
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **`DataTable` เลิกใช้ข้อความอังกฤษตายตัว** — "Rows per page", "Page X of Y", "N of M row(s) selected", "No results.", เมนู Edit/Delete, กล่องยืนยันลบ และ toast ตอนลบ ทั้งหมดมาจาก `UiText.table` (ค่าเริ่มต้นภาษาไทย) — `resourceName` แทรกผ่าน `{resource}`
|
|
19
|
+
|
|
5
20
|
## [0.27.1] — 2026-10-05
|
|
6
21
|
|
|
7
22
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
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 เอง",
|
|
@@ -36,13 +36,16 @@ export function DataTablePagination<TData>({
|
|
|
36
36
|
>
|
|
37
37
|
{isRowSelectionEnabled && (
|
|
38
38
|
<div className="flex-1 text-muted-foreground text-sm max-sm:text-center">
|
|
39
|
-
{
|
|
40
|
-
|
|
39
|
+
{(text.table?.rowsSelected ?? "เลือก {selected} จาก {total} แถว")
|
|
40
|
+
.replace("{selected}", String(table.getFilteredSelectedRowModel().rows.length))
|
|
41
|
+
.replace("{total}", String(table.getFilteredRowModel().rows.length))}
|
|
41
42
|
</div>
|
|
42
43
|
)}
|
|
43
44
|
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 lg:gap-8">
|
|
44
45
|
<div className="flex items-center gap-2">
|
|
45
|
-
<p className="font-medium text-sm max-sm:hidden">
|
|
46
|
+
<p className="font-medium text-sm max-sm:hidden">
|
|
47
|
+
{text.table?.rowsPerPage ?? "แถวต่อหน้า"}
|
|
48
|
+
</p>
|
|
46
49
|
<Select
|
|
47
50
|
value={`${table.getState().pagination.pageSize}`}
|
|
48
51
|
onValueChange={(value) => {
|
|
@@ -66,7 +69,9 @@ export function DataTablePagination<TData>({
|
|
|
66
69
|
</Select>
|
|
67
70
|
</div>
|
|
68
71
|
<div className="flex w-[100px] items-center justify-center font-medium text-sm">
|
|
69
|
-
|
|
72
|
+
{(text.table?.pageOf ?? "หน้า {page} จาก {pages}")
|
|
73
|
+
.replace("{page}", String(table.getState().pagination.pageIndex + 1))
|
|
74
|
+
.replace("{pages}", String(table.getPageCount()))}
|
|
70
75
|
</div>
|
|
71
76
|
<div className="flex items-center gap-2">
|
|
72
77
|
<Button
|
|
@@ -76,6 +76,10 @@ export function DataTableRowActions<TData>({
|
|
|
76
76
|
idKey = "id" as keyof TData,
|
|
77
77
|
}: DataTableRowActionsProps<TData>) {
|
|
78
78
|
const text = useUiText();
|
|
79
|
+
// ชื่อชนิดข้อมูล (เช่น "โปรเจกต์") แทรกในข้อความลบ — ไม่ส่งมา = ข้อความไม่มีคำนี้
|
|
80
|
+
const resource = resourceName ? ` ${resourceName}` : "";
|
|
81
|
+
const fill = (template: string, name: string) =>
|
|
82
|
+
template.replace("{resource}", resource).replace("{name}", name);
|
|
79
83
|
const item = row.original;
|
|
80
84
|
const itemRecord = item as Record<string, unknown>;
|
|
81
85
|
const itemName = String(
|
|
@@ -96,8 +100,8 @@ export function DataTableRowActions<TData>({
|
|
|
96
100
|
});
|
|
97
101
|
|
|
98
102
|
toast.promise(promise, {
|
|
99
|
-
loading:
|
|
100
|
-
success:
|
|
103
|
+
loading: fill(text.table?.deleting ?? "กำลังลบ{resource} “{name}”…", itemName),
|
|
104
|
+
success: fill(text.table?.deleted ?? "ลบ{resource} “{name}” แล้ว", itemName),
|
|
101
105
|
error: (err: Error) => err.message,
|
|
102
106
|
});
|
|
103
107
|
});
|
|
@@ -125,7 +129,7 @@ export function DataTableRowActions<TData>({
|
|
|
125
129
|
{EditDialog && (
|
|
126
130
|
<DropdownMenuItem onClick={() => setIsEditDialogOpen(true)}>
|
|
127
131
|
<Pen className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
128
|
-
|
|
132
|
+
{text.edit ?? "แก้ไข"}
|
|
129
133
|
</DropdownMenuItem>
|
|
130
134
|
)}
|
|
131
135
|
|
|
@@ -137,7 +141,7 @@ export function DataTableRowActions<TData>({
|
|
|
137
141
|
onClick={() => setIsDeleteDialogOpen(true)}
|
|
138
142
|
>
|
|
139
143
|
<Trash className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
140
|
-
|
|
144
|
+
{text.table?.delete ?? "ลบ"}
|
|
141
145
|
</DropdownMenuItem>
|
|
142
146
|
)}
|
|
143
147
|
</DropdownMenuGroup>
|
|
@@ -153,15 +157,16 @@ export function DataTableRowActions<TData>({
|
|
|
153
157
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
154
158
|
<AlertDialogContent>
|
|
155
159
|
<AlertDialogHeader>
|
|
156
|
-
<AlertDialogTitle>
|
|
160
|
+
<AlertDialogTitle>{text.table?.deleteTitle ?? "ยืนยันการลบ"}</AlertDialogTitle>
|
|
157
161
|
<AlertDialogDescription>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
{fill(
|
|
163
|
+
text.table?.deleteDescription ?? "ลบ{resource} “{name}” แล้วกู้คืนไม่ได้",
|
|
164
|
+
itemName,
|
|
165
|
+
)}
|
|
161
166
|
</AlertDialogDescription>
|
|
162
167
|
</AlertDialogHeader>
|
|
163
168
|
<AlertDialogFooter>
|
|
164
|
-
<AlertDialogCancel>
|
|
169
|
+
<AlertDialogCancel>{text.cancel}</AlertDialogCancel>
|
|
165
170
|
<DeleteButton onClick={handleDelete} isLoading={isPending} />
|
|
166
171
|
</AlertDialogFooter>
|
|
167
172
|
</AlertDialogContent>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
3
4
|
import {
|
|
4
5
|
TableBody,
|
|
5
6
|
TableCell,
|
|
@@ -80,6 +81,7 @@ export function DataTable<TData, TValue>({
|
|
|
80
81
|
onReorder,
|
|
81
82
|
renderCustomView,
|
|
82
83
|
}: DataTableProps<TData, TValue>) {
|
|
84
|
+
const text = useUiText();
|
|
83
85
|
const { table } = useDataTable({
|
|
84
86
|
columns,
|
|
85
87
|
data,
|
|
@@ -221,7 +223,7 @@ export function DataTable<TData, TValue>({
|
|
|
221
223
|
) : (
|
|
222
224
|
<TableRow>
|
|
223
225
|
<TableCell colSpan={table.getVisibleLeafColumns().length} className="h-24 text-center">
|
|
224
|
-
{emptyStateComponent ||
|
|
226
|
+
{emptyStateComponent || (text.table?.noResults ?? "ไม่พบข้อมูล")}
|
|
225
227
|
</TableCell>
|
|
226
228
|
</TableRow>
|
|
227
229
|
)}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Initials } from "@suphark/ui/components/shared/initials";
|
|
4
|
+
import { ClientDateFormatter } from "@suphark/ui/components/shared/time/client-date-formatter";
|
|
5
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
|
|
8
|
+
export type ActivityTone = "default" | "info" | "success" | "warning" | "destructive";
|
|
9
|
+
|
|
10
|
+
export interface ActivityItem {
|
|
11
|
+
id: string;
|
|
12
|
+
/** สิ่งที่เกิดขึ้น เช่น "แก้ไขสิทธิ์ของสมชาย" */
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
/** รายละเอียดเพิ่ม (ก่อน → หลัง, เหตุผล, IP) */
|
|
15
|
+
description?: ReactNode;
|
|
16
|
+
/** ชื่อผู้ทำ — ไม่ส่ง = เหตุการณ์ของระบบ */
|
|
17
|
+
actor?: string | null;
|
|
18
|
+
/** รูปผู้ทำ (ไม่มี = ตัวย่อชื่อ) */
|
|
19
|
+
actorSrc?: string | null;
|
|
20
|
+
at: Date | string | number;
|
|
21
|
+
/** ไอคอนแทน avatar (เหตุการณ์ของระบบ เช่น `<KeyIcon />`) */
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
tone?: ActivityTone;
|
|
24
|
+
/** มุมขวาของแถว เช่น `<StatusBadge>` หรือปุ่มดูรายละเอียด */
|
|
25
|
+
action?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DOT_TONE: Record<ActivityTone, string> = {
|
|
29
|
+
default: "bg-muted text-muted-foreground",
|
|
30
|
+
info: "bg-info-subtle text-info-subtle-foreground",
|
|
31
|
+
success: "bg-success-subtle text-success-subtle-foreground",
|
|
32
|
+
warning: "bg-warning-subtle text-warning-subtle-foreground",
|
|
33
|
+
destructive: "bg-destructive-subtle text-destructive-subtle-foreground",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* เส้นเวลา "ใครทำอะไรเมื่อไหร่" จาก array เดียว — audit log (Pi Auth), ประวัติงาน (Pi Done), timeline ของเอกสาร
|
|
38
|
+
* เวลาคำนวณฝั่ง client หลัง mount (ไม่ทำ hydration mismatch) และอัปเดตเองทุกนาที
|
|
39
|
+
* รายการที่มี `icon` ใช้ไอคอนแทน avatar; ไม่มีทั้งคู่ = ตัวย่อชื่อผู้ทำ
|
|
40
|
+
*
|
|
41
|
+
* <ActivityFeed items={[{ id: "1", title: "เปลี่ยนสิทธิ์เป็นผู้ดูแล", actor: "สมชาย ใจดี", at: row.createdAt }]} />
|
|
42
|
+
*/
|
|
43
|
+
export function ActivityFeed({
|
|
44
|
+
items,
|
|
45
|
+
relative = true,
|
|
46
|
+
emptyLabel,
|
|
47
|
+
className,
|
|
48
|
+
}: {
|
|
49
|
+
items: ReadonlyArray<ActivityItem>;
|
|
50
|
+
/** true = "5 นาทีที่แล้ว" (ค่าเริ่มต้น) · false = วันที่/เวลาเต็ม */
|
|
51
|
+
relative?: boolean;
|
|
52
|
+
/** ไม่มีรายการ (ค่าเริ่มต้น "ยังไม่มีรายการ") */
|
|
53
|
+
emptyLabel?: ReactNode;
|
|
54
|
+
className?: string;
|
|
55
|
+
}) {
|
|
56
|
+
if (items.length === 0) {
|
|
57
|
+
return (
|
|
58
|
+
<p className={cn("py-6 text-center text-muted-foreground text-sm", className)}>
|
|
59
|
+
{emptyLabel ?? "ยังไม่มีรายการ"}
|
|
60
|
+
</p>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ol data-slot="activity-feed" className={cn("flex flex-col", className)}>
|
|
66
|
+
{items.map((item, index) => {
|
|
67
|
+
const tone = item.tone ?? "default";
|
|
68
|
+
const last = index === items.length - 1;
|
|
69
|
+
return (
|
|
70
|
+
<li key={item.id} className="flex gap-3">
|
|
71
|
+
{/* เส้นแนวตั้ง + จุด/avatar */}
|
|
72
|
+
<div className="flex flex-col items-center">
|
|
73
|
+
{item.icon ? (
|
|
74
|
+
<span
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex size-7 shrink-0 items-center justify-center rounded-full [&_svg]:size-3.5",
|
|
77
|
+
DOT_TONE[tone],
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{item.icon}
|
|
81
|
+
</span>
|
|
82
|
+
) : (
|
|
83
|
+
<Initials name={item.actor ?? ""} src={item.actorSrc} className="size-7" />
|
|
84
|
+
)}
|
|
85
|
+
{!last && <span className="w-px flex-1 bg-border" aria-hidden="true" />}
|
|
86
|
+
</div>
|
|
87
|
+
<div className={cn("flex min-w-0 flex-1 gap-3", last ? "pb-0" : "pb-4")}>
|
|
88
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
89
|
+
<p className="text-sm leading-snug">
|
|
90
|
+
{item.actor && <span className="font-medium">{item.actor} </span>}
|
|
91
|
+
{item.title}
|
|
92
|
+
</p>
|
|
93
|
+
{item.description && (
|
|
94
|
+
<div className="text-muted-foreground text-xs">{item.description}</div>
|
|
95
|
+
)}
|
|
96
|
+
<ClientDateFormatter
|
|
97
|
+
date={item.at}
|
|
98
|
+
formatType={relative ? "relative" : "standard"}
|
|
99
|
+
options={
|
|
100
|
+
relative
|
|
101
|
+
? undefined
|
|
102
|
+
: { day: "numeric", month: "short", year: "numeric", timeStyle: "short" }
|
|
103
|
+
}
|
|
104
|
+
className="text-muted-foreground text-xs"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
{item.action && <div className="shrink-0">{item.action}</div>}
|
|
108
|
+
</div>
|
|
109
|
+
</li>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</ol>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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,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
|
-
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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}
|
|
@@ -20,6 +20,23 @@ export interface UiText {
|
|
|
20
20
|
confirmTitle?: string;
|
|
21
21
|
/** RowActions/ConfirmActionButton: action โยน error (optional — ค่าเริ่มต้น "ดำเนินการไม่สำเร็จ") */
|
|
22
22
|
actionFailed?: string;
|
|
23
|
+
/** InlineEdit: ปุ่มดินสอ (optional — "แก้ไข") */
|
|
24
|
+
edit?: string;
|
|
25
|
+
/** SelectionBar (optional) — `selected` ใช้ `{count}` */
|
|
26
|
+
selection?: Partial<{ selected: string; clear: string }>;
|
|
27
|
+
/** DataTable (optional) — `rowsSelected` ใช้ `{selected}`/`{total}`, `pageOf` ใช้ `{page}`/`{pages}` */
|
|
28
|
+
table?: Partial<{
|
|
29
|
+
rowsPerPage: string;
|
|
30
|
+
pageOf: string;
|
|
31
|
+
rowsSelected: string;
|
|
32
|
+
noResults: string;
|
|
33
|
+
deleteTitle: string;
|
|
34
|
+
/** ใช้ `{name}` และ `{resource}` (ชื่อชนิดข้อมูลที่ส่งมาทาง `resourceName`) */
|
|
35
|
+
deleteDescription: string;
|
|
36
|
+
deleting: string;
|
|
37
|
+
deleted: string;
|
|
38
|
+
delete: string;
|
|
39
|
+
}>;
|
|
23
40
|
/** DialogsProvider: ปุ่มปิดของ alert (optional — ค่าเริ่มต้น "ตกลง") */
|
|
24
41
|
ok?: string;
|
|
25
42
|
/** DialogsProvider: หัวกล่อง alert (optional — ค่าเริ่มต้น "แจ้งให้ทราบ") */
|
|
@@ -135,6 +152,19 @@ export const UI_TEXT_TH: UiText = {
|
|
|
135
152
|
confirm: "ยืนยัน",
|
|
136
153
|
confirmTitle: "ยืนยันการทำรายการ",
|
|
137
154
|
actionFailed: "ดำเนินการไม่สำเร็จ",
|
|
155
|
+
edit: "แก้ไข",
|
|
156
|
+
selection: { selected: "เลือก {count} รายการ", clear: "ล้างการเลือก" },
|
|
157
|
+
table: {
|
|
158
|
+
rowsPerPage: "แถวต่อหน้า",
|
|
159
|
+
pageOf: "หน้า {page} จาก {pages}",
|
|
160
|
+
rowsSelected: "เลือก {selected} จาก {total} แถว",
|
|
161
|
+
noResults: "ไม่พบข้อมูล",
|
|
162
|
+
deleteTitle: "ยืนยันการลบ",
|
|
163
|
+
deleteDescription: "ลบ{resource} “{name}” แล้วกู้คืนไม่ได้",
|
|
164
|
+
deleting: "กำลังลบ{resource} “{name}”…",
|
|
165
|
+
deleted: "ลบ{resource} “{name}” แล้ว",
|
|
166
|
+
delete: "ลบ",
|
|
167
|
+
},
|
|
138
168
|
ok: "ตกลง",
|
|
139
169
|
alertTitle: "แจ้งให้ทราบ",
|
|
140
170
|
promptTitle: "กรอกข้อมูล",
|
|
@@ -233,6 +263,19 @@ export const UI_TEXT_EN: UiText = {
|
|
|
233
263
|
confirm: "Confirm",
|
|
234
264
|
confirmTitle: "Please confirm",
|
|
235
265
|
actionFailed: "Action failed",
|
|
266
|
+
edit: "Edit",
|
|
267
|
+
selection: { selected: "{count} selected", clear: "Clear selection" },
|
|
268
|
+
table: {
|
|
269
|
+
rowsPerPage: "Rows per page",
|
|
270
|
+
pageOf: "Page {page} of {pages}",
|
|
271
|
+
rowsSelected: "{selected} of {total} row(s) selected",
|
|
272
|
+
noResults: "No results",
|
|
273
|
+
deleteTitle: "Are you sure?",
|
|
274
|
+
deleteDescription: "Deleting{resource} “{name}” cannot be undone",
|
|
275
|
+
deleting: "Deleting{resource} “{name}”…",
|
|
276
|
+
deleted: "“{name}”{resource} deleted",
|
|
277
|
+
delete: "Delete",
|
|
278
|
+
},
|
|
236
279
|
ok: "OK",
|
|
237
280
|
alertTitle: "Notice",
|
|
238
281
|
promptTitle: "Input required",
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,11 @@ export {
|
|
|
19
19
|
type ActionMenuCommand,
|
|
20
20
|
type ActionMenuItem,
|
|
21
21
|
} from "./components/shared/action-menu";
|
|
22
|
+
export {
|
|
23
|
+
ActivityFeed,
|
|
24
|
+
type ActivityItem,
|
|
25
|
+
type ActivityTone,
|
|
26
|
+
} from "./components/shared/activity-feed";
|
|
22
27
|
export { AuthLayout, type AuthLayoutProps } from "./components/shared/auth-layout";
|
|
23
28
|
export {
|
|
24
29
|
AuthDivider,
|
|
@@ -144,6 +149,7 @@ export {
|
|
|
144
149
|
} from "./components/shared/icon-picker";
|
|
145
150
|
export { InfoHint } from "./components/shared/info-hint";
|
|
146
151
|
export { Initials, initialsOf } from "./components/shared/initials";
|
|
152
|
+
export { InlineEdit, type InlineEditProps } from "./components/shared/inline-edit";
|
|
147
153
|
export {
|
|
148
154
|
LanguageMenuItems,
|
|
149
155
|
type LanguageOption,
|
|
@@ -192,6 +198,11 @@ export { type SaveState, SaveStatus, type SaveStatusText } from "./components/sh
|
|
|
192
198
|
export { SecretReveal } from "./components/shared/secret-reveal";
|
|
193
199
|
export { SectionCard } from "./components/shared/section-card";
|
|
194
200
|
export { SegmentedControl, type SegmentedOption } from "./components/shared/segmented-control";
|
|
201
|
+
export {
|
|
202
|
+
type SelectionAction,
|
|
203
|
+
SelectionBar,
|
|
204
|
+
type SelectionBarProps,
|
|
205
|
+
} from "./components/shared/selection-bar";
|
|
195
206
|
export { SelectionButton } from "./components/shared/selection-button";
|
|
196
207
|
export { SettingsLayout, type SettingsSection } from "./components/shared/settings-layout";
|
|
197
208
|
export { DangerZone, SettingsRow, SwitchRow } from "./components/shared/settings-row";
|