@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.
- package/CHANGELOG.md +30 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/components/app-shell/app-shell.tsx +11 -2
- package/src/components/app-shell/organization-switcher.tsx +1 -1
- package/src/components/app-shell/user-menu.tsx +1 -1
- package/src/components/data-table/data-table-column-header.tsx +1 -1
- package/src/components/data-table/data-table-faceted-filter.tsx +1 -1
- package/src/components/data-table/data-table-pagination.tsx +9 -4
- package/src/components/data-table/data-table-row-actions.tsx +24 -12
- package/src/components/data-table/data-table-view-option.tsx +1 -1
- package/src/components/data-table/data-table.tsx +3 -1
- package/src/components/design-system/sections/hero-specimen.tsx +1 -1
- package/src/components/design-system/sections/showcase-data-display.tsx +1 -1
- package/src/components/design-system/sections/showcase-feedback.tsx +1 -1
- package/src/components/design-system/sections/showcase-forms.tsx +1 -1
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +1 -1
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +1 -1
- package/src/components/design-system/sections/showcase-overview.tsx +1 -1
- package/src/components/design-system/sections/showcase-primitives.tsx +1 -1
- package/src/components/pwa/pwa-install-card.tsx +1 -1
- package/src/components/shared/action-form.tsx +1 -1
- package/src/components/shared/activity-feed.tsx +114 -0
- package/src/components/shared/button/base/action-button.tsx +8 -3
- package/src/components/shared/button/base/link-button.tsx +3 -1
- package/src/components/shared/button/button.tsx +8 -1
- package/src/components/shared/button/copy-button.tsx +1 -1
- package/src/components/shared/button/floating-save-button.tsx +1 -1
- package/src/components/shared/combobox.tsx +1 -1
- package/src/components/shared/date/date-range-picker.tsx +1 -1
- package/src/components/shared/dropzone.tsx +1 -1
- package/src/components/shared/env-snippet.tsx +1 -1
- package/src/components/shared/filter-bar.tsx +1 -1
- package/src/components/shared/form/date-time-picker.tsx +1 -1
- package/src/components/shared/form/form-field-combobox.tsx +1 -1
- package/src/components/shared/form/form-field-select.tsx +1 -1
- package/src/components/shared/form-dialog.tsx +1 -1
- package/src/components/shared/inline-edit.tsx +175 -0
- package/src/components/shared/language/language-switch.tsx +1 -1
- package/src/components/shared/layout/error-state.tsx +2 -1
- package/src/components/shared/multi-select-combobox.tsx +1 -1
- package/src/components/shared/selection-bar.tsx +94 -0
- package/src/components/shared/selection-button.tsx +1 -1
- package/src/components/shared/simple-table.tsx +51 -2
- package/src/components/shared/simple-tabs.tsx +54 -13
- package/src/components/shared/step-form.tsx +1 -1
- package/src/components/shared/ui-text.tsx +47 -0
- package/src/index.ts +11 -0
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.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
|
+
|
|
20
|
+
## [0.27.1] — 2026-10-05
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `Button`: ถ้า `aria-label` ไปอยู่บน element ใน `render=` (เช่น `render={<Link aria-label="…"/>}`) ก็ใช้เป็น tooltip ได้ (เดิมต้องอยู่บน `Button` เท่านั้น — รายงานจาก Pi Auth)
|
|
25
|
+
- เทสต์ชุดใหม่ `button-tooltip-triggers`: ยืนยันว่าปุ่มไอคอนที่ส่งเป็น `render=`/`trigger` ของ `DropdownMenuTrigger` / `PopoverTrigger` / `FormDialog` / `RowActions` ยังได้ tooltip จริง (เช็กจาก popup ที่ขึ้น ไม่ใช่ `data-slot` — `data-slot` ของปุ่มจะกลายเป็นของ trigger ชั้นนอกเสมอ ใช้ชี้วัดไม่ได้)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **ตัวห่อปุ่มของแพ็กเกจเองหล่น tooltip** (รายงานจาก Pi Auth): `LinkButton`/`ActionButton`/`CopyButton`/`DeleteManyButton`/`FloatingSaveButton`/`SelectionButton`, `DataTable*`, `UserMenu`, `OrganizationSwitcher`, `FilterBar`, `Combobox`, `Dropzone`, `StepForm`, `ActionForm`, `ErrorState`, `EnvSnippet`, `DateRangePicker`, `DateTimePicker`, `FormField*`, `LanguageSwitch`, `PwaInstallCard` และตัวอย่างใน `design-system` — ทั้งหมด import `Button` จากไฟล์ shadcn ดิบ (ไม่มี tooltip) ตอนนี้ย้ายมาใช้ตัวที่ห่อ tooltip แล้ว 33 ไฟล์ (ปุ่มไอคอนอย่าง `<LinkButton aria-label="…"><KeyIcon/></LinkButton>` จึงได้ tooltip ด้วย)
|
|
30
|
+
- `ActionButton` / `LinkButton`: มี `tooltip` ของตัวเองอยู่แล้ว → ส่ง `tooltip={false}` ให้ `Button` ชั้นใน กันขึ้นสองชั้น (props `tooltip`/`tooltipSide` ของ `ActionButton` ยังเป็นแบบเดิมทุกประการ)
|
|
31
|
+
- `DataTableRowActions`: ปุ่ม "…" ใช้ `aria-label` จาก `UiText.actions` แทน `sr-only` อังกฤษ "Open menu"
|
|
32
|
+
- **กฎใหม่กันพลาดซ้ำ**: `pnpm check:button` (อยู่ใน `verify` และ CI) ห้ามไฟล์ในแพ็กเกจ import `Button` จาก `components/ui/button` ยกเว้นไฟล์ shadcn เองกับตัวห่อ
|
|
33
|
+
- **`AppShell` ปุ่มย่อ/ขยายเมนู**: ปุ่มในหัวเรื่อง (`SidebarTrigger`) ได้ tooltip เหมือนปุ่มไอคอนอื่น และแถบลาก (`SidebarRail`) เลิกใช้ `title` ของ shadcn ที่ทำ tooltip ของเบราว์เซอร์ซ้อนขึ้นมา — ทั้งคู่ใช้ป้ายภาษาไทยจาก `UiText.toggleSidebar` (คีย์ใหม่, ค่าเริ่มต้น "ย่อ/ขยายเมนู" / EN "Toggle sidebar") แทน "Toggle Sidebar" ที่ฝังอยู่ในไฟล์ shadcn (รายงานจาก Pi Files หลัง bump ^0.27.0 — แก้ที่ `app-shell.tsx` ไม่แตะ `components/ui/sidebar.tsx`)
|
|
34
|
+
|
|
5
35
|
## [0.27.0] — 2026-10-05
|
|
6
36
|
|
|
7
37
|
### Added
|
package/README.md
CHANGED
|
@@ -178,7 +178,8 @@ export default async function Layout({ children }) {
|
|
|
178
178
|
- ไม่มี `aria-label` แต่ใช้รูปแบบ shadcn `<span className="sr-only">ข้อความ</span>` ข้างไอคอน ก็ใช้ข้อความนั้นเป็น tooltip ให้เหมือนกัน
|
|
179
179
|
- ปุ่มที่ `disabled` ไม่ขึ้น tooltip (ปุ่มที่ปิดอยู่ไม่รับ pointer ตามสไตล์ของ shadcn) — ถ้าต้องอธิบายว่าทำไมกดไม่ได้ ให้ห่อ `SimpleTooltip` ที่ `<span>` ครอบปุ่มแทน
|
|
180
180
|
- มี `title="…"` อยู่เดิมให้เอาออก ไม่งั้นจะได้ tooltip ซ้อนกับของเบราว์เซอร์
|
|
181
|
-
- `<button>` ดิบ (ไม่ผ่าน `Button`) ไม่ได้ของนี้ — อีกเหตุผลที่ห้ามเขียนปุ่มเอง
|
|
181
|
+
- `<button>` ดิบ (ไม่ผ่าน `Button`) ไม่ได้ของนี้ — อีกเหตุผลที่ห้ามเขียนปุ่มเอง · และต้อง import `Button` **จาก `@suphark/ui`** เท่านั้น: `@suphark/ui/components/ui/button` คือไฟล์ shadcn ดิบที่ไม่มี tooltip
|
|
182
|
+
- ตรวจว่าปุ่มมี tooltip ไหม **อย่าดูจาก `data-slot`** — ปุ่มที่เป็น `render=`/`trigger` ของ Dialog/Popover/DropdownMenu จะได้ `data-slot` ของ trigger ชั้นนอก (`dialog-trigger` ฯลฯ) ทั้งที่มี tooltip อยู่ ให้เอาเมาส์ไปวางจริง หรือเช็ก `[data-slot=tooltip-content]` หลัง hover
|
|
182
183
|
- ปุ่มไอคอนของแพ็กเกจเอง (`RowActions`, `ActionMenu`, `SimpleDialog` ปุ่มปิด, `DataTable` แบ่งหน้า, `ThemeToggle`, `Notice`, `UploadList`, `TimePicker`, `MonthStepper`, `SecretReveal`, `CommandPalette` …) ได้ tooltip ไปพร้อมกันตั้งแต่ 0.27.0
|
|
183
184
|
|
|
184
185
|
### ยืนยันก่อนทำ — ห้ามใช้ `window.confirm`
|
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 เอง",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SimpleTooltip } from "@suphark/ui/components/shared/simple-tooltip";
|
|
3
4
|
import { SkipLink } from "@suphark/ui/components/shared/skip-link";
|
|
5
|
+
import { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
4
6
|
import {
|
|
5
7
|
InputGroup,
|
|
6
8
|
InputGroupAddon,
|
|
@@ -156,6 +158,8 @@ function SidebarShell({
|
|
|
156
158
|
mobileTabs,
|
|
157
159
|
children,
|
|
158
160
|
}: AppShellProps) {
|
|
161
|
+
const text = useUiText();
|
|
162
|
+
const toggleSidebarLabel = text.toggleSidebar ?? "ย่อ/ขยายเมนู";
|
|
159
163
|
const style = brandColor
|
|
160
164
|
? ({
|
|
161
165
|
"--brand": brandColor,
|
|
@@ -203,7 +207,9 @@ function SidebarShell({
|
|
|
203
207
|
)}
|
|
204
208
|
>
|
|
205
209
|
<div className="flex h-12 min-w-0 flex-1 items-center gap-2 md:h-full">
|
|
206
|
-
<
|
|
210
|
+
<SimpleTooltip content={toggleSidebarLabel}>
|
|
211
|
+
<SidebarTrigger className="-ml-1" aria-label={toggleSidebarLabel} />
|
|
212
|
+
</SimpleTooltip>
|
|
207
213
|
{breadcrumbs && (
|
|
208
214
|
<>
|
|
209
215
|
<Separator orientation="vertical" className="mr-2 hidden h-4 md:block" />
|
|
@@ -379,6 +385,8 @@ function ShellSidebar({
|
|
|
379
385
|
| "collapsible"
|
|
380
386
|
>) {
|
|
381
387
|
const { state, isMobile } = useSidebar();
|
|
388
|
+
const text = useUiText();
|
|
389
|
+
const railLabel = text.toggleSidebar ?? "ย่อ/ขยายเมนู";
|
|
382
390
|
const [query, setQuery] = useState("");
|
|
383
391
|
const collapsed = state === "collapsed" && !isMobile;
|
|
384
392
|
useEffect(() => {
|
|
@@ -436,7 +444,8 @@ function ShellSidebar({
|
|
|
436
444
|
)}
|
|
437
445
|
</SidebarContent>
|
|
438
446
|
{sidebarFooter && <SidebarFooter>{sidebarFooter}</SidebarFooter>}
|
|
439
|
-
{
|
|
447
|
+
{/* title ของ shadcn เป็นอังกฤษและทำ tooltip ของเบราว์เซอร์ซ้อนกับของเรา → ปิดด้วย title={undefined} */}
|
|
448
|
+
{collapsible !== "none" && <SidebarRail aria-label={railLabel} title={undefined} />}
|
|
440
449
|
</Sidebar>
|
|
441
450
|
);
|
|
442
451
|
}
|
|
@@ -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 { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
5
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
6
6
|
import {
|
|
7
7
|
DropdownMenu,
|
|
8
8
|
DropdownMenuContent,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
3
4
|
import { Initials } from "@suphark/ui/components/shared/initials";
|
|
4
5
|
import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
|
|
5
6
|
import { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
6
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
7
7
|
import {
|
|
8
8
|
DropdownMenu,
|
|
9
9
|
DropdownMenuContent,
|
|
@@ -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
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
3
4
|
import { DeleteButton } from "@suphark/ui/components/shared/button/delete-button";
|
|
5
|
+
import { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
4
6
|
import {
|
|
5
7
|
AlertDialog,
|
|
6
8
|
AlertDialogCancel,
|
|
@@ -10,7 +12,6 @@ import {
|
|
|
10
12
|
AlertDialogHeader,
|
|
11
13
|
AlertDialogTitle,
|
|
12
14
|
} from "@suphark/ui/components/ui/alert-dialog";
|
|
13
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
14
15
|
import {
|
|
15
16
|
DropdownMenu,
|
|
16
17
|
DropdownMenuContent,
|
|
@@ -74,6 +75,11 @@ export function DataTableRowActions<TData>({
|
|
|
74
75
|
nameKey = "name" as keyof TData,
|
|
75
76
|
idKey = "id" as keyof TData,
|
|
76
77
|
}: DataTableRowActionsProps<TData>) {
|
|
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);
|
|
77
83
|
const item = row.original;
|
|
78
84
|
const itemRecord = item as Record<string, unknown>;
|
|
79
85
|
const itemName = String(
|
|
@@ -94,8 +100,8 @@ export function DataTableRowActions<TData>({
|
|
|
94
100
|
});
|
|
95
101
|
|
|
96
102
|
toast.promise(promise, {
|
|
97
|
-
loading:
|
|
98
|
-
success:
|
|
103
|
+
loading: fill(text.table?.deleting ?? "กำลังลบ{resource} “{name}”…", itemName),
|
|
104
|
+
success: fill(text.table?.deleted ?? "ลบ{resource} “{name}” แล้ว", itemName),
|
|
99
105
|
error: (err: Error) => err.message,
|
|
100
106
|
});
|
|
101
107
|
});
|
|
@@ -105,10 +111,15 @@ export function DataTableRowActions<TData>({
|
|
|
105
111
|
<div className="flex justify-end">
|
|
106
112
|
<DropdownMenu>
|
|
107
113
|
<DropdownMenuTrigger
|
|
108
|
-
render={
|
|
114
|
+
render={
|
|
115
|
+
<Button
|
|
116
|
+
variant="ghost"
|
|
117
|
+
aria-label={text.actions}
|
|
118
|
+
className="flex h-8 w-8 p-0 data-popup-open:bg-muted"
|
|
119
|
+
/>
|
|
120
|
+
}
|
|
109
121
|
>
|
|
110
122
|
<Ellipsis className="h-4 w-4" />
|
|
111
|
-
<span className="sr-only">Open menu</span>
|
|
112
123
|
</DropdownMenuTrigger>
|
|
113
124
|
<DropdownMenuContent align="end" className="w-[160px]">
|
|
114
125
|
{children}
|
|
@@ -118,7 +129,7 @@ export function DataTableRowActions<TData>({
|
|
|
118
129
|
{EditDialog && (
|
|
119
130
|
<DropdownMenuItem onClick={() => setIsEditDialogOpen(true)}>
|
|
120
131
|
<Pen className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
121
|
-
|
|
132
|
+
{text.edit ?? "แก้ไข"}
|
|
122
133
|
</DropdownMenuItem>
|
|
123
134
|
)}
|
|
124
135
|
|
|
@@ -130,7 +141,7 @@ export function DataTableRowActions<TData>({
|
|
|
130
141
|
onClick={() => setIsDeleteDialogOpen(true)}
|
|
131
142
|
>
|
|
132
143
|
<Trash className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
133
|
-
|
|
144
|
+
{text.table?.delete ?? "ลบ"}
|
|
134
145
|
</DropdownMenuItem>
|
|
135
146
|
)}
|
|
136
147
|
</DropdownMenuGroup>
|
|
@@ -146,15 +157,16 @@ export function DataTableRowActions<TData>({
|
|
|
146
157
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
147
158
|
<AlertDialogContent>
|
|
148
159
|
<AlertDialogHeader>
|
|
149
|
-
<AlertDialogTitle>
|
|
160
|
+
<AlertDialogTitle>{text.table?.deleteTitle ?? "ยืนยันการลบ"}</AlertDialogTitle>
|
|
150
161
|
<AlertDialogDescription>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
{fill(
|
|
163
|
+
text.table?.deleteDescription ?? "ลบ{resource} “{name}” แล้วกู้คืนไม่ได้",
|
|
164
|
+
itemName,
|
|
165
|
+
)}
|
|
154
166
|
</AlertDialogDescription>
|
|
155
167
|
</AlertDialogHeader>
|
|
156
168
|
<AlertDialogFooter>
|
|
157
|
-
<AlertDialogCancel>
|
|
169
|
+
<AlertDialogCancel>{text.cancel}</AlertDialogCancel>
|
|
158
170
|
<DeleteButton onClick={handleDelete} isLoading={isPending} />
|
|
159
171
|
</AlertDialogFooter>
|
|
160
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
|
)}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
3
4
|
import { Initials } from "@suphark/ui/components/shared/initials";
|
|
4
5
|
import { StatusBadge } from "@suphark/ui/components/shared/status-badge";
|
|
5
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
6
6
|
import { Checkbox } from "@suphark/ui/components/ui/checkbox";
|
|
7
7
|
import {
|
|
8
8
|
InputGroup,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
3
4
|
import { FormattedNumber } from "@suphark/ui/components/shared/number/formatted-number";
|
|
4
5
|
import { PrimarySecondaryText } from "@suphark/ui/components/shared/text/primary-secondary-text";
|
|
5
6
|
import {
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
AttachmentTitle,
|
|
14
15
|
} from "@suphark/ui/components/ui/attachment";
|
|
15
16
|
import { Badge } from "@suphark/ui/components/ui/badge";
|
|
16
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
17
17
|
import {
|
|
18
18
|
Card,
|
|
19
19
|
CardContent,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
3
4
|
import { Alert, AlertDescription, AlertTitle } from "@suphark/ui/components/ui/alert";
|
|
4
5
|
import { Badge } from "@suphark/ui/components/ui/badge";
|
|
5
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
6
6
|
import {
|
|
7
7
|
Card,
|
|
8
8
|
CardContent,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
3
4
|
import { Combobox } from "@suphark/ui/components/shared/combobox";
|
|
4
5
|
import { MultiSelectCombobox } from "@suphark/ui/components/shared/multi-select-combobox";
|
|
5
6
|
import { Badge } from "@suphark/ui/components/ui/badge";
|
|
6
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
7
7
|
import {
|
|
8
8
|
Card,
|
|
9
9
|
CardContent,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
3
4
|
import {
|
|
4
5
|
AlertDialog,
|
|
5
6
|
AlertDialogAction,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
BreadcrumbPage,
|
|
21
22
|
BreadcrumbSeparator,
|
|
22
23
|
} from "@suphark/ui/components/ui/breadcrumb";
|
|
23
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
24
24
|
import {
|
|
25
25
|
Card,
|
|
26
26
|
CardContent,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PrintButton,
|
|
9
9
|
SaveButton,
|
|
10
10
|
} from "@suphark/ui/components/shared/button";
|
|
11
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
11
12
|
import {
|
|
12
13
|
Accordion,
|
|
13
14
|
AccordionContent,
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
} from "@suphark/ui/components/ui/accordion";
|
|
17
18
|
import { Avatar, AvatarFallback, AvatarImage } from "@suphark/ui/components/ui/avatar";
|
|
18
19
|
import { Badge } from "@suphark/ui/components/ui/badge";
|
|
19
|
-
import { Button } from "@suphark/ui/components/ui/button";
|
|
20
20
|
import { ButtonGroup, ButtonGroupSeparator } from "@suphark/ui/components/ui/button-group";
|
|
21
21
|
import {
|
|
22
22
|
Card,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Button } from "@suphark/ui/components/
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
4
4
|
import { cn } from "@suphark/ui/lib/utils";
|
|
5
5
|
import { Check, Download, MonitorSmartphone, Share, Smartphone } from "lucide-react";
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Button } from "@suphark/ui/components/
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
4
4
|
import { Card, CardContent } from "@suphark/ui/components/ui/card";
|
|
5
5
|
import { cn } from "@suphark/ui/lib/utils";
|
|
6
6
|
import { type ComponentProps, type ReactNode, startTransition, useActionState } from "react";
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Button } from "@suphark/ui/components/
|
|
3
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
4
4
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@suphark/ui/components/ui/tooltip";
|
|
5
5
|
import {
|
|
6
6
|
type CheckPermissionOptions,
|
|
@@ -11,8 +11,11 @@ import { cn } from "@suphark/ui/lib/utils";
|
|
|
11
11
|
import { Loader2 } from "lucide-react";
|
|
12
12
|
import * as React from "react";
|
|
13
13
|
|
|
14
|
-
// ดึง Type Props มาจาก Component โดยตรง
|
|
15
|
-
type ButtonProps =
|
|
14
|
+
// ดึง Type Props มาจาก Component โดยตรง — tooltip ของตัวนี้คุมเอง (ReactNode + side ของ Base UI)
|
|
15
|
+
type ButtonProps = Omit<
|
|
16
|
+
React.ComponentProps<typeof Button>,
|
|
17
|
+
"tooltip" | "tooltipSide" | "tooltipDelay"
|
|
18
|
+
>;
|
|
16
19
|
|
|
17
20
|
type TooltipContentProps = React.ComponentProps<typeof TooltipContent>;
|
|
18
21
|
|
|
@@ -100,6 +103,8 @@ export function ActionButton({
|
|
|
100
103
|
const buttonContent = (
|
|
101
104
|
<Button
|
|
102
105
|
className={cn(responsive && "max-sm:px-2", className)}
|
|
106
|
+
// มี tooltip ของตัวเองแล้ว ไม่ให้ Button สร้างซ้อนอีกชั้น (ไม่มี = ปล่อยให้ Button ใช้ aria-label เอง)
|
|
107
|
+
tooltip={finalTooltip ? false : undefined}
|
|
103
108
|
disabled={isLoading || disabled} // Disable if loading or explicitly disabled
|
|
104
109
|
onClick={onClick}
|
|
105
110
|
variant={variant}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// [FIX 1.1] นำเข้าเฉพาะ Button
|
|
4
|
-
import { Button } from "@suphark/ui/components/
|
|
4
|
+
import { Button } from "@suphark/ui/components/shared/button/button";
|
|
5
5
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@suphark/ui/components/ui/tooltip";
|
|
6
6
|
import {
|
|
7
7
|
type CheckPermissionOptions,
|
|
@@ -97,6 +97,8 @@ export function LinkButton({
|
|
|
97
97
|
const buttonContent = (
|
|
98
98
|
<Button
|
|
99
99
|
className={className}
|
|
100
|
+
// มี tooltip ของตัวเองแล้ว ไม่ให้ Button สร้างซ้อนอีกชั้น (ไม่มี = ปล่อยให้ Button ใช้ aria-label เอง)
|
|
101
|
+
tooltip={tooltip ? false : undefined}
|
|
100
102
|
disabled={isPending || disabled} // Disable if pending or explicitly disabled
|
|
101
103
|
onClick={handleClick}
|
|
102
104
|
{...props}
|
|
@@ -36,6 +36,13 @@ export function ButtonTooltipSuppress({ children }: { children: ReactNode }) {
|
|
|
36
36
|
return <NoAutoTooltip.Provider value={true}>{children}</NoAutoTooltip.Provider>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** `aria-label` ที่เผลอไปอยู่บน element ใน `render=` (เช่น `render={<Link aria-label="…"/>}`) */
|
|
40
|
+
function renderLabel(render: unknown): string | null {
|
|
41
|
+
if (!isValidElement(render)) return null;
|
|
42
|
+
const label = (render.props as { "aria-label"?: unknown })["aria-label"];
|
|
43
|
+
return typeof label === "string" && label.trim() !== "" ? label : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
/** ข้อความใน `<span className="sr-only">` (รูปแบบของ shadcn ที่ใช้แทน aria-label) */
|
|
40
47
|
function srOnlyText(children: ReactNode): string | null {
|
|
41
48
|
let found: string | null = null;
|
|
@@ -89,7 +96,7 @@ export function Button({ tooltip, tooltipSide = "top", tooltipDelay, ...props }:
|
|
|
89
96
|
const label =
|
|
90
97
|
typeof props["aria-label"] === "string" && props["aria-label"].trim() !== ""
|
|
91
98
|
? props["aria-label"]
|
|
92
|
-
: srOnlyText(props.children);
|
|
99
|
+
: (renderLabel(props.render) ?? srOnlyText(props.children));
|
|
93
100
|
const explicit = tooltip !== undefined && tooltip !== null && tooltip !== "";
|
|
94
101
|
const auto =
|
|
95
102
|
!suppressed &&
|
|
@@ -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 { useCopyToClipboard } from "@suphark/ui/hooks/use-copy-to-clipboard";
|
|
6
6
|
import { cn } from "@suphark/ui/lib/utils";
|
|
7
7
|
import { Check, Copy } from "lucide-react";
|