@suphark/ui 0.26.1 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +28 -0
  3. package/package.json +1 -1
  4. package/src/components/app-shell/app-shell.tsx +11 -2
  5. package/src/components/app-shell/command-palette.tsx +1 -1
  6. package/src/components/app-shell/organization-switcher.tsx +1 -1
  7. package/src/components/app-shell/user-menu.tsx +1 -1
  8. package/src/components/data-table/data-table-column-header.tsx +1 -1
  9. package/src/components/data-table/data-table-faceted-filter.tsx +1 -1
  10. package/src/components/data-table/data-table-pagination.tsx +7 -5
  11. package/src/components/data-table/data-table-row-actions.tsx +10 -3
  12. package/src/components/data-table/data-table-toolbar.tsx +1 -1
  13. package/src/components/data-table/data-table-view-option.tsx +1 -1
  14. package/src/components/design-system/sections/hero-specimen.tsx +1 -1
  15. package/src/components/design-system/sections/showcase-data-display.tsx +1 -1
  16. package/src/components/design-system/sections/showcase-feedback.tsx +1 -1
  17. package/src/components/design-system/sections/showcase-forms.tsx +1 -1
  18. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +1 -1
  19. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +1 -1
  20. package/src/components/design-system/sections/showcase-overview.tsx +1 -1
  21. package/src/components/design-system/sections/showcase-primitives.tsx +1 -1
  22. package/src/components/pwa/pwa-install-card.tsx +1 -1
  23. package/src/components/shared/action-form.tsx +1 -1
  24. package/src/components/shared/action-menu.tsx +1 -1
  25. package/src/components/shared/auth-parts.tsx +1 -1
  26. package/src/components/shared/button/base/action-button.tsx +8 -3
  27. package/src/components/shared/button/base/link-button.tsx +3 -1
  28. package/src/components/shared/button/button.tsx +117 -0
  29. package/src/components/shared/button/copy-button.tsx +1 -1
  30. package/src/components/shared/button/floating-save-button.tsx +1 -1
  31. package/src/components/shared/button/index.ts +6 -0
  32. package/src/components/shared/combobox.tsx +1 -1
  33. package/src/components/shared/date/date-range-picker.tsx +1 -1
  34. package/src/components/shared/dropzone.tsx +1 -1
  35. package/src/components/shared/env-snippet.tsx +1 -1
  36. package/src/components/shared/filter-bar.tsx +1 -1
  37. package/src/components/shared/form/date-time-picker.tsx +1 -1
  38. package/src/components/shared/form/form-field-combobox.tsx +1 -1
  39. package/src/components/shared/form/form-field-select.tsx +1 -1
  40. package/src/components/shared/form/time-picker.tsx +1 -1
  41. package/src/components/shared/form-dialog.tsx +1 -1
  42. package/src/components/shared/language/language-switch.tsx +1 -1
  43. package/src/components/shared/layout/error-state.tsx +2 -1
  44. package/src/components/shared/month-stepper.tsx +1 -1
  45. package/src/components/shared/multi-select-combobox.tsx +1 -1
  46. package/src/components/shared/notice.tsx +1 -1
  47. package/src/components/shared/resource-property-filter.tsx +1 -1
  48. package/src/components/shared/row-actions.tsx +1 -1
  49. package/src/components/shared/rule-builder.tsx +1 -1
  50. package/src/components/shared/secret-reveal.tsx +1 -1
  51. package/src/components/shared/selection-button.tsx +1 -1
  52. package/src/components/shared/simple-dialog.tsx +1 -1
  53. package/src/components/shared/simple-pagination.tsx +1 -1
  54. package/src/components/shared/simple-tooltip.tsx +11 -6
  55. package/src/components/shared/step-form.tsx +1 -1
  56. package/src/components/shared/theme/theme-switch.tsx +1 -1
  57. package/src/components/shared/theme/theme-toggle.tsx +5 -3
  58. package/src/components/shared/ui-text.tsx +29 -4
  59. package/src/components/shared/upload-list.tsx +1 -1
  60. package/src/index.ts +7 -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.27.1] — 2026-10-05
6
+
7
+ ### Added
8
+
9
+ - `Button`: ถ้า `aria-label` ไปอยู่บน element ใน `render=` (เช่น `render={<Link aria-label="…"/>}`) ก็ใช้เป็น tooltip ได้ (เดิมต้องอยู่บน `Button` เท่านั้น — รายงานจาก Pi Auth)
10
+ - เทสต์ชุดใหม่ `button-tooltip-triggers`: ยืนยันว่าปุ่มไอคอนที่ส่งเป็น `render=`/`trigger` ของ `DropdownMenuTrigger` / `PopoverTrigger` / `FormDialog` / `RowActions` ยังได้ tooltip จริง (เช็กจาก popup ที่ขึ้น ไม่ใช่ `data-slot` — `data-slot` ของปุ่มจะกลายเป็นของ trigger ชั้นนอกเสมอ ใช้ชี้วัดไม่ได้)
11
+
12
+ ### Fixed
13
+
14
+ - **ตัวห่อปุ่มของแพ็กเกจเองหล่น 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 ด้วย)
15
+ - `ActionButton` / `LinkButton`: มี `tooltip` ของตัวเองอยู่แล้ว → ส่ง `tooltip={false}` ให้ `Button` ชั้นใน กันขึ้นสองชั้น (props `tooltip`/`tooltipSide` ของ `ActionButton` ยังเป็นแบบเดิมทุกประการ)
16
+ - `DataTableRowActions`: ปุ่ม "…" ใช้ `aria-label` จาก `UiText.actions` แทน `sr-only` อังกฤษ "Open menu"
17
+ - **กฎใหม่กันพลาดซ้ำ**: `pnpm check:button` (อยู่ใน `verify` และ CI) ห้ามไฟล์ในแพ็กเกจ import `Button` จาก `components/ui/button` ยกเว้นไฟล์ shadcn เองกับตัวห่อ
18
+ - **`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`)
19
+
20
+ ## [0.27.0] — 2026-10-05
21
+
22
+ ### Added
23
+
24
+ - **`Button`: tooltip อัตโนมัติสำหรับปุ่มไอคอน** — มาตรฐานใหม่ของทุกโครงการ (เจ้าของสั่ง 2026-10-05): ไอคอนที่ไม่มีข้อความกำกับต้องบอกได้ว่าคืออะไรเมื่อวางเมาส์ ถ้า `Button` มี `aria-label` และ children ไม่มีข้อความ (ไอคอน/`sr-only` ไม่นับ) จะห่อ `Tooltip` ให้เองจาก `aria-label` — ทำที่ `Button` ชั้นในสุดที่เดียว **call site ทุกที่ของทุกแอปได้ทันทีโดยไม่ต้องแก้โค้ด** รวมถึงปุ่มที่ส่งเข้า `render=`/`trigger` ของ `FormDialog`/`SimpleDialog`/`DropdownMenu`/`RowActions` (เคสที่ห่อ `SimpleTooltip` เองไม่ได้เพราะ trigger ส่ง ref/props ให้)
25
+ - props ใหม่: `tooltip` (ทับข้อความ, `false` = ปิด), `tooltipSide`, `tooltipDelay` — ไม่ต้องมี `TooltipProvider` ใน tree (ใส่ได้ถ้าอยากคุม delay ร่วมกัน)
26
+ - `SimpleTooltip` ปิด tooltip อัตโนมัติของ `Button` ที่ห่ออยู่ให้แล้ว (ไม่ขึ้นสองชั้น) — แอปที่ห่อ tooltip ด้วยกลไกอื่นใช้ `ButtonTooltipSuppress` (export ใหม่) หรือ `tooltip={false}`
27
+ - barrel export `Button` จาก `components/shared/button/button.tsx` (ครอบ `components/ui/button.tsx` ที่ยังเป็นไฟล์ shadcn ดิบ ไม่ถูกแก้มือ); `buttonVariants` เหมือนเดิม; props อื่นและหน้าตาไม่เปลี่ยน
28
+ - `DataTablePagination`: ปุ่มเลื่อนหน้าทั้ง 4 ใช้ `aria-label` จาก `UiText` (เดิมเป็นข้อความอังกฤษตายตัวใน `sr-only`); `ThemeToggle` ใช้ `aria-label` จาก `UiText.theme.label` แทน `title` (เลิก tooltip ซ้อนของเบราว์เซอร์)
29
+ - README หัวข้อ "ปุ่มไอคอน — tooltip มาให้เอง (มาตรฐานทุกโครงการ)" + แถวใหม่ใน `docs/agent-rules.md`
30
+ - ใช้ข้อความใน `<span className="sr-only">` (รูปแบบของ shadcn) เป็น tooltip ได้เมื่อไม่มี `aria-label`
31
+ - **ปุ่มไอคอนในแพ็กเกจเองได้ tooltip ครบ**: `RowActions`, `ActionMenu`, `SimpleDialog`/`SimpleSheet` (ปุ่มปิด), `SimplePagination`, `DataTablePagination`, `DataTableToolbar`, `CommandPalette`, `ThemeToggle`/`ThemeSwitch`, `Notice`, `UploadList`, `SecretReveal`, `TimePicker`, `MonthStepper`, `RuleBuilder`, `ResourcePropertyFilter`, `AuthParts` (17 ไฟล์เปลี่ยนมา import `Button` ตัวที่มี tooltip)
32
+ - `UiText.pagination` เพิ่ม `first` / `last` (หน้าแรก/หน้าสุดท้าย)
33
+ - showcase: หน้า `/components/button` เพิ่มแถวปุ่มไอคอน (auto / `tooltip="…"` / `tooltip={false}` / trigger ของ dialog / ห่อ `SimpleTooltip`)
34
+
5
35
  ## [0.26.1] — 2026-10-03
6
36
 
7
37
  แก้ตามผล audit 2026-10-03 (`docs/reports/audit-2026-10-03.md`) — ไม่เปลี่ยน props
package/README.md CHANGED
@@ -154,6 +154,34 @@ export default async function Layout({ children }) {
154
154
  <div style={{ "--brand": tenant.color, "--brand-foreground": "oklch(0.985 0 0)" } as React.CSSProperties}>…</div>
155
155
  ```
156
156
 
157
+ ### ปุ่มไอคอน — tooltip มาให้เอง (มาตรฐานทุกโครงการ)
158
+
159
+ ไอคอนที่ไม่มีข้อความกำกับ ต้องบอกได้ว่าคืออะไรเมื่อวางเมาส์ — `Button` ของแพ็กเกจ **ห่อ `Tooltip` ให้เองเมื่อมี `aria-label` และ children ไม่มีข้อความ** (ไอคอนหรือ `<span className="sr-only">` ไม่นับเป็นข้อความ) ทุก call site จึงได้ทันทีโดยไม่ต้องแก้อะไร รวมถึงปุ่มที่เป็น trigger ของ dialog/menu เพราะปุ่มอยู่ชั้นในสุดของ `render=`
160
+
161
+ ```tsx
162
+ <Button variant="ghost" size="icon-sm" aria-label="ดู client secret"><KeySquareIcon /></Button>
163
+ // → hover/focus แล้วขึ้น "ดู client secret" โดยไม่ต้องห่อ SimpleTooltip
164
+
165
+ <FormDialog trigger={<Button size="icon-sm" aria-label="แก้ไข"><PencilIcon /></Button>} … />
166
+ // → ได้ tooltip ด้วย (ไม่ต้องมี prop ใหม่ที่ FormDialog/RowActions)
167
+ ```
168
+
169
+ | ต้องการ | เขียน |
170
+ | --- | --- |
171
+ | ข้อความอื่นที่ไม่ใช่ `aria-label` (หรือปุ่มที่มีข้อความแล้วแต่อยากอธิบายเพิ่ม) | `tooltip="ตรวจสอบอีกครั้ง"` |
172
+ | ปิด (เช่นปุ่มในเมนูที่มีข้อความอยู่แล้ว) | `tooltip={false}` |
173
+ | เปลี่ยนด้าน / หน่วงเวลา | `tooltipSide="bottom"`, `tooltipDelay={0}` |
174
+
175
+ - **ไม่ต้องมี `TooltipProvider`** — Base UI ไม่บังคับ; ใส่ที่ root layout ได้ถ้าอยากคุม delay ร่วมกันทุก tooltip (ค่าเริ่มต้นของ Base UI = 600ms, `TooltipProvider` ของแพ็กเกจตั้ง 0)
176
+ - ปุ่มที่ถูกห่อ `SimpleTooltip` อยู่แล้ว **ไม่ขึ้นสองชั้น** (`SimpleTooltip` ปิดตัวอัตโนมัติให้ด้วย `ButtonTooltipSuppress`) — ถ้าแอปห่อ tooltip ด้วยกลไกอื่นเอง ครอบ `<ButtonTooltipSuppress>` หรือใส่ `tooltip={false}`
177
+ - จอสัมผัสไม่มี hover — `aria-label` ยังทำหน้าที่เดิมให้ screen reader; ข้อความที่ "ต้องอ่านได้ทุกคน" อย่าเก็บไว้ใน tooltip เท่านั้น
178
+ - ไม่มี `aria-label` แต่ใช้รูปแบบ shadcn `<span className="sr-only">ข้อความ</span>` ข้างไอคอน ก็ใช้ข้อความนั้นเป็น tooltip ให้เหมือนกัน
179
+ - ปุ่มที่ `disabled` ไม่ขึ้น tooltip (ปุ่มที่ปิดอยู่ไม่รับ pointer ตามสไตล์ของ shadcn) — ถ้าต้องอธิบายว่าทำไมกดไม่ได้ ให้ห่อ `SimpleTooltip` ที่ `<span>` ครอบปุ่มแทน
180
+ - มี `title="…"` อยู่เดิมให้เอาออก ไม่งั้นจะได้ tooltip ซ้อนกับของเบราว์เซอร์
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
183
+ - ปุ่มไอคอนของแพ็กเกจเอง (`RowActions`, `ActionMenu`, `SimpleDialog` ปุ่มปิด, `DataTable` แบ่งหน้า, `ThemeToggle`, `Notice`, `UploadList`, `TimePicker`, `MonthStepper`, `SecretReveal`, `CommandPalette` …) ได้ tooltip ไปพร้อมกันตั้งแต่ 0.27.0
184
+
157
185
  ### ยืนยันก่อนทำ — ห้ามใช้ `window.confirm`
158
186
 
159
187
  webview/automation บางตัวปิด `window.confirm` ทิ้ง (กดแล้วไม่เกิดอะไร) และไม่เข้า design system — ใช้ 3 ตัวนี้แทน
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/ui",
3
- "version": "0.26.1",
3
+ "version": "0.27.1",
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
- <SidebarTrigger className="-ml-1" />
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
- {collapsible !== "none" && <SidebarRail />}
447
+ {/* title ของ shadcn เป็นอังกฤษและทำ tooltip ของเบราว์เซอร์ซ้อนกับของเรา → ปิดด้วย title={undefined} */}
448
+ {collapsible !== "none" && <SidebarRail aria-label={railLabel} title={undefined} />}
440
449
  </Sidebar>
441
450
  );
442
451
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { useUiText } from "@suphark/ui/components/shared/ui-text";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Command,
7
7
  CommandDialog,
@@ -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,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Badge } from "@suphark/ui/components/ui/badge";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Command,
7
7
  CommandEmpty,
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
+ import { useUiText } from "@suphark/ui/components/shared/ui-text";
4
5
  import {
5
6
  Select,
6
7
  SelectContent,
@@ -23,6 +24,7 @@ export function DataTablePagination<TData>({
23
24
  pageSizeOptions = [10, 20, 30, 40, 50],
24
25
  onPageSizeChange,
25
26
  }: DataTablePaginationProps<TData>) {
27
+ const text = useUiText();
26
28
  const isRowSelectionEnabled = table.options.enableRowSelection !== false;
27
29
 
28
30
  return (
@@ -71,40 +73,40 @@ export function DataTablePagination<TData>({
71
73
  variant="outline"
72
74
  size="icon"
73
75
  className="hidden size-8 lg:flex"
76
+ aria-label={text.pagination?.first ?? "หน้าแรก"}
74
77
  onClick={() => table.setPageIndex(0)}
75
78
  disabled={!table.getCanPreviousPage()}
76
79
  >
77
- <span className="sr-only">Go to first page</span>
78
80
  <ChevronsLeft />
79
81
  </Button>
80
82
  <Button
81
83
  variant="outline"
82
84
  size="icon"
83
85
  className="size-8"
86
+ aria-label={text.pagination?.previous ?? "ก่อนหน้า"}
84
87
  onClick={() => table.previousPage()}
85
88
  disabled={!table.getCanPreviousPage()}
86
89
  >
87
- <span className="sr-only">Go to previous page</span>
88
90
  <ChevronLeft />
89
91
  </Button>
90
92
  <Button
91
93
  variant="outline"
92
94
  size="icon"
93
95
  className="size-8"
96
+ aria-label={text.pagination?.next ?? "ถัดไป"}
94
97
  onClick={() => table.nextPage()}
95
98
  disabled={!table.getCanNextPage()}
96
99
  >
97
- <span className="sr-only">Go to next page</span>
98
100
  <ChevronRight />
99
101
  </Button>
100
102
  <Button
101
103
  variant="outline"
102
104
  size="icon"
103
105
  className="hidden size-8 lg:flex"
106
+ aria-label={text.pagination?.last ?? "หน้าสุดท้าย"}
104
107
  onClick={() => table.setPageIndex(table.getPageCount() - 1)}
105
108
  disabled={!table.getCanNextPage()}
106
109
  >
107
- <span className="sr-only">Go to last page</span>
108
110
  <ChevronsRight />
109
111
  </Button>
110
112
  </div>
@@ -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,7 @@ 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();
77
79
  const item = row.original;
78
80
  const itemRecord = item as Record<string, unknown>;
79
81
  const itemName = String(
@@ -105,10 +107,15 @@ export function DataTableRowActions<TData>({
105
107
  <div className="flex justify-end">
106
108
  <DropdownMenu>
107
109
  <DropdownMenuTrigger
108
- render={<Button variant="ghost" className="flex h-8 w-8 p-0 data-popup-open:bg-muted" />}
110
+ render={
111
+ <Button
112
+ variant="ghost"
113
+ aria-label={text.actions}
114
+ className="flex h-8 w-8 p-0 data-popup-open:bg-muted"
115
+ />
116
+ }
109
117
  >
110
118
  <Ellipsis className="h-4 w-4" />
111
- <span className="sr-only">Open menu</span>
112
119
  </DropdownMenuTrigger>
113
120
  <DropdownMenuContent align="end" className="w-[160px]">
114
121
  {children}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Badge } from "@suphark/ui/components/ui/badge";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  InputGroup,
7
7
  InputGroupAddon,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  DropdownMenu,
6
6
  DropdownMenuCheckboxItem,
@@ -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,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Badge } from "@suphark/ui/components/ui/badge";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Card,
7
7
  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,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Badge } from "@suphark/ui/components/ui/badge";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Card,
7
7
  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/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { cn } from "@suphark/ui/lib/utils";
5
5
  import { Check, 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/ui/button";
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";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  InputGroup,
6
6
  InputGroupAddon,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { 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 = React.ComponentProps<typeof Button>;
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/ui/button";
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}
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { Button as ButtonBase } from "@suphark/ui/components/ui/button";
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@suphark/ui/components/ui/tooltip";
5
+ import {
6
+ Children,
7
+ type ComponentProps,
8
+ createContext,
9
+ Fragment,
10
+ isValidElement,
11
+ type ReactNode,
12
+ useContext,
13
+ } from "react";
14
+
15
+ export type ButtonTooltipSide = "top" | "bottom" | "left" | "right";
16
+
17
+ export interface ButtonProps extends ComponentProps<typeof ButtonBase> {
18
+ /**
19
+ * ข้อความ tooltip — ไม่ใส่ = ใช้ `aria-label` (หรือ `<span className="sr-only">` แบบ shadcn)
20
+ * ให้เองเมื่อปุ่ม "มีแต่ไอคอน" (ไม่มี children ที่เป็นข้อความ)
21
+ * ใส่ข้อความ = ทับค่าอัตโนมัติ (ใช้กับปุ่มที่มีข้อความแล้วแต่ยังอยากอธิบายเพิ่ม) · `false` = ปิด
22
+ */
23
+ tooltip?: ReactNode | false;
24
+ tooltipSide?: ButtonTooltipSide;
25
+ /** หน่วงก่อนโชว์ (ms) — ไม่ใส่ = ตาม `TooltipProvider` ของแอป หรือ 600 ตามค่าของ Base UI */
26
+ tooltipDelay?: number;
27
+ }
28
+
29
+ const NoAutoTooltip = createContext(false);
30
+
31
+ /**
32
+ * ปิด tooltip อัตโนมัติของ `Button` ทั้งซับทรี — `SimpleTooltip` ใส่ให้แล้ว
33
+ * (ปุ่มที่ถูกห่อ tooltip อยู่แล้วจะไม่ขึ้นสองชั้น) ใช้เองได้เมื่อแอปห่อ tooltip ด้วยกลไกอื่น
34
+ */
35
+ export function ButtonTooltipSuppress({ children }: { children: ReactNode }) {
36
+ return <NoAutoTooltip.Provider value={true}>{children}</NoAutoTooltip.Provider>;
37
+ }
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
+
46
+ /** ข้อความใน `<span className="sr-only">` (รูปแบบของ shadcn ที่ใช้แทน aria-label) */
47
+ function srOnlyText(children: ReactNode): string | null {
48
+ let found: string | null = null;
49
+ Children.forEach(children, (child) => {
50
+ if (found || !isValidElement(child)) return;
51
+ const props = child.props as { className?: unknown; children?: ReactNode };
52
+ if (child.type === Fragment) {
53
+ found = srOnlyText(props.children);
54
+ return;
55
+ }
56
+ if (typeof props.className !== "string" || !props.className.split(/\s+/).includes("sr-only"))
57
+ return;
58
+ if (typeof props.children === "string" && props.children.trim() !== "")
59
+ found = props.children.trim();
60
+ });
61
+ return found;
62
+ }
63
+
64
+ /** มี children ที่เป็นข้อความจริง ๆ ไหม (ไอคอน/`<span className="sr-only">` ไม่นับ) */
65
+ function hasTextChild(children: ReactNode): boolean {
66
+ let found = false;
67
+ Children.forEach(children, (child) => {
68
+ if (found) return;
69
+ if (typeof child === "string") {
70
+ if (child.trim() !== "") found = true;
71
+ return;
72
+ }
73
+ if (typeof child === "number") {
74
+ found = true;
75
+ return;
76
+ }
77
+ // <>{icon} ข้อความ</> — Children.forEach ไม่ไล่เข้า fragment ให้
78
+ if (isValidElement(child) && child.type === Fragment) {
79
+ const inner = (child.props as { children?: ReactNode }).children;
80
+ if (hasTextChild(inner)) found = true;
81
+ }
82
+ });
83
+ return found;
84
+ }
85
+
86
+ /**
87
+ * ปุ่มของแพ็กเกจ = `Button` ของ shadcn + **tooltip อัตโนมัติสำหรับปุ่มไอคอน**
88
+ * มาตรฐานของทุกโครงการ: ไอคอนที่ไม่มีข้อความกำกับ ต้องบอกได้ว่าคืออะไรเมื่อวางเมาส์
89
+ * → ถ้ามี `aria-label` และ children ไม่มีข้อความ จะห่อ `Tooltip` ให้เอง (ทำที่นี่ที่เดียว
90
+ * call site ทุกที่จึงได้ทันที รวมถึงปุ่มที่เป็น trigger ของ dialog/menu เพราะปุ่มอยู่ชั้นในสุด)
91
+ * ปิดเป็นรายปุ่มด้วย `tooltip={false}`; ทับข้อความด้วย `tooltip="…"`
92
+ * ไฟล์ shadcn ดิบอยู่ที่ `components/ui/button.tsx` (ห้ามแก้มือ) — barrel export ตัวนี้
93
+ */
94
+ export function Button({ tooltip, tooltipSide = "top", tooltipDelay, ...props }: ButtonProps) {
95
+ const suppressed = useContext(NoAutoTooltip);
96
+ const label =
97
+ typeof props["aria-label"] === "string" && props["aria-label"].trim() !== ""
98
+ ? props["aria-label"]
99
+ : (renderLabel(props.render) ?? srOnlyText(props.children));
100
+ const explicit = tooltip !== undefined && tooltip !== null && tooltip !== "";
101
+ const auto =
102
+ !suppressed &&
103
+ tooltip === undefined &&
104
+ typeof label === "string" &&
105
+ !hasTextChild(props.children);
106
+ const content = tooltip === false ? null : explicit ? tooltip : auto ? label : null;
107
+
108
+ if (content === null || content === undefined) return <ButtonBase {...props} />;
109
+ return (
110
+ <Tooltip>
111
+ <TooltipTrigger delay={tooltipDelay} render={<ButtonBase {...props} />} />
112
+ <TooltipContent side={tooltipSide} className="max-w-xs text-pretty">
113
+ {content}
114
+ </TooltipContent>
115
+ </Tooltip>
116
+ );
117
+ }
@@ -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";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { cn } from "@suphark/ui/lib/utils";
5
5
  import { Check, Loader2, Save, X } from "lucide-react";
6
6
  import { useEffect, useRef, useState } from "react";
@@ -4,6 +4,12 @@
4
4
  */
5
5
  export { AddNewButton } from "./add-new-button";
6
6
  export { ActionButton, type ActionButtonProps } from "./base/action-button";
7
+ export {
8
+ Button,
9
+ type ButtonProps,
10
+ type ButtonTooltipSide,
11
+ ButtonTooltipSuppress,
12
+ } from "./button";
7
13
  export { CancelButton } from "./cancel-button";
8
14
  export { ConfirmButton } from "./confirm-button";
9
15
  export { CopyButton, type CopyButtonProps } from "./copy-button";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Command,
7
7
  CommandEmpty,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Badge } from "@suphark/ui/components/ui/badge";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import { Calendar } from "@suphark/ui/components/ui/calendar";
6
6
  import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
7
7
  import { Separator } from "@suphark/ui/components/ui/separator";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { formatBytes } from "@suphark/ui/lib/formatters";
5
5
  import { cn } from "@suphark/ui/lib/utils";
6
6
  import { UploadCloud } from "lucide-react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { copyText, selectContents } from "@suphark/ui/lib/copy-text";
5
5
  import { CheckIcon, CopyIcon } from "lucide-react";
6
6
  import { useRef, useState } from "react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { NativeSelect, NativeSelectOption } from "@suphark/ui/components/ui/native-select";
5
5
  import { cn } from "@suphark/ui/lib/utils";
6
6
  import { XIcon } from "lucide-react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { Calendar } from "@suphark/ui/components/ui/calendar";
5
5
  import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
6
6
  import { useMounted } from "@suphark/ui/hooks/use-mounted";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Command,
7
7
  CommandEmpty,
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { MultiSelectCombobox } from "@suphark/ui/components/shared/multi-select-combobox";
4
5
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
5
- import { Button } from "@suphark/ui/components/ui/button";
6
6
  import {
7
7
  Command,
8
8
  CommandEmpty,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  InputGroup,
6
6
  InputGroupAddon,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SecretReveal } from "@suphark/ui/components/shared/secret-reveal";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  Dialog,
7
7
  DialogContent,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { useUiText } from "@suphark/ui/components/shared/ui-text";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import {
6
6
  DropdownMenu,
7
7
  DropdownMenuContent,
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { Button, buttonVariants } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
+ import { buttonVariants } from "@suphark/ui/components/ui/button";
4
5
  import { Card, CardContent, CardHeader, CardTitle } from "@suphark/ui/components/ui/card";
5
6
  import { cn } from "@suphark/ui/lib/utils";
6
7
  import { AlertTriangle, Home, LogIn, RotateCcw } from "lucide-react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  compareMonth,
6
6
  currentMonthKey,
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { SwitcherChevron } from "@suphark/ui/components/shared/switcher-chevron";
4
5
  import { Badge } from "@suphark/ui/components/ui/badge";
5
- import { Button } from "@suphark/ui/components/ui/button";
6
6
  import {
7
7
  Command,
8
8
  CommandEmpty,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Alert, AlertAction, AlertDescription, AlertTitle } from "@suphark/ui/components/ui/alert";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import { cn } from "@suphark/ui/lib/utils";
6
6
  import { CircleAlertIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon, XIcon } from "lucide-react";
7
7
  import { type ComponentProps, type ReactNode, useState } from "react";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
3
4
  import { Badge } from "@suphark/ui/components/ui/badge";
4
- import { Button } from "@suphark/ui/components/ui/button";
5
5
  import { Checkbox } from "@suphark/ui/components/ui/checkbox";
6
6
  import { Empty, EmptyHeader, EmptyTitle } from "@suphark/ui/components/ui/empty";
7
7
  import { Field, FieldGroup, FieldLabel } from "@suphark/ui/components/ui/field";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  NativeSelect,
6
6
  NativeSelectOptGroup,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { copyText } from "@suphark/ui/lib/copy-text";
5
5
  import { CheckIcon, CopyIcon } from "lucide-react";
6
6
  import { useState } from "react";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { cn } from "@suphark/ui/lib/utils";
5
5
  import type { ComponentProps } from "react";
6
6
 
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import {
5
5
  Dialog,
6
6
  DialogClose,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { cn } from "@suphark/ui/lib/utils";
5
5
  import { ChevronLeft, ChevronRight } from "lucide-react";
6
6
  import { useUiText } from "./ui-text";
@@ -11,12 +11,14 @@ import {
11
11
  import { Tooltip, TooltipContent, TooltipTrigger } from "@suphark/ui/components/ui/tooltip";
12
12
  import { cn } from "@suphark/ui/lib/utils";
13
13
  import type { ReactElement, ReactNode } from "react";
14
+ import { ButtonTooltipSuppress } from "./button/button";
14
15
 
15
16
  type Side = "top" | "bottom" | "left" | "right";
16
17
 
17
18
  /**
18
19
  * tooltip สำเร็จรูป: ห่อ element เดียว + ข้อความ (แทน Tooltip/TooltipTrigger/TooltipContent 3 ชิ้น)
19
20
  * `content` ว่าง = ไม่มี tooltip (ไม่ต้องเขียนเงื่อนไขเอง) · ปุ่มไอคอนอย่างเดียวยังต้องมี aria-label ของตัวเอง
21
+ * ปุ่มไอคอน (`Button` + `aria-label`) **ไม่ต้องห่อตัวนี้แล้ว** ตั้งแต่ 0.27.0 — ห่อได้ถ้าอยากใช้ข้อความอื่น
20
22
  * คำอธิบายยาวหลังไอคอน (i) ใช้ `InfoHint`
21
23
  *
22
24
  * <SimpleTooltip content="คัดลอกลิงก์"><Button size="icon" aria-label="คัดลอกลิงก์"><Copy /></Button></SimpleTooltip>
@@ -34,13 +36,16 @@ export function SimpleTooltip({
34
36
  className?: string;
35
37
  }) {
36
38
  if (content === null || content === undefined || content === "") return children;
39
+ // ลูกที่เป็น `Button` ของแพ็กเกจจะไม่สร้าง tooltip อัตโนมัติซ้อนขึ้นมาอีกชั้น
37
40
  return (
38
- <Tooltip>
39
- <TooltipTrigger render={children} />
40
- <TooltipContent side={side} className={cn("max-w-xs text-pretty", className)}>
41
- {content}
42
- </TooltipContent>
43
- </Tooltip>
41
+ <ButtonTooltipSuppress>
42
+ <Tooltip>
43
+ <TooltipTrigger render={children} />
44
+ <TooltipContent side={side} className={cn("max-w-xs text-pretty", className)}>
45
+ {content}
46
+ </TooltipContent>
47
+ </Tooltip>
48
+ </ButtonTooltipSuppress>
44
49
  );
45
50
  }
46
51
 
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { Spinner } from "@suphark/ui/components/ui/spinner";
5
5
  import { errorMessage, failedResult, isNavigationError } from "@suphark/ui/lib/action-result";
6
6
  import { cn } from "@suphark/ui/lib/utils";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { MoonIcon, SunIcon } from "lucide-react";
5
5
  import { useTheme } from "next-themes";
6
6
  import { useEffect, useState } from "react";
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
+ import { useUiText } from "@suphark/ui/components/shared/ui-text";
4
5
  import {
5
6
  DropdownMenu,
6
7
  DropdownMenuContent,
@@ -30,6 +31,8 @@ export function ThemeToggle({ persistedTheme, onThemeChange }: ThemeToggleProps)
30
31
  lastPersisted.current = persistedTheme;
31
32
  }, [persistedTheme]);
32
33
 
34
+ const text = useUiText();
35
+
33
36
  const handleThemeChange = async (newTheme: ThemeValue) => {
34
37
  setTheme(newTheme);
35
38
  await onThemeChange?.(newTheme);
@@ -43,13 +46,12 @@ export function ThemeToggle({ persistedTheme, onThemeChange }: ThemeToggleProps)
43
46
  variant="ghost"
44
47
  size="icon"
45
48
  className="relative h-9 w-9 rounded-md hover:bg-accent hover:text-accent-foreground"
46
- title="Change theme"
49
+ aria-label={text.theme?.label ?? "Change theme"}
47
50
  />
48
51
  }
49
52
  >
50
53
  <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
51
54
  <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
52
- <span className="sr-only">Change theme</span>
53
55
  </DropdownMenuTrigger>
54
56
  <DropdownMenuContent align="end" className="w-36">
55
57
  <DropdownMenuItem
@@ -43,6 +43,8 @@ export interface UiText {
43
43
  switcher?: Partial<{ choose: string; organization: string }>;
44
44
  /** UserMenu: aria-label ปุ่มเปิดเมนู (optional — "เมนูผู้ใช้") */
45
45
  userMenu?: string;
46
+ /** AppShell: ปุ่มย่อ/ขยายเมนูข้าง (optional — "ย่อ/ขยายเมนู") */
47
+ toggleSidebar?: string;
46
48
  /** UserMenu: ปุ่มออกจากระบบ (optional — "ออกจากระบบ") */
47
49
  signOut?: string;
48
50
  /** ThemeMenuItems: หัวกลุ่ม + ตัวเลือก (optional) */
@@ -81,8 +83,15 @@ export interface UiText {
81
83
  }>;
82
84
  /** ChatThread (optional) */
83
85
  chat?: Partial<{ placeholder: string; send: string; typing: string; scrollToLatest: string }>;
84
- /** SimplePagination (optional) */
85
- pagination?: Partial<{ previous: string; next: string; label: string; page: string }>;
86
+ /** SimplePagination / DataTablePagination (optional) */
87
+ pagination?: Partial<{
88
+ previous: string;
89
+ next: string;
90
+ first: string;
91
+ last: string;
92
+ label: string;
93
+ page: string;
94
+ }>;
86
95
  /** TableSkeleton/ListSkeleton/CardSkeleton (optional) — ข้อความสำหรับ screen reader */
87
96
  loading?: string;
88
97
  /** สถานะออนไลน์ของ UserIdentity/Initials (optional) */
@@ -141,6 +150,7 @@ export const UI_TEXT_TH: UiText = {
141
150
  },
142
151
  switcher: { choose: "เลือกองค์กร", organization: "องค์กร" },
143
152
  userMenu: "เมนูผู้ใช้",
153
+ toggleSidebar: "ย่อ/ขยายเมนู",
144
154
  signOut: "ออกจากระบบ",
145
155
  theme: { label: "ธีม", light: "สว่าง", dark: "มืด", system: "ตามระบบ" },
146
156
  sectionNav: { tabs: "แท็บย่อย" },
@@ -174,7 +184,14 @@ export const UI_TEXT_TH: UiText = {
174
184
  typing: "กำลังพิมพ์…",
175
185
  scrollToLatest: "ไปข้อความล่าสุด",
176
186
  },
177
- pagination: { previous: "ก่อนหน้า", next: "ถัดไป", label: "แบ่งหน้า", page: "หน้า" },
187
+ pagination: {
188
+ previous: "ก่อนหน้า",
189
+ next: "ถัดไป",
190
+ first: "หน้าแรก",
191
+ last: "หน้าสุดท้าย",
192
+ label: "แบ่งหน้า",
193
+ page: "หน้า",
194
+ },
178
195
  loading: "กำลังโหลด…",
179
196
  presence: { online: "ออนไลน์", away: "ไม่อยู่", busy: "ไม่ว่าง", offline: "ออฟไลน์" },
180
197
  characterCount: "พิมพ์แล้ว {count} จาก {max} ตัวอักษร",
@@ -231,6 +248,7 @@ export const UI_TEXT_EN: UiText = {
231
248
  },
232
249
  switcher: { choose: "Choose organization", organization: "Organization" },
233
250
  userMenu: "User menu",
251
+ toggleSidebar: "Toggle sidebar",
234
252
  signOut: "Sign out",
235
253
  theme: { label: "Theme", light: "Light", dark: "Dark", system: "System" },
236
254
  sectionNav: { tabs: "Section tabs" },
@@ -264,7 +282,14 @@ export const UI_TEXT_EN: UiText = {
264
282
  typing: "Typing…",
265
283
  scrollToLatest: "Jump to latest",
266
284
  },
267
- pagination: { previous: "Previous", next: "Next", label: "Pagination", page: "Page" },
285
+ pagination: {
286
+ previous: "Previous",
287
+ next: "Next",
288
+ first: "First page",
289
+ last: "Last page",
290
+ label: "Pagination",
291
+ page: "Page",
292
+ },
268
293
  loading: "Loading…",
269
294
  presence: { online: "Online", away: "Away", busy: "Busy", offline: "Offline" },
270
295
  characterCount: "{count} of {max} characters",
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@suphark/ui/components/ui/button";
3
+ import { Button } from "@suphark/ui/components/shared/button/button";
4
4
  import { formatBytes } from "@suphark/ui/lib/formatters";
5
5
  import { cn } from "@suphark/ui/lib/utils";
6
6
  import { AlertCircle, Check, ChevronDown, RotateCcw, X } from "lucide-react";
package/src/index.ts CHANGED
@@ -40,6 +40,12 @@ export {
40
40
  SaveButton,
41
41
  SubmitButton,
42
42
  } from "./components/shared/button";
43
+ export {
44
+ Button,
45
+ type ButtonProps,
46
+ type ButtonTooltipSide,
47
+ ButtonTooltipSuppress,
48
+ } from "./components/shared/button/button";
43
49
  export {
44
50
  type CalendarDayContext,
45
51
  type CalendarDayState,
@@ -296,7 +302,7 @@ export {
296
302
  BreadcrumbSeparator,
297
303
  } from "./components/ui/breadcrumb";
298
304
  export { Bubble, BubbleContent, BubbleGroup, BubbleReactions } from "./components/ui/bubble";
299
- export { Button, buttonVariants } from "./components/ui/button";
305
+ export { buttonVariants } from "./components/ui/button";
300
306
  export {
301
307
  ButtonGroup,
302
308
  ButtonGroupSeparator,