@suphark/ui 0.59.1 → 0.60.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 CHANGED
@@ -1,1125 +1,1139 @@
1
- # Changelog
2
-
3
- รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
-
5
- ## [0.59.1] — 2026-10-24
6
-
7
- ### Fixed
8
- - `DataTable variant="quiet"` บนจอสัมผัส (`pointer-coarse`): ปุ่ม "ตัวกรอง", ช่องค้นหา, ปุ่มก่อนหน้า/ถัดไป สูง 44px · พื้นที่แตะช่องเลือกของรายการมือถือขยายเป็น 44px (แตะส่วนที่เกินช่องก็สลับการเลือก) · ปุ่มลบชิปตัวกรองมีพื้นที่แตะกว้างขึ้น · วัด contrast ข้อความรอง/สถานะแล้ว ≥ 4.5:1 ทั้งโหมดสว่างและมืด (ไม่ต้องแก้)
9
-
10
- ## [0.59.0] — 2026-10-24
11
-
12
- ### Added
13
- - `DataTable variant="quiet"` (opt-in, ค่าเริ่มต้น `default` เหมือนเดิม — ตาม mockup "Calm toolbar, dense rows"): แถบเครื่องมือบรรทัดเดียว — ช่องค้นหา + ชิปตัวกรองที่ลบได้ + ปุ่ม "ตัวกรอง" ปุ่มเดียว (เปิดแผ่นตัวกรอง: ขวาบนเดสก์ท็อป · ล่างบนมือถือ) + ตัวนับ "N จาก M รายการ" + สลับความหนาแน่น (`defaultDensity`) + ไอคอนคอลัมน์ · หัวตารางเรียบ (ลูกศรเรียงของคอลัมน์ที่ไม่ได้เรียงโผล่เมื่อชี้) · ช่องเลือก (`[role=checkbox]`) และปุ่มท้ายแถว (`DataTableRowActions`) โผล่เมื่อชี้/โฟกัส/เลือก (จอสัมผัสเห็นตลอด) · `StatusBadge` ในตารางเป็นจุดสี · แถวที่เลือกมีแถบสีซ้าย · เลือกแล้วขึ้นแคปซูล `SelectionBar variant="pill"` (`selectionActions` ใส่ปุ่มคำสั่ง) แทนข้อความ "เลือก N จาก M แถว" · เลขหน้าย่อ "1–10 จาก 128 · ‹ 1 / 13 ›"
14
- - มือถือ (< sm) ของ `variant="quiet"`: คอลัมน์ที่ใส่ `columnDef.meta = { mobile: "title" | "end" | "sub" | "status" | "meta" | "hidden" }` (type `DataTableMobileSlot`) แสดงเป็นรายการแถวละ 3 บรรทัด (ชื่อ + ยอด / รอง / สถานะ · meta) แทนตารางเลื่อนแนวนอน · ไม่ใส่ slot ใดเลย = ตารางเหมือนเดิม · คอลัมน์ `select` แสดงที่ซ้ายของรายการ
15
- - `DataTablePagination variant="quiet"`, `DataTableViewOptions iconOnly`, `UiText.table.rangeOf` / `shownOf` / `density`
16
- - `NotificationCenter headerAction` (ขอโดย pi-done): ปุ่มคำสั่งของทั้งกล่อง เช่น "อ่านทั้งหมดแล้ว" ที่หัว ข้างปุ่ม "ดูทั้งหมด" — ไม่ต้องยืม `viewAll`
17
- - `SwitchField labelClassName` (ขอโดย pi-done): ทับสไตล์ข้อความป้าย (ค่าเริ่มต้น `text-sm`)
18
-
19
- ### Fixed
20
- - `useIsMobile` ไม่พังเมื่อไม่มี `window.matchMedia` (jsdom/เบราว์เซอร์เก่า) — ใช้ค่าตอน mount อย่างเดียว (กระทบ `DataTableToolbar` ที่เรียก hook นี้ แอปที่เทสต์ DataTable ใน jsdom ไม่ต้อง mock matchMedia)
21
- - showcase: `/components/data-table` ตัวอย่างใหม่ (สลับ default/quiet, เลือกแถว, ใบสั่งซื้อสมมติ)
22
-
23
- ## [0.58.0] — 2026-10-24
24
-
25
- ### Added
26
- - `PullToRefresh variant="circle"` (`@suphark/ui/pwa`, ขอโดย pi-done — เจ้าของเลือกแบบวงกลม): วงกลม 40px ไอคอนล้วน (ลูกศร → หมุนตอนรีเฟรช) กลางแนวนอน พักที่ 16px ใต้ขอบบนของ root · เริ่มทำงานที่ 64px สูงสุด 104px แรงต้าน 0.5 · `role="status"` + ข้อความ `sr-only` จาก `text` (ปกติ `PULL_TO_REFRESH_TEXT_*`) · ค่าเริ่มต้นยัง `variant="pill"` (แคปซูลมีข้อความ) เหมือนเดิม
27
-
28
- ## [0.57.0] — 2026-10-24
29
-
30
- ยกจาก pi-done (รอบสองของคำขอย้าย component ที่แอปทำเอง)
31
-
32
- ### Added
33
- - `RowTable` / `RowTableHeader` / `RowTableRow` / `RowTableCell` / `RowTableActions` / `RowTableGroup` — ตาราง grid ที่หัว/แถว/แถวกลุ่มใช้คอลัมน์ชุดเดียว (`columns` + `mobileColumns` เป็น `grid-template-columns` ผ่านตัวแปร CSS) หัวซ่อนบนมือถือ ช่อง `hideOnMobile` ซ่อนบนมือถือ ปุ่มท้ายแถวโผล่ตอนชี้/โฟกัส (เห็นตลอดบนจอสัมผัส) — แทนที่ pi-done เขียนซ้ำ 4 ที่
34
- - `ListSection` / `ListSectionHeader` — กลุ่มรายการแบบไม่มีกล่อง (หัว + จำนวน + ปุ่มขวา + เส้นบาง + แถวคั่นเส้น) = `SectionPanel plain` + `TaskGroupHeader`
35
- - `SidebarNavItem` / `SidebarCountBadge` — แถวเมนูข้างแบบเรียบ (active = `bg-primary/10 text-primary`, ตัวเลขเป็นข้อความเทา ไม่ใช่ pill, 44px บนจอสัมผัส, `draggingOver`)
36
- - `ProgressBar tone="auto"` — เขียวเมื่อครบ (value ≥ max) นอกนั้นสีหลัก (ครอบคลุม `ProgressBar` ของ pi-done: ใช้ `<ProgressBar value={percent} tone="auto" className="w-10" />`)
37
- - showcase: หน้า `/components/row-table`, `/components/list-section`, `/components/sidebar-nav-item`
38
-
39
- ## [0.56.0] — 2026-10-24
40
-
41
- ### Added
42
- - `DatePicker` `minDate` / `maxDate` (`"YYYY-MM-DD"` | Date, รวมวันนั้น) ทั้งวันเดียวและ `mode="range"` (ขอโดย DueBell แทนการกันที่ `onChange`): ปฏิทินกดวันนอกช่วงไม่ได้ · เดือน/ปีเลื่อนไปนอกช่วงไม่ได้ (ลูกศรเดือน + ตารางเลือกเดือน) · ค่าที่พิมพ์แล้วอยู่นอกช่วง = ไม่ยืนยัน (Enter/Tab/ปุ่มผล) และกล่องผลแสดงเหตุผลสีแดงแทน · ค่าที่ส่งเข้ามาเองนอกช่วงไม่ถูกแก้ ช่องขึ้น `aria-invalid` · ข้อความ `UiText.datePicker.beforeMin` / `afterMax` (`{date}` ตาม `locale` ของช่อง) แอปทับได้
43
-
44
- ## [0.55.0] — 2026-10-24
45
-
46
- ยกจาก pi-done (รอบแรกของคำขอย้าย component ที่แอปทำเอง)
47
-
48
- ### Added
49
- - `ResponsivePopover` / `ResponsivePopoverTrigger` / `ResponsivePopoverContent` / `ResponsivePopoverItem` / `useIsResponsiveSheet()` — Popover บนจอ ≥ 768px · แผ่นล่าง (Drawer) บนมือถือ: สูงตามเนื้อหา, `footer` ค้างล่าง, ปัดลงปิด; `useIsResponsiveSheet()` ให้ picker ขยายพื้นที่แตะ (`ResponsivePopoverItem` = 44px ในแผ่น) · `sheetContentClassName` ทับสไตล์ตัวแผ่น
50
- - `SwitchField` — สวิตช์ + ป้ายในตัว (คลิกป้ายสลับ, `hint` ได้, ป้ายสูง 44px บนจอสัมผัส) แทน `<label><Switch/>…</label>` ที่ต้อง biome-ignore เพราะ Switch เป็น span
51
- - `PullToRefresh` (`@suphark/ui/pwa`) prop `onRefresh` — รีเฟรชเองได้ (แอป local-first ที่ `router.refresh()` ไปไม่ถึง) · คืน promise = หมุนจนเสร็จ ขั้นต่ำ 500ms (ไม่ส่ง = `router.refresh()` แล้วหมุน 700ms เหมือนเดิม)
52
-
53
- ### Fixed
54
- - `PullToRefresh`: วงโผล่ใต้ขอบบนของ root (เดิมติดขอบจอ จมใต้ header ที่ sticky) และ `z-50` · เริ่มดึงได้เฉพาะเมื่อแตะ "ในส่วน root" ที่เลื่อนอยู่บนสุด — sheet/dialog ที่ portal ออกไปนอก root จึงปัดลงของตัวเองได้ ไม่ถูกแย่งไปรีเฟรชหน้า (root เป็น `<html>`/`<body>` ทำงานเหมือนเดิม)
55
- - showcase: หน้า `/components/responsive-popover` ใหม่ · labeled-field มีตัวอย่าง `SwitchField`
56
-
57
- ## [0.54.0] — 2026-10-24
58
-
59
- ### Added
60
- - `AccessibleCalendar` — `Calendar` ที่ลูกศร/Home/End/PageUp/PageDown เลือกวันด้วยคีย์บอร์ดได้จริง (props เหมือน `Calendar` ทุกอย่าง) · ใช้แทน `Calendar` ดิบเมื่อวางปฏิทินเอง (ปุ่มวันของ shadcn ไม่ผูก `ref` ลูกศรจึงเลื่อนได้แค่สถานะภายใน) · `FocusableDayButton` ย้ายจาก `date-picker.tsx` มาอยู่ `shared/date/accessible-calendar.tsx`
61
-
62
- ### Fixed
63
- - `DateRangePicker` (ตัวกรอง) และ `DateTimePicker`: ปฏิทินใช้ `AccessibleCalendar` — เลือกวันด้วยคีย์บอร์ดได้จริง (เดิมโฟกัสค้างที่เดิม)
64
- - `DateRangePicker`: ข้อความ placeholder/"ล้างช่วงวันที่" มาจาก `UiText.datePicker` (`rangePlaceholder`, ใหม่ `clearRange`) — แอปที่ไม่ส่ง `placeholder` และใช้ UiText ไทยจะเห็นข้อความไทยแทนอังกฤษ; ส่ง `placeholder` เองยังชนะเสมอ
65
- - showcase: `/components/calendar` ใช้ `AccessibleCalendar`
66
-
67
- ## [0.53.0] — 2026-10-23
68
-
69
- ### Changed
70
- - showcase: `/components/sortable-list` ปรับให้ตรง mockup — หัวตาราง "ตัวแปรสภาพแวดล้อม · N คีย์" + ปุ่ม ? ด้านบน, ตัวเลขจำนวนคีย์ในหัวกลุ่ม, เส้นนำแนวตั้งของคีย์ในกลุ่ม, สีตอนชี้แถว, เส้นแทรกสีหลักที่แถวที่กำลังลาก, แคปซูลเลือกหมู่อยู่ใต้ตาราง
71
- - `DatePicker` ปรับให้ตรง mockup "Type first" (0.49.0 วาดไม่ตรง): ไอคอนปฏิทินอยู่ซ้ายของช่อง · ป้าย `Enter` อยู่ในช่องขวา (เดิมอยู่ในกล่องผล) · กล่องผลที่อ่านออก (เขียว) แยกเป็นกล่องของตัวเองเหนือการ์ดปฏิทิน · หัวปฏิทินเป็นปุ่มเดียว "ตุลาคม 2569 ▾" กดแล้วเป็นตารางเลือกเดือน/ปี (แทนเมนูเดือน + เมนูปีสองอัน) · แผ่น "ทั้งหมด" มีหัว "พิมพ์ลัดทั้งหมด · กดเพื่อเติมในช่อง" · `UiText.datePicker.allShortcuts`/`tapToFill`
72
-
73
- ## [0.52.0] — 2026-10-23
74
-
75
- ### Changed
76
- - `variant="quiet"` ของ `RecordCard`/`NotificationCenter` ปรับให้ตรงแบบ mockup จริง (0.51.0 วาดไม่ตรง): ไอคอนอยู่ในกรอบ 36px พื้น muted สีตาม accent · `StatusBadge` ที่อยู่ในแถว quiet กลายเป็น "จุดสี + ข้อความสี" ไม่มีกรอบ/พื้นอัตโนมัติ (CSS `in-data-[variant=quiet]`, นอก quiet เหมือนเดิม) · ปุ่มรองเป็น outline (เดิม ghost) ปุ่มหลักใช้แถวเดียว
77
-
78
- ### Added
79
- - `RecordCard`/`NotificationEntry` `highlight` (quiet: พื้น accent จาง ๆ เน้นแถวที่ต้องจัดการก่อน) และ `progress` (quiet: แถบความคืบหน้า 0–100 ใต้ยอด)
80
- - showcase: notification-center โหมด quiet ตามแบบ — ยอดไม่มีสตางค์, แถวแรกเน้น + ปุ่มหลักตัวเดียว, แถบ 35/60 รอบ, เมนู … ทุกแถว
81
-
82
- ## [0.51.0] — 2026-10-23
83
-
84
- ### Added
85
- - `useSortableRow().toggleSelected()` — เลือก/ไม่เลือกแถวจากช่องติ๊กในแถว (ช่องติ๊กโผล่เมื่อชี้ เหมือนที่จับ)
86
- - `SelectionBar variant="pill"` — แคปซูลสีหลักลอยกลางล่างจอ เด่นกว่า `floating` (ใช้กับแถบเลือกหมู่ของ Sortable)
87
- - `RecordCard variant="quiet"` (opt-in, ค่าเริ่มต้น `card` เหมือนเดิม) — แถวเรียบแบบสมุดบัญชี "Quiet ledger": ไม่มีกรอบ/พื้นสี สีของ `accent` เหลือแค่ไอคอน, ชื่อ + (สถานะ · meta) ซ้าย, ยอดเงินขวา, ปุ่มท้ายแถว — เห็นได้มากกว่าเท่าตัวในที่เท่ากัน · props เดิมทั้งหมดใช้ต่อได้ ไม่ใช่ major
88
- - `NotificationCenter variant="quiet"` + `NotificationEntry.section` — ชิปนับเป็นแท็บขีดเส้นใต้ (ตัวเลข danger เป็นสีแดง) รายการเป็นแถว quiet ในกรอบเดียว มีหัวกลุ่มคั่นตาม `section`
89
- - showcase: sortable-list มีช่องติ๊กโผล่เมื่อชี้ + แคปซูลเลือกหมู่ + กรอบบอกตำแหน่งเดิมตอนลาก; record-card / notification-center มีตัวอย่าง quiet (notification-center สลับ card/quiet ได้)
90
-
91
- ## [0.50.0] — 2026-10-23
92
-
93
- ### Added
94
- - `@suphark/ui/sortable`: `DragHandle reveal` (opt-in, ค่าเริ่มต้นเหมือนเดิม) — "สงบจนกว่าจะแตะ": ซ่อนที่จับ โผล่เมื่อชี้แถว / โฟกัสในแถว / แถวถูกเลือก (จอสัมผัสเห็นตลอด, Tab ถึงที่จับได้เสมอ) · แอปใส่ `group/row` ที่แถว
95
- - showcase: `/components/sortable-list` ใช้ `reveal` และย้ายข้อความสอนยาวไว้หลังปุ่ม ? (แผ่นลัดแป้นพิมพ์)
96
-
97
- ## [0.49.0] — 2026-10-23
98
-
99
- ### Changed
100
-
101
- - **`DatePicker` redesign "Type first"** (ทั้งวันเดียวและ `mode="range"`; API/props/ค่าที่ส่งในฟอร์ม `name`/`defaultValue`/`required` เหมือนเดิมทุกตัว — แอปที่ใช้อยู่ไม่ต้องแก้):
102
- - **ช่องคือ input ที่พิมพ์ได้โดยตรง** (เดิมเป็นปุ่มที่ต้องเปิดป๊อปอัปก่อนถึงมีช่องพิมพ์ซ้อนอยู่ข้างใน) — คลิกช่อง = เลือกทั้งข้อความ พิมพ์ทับได้เลย · `<label>` ที่ครอบ/ชี้ (`LabeledField`) โฟกัสที่ช่องพิมพ์โดยตรง
103
- - **เห็นผลที่อ่านออกใต้ช่องทันที**: "จ. 26 ต.ค. 2569 · ในอีก 3 วัน" / ช่วง "16–23 ต.ค. 2569 · 8 วัน" (กดผลนั้นหรือ Enter เพื่อยืนยัน) — แก้ปัญหา "10/02 อ่านได้สองแบบ"; อ่านไม่ออก = "อ่านไม่ออก — ลองพิมพ์ t+3" ไม่ทิ้งค่าที่พิมพ์ทิ้งเงียบ ๆ; พิมพ์วันเดียวในช่องช่วง = ช่วงหนึ่งวัน
104
- - ปฏิทินเลื่อนตามข้อความที่พิมพ์และไฮไลต์วันที่อ่านได้ทันที · แถวพิมพ์ลัดบรรทัดเดียวใต้ปฏิทิน (กดเติมลงช่อง) + "ทั้งหมด ›" เปิดแผ่นตัวอย่างวิธีพิมพ์ในกล่องเดียวกัน — **เลิกปุ่ม (i) / ตารางวิธีพิมพ์ที่กินที่ / ปุ่ม "ตกลง"**
105
- - ปุ่ม ✕ ล้างค่าและปุ่มเปิดปฏิทินเป็น `<button>` จริง (เดิม ✕ เป็น `<span role="button">`) · วันที่สัมพัทธ์ "(พรุ่งนี้)" ยังแสดงท้ายช่องตาม `showRelative`
106
- - **ปฏิทินใช้ภาษา/ปีเดียวกับช่อง** (เดิมไม่ส่ง `locale` แล้วขึ้น "Oct 2026" อังกฤษ ทั้งที่ช่องแสดง พ.ศ.) · หัวคอลัมน์วันไทย 2 ตัว (อา จ อ พ พฤ ศ ส) แทนตัวเดียวที่ "อ"/"พ" กำกวม
107
- - คลิกวันที่ที่เลือกอยู่แล้วในปฏิทิน = ยืนยันวันนั้น (เดิมล้างค่าแบบ toggle — ล้างใช้ปุ่ม ✕)
108
- - `showSmartInput={false}` = ช่องอ่านอย่างเดียว กดแล้วเปิดปฏิทินอย่างเดียว (แบบเดิม ไม่มีแถบพิมพ์ลัด) · `showPresets` ยัง deprecated ไม่มีผล
109
- - ป๊อปอัปไม่มี animation ตอนปิด (Base UI ถอดตัวดักโฟกัส `<span tabindex=0>` ที่วางต่อท้ายช่องก็ต่อเมื่อ animation จบ ระหว่างนั้น Tab ตกไปที่มัน)
110
- - `UiText.datePicker` เพิ่ม `cannotRead` (`{example}`), `shortcutsLabel`, `showAll`, `backToCalendar`, `openCalendar`, `dayCount` (`{count}`) — ไทย/อังกฤษในตัว
111
-
112
- ### Fixed
113
-
114
- - **คีย์บอร์ด: ลูกศรเลือกวันในปฏิทินไม่ย้ายโฟกัสจริง** — `CalendarDayButton` ใน `components/ui/calendar.tsx` (ไฟล์ shadcn) มี effect สั่ง `ref.current?.focus()` แต่ **ไม่เคยผูก `ref` กับปุ่ม** ลูกศรจึงเลื่อนแค่สถานะภายในของ react-day-picker โฟกัสจริงค้างที่เดิม (กระทบ `Calendar` ทุกที่ รวม DatePicker แบบเดิม) · `DatePicker` แก้ที่ฝั่งตัวเองด้วย `components={{ DayButton }}` ที่ผูก ref ให้ · **`Calendar` ดิบที่แอปใช้เองยังมีบั๊กนี้** (ห้ามแก้ไฟล์ shadcn — จดใน todo)
115
- - ป๊อปอัป DatePicker สูง ~400px ไม่พอทั้งบนและล่างแล้ว Base UI ย้ายไปวางข้างช่อง (ทับเนื้อหา) → พลิกได้เฉพาะบน↔ล่าง
116
- - Tab ออกจากช่องที่เปิดป๊อปอัปอยู่ แล้วโฟกัสหลุดไป `body` แทนที่จะไปช่องถัดไป (ตัวดักโฟกัสของ Base UI)
117
-
118
- ### Security
119
-
120
- - อัป `next` (devDependency ของแพ็กเกจ + showcase ui.suphark.com) เป็น `^16.4.2` — ปิดช่องโหว่ใน `next >=16.0.0 <=16.4.0`: **critical** RCE ไม่ต้องล็อกอินใน Image Optimization API เมื่อใช้ไฟล์ SVG (GHSA-2464-fhr4-w9xw) · **critical** RCE ไม่ต้องล็อกอินใน `use cache` ที่คืน typed array (GHSA-673q-mjm6-r5xr) · high DoS ใน App Router ที่ใช้ Server Actions · override `source-map-js >=1.2.2` (high DoS ที่ติดมากับ postcss) · `pnpm audit --prod --audit-level=high` สะอาด · **peerDependency `next >=15` ไม่เปลี่ยน** — แอปที่ใช้ Next 16 อัปเองเป็น `^16.4.2` (แพ็กเกจส่ง source ไม่ได้บังคับเวอร์ชัน next)
121
-
122
- ### Added
123
-
124
- - `HeaderBreadcrumbs` (และ `AppShell` `header.breadcrumbs: { upButton }`): **`upButton`** + `upLabel` — ปุ่ม X ท้าย breadcrumb ถอยขึ้นหนึ่งระดับแบบ Vercel (ไปที่ segment ก่อนหน้า หรือ `homeHref` ถ้ามี segment เดียว · ไม่แสดงหน้าแรก) · ปิดไว้เป็นค่าเริ่มต้น แอปเดิมไม่เปลี่ยน (ขอจาก Pi Config ซึ่งทำเองใน `breadcrumbs.tsx` แล้ว) · showcase `/shell` เปิดใช้
125
-
126
- ## [0.48.0] — 2026-10-10
127
-
128
- > ประกาศพร้อม publish แล้วแต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.49.0
129
-
130
- ### Added
131
-
132
- - showcase: หน้า `/components/web-push-row` (demo WebPushRow)
133
- - WebPushRow (@suphark/ui/pwa): แถวตั้งค่าเปิด/ปิดการแจ้งเตือน Web Push บนเบราว์เซอร์ พร้อมจัดการ Service Worker registration, PushManager subscription, permission check, และ Feedback toast อัตโนมัติ
134
-
135
- ## [0.47.0] — 2026-10-08
136
-
137
- ### Changed
138
-
139
- - **`@suphark/ui/sortable` รองรับโครงเดียว: หัวกลุ่มกับแถวรายการเป็นพี่น้องกัน** (`<tr>` ใน `<tbody>` เดียวกัน หรือการ์ดในลิสต์เดียวกัน) — เจ้าของเลือกให้เหลือรูปแบบเดียวเพื่อดูแลง่าย หลัง Pi Config ย้ายจากโครง "`<tbody>` ต่อกลุ่ม" มาแบบนี้แล้ว (71735f0) · **ห้ามให้ node ของกลุ่มครอบแถวรายการไว้** (เช่นแปะ `ref` ของกลุ่มที่ `<tbody>` ที่ห่อแถวคีย์) ไม่งั้นแถวลูกจะเลื่อนสองชั้นและกรอบของก้อนถูกนับซ้ำ · ถอดโค้ดที่เคยรองรับโครงนั้นออก (`insideGroup` ใน context ภายใน, การกันนับซ้ำใน `blockRect`) — ไม่มี props ใดเปลี่ยน แอปที่ทำตามตัวอย่างใน `/components/sortable-list` ไม่ต้องแก้อะไร
140
-
141
-
142
- ### Added
143
-
144
- - type `DistributiveOmit<T, K>` / `DistributivePick<T, K>` (ขอจาก Pi Auth): `Omit` ปกติยุบ union ของ props ที่มี discriminant — `Omit<DatePickerProps, "locale">` ทำให้ `name` กลายเป็น `string | [string, string]` แยกตาม `mode` ไม่ได้อีก ตัวห่อของแอปจึงพัง · ใช้ `DistributiveOmit<DatePickerProps, "locale">` แทนได้เลย ไม่ต้อง cast (มีเทสต์ type ที่พิสูจน์แล้วว่าแดงเมื่อถอดตัวแก้)
145
-
146
-
147
- ### Fixed
148
-
149
- - `@suphark/ui/sortable`: ลากรายการเข้ากลุ่มอื่นไปวาง "ใต้" แถวหนึ่ง ระหว่างลากอยู่ถูกที่ แต่ปล่อยแล้วกระโดดไปอยู่ "เหนือ" แถวนั้น (เจ้าของเจอในตัวอย่างโครง `<tbody>`) — ตอนข้ามกลุ่มจัดตำแหน่งก่อน/หลังไว้แล้ว แต่ตอนปล่อย `reorderItems` ย้ายไปแทนที่แถวที่ชี้ซ้ำอีกรอบ · ตอนนี้ถ้าปล่อยตอนยังชี้แถวเดิมที่ข้ามกลุ่มเข้ามา ใช้ตำแหน่งที่จัดไว้เลย
150
- - showcase: `/components/sortable-list` **เอาตัวอย่างโครง `<tbody>` ต่อกลุ่มออก** — เจ้าของเลือกให้มีรูปแบบเดียว (หัวกลุ่ม + แถวรายการเป็น `<tr>` ใน `<tbody>` เดียวกัน) เพื่อดูแลง่าย
151
-
152
-
153
- ### Fixed
154
-
155
- - `AuthLayout`: โลโก้แนวนอนถูกบีบในกรอบ 44×44 จนอ่านไม่ออก (Pi Auth — wordmark ของลูกค้า 315×70 เหลือสูง ~10px) · ตอนนี้ `<img>` สูง 44px กว้างตามสัดส่วน (ไม่เกิน 220px) ส่วนไอคอน/ตัวอักษร (svg, span) ยังเป็นกรอบจัตุรัสมนเหมือนเดิม — เลือกเองตามชนิดลูก แอปไม่ต้องรู้สัดส่วนรูป
156
- - showcase: `/components/auth-layout` เพิ่มแท็บ "โลโก้แนวนอน"
157
-
158
- ## [0.45.0] — 2026-10-08
159
-
160
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.45.1
161
-
162
-
163
- ### Added
164
-
165
- - `RecordCard iconSize="sm" | "md"` (ขอจาก DueBell — การ์ดบิลโหมดกะทัดรัด): `sm` = กรอบ 36px (ไอคอน 16px) เยื้องรายละเอียด/แถบล่าง `ml-12` · `md` (ค่าเริ่มต้น) = 44px / `ml-14` เหมือนเดิม · ใช้คู่ `iconTile={false}` ได้เพื่อตั้งระยะเยื้องให้ตรงไอคอนของแอปเอง
166
- - showcase: `/components/record-card` เพิ่มการ์ดแบบ `iconSize="sm"`
167
-
168
- ## [0.44.0] — 2026-10-08
169
-
170
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.45.0
171
-
172
-
173
- ### Added
174
-
175
- - **`Button tone`** (สเปกจาก DueBell — `ToneButton`): ปุ่มขอบโครงเดียวกัน ต่างแค่สีตามความหมายของรายการ — `income` เงินเข้า/รับเงิน · `expense` เงินออก/จ่าย · `reminder` รายการเตือน (ม่วง) · `warning` ย้อนกลับได้ (พักรายการ, รอยืนยัน) · `revert` เลิกทำ/ย้อนสถานะ (สีเดียวกับ expense) — ลบถาวรยังเป็น `variant="destructive"` · type `ButtonTone`
176
- - token `--tone-{income,expense,reminder,warning}` / `-border` / `-hover` ทั้งสองธีม — **แอป override เป็นสีประเภทรายการของตัวเองได้** (ค่าเริ่มต้นผูกกับตระกูล success/destructive/warning + ม่วงใหม่) · วัดบนเบราว์เซอร์แล้ว: ตัวอักษร 7.1–8.4:1 (สว่าง) / 12.5–16:1 (มืด), ขอบ 3.2–3.8:1 / 3.4–4.0:1
177
- - showcase: `/components/button` เพิ่มแถวปุ่มโทน
178
-
179
- ### Changed
180
-
181
- - `RecordCard` กรอบไอคอน: ขอบ `color-mix(accent 25%)` (เดิม 35%) ให้ตรงสูตร `CategoryIcon` ของ DueBell (พื้น 12% / ขอบ 25%) — แอปส่งไอคอนเปล่าได้เลย
182
-
183
- ## [0.43.0] — 2026-10-08
184
-
185
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.44.0
186
-
187
-
188
- ### Added
189
-
190
- - **`RecordCard` — การ์ดรายการมาตรฐาน** (ยกจาก `LedgerCard` ของ DueBell ที่ใช้ 3 ที่: รายการบิล, การ์ดแค็ตตาล็อก, ศูนย์แจ้งเตือน): หัว = ไอคอน + ชื่อ/สถานะ + meta → รายละเอียด → แถบล่าง (`footerSummary` ซ้าย, `actions` ขวา) โดยรายละเอียดและแถบล่างเยื้องตรงคอลัมน์ข้อความ · `accent` รับ tone (`neutral/info/success/warning/danger` + ชื่อเดิม `default/destructive`) **หรือสี CSS ตรง ๆ** (`var(--type-income)`) → ขอบซ้าย 3px + ขอบจาง 30% + พื้นไล่สี 11% + กรอบไอคอนตามสี · ชื่อยาวตัดที่ 2 บรรทัด ไม่ดันป้ายสถานะตก · `unread`, `onSelect` (ปุ่มในแถบล่างไม่ไปโดน), `as="li"`, `titleAs` · `recordAccentColor()` · `UiText.record.details`
191
-
192
- ### Changed
193
-
194
- - **`NotificationCenter` ประกอบจาก `RecordCard` + `StatusBadge`** (DueBell: แถวเดิมเขียนเลย์เอาต์ซ้ำกับการ์ดของแอปและเป็น private) — เพิ่มอย่างเดียว ของเดิมใช้ต่อได้:
195
- - แถวคือ `RecordCard` · `NotificationEntry.accent` (สี CSS สำหรับสีของประเภทรายการ เช่น รายรับ/รายจ่าย/เตือน) + `details`
196
- - `renderItem(item, { busy })` เรนเดอร์แถวเองได้
197
- - ชิปนับเป็น `StatusBadge` (ชิปทั้งระบบชุดเดียว) · tone รับ `danger`/`neutral` แบบ `StatusBadge` (`destructive`/`default` ยังได้)
198
- - `NotificationAction.render` ใส่ปุ่มของแอปเองได้ (เช่น `ToneButton`) · `onSelect` ไม่บังคับเมื่อใช้ `render`
199
- - `headingAs` / `headingClassName` (หัวข้อแบบ label ตัวเล็กของแอป)
200
-
201
- ### Fixed
202
-
203
- - **`@suphark/ui/sortable` ลากกลุ่มแล้วกลุ่มอื่นไม่เลื่อนหลบ เมื่อแอปแปะ ref ของกลุ่มที่ `<tbody>` ทั้งก้อน** (Pi Config รายงานหลัง bump 0.42.1): ตัดสินก่อน/หลังจากกลางของสิ่งที่ลาก — ก้อน `<tbody>` สูงจนกลางไม่เคยเลยครึ่งของกลุ่มเป้าหมาย → ได้ "ที่เดิม" เสมอ · ตอนนี้ใช้ตำแหน่งเมาส์ · และแถวที่อยู่ใน node ของกลุ่มเองไม่ได้ transform ซ้ำอีกชั้น (เดิมเลื่อนสองเท่า) และไม่ถูกนับซ้ำตอนวัดขนาดก้อน
204
- - showcase: `/components/record-card` ใหม่ · `/components/notification-center` เพิ่มรายการเตือนสีม่วง (accent สี CSS) + รายละเอียด · `/components/sortable-list` เพิ่มตัวอย่างโครง `<tbody>` ต่อกลุ่ม (แบบ Pi Config)
205
-
206
-
207
- ### Fixed
208
-
209
- - **วันที่สัมพัทธ์นับผิด 1 วัน** (เจอตอนตรวจ 0.42.0 บน production: พิมพ์ `t+3` ได้ 10 ต.ค. ถูก แต่วงเล็บบอก "(ใน 2 วัน)") — `formatRelativeThaiDate` ใช้ `formatDistanceToNow` ซึ่งนับเวลาจาก "ตอนนี้" ถึงเที่ยงคืนของวันนั้น (บ่าย/ค่ำเหลือ ~2.4 วัน → 2) และมีคำว่า "ประมาณ 1 เดือน" · ตอนนี้นับเป็นวันปฏิทินด้วย `Intl.RelativeTimeFormat`: "ในอีก 3 วัน", "10 วันที่ผ่านมา", "เดือนหน้า", "ปีที่แล้ว" (< 31 วัน = วัน, < 12 เดือน = เดือน, ที่เหลือ = ปี) — กระทบ `DatePicker` และ `RelativeDate` · `DatePicker locale` ใช้สูตรเดียวกัน (เดิมนับเป็นวันอย่างเดียว "in 45 days")
210
- - `formatRelativeCalendarDate(date, locale)` ใน `lib/formatters` (ใช้ร่วมกันข้างใน)
211
-
212
-
213
- ### Added
214
-
215
- - **`DatePicker` พิมพ์ลัด** (เจ้าของขอ): `t` = วันนี้, `t+3` / `t+3d` = อีก 3 วัน, `t-2` = 2 วันก่อน, `t+2w` สัปดาห์, `t+1m` เดือน, `t+1y` ปี (`today`/`วันนี้` และหน่วยไทย วัน/สัปดาห์/เดือน/ปี ก็ได้; ปลายเดือนตัดให้อยู่ในเดือนถัดไปแบบ date-fns) · ช่วง: `t-7..t`, `t ~ t+1m`, `t ถึง t+3` — ใช้ได้กับ `parseSmartDateInput` / `parseSmartDateRange` ทุกที่ (รวม `FormFieldDate`)
216
- - ปุ่ม (i) ท้ายช่องพิมพ์ เปิดตารางวิธีพิมพ์ในกล่องเดียวกัน (ไม่ใช่ tooltip — popup ซ้อน popup และจอสัมผัสเปิด tooltip ไม่ได้) · ข้อความจาก `UiText.datePicker.help*`
217
-
218
- ### Changed
219
-
220
- - **เลิกปุ่มทางลัด** (วันนี้/พรุ่งนี้/+1 สัปดาห์ …) ทั้งแบบวันเดียวและช่วง — ใช้การพิมพ์ลัดแทนเพื่อลดพื้นที่ · `showPresets` ยังรับได้แต่ไม่มีผล (deprecated)
221
- - ช่วงวันที่: **ไม่ใส่ปี = ปีปัจจุบันเสมอ** (0.41.0 ยืมปีจากฝั่งขวา)
222
- - placeholder ช่องพิมพ์: "เช่น 15/3, t+3, t+1m" / ช่วง "เช่น 1/7-5/7, t-7..t"
223
-
224
- ## [0.41.0] — 2026-10-07
225
-
226
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.42.0
227
-
228
-
229
- ### Added
230
-
231
- - **`DatePicker mode="range"` — เลือกช่วงวันที่** (เจ้าของขอ): คลิกวันเริ่มแล้ววันสุดท้าย (มีบรรทัดบอก "เลือกวันสุดท้ายของช่วง" ระหว่างเลือก) หรือพิมพ์ **"1/7-5/7" = 1–5 ก.ค.** · รองรับ "1-5/7", "1/7 - 5/8/68", "1/7 ถึง 5/7", "1/7 to 5/7", "1/7–5/7" · ปีที่ไม่ได้พิมพ์ฝั่งซ้ายใช้ปีฝั่งขวา · ใส่กลับด้านแล้วสลับให้ · "1-7-2568" ยังเป็นวันเดียวเหมือนเดิม (`-` เป็นตัวคั่นช่วงเมื่อมี `/` หรือ `.`)
232
- - แสดงด้วย `Intl.DateTimeFormat#formatRange` ตาม `locale`: "1–5 ก.ค. 2569" / en-GB "1–5 Jul 2026"
233
- - ทางลัดของช่วง: วันนี้, 7/30 วันล่าสุด, เดือนนี้, เดือนที่แล้ว, 7/30 วันข้างหน้า
234
- - ฟอร์ม: `name={["from", "to"]}` ส่ง `YYYY-MM-DD` สองช่อง (ใช้กับ `<form method="get">` ของตัวกรองได้ตรง ๆ) + `required` · `value`/`defaultValue` = `{ from, to }` (Date หรือ "YYYY-MM-DD") · `onChange(range | null)`
235
- - `parseSmartDateRange(input)` · types `DatePickerRange`, `DatePickerSingleProps`, `DatePickerRangeProps` (`DatePickerProps` เป็น union — ของเดิมที่ไม่ส่ง `mode` ใช้ได้เหมือนเดิม)
236
- - `UiText.datePicker`: `rangePlaceholder`, `rangeSmartPlaceholder`, `pickEnd`, ทางลัดของช่วง
237
- - showcase: `/components/date-picker` เพิ่มตัวอย่างช่วงวันที่
238
-
239
-
240
- ### Fixed
241
-
242
- - `DatePicker`: ปฏิทินใน popup มีที่ว่างซ้ายขวามาก (เจ้าของแจ้ง) — กล่องกว้างตายตัว 320–340px แต่ช่องวันที่ 28px ปฏิทินจึงกว้างแค่ ~200px · ตอนนี้ช่องวันที่ 40px (กดง่ายขึ้นด้วย) และกล่องกว้างพอดีปฏิทิน 306px (commit แรกหลุด format ของ biome — แก้ในรุ่นเดียวกัน)
243
-
244
- ## [0.40.0] — 2026-10-07
245
-
246
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.40.1
247
-
248
-
249
- ### Added
250
-
251
- - **`DatePicker` ใช้ในฟอร์มและหลายภาษาได้** (ขอจาก Pi Auth — `<input type="date">` แสดง "10/02/2027" อ่านได้ทั้ง 2 ต.ค. และ 10 ก.พ.):
252
- - `locale` (BCP 47): แสดงผ่าน Intl `{ day, month: "short", year }` ทั้งในช่อง ปฏิทิน และวันที่สัมพัทธ์ — `"en-GB"` = "2 Oct 2027", `"en"` = "Oct 2, 2027", `"th"` = "2 ต.ค. 2570" · ไม่ส่ง = รูปแบบไทยเดิม
253
- - `name` (+ `defaultValue` แบบไม่ควบคุม, `value`/`onChange` ไม่บังคับแล้ว): ส่ง `YYYY-MM-DD` ตามวันที่ local ไปกับ FormData ของ `<form action>` · `required` ทำงานกับการตรวจของเบราว์เซอร์และตาข่ายฟอร์ม (ชื่อช่องจาก `LabeledField`)
254
- - `showRelative` (ค่าเริ่มต้น true) · `toDateInputValue(date)` export ให้ใช้เอง · type `DatePickerProps`
255
- - `UiText.datePicker` (placeholder, ทางลัด, ปุ่ม) ไทย/อังกฤษในตัว
256
-
257
- ### Changed
258
-
259
- - `DatePicker` ย้ายไป `components/shared/date/date-picker.tsx` — ไม่ได้มาจาก shadcn CLI (registry ของ base-nova ไม่มี date-picker) แต่ยกมาจาก supplychain; `components/ui/date-picker` ยัง re-export ให้ import path เดิมใช้ได้
260
- - ข้อความเดิมที่ฝังไว้ย้ายไป UiText: placeholder ค่าเริ่มต้นจาก "เลือกวันที่อ้างอิงราคา..." (ของ supplychain) เป็น "เลือกวันที่", ทางลัดเป็นภาษาไทย ("วันนี้", "+1 สัปดาห์" …) แทนอังกฤษปนไทย, หัวข้อ "Quick Preset" → "ทางลัด"
261
- - สีดิบ (slate/emerald) → semantic token · ปุ่ม "ตกลง" ของช่องพิมพ์วันที่เป็น `type="button"` (เดิม submit — ส่งฟอร์มแม่ได้ถ้าไม่ได้กด Enter) · ปุ่มล้างมี `aria-label` และกดด้วยคีย์บอร์ดได้
262
-
263
- ### Fixed
264
-
265
- - `value="YYYY-MM-DD"` อ่านเป็น UTC แล้วเลื่อนวันในโซนเวลาตะวันตก → อ่านเป็นวันที่ local
266
- - วันที่สัมพัทธ์ ("(พรุ่งนี้)") แสดงหลัง mount — ขึ้นกับวันของเครื่องผู้ใช้ จึง hydration mismatch ได้ข้ามวัน/โซนเวลา (ค้างใน todo)
267
- - showcase: `/components/date-picker` เพิ่มตัวอย่างในฟอร์ม server action (`name` + `locale="en-GB"` + `required`)
268
-
269
-
270
- ### Added
271
-
272
- - **`SortableProvider freeLayout` — ชั้นบนสุดจัดได้อิสระ** (เจ้าของแจ้งว่าเดิม "ล็อก" ให้รายการไม่มีกลุ่มอยู่บนเสมอ กลุ่มอยู่ล่างเสมอ): กลุ่มกับรายการไม่มีกลุ่มสลับที่กันได้ทุกแบบ — กลุ่มขึ้นไปเหนือรายการ, รายการไปอยู่ระหว่างกลุ่ม, เอารายการออกจากกลุ่มไปวางก่อนกลุ่มไหนก็ได้ · หัวกลุ่ม: **ครึ่งบน = วางก่อนกลุ่ม, ครึ่งล่าง = เข้ากลุ่มเป็นตัวแรก** · ระหว่างลากทุกอย่างในชั้นบนสุดเลื่อนหลบเป็นก้อน (กลุ่ม = หัว + รายการ) · โซนท้ายตาราง = วางท้ายสุด · ไม่เปิด = แบบเดิม (แอปที่ยังเรนเดอร์รายการไม่มีกลุ่มก่อนไม่เปลี่ยนพฤติกรรม)
273
- - `SortableOrder.root` (optional) + `rootEntries(order)` (ลำดับชั้นบนสุดสำหรับวนเรนเดอร์), `moveRootEntry`, `moveItemToRoot`, type `SortableRootEntry` · `sortableOrderToPayload(order).root` + `sortableOrderOf(..., { root })` สำหรับบันทึก/โหลด — `groups` / `items[UNGROUPED]` ยังตรงกับ root เสมอ (payload เดิมใช้ต่อได้)
274
- - showcase: `/components/sortable-list` เปิด `freeLayout` และเรนเดอร์ตาม `rootEntries`
275
-
276
- ### Fixed
277
-
278
- - (จาก 0.38.1 ที่ไม่ได้ขึ้น npm แยก) ลากรายการเข้ากลุ่มแล้ววางเป็นรายการแรกไม่ได้ — ดูด้านล่าง
279
- - โซน "ท้ายตาราง" (`useSortableDropZone(UNGROUPED)`) ถูก collision เปลี่ยนเป้าหมายเป็นรายการไม่มีกลุ่มตัวแรก → วางแล้วไปโผล่บนสุดแทนท้ายสุด
280
-
281
- ## [0.38.1] — 2026-10-07
282
-
283
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.39.0
284
-
285
- ### Fixed
286
-
287
- - **`@suphark/ui/sortable` ลากรายการเข้ากลุ่มแล้ววางเป็นรายการแรกไม่ได้** (เจ้าของแจ้ง): ระหว่างลากขึ้นเป็นตัวแรกถูก แต่พอปล่อยกลับไปเป็นตัวที่สอง — ตอนชี้หัวกลุ่ม collision เลือก "รายการที่ใกล้ที่สุดที่ไม่ใช่ตัวที่ลาก" (= ตัวแรกเดิม) แล้ว `onDragEnd` ย้ายไปแทนที่ตัวนั้น · ตอนนี้ชี้หัวกลุ่ม = รายการแรกที่แสดงอยู่ **รวมตัวที่ลากเอง** · ยืนยันบน demo: ลากจากนอกกลุ่ม / จากกลุ่มอื่น / จากท้ายกลุ่มเดียวกันขึ้นหัวกลุ่ม → เป็นตัวแรกทุกกรณี, วางครึ่งล่างของแถวยังไปอยู่ "หลัง" แถวนั้นตามเดิม (ทำซ้ำบน production v0.38.0 ได้ก่อนแก้)
288
-
289
-
290
- ### Fixed
291
-
292
- - **`@suphark/ui/sortable` ลากกลุ่มแล้วไม่เห็นอะไรขยับ** (เจ้าของแจ้ง): หัวกลุ่มมักอยู่ใน `SortableGroup` ซึ่งเป็น context ของรายการ dnd-kit จึงหากลุ่มในลำดับไม่เจอ ไม่มีแถวไหนได้ transform เลยจนปล่อย · ตอนนี้ provider คำนวณระยะเลื่อนของกลุ่มเอง และเลื่อน **ทั้งก้อน** (หัวกลุ่ม + แถวรายการ) — กลุ่มอื่นหลบเท่าความสูงทั้งก้อน, ช่องจาง ๆ ของกลุ่มที่ลากเลื่อนไปจุดวาง (ไม่มี overlay = ทั้งก้อนลอยตามเมาส์) · ที่วางของกลุ่มวัดรวมแถวรายการ ใช้ได้ทุกโครงสร้างโดยไม่ต้องแก้ call site · วัดบนหน้า demo: ลาก "ฐานข้อมูล" ลงเลย "อีเมล" → ฐานข้อมูลทั้งก้อน +134px, อีเมลทั้งก้อน −135px, ปล่อยแล้วไม่มี style ค้าง
293
- - แถวต้นทางจางค้างหลังปล่อย (เจอกับหัวกลุ่ม): side effect ของ drop animation ใน dnd-kit จำ opacity ที่เราตั้งไว้เป็นค่าเดิมแล้วเขียนคืน — ปิด side effect นั้น (opacity คุมผ่าน style ของ React อยู่แล้ว)
294
-
295
- ### Added
296
-
297
- - `SortableProvider multiSelect` (+ `selected`/`onSelectedChange` ถ้าจะคุมเอง): เลือกหลายรายการแล้วลากไปพร้อมกัน — ⌘/Ctrl+คลิก ทีละตัว, Shift+คลิก เป็นช่วง (เฉพาะแถวที่แสดง), คลิกเฉย ๆ ล้าง (คลิกปุ่ม/ช่องกรอกในแถวไม่นับ) · ที่จับ: `x` เลือก/ไม่เลือก, ⌘/Ctrl+A ทั้งหมด, Esc ล้าง · ลากตัวที่เลือก = ทุกตัวที่เลือกจางลงตรงที่เดิมแล้วไปรวมที่จุดวาง เรียงตามลำดับเดิม (ข้ามกลุ่มได้) · ลากตัวที่ไม่ได้เลือก = ลากตัวเดียวและล้างการเลือก
298
- - `useSortableRow` คืน `selected` + `selectProps` (กระจายลงแถว มี `data-selected` ไว้ทำสี) · `useSortableState` คืน `selected`/`clearSelection` (ใช้คู่ `SelectionBar`)
299
- - `renderOverlay` ได้ `SortableDrag` = `{ kind, id, ids }` (`ids` = ทุกตัวที่ลากไปด้วยกัน — ทำการ์ดซ้อน + จำนวน) · `SortableAnnouncementContext.count` + `UiText.sortable.startedMany` ("เริ่มลาก {count} รายการที่เลือกไว้")
300
- - helper pure: `flatItems(order)`, `gatherItems(order, activeId, ids)`
301
- - showcase: `/components/sortable-list` เปิด `multiSelect` + `SelectionBar` + การ์ดซ้อนตอนลากหลายคีย์
302
-
303
-
304
- ### Added
305
-
306
- - **`FormInvalidNet`** (ข้อเสนอจาก Pi Auth — มี `<form action>` เขียนเอง ~60 จุด): ตาข่ายฟอร์ม invalid ระดับแอป วางครั้งเดียว ฟัง `invalid` ทั้ง document แล้วแจ้งเป็น toast ข้อความเดียวกับ `UiText.invalidForm` + พาไปช่องแรกที่โฟกัสไม่ได้ · ข้ามฟอร์มที่มีตาข่ายของตัวเอง (`FormDialog`/`ActionForm`/`useFormInvalidNotice` ใส่ `data-invalid-notice` ให้; ใส่เองที่ฟอร์มเพื่อปิดได้) · วางซ้ำหลายที่ก็ไม่ขึ้น toast ซ้อน
307
- - `AppShell` วาง `FormInvalidNet` ให้เอง — หน้านอก AppShell (เช่น /login) วางใน root layout ใต้ `Toaster`
308
- - `useFormInvalidNotice().formProps` มี `data-invalid-notice` แล้ว
309
- - showcase: root layout วาง `FormInvalidNet`; `/components/action-form` มีตัวอย่าง `<form>` ดิบ
310
-
311
- ## [0.36.0] — 2026-10-07
312
-
313
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.37.0
314
-
315
- มาตรฐานใหม่ (ขอจาก Pi Auth): **ปุ่มที่มีงานค้างต้องหมุน และฟอร์มที่ส่งไม่ได้ต้องบอกผู้ใช้เสมอ — ห้ามเงียบ**
316
-
317
- ### Added
318
-
319
- - `Button loading`: ไอคอนนำหน้าเปลี่ยนเป็น spinner (ไม่มีไอคอน = เติมหน้าข้อความ ความกว้างปุ่มไม่กระตุก), กดซ้ำไม่ได้, `aria-busy="true"`, ปุ่มยังรับโฟกัส (`focusableWhenDisabled` — โฟกัสไม่หลุดไป body ตอนกด) · ไม่ใส่ `loading` + `type="submit"` ภายใน `<form action={…}>` = หมุนเองจาก `useFormStatus()` (ทุกปุ่ม submit ในฟอร์มนั้น) · `loading={false}` ปิดการตรวจอัตโนมัติ
320
- - `useFormInvalidNotice()` → `{ formProps, message, notice }`: จับ event `invalid` ของฟอร์ม รวมชื่อช่องที่ไม่ผ่านเป็นข้อความเดียว "กรุณากรอกหรือแก้ไข: ชื่อ, อีเมล" (`role="alert"`) — รวมช่องที่เบราว์เซอร์โฟกัสไม่ได้ (ซ่อนในแท็บ/ขั้นที่ปิด, input ซ่อนของ Select) ซึ่งปกติไม่มีอะไรขึ้นเลยนอกจาก console · เลื่อนไปที่ Field ของช่องแรกและโฟกัส control ที่กดได้ในนั้น · หายเองเมื่อแก้ครบ · ชื่อช่องจาก `<label>` (รวมแบบครอบของ `LabeledField`), `aria-label`, `aria-labelledby`, label ของ `Field`, placeholder, `name`
321
- - `UiText.invalidForm` (`fields` ใช้ `{fields}`, `generic`) — ไทย/อังกฤษในตัว
322
-
323
- ### Changed
324
-
325
- - `FormDialog` / `ActionForm`: ปุ่มส่งใช้ `loading` ระหว่างรอ action (เดิมแค่ disabled จาง ๆ) และใส่ตาข่าย `useFormInvalidNotice` ให้เอง — แอปไม่ต้องแก้อะไร
326
-
327
- ## [0.35.0] — 2026-10-07
328
-
329
- > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — publish 0.36.0 ทีเดียวได้ทั้งสองรุ่น
330
-
331
- ### Added
332
-
333
- - `SortableProvider renderOverlay={(drag) => …}`: วาดการ์ดลอยตามเมาส์แบบ Trello/Todoist (portal ไป `document.body` + drop animation 200ms) แถวต้นทางเหลือเป็นช่องจาง ๆ บอกตำแหน่งที่จะวาง · `useSortableState().overlay` บอกว่ากำลังใช้ overlay
334
- - `useSortableDropZone(groupId, { onDwell, dwellMs = 600 })`: ลากค้างเหนือโซนครบเวลาแล้วเรียก `onDwell` — ใช้เปิดโฟลเดอร์ที่ย่ออยู่ให้เอง
335
-
336
- ### Changed
337
-
338
- - **`@suphark/ui/sortable` ลากลื่นขึ้น** (ผู้ใช้แจ้งว่าสะดุดตอนเอาเข้าโฟลเดอร์): หาเป้าหมายจากตำแหน่งเมาส์ (`pointerWithin` → `rectIntersection`) แทน `closestCenter` ทั้งตาราง, จำเป้าหมายล่าสุดกันสั่นตอนรายการย้ายข้ามกลุ่ม, ย้ายข้ามกลุ่มแทรก **ก่อนหรือหลัง** แถวเป้าหมายตามครึ่งแถว (เดิมแทรกก่อนเสมอ → ลากเข้าจากด้านล่างแล้วกระโดดข้าม), วางบนหัวโฟลเดอร์ = ขึ้นบนสุดของโฟลเดอร์ (เดิมไปท้ายกลุ่ม/ท้ายตาราง), วัดตำแหน่งใหม่ทุกครั้งที่ layout เปลี่ยน (`MeasuringStrategy.Always`) และ transition 200ms แบบ ease-out · วัดด้วย probe ลาก APP_URL ลง 222px: เดิมกระโดดไปท้ายกลุ่มแล้วไปท้ายตาราง, ตอนนี้ลงตรงใต้เมาส์และไม่เปลี่ยนเลยระหว่างค้างนิ่ง
339
- - showcase: `/components/sortable-list` ใช้การ์ดลอย + ลากค้างเหนือโฟลเดอร์ที่ย่ออยู่แล้วเปิดให้เอง
340
-
341
- ### Docs
342
-
343
- - README (`@suphark/ui/sortable`): วิธีตรวจว่า `id` ทำงาน — `aria-describedby` ของที่จับเป็น id ที่ส่งเข้าไปตรง ๆ ไม่มี prefix `DndDescribedBy-` (prefix มีเฉพาะตอน dnd-kit สร้างเอง) และ element คำอธิบายถูกสร้างหลัง hydrate ต้องรอ mount ก่อน query (Pi Config ยืนยันบนของจริงหลัง bump 0.34.0)
344
-
345
- ## [0.34.0] — 2026-10-06
346
-
347
- ### Fixed
348
-
349
- - **`SortableProvider` ทำ hydration mismatch ทุกหน้าที่ SSR** (รายงานจาก Pi Config ตอนลากบนของจริง): ไม่ได้ส่ง `id` ให้ `DndContext` → dnd-kit สร้าง `aria-describedby="DndDescribedBy-N"` จากตัวนับที่เดินไม่ตรงกันระหว่าง server กับ client, React 19 ขึ้น error และ **ไม่ซ่อมให้** — attribute ค้างชี้ element ที่ไม่มีอยู่ ผู้ใช้ screen reader จึงไม่ได้ยินคำอธิบายวิธีลาก · ตอนนี้ค่าเริ่มต้นเป็น `useId()` (ตรงกันทั้งสองฝั่ง) และรับ prop **`id`** สำหรับ id ที่สื่อความหมาย — กระทบทุกแอปที่ใช้ `@suphark/ui/sortable` ใน Next.js
350
- - `@suphark/ui/sortable`: เสียงประกาศระหว่างลากในกลุ่มเดียวกันบอกตำแหน่ง **เดิม** ซ้ำ ("ย้ายไปตำแหน่งที่ 1 จาก 2" ทั้งที่เลื่อนลงแล้ว) เพราะลำดับจริงเปลี่ยนตอนปล่อย — ตอนนี้ระหว่างลากประกาศตำแหน่งของเป้าหมาย และตอนปล่อยประกาศตำแหน่งจริงหลังย้าย (เจอระหว่างทดสอบลากด้วยคีย์บอร์ดตามที่ Pi Config ถาม; ลากด้วยคีย์บอร์ดเองทำงานถูกอยู่แล้ว)
351
- - showcase: `/components/sortable-list` ย้ายแถว "ลากมาที่นี่เพื่อเอาออกจากกลุ่ม" ไปท้ายตาราง — เดิมโผล่เหนือกลุ่มตอนเริ่มลาก ดันแถวที่กำลังลากลง จนลากคีย์ในกลุ่มด้วยคีย์บอร์ดไม่ขยับ (README เพิ่มคำเตือนนี้ + เรื่อง `id`)
352
-
353
- ## [0.33.0] — 2026-10-06
354
-
355
- ### Fixed
356
-
357
- - **คอนทราสต์ไม่ผ่าน WCAG AA 3 จุด** (DueBell วัดจากหน้าจริงทั้งสองธีม — ผมคำนวณจาก token ซ้ำได้ตัวเลขตรงกันทุกจุด):
358
- - `--info` ธีมสว่าง: `text-info` บนพื้นขาว **4.02:1** → ลดความสว่าง L 0.588 → 0.53 = **5.05:1** (ตัวหนังสือขาวบน `bg-info` ก็ขึ้นเป็น 5.05:1 เท่ากัน) — สีฟ้าเข้มขึ้นเล็กน้อย
359
- - `--muted-foreground` ธีมสว่าง: บนพื้น `--muted` **4.35:1** (กระทบ `CountBadge tone="muted"`, แท็บที่ไม่ได้เลือกบน `TabsList`, ป้ายนับใน `SegmentedControl` …) → L 0.554 → 0.52 = **5.02:1** (บนขาว 5.4:1) — ตัวหนังสือรองเข้มขึ้นเล็กน้อยทั้งระบบ
360
- - ป้ายนับ `CountBadge tone="destructive"` (รวมปุ่มแจ้งเตือนใน `AppShell`/`MobileTabBar`) ธีมมืด: ตัวหนังสือขาวบน `--destructive` **2.89:1** → token ใหม่ **`--destructive-foreground`** (สว่าง = ขาว 4.76:1, มืด = สีพื้นเข้ม **6.97:1**) — ไม่ลดความสว่างของ `--destructive` ธีมมืดเพราะ `text-destructive` บนพื้นมืดจะไม่ผ่านแทน
361
- - ใช้ `text-destructive-foreground` คู่ `bg-destructive` ได้ทุกที่ (ใหม่ใน `@theme`)
362
-
363
- ## [0.32.0] — 2026-10-06
364
-
365
- > แก้บั๊กที่แอปรายงานหลัง 0.30.0 — เดิมเตรียมไว้ใน 0.31.0 แต่ 0.31.0 ถูก publish (12:53 น.) ก่อน commit ทั้งสามนี้ (12:56–13:09 น.) ตรวจ tarball แล้วไม่มี จึงออกเป็น 0.32.0
366
-
367
- ### Fixed
368
-
369
- - **`@suphark/ui/sortable`: แถวกลุ่มไม่ได้อยู่ใน `SortableContext`** (รายงานจาก Pi Config ตอนย้าย `values-table`) — `SortableGroup` สร้าง context ให้เฉพาะรายการในกลุ่ม ส่วนแถวกลุ่มที่ลงทะเบียนด้วย `useSortableRow({ type: "group" })` ไม่มี context ไหนรับ ทำให้ลากกลุ่มแล้วไม่มีการเลื่อนแถวให้เห็นระหว่างลาก (ปล่อยแล้วลำดับยังถูก) — ตอนนี้ `SortableProvider` สร้าง `SortableContext` ชั้นนอกจาก `order.groups` ให้เอง **ไม่ต้องแก้ call site และลำดับใน context หลุดจากลำดับจริงไม่ได้**
370
-
371
- - **`ActivityFeed relative={false}` พังทุกแถว** (รายงานจาก Pi Auth): วันที่กลายเป็น `-` และ console เต็มไปด้วย `TypeError: Invalid option` เพราะส่ง `timeStyle` ปนกับ `day`/`month`/`year` ซึ่ง `Intl.DateTimeFormat` ไม่ยอม — ต้นเหตุจริงอยู่ที่ **`ClientDateFormatter` เอา default (`day`/`month`/`year`) ไปผสมกับ options ของผู้เรียกเสมอ** ทำให้ทุกแอปที่ส่ง `dateStyle`/`timeStyle` พังเหมือนกัน; ตอนนี้ถ้าผู้เรียกใช้ `dateStyle`/`timeStyle` จะไม่ผสม default ให้ และ `ActivityFeed` เปลี่ยนไปใช้ `{ dateStyle: "medium", timeStyle: "short" }` (เทสต์ใหม่ครอบทั้งสองชั้น)
372
-
373
- ### Added
374
-
375
- - `prepublishOnly` เรียก `scripts/release-check.mjs` ก่อน publish: working tree ต้องสะอาด, HEAD ต้องตรงกับ `origin/main` หลัง fetch และ CHANGELOG ต้องมีหัวข้อของเวอร์ชันนั้น — กันปล่อยรุ่นที่ขาด commit (เกิดมาแล้ว 3 ครั้ง: 0.17.0, 0.30.0, 0.31.0; ข้อเสนอจาก Pi Auth/Pi Config)
376
-
377
- ### Changed
378
-
379
- - `ActivityFeed`: รับ `locale` แล้วส่งต่อให้ตัวฟอร์แมตวันที่ (ไม่ส่ง = `th-TH` เหมือนเดิม) — เดิมวันที่เป็นไทยตายตัวสำหรับทุกคน แอปสองภาษาจึงแสดงผิดเมื่อผู้ใช้เลือกอังกฤษ (รายงานจาก Pi Auth ตอนทำหน้า audit log); `emptyLabel` ค่าเริ่มต้นย้ายไปอ่านจาก `UiText.activity.empty` (คีย์ใหม่)
380
-
381
- ## [0.31.0] — 2026-10-06
382
-
383
- > 0.30.0 ถูก publish ก่อน commit ของ `NotificationCenter` (ตรวจ tarball แล้วไม่มีไฟล์ `shared/notification-center.tsx`) จึงเลื่อนมาออกเป็น 0.31.0 — ของอื่นใน 0.30.0 อยู่ครบตามรายการข้างล่าง
384
-
385
- ### Added
386
-
387
- - **`NotificationCenter`** — ศูนย์การแจ้งเตือน (ยกจาก DueBell `notification-modal` + `urgent-bills-widget`): ชิปนับหลายกลุ่มที่กดกรองได้ + รายการที่ต้องจัดการ พร้อมปุ่มลัดต่อรายการและเมนู "…" · ออกแบบตามข้อกำหนดที่ DueBell ให้มา — ชิปเป็น array (`counts`) ไม่ใช่ prop ตายตัว, คำสั่งเป็น array (`actions` + `menuActions`), ของโดเมนส่งเป็น node ที่แอปเตรียมแล้ว (`status`/`meta`/`footer` — ยอดเงินที่ซ่อนได้, ความคืบหน้ารอบ), **แพ็กเกจไม่ฟอร์แมตวันที่เอง** และ **ไม่ตัดรายการเอง** (จะจำกัดจำนวนให้แอป slice ก่อนแล้วส่ง `viewAll` — ตัวเลขในชิปจึงตรงกับที่ผู้ใช้เห็นเสมอ); `busy` ปิดปุ่มทุกตัวระหว่างทำคำสั่งลัด · คีย์ `UiText.notifications`
388
-
389
- ## [0.30.0] — 2026-10-05
390
-
391
- ### Added
392
-
393
- - **ข้อความที่ผู้ใช้เห็นในแพ็กเกจเปลี่ยนภาษาได้ครบแล้ว** — เก็บข้อความไทยที่ฝังตายใน JSX อีก 34 จุดเข้า `UiText`: `DataTableToolbar` (ค้นหา/ล้าง/ตัวกรอง/คำอธิบาย/ปิด), `DataTableFacetedFilter` + `FormFieldCombobox` ("ไม่พบข้อมูล"), `MultiSelectCombobox` (ล้างทั้งหมด/ลบ/เพิ่ม), `ResourcePropertyFilter` (ทั้งแผง 14 จุด), `AppShell`/`SubNav`/`TopTabs` (เมนูหลัก/เมนูย่อย/เลือกหน้า/ไม่พบ/ล้างคำค้นหา), `ErrorState` (เซสชันหมดอายุ) — คีย์ใหม่: `UiText.combobox`, `UiText.filter`, `UiText.nav`, `UiText.sessionExpired` และ `UiText.table.{search,clearSearch,filters,filtersDescription,closeFilters,clearFilter}`
394
- - **เทสต์กันข้อความฝังตาย** `no-hardcoded-text` (อยู่ใน `pnpm test`/CI): ไล่ JSX ของ `shared`/`app-shell`/`data-table`/`sortable` หาข้อความไทยที่ไม่มีทาง override — ยกเว้นได้แต่ต้องเขียนเหตุผลใน ALLOW; มีชุด "ปลูกการละเมิด" พิสูจน์ว่าตัวตรวจจับของจริงได้และไม่จับของที่ถูกต้อง (คอมเมนต์/JSDoc/`?? "…"`/พจนานุกรมของ component) — ไอเดียจาก Pi Config ที่เจอว่าเทสต์แบบนี้ล้มเหลวเงียบได้ง่าย
395
- - `SegmentedControl`: `tone` ต่อตัวเลือก (`success` / `warning` / `destructive` / `info`) — ลงสีเฉพาะตัวที่ถูกเลือก ใช้กับตัวกรองที่ "ประเภท" มีความหมายทางสี เช่นรายรับ/รายจ่าย (คำขอจาก DueBell ที่ต้องใช้ไอคอนแทนไปก่อน)
396
-
397
- ### Fixed
398
-
399
- - `MonthStepper`: ปุ่มกลางตอนอยู่เดือนปัจจุบันเคยเป็น `<Button disabled>` ซึ่ง screen reader อ่านว่า "ปุ่ม ปิดใช้งาน" ทั้งที่มันคือป้ายชื่อเดือน — ตอนนี้เรนเดอร์เป็นป้ายธรรมดาเมื่อไม่มีอะไรให้กด (รายงานจาก DueBell) และตอนกดได้ใช้ tooltip ของ `Button` แทน `title` (เลิก tooltip ซ้อนของเบราว์เซอร์)
400
-
401
- ## [0.29.0] — 2026-10-05
402
-
403
- ### Added
404
-
405
- - `InlineEdit`: คุมโหมดแก้ไขจากข้างนอกได้ — `editing` + `onEditingChange` (controlled), `defaultEditing` และ `onCancel` (กด Esc/✕) สำหรับแอปที่เข้าโหมดแก้ไขด้วย F2 หรือเมนูคลิกขวา "เปลี่ยนชื่อ" (คำขอจาก Pi Files — เดิมต้อง `.click()` ปุ่มดินสอผ่าน DOM)
406
- - **`@suphark/ui/sortable` — ลากจัดลำดับ 2 ระดับ (กลุ่ม + รายการในกลุ่ม)** entry ใหม่บน dnd-kit (แยกจาก barrel เหมือน `data-table`/`kanban` เพราะ dnd-kit สร้าง context ตอน import) — ยกสมองส่วนลากออกจากแอป ส่วนการเรนเดอร์แถวยังเป็นของแอป (ตาราง/การ์ด/รายการ) ตามสเปกของ Pi Config (`values-table.tsx`)
407
- - `SortableProvider` — sensors (เมาส์ + คีย์บอร์ด), collision ที่กันลากข้ามชนิด (กลุ่มชนได้แค่กลุ่ม, รายการชนได้แค่รายการกับที่วางของกลุ่ม), ย้ายข้ามกลุ่มระหว่างลาก, commit ตอนปล่อย และ **คืนลำดับเดิมอัตโนมัติเมื่อบันทึกล้มเหลว** (throw หรือ `{ ok: false, error }`) — ไม่มี `onCommitError` = ขึ้น toast ให้; ส่งมา = ส่ง error ดิบให้แอปจัดการเอง ไม่กลืนเงียบ
408
- - `SortableGroup` (ขอบเขตการเรียงของกลุ่ม), `useSortableRow({ type, id })` → `{ ref, style, handleProps, isDragging }`, `useSortableDropZone(groupId)` (แปะที่ไหนก็ได้ — หัวกลุ่ม หรือแถว placeholder ของกลุ่มที่ไม่มีหัว; กลุ่มที่ย่ออยู่ก็ยังรับการวางได้), `useSortableState()` → `{ dragging, disabled }`, `DragHandle` (ใช้ `Button` ของแพ็กเกจ จึงได้ tooltip จาก `aria-label`; ปิดการลากแล้วที่จับจางลงเอง หรือซ่อนไปเลยด้วย `hideWhenDisabled`)
409
- - `announcements` + `screenReaderInstructions` ส่งเป็น prop ได้ (callback ได้ `{ id, kind, groupId, index, total }` ให้แอปประกอบข้อความจากชื่อจริง) ไม่ส่ง = แม่แบบจาก `UiText.sortable`
410
- - helper แบบ pure ใน `lib/sortable-order`: `sortableOrderOf`, `reorderGroups`, `reorderItems`, `moveItemToGroup`, `groupOfItem`, `sameOrder`, `moveWithin`, `sortableOrderToPayload` (คืน `{ groups: string[], items: [{ groupId: string | null, itemIds }] }` — ลำดับกลุ่มกับรายการในกลุ่มครบในก้อนเดียว, `UNGROUPED` → `null`)
411
- - `UiText.sortable` — `handle`, `instructions`, `started`, `moved`, `dropped`, `cancelled` (`{index}` / `{total}`)
412
- - showcase: หน้า `/components/sortable-list` — ตาราง 2 ระดับ ลากเรียงกลุ่ม/คีย์/ย้ายข้ามกลุ่ม พร้อมแถว "ลากมาที่นี่เพื่อเอาออกจากกลุ่ม" ที่โผล่เฉพาะตอนลากคีย์
413
-
414
- ## [0.28.1] — 2026-10-05
415
-
416
- ### Fixed
417
-
418
- - **`AppShell` ปุ่มย่อ/ขยายเมนูยังมี "Toggle Sidebar" อังกฤษค้างอยู่ใน `sr-only`** (รายงานจาก DueBell): `SidebarTrigger` ของ shadcn ฝังข้อความนั้นเป็น children ซึ่งแทนด้วย props ไม่ได้ (JSX children ชนะ) — 0.27.1 เปลี่ยนได้แค่ `aria-label` ตอนนี้ `AppShell` เรนเดอร์ปุ่มเองจาก `Button` ของแพ็กเกจ + `useSidebar()` จึงใช้ `UiText.toggleSidebar` ทั้งชื่อและ tooltip และไม่มีข้อความอังกฤษเหลือในปุ่ม (ยังคง `data-sidebar="trigger"` ไว้ให้ selector เดิมใช้ได้)
419
-
420
- ## [0.28.0] — 2026-10-05
421
-
422
- ### Added
423
-
424
- - **`ActivityFeed`** — เส้นเวลา "ใครทำอะไรเมื่อไหร่" จาก array เดียว (audit log ของ Pi Auth, ประวัติงานของ Pi Done, timeline ของเอกสาร): avatar ของผู้ทำหรือไอคอน+โทนสำหรับเหตุการณ์ของระบบ, เวลาสัมพัทธ์ที่คำนวณฝั่ง client หลัง mount (ไม่ทำ hydration mismatch) และช่องใส่ปุ่ม/ป้ายท้ายแถว
425
- - **`InlineEdit`** — แก้ค่าในที่: คลิกดินสอ → ช่องกรอก (เลือกข้อความเดิมไว้ให้พิมพ์ทับได้ทันที) → Enter/✓ บันทึก, Esc/✕ ยกเลิก พร้อม `validate`, สถานะกำลังบันทึก และ toast เมื่อล้มเหลว (ถือ `{ ok: false, error }` เป็นล้มเหลว, `redirect()` เป็นสำเร็จ) — ใช้กับค่าสั้น ๆ ที่ไม่คุ้มเปิด `FormDialog`
426
- - **`SelectionBar`** — แถบคำสั่งหมู่ "เลือก N รายการ" + ปุ่มคำสั่ง + ปุ่มล้าง (`floating` กลางล่างจอ หรือ `inline` ในหน้า) ใช้คู่ `DataTable` หรือ `SimpleTable`; ข้อความจาก `UiText.selection`
427
- - **`SimpleTable` เลือกหลายแถวได้** — `selected` + `onSelectedChange` เปิดช่องติ๊กหน้าแถวและหัวตาราง (เลือกบางส่วนแสดงเป็นขีด), แถวที่เลือกมีพื้นจาง; คลิกช่องติ๊กไม่ไปโดน `onRowClick`
428
- - `SimpleTabs`: แท็บล้นความกว้างแล้วเลื่อนแนวนอนได้ (ซ่อนแถบเลื่อน) + เงาขอบบอกว่ายังมีแท็บต่อ
429
- - `UiText` คีย์ใหม่: `edit`, `selection.{selected,clear}`, `table.{rowsPerPage,pageOf,rowsSelected,noResults,deleteTitle,deleteDescription,deleting,deleted,delete}`
430
-
431
- ### Fixed
432
-
433
- - **`DataTable` เลิกใช้ข้อความอังกฤษตายตัว** — "Rows per page", "Page X of Y", "N of M row(s) selected", "No results.", เมนู Edit/Delete, กล่องยืนยันลบ และ toast ตอนลบ ทั้งหมดมาจาก `UiText.table` (ค่าเริ่มต้นภาษาไทย) — `resourceName` แทรกผ่าน `{resource}`
434
-
435
- ## [0.27.1] — 2026-10-05
436
-
437
- ### Added
438
-
439
- - `Button`: ถ้า `aria-label` ไปอยู่บน element ใน `render=` (เช่น `render={<Link aria-label="…"/>}`) ก็ใช้เป็น tooltip ได้ (เดิมต้องอยู่บน `Button` เท่านั้น — รายงานจาก Pi Auth)
440
- - เทสต์ชุดใหม่ `button-tooltip-triggers`: ยืนยันว่าปุ่มไอคอนที่ส่งเป็น `render=`/`trigger` ของ `DropdownMenuTrigger` / `PopoverTrigger` / `FormDialog` / `RowActions` ยังได้ tooltip จริง (เช็กจาก popup ที่ขึ้น ไม่ใช่ `data-slot` — `data-slot` ของปุ่มจะกลายเป็นของ trigger ชั้นนอกเสมอ ใช้ชี้วัดไม่ได้)
441
-
442
- ### Fixed
443
-
444
- - **ตัวห่อปุ่มของแพ็กเกจเองหล่น 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 ด้วย)
445
- - `ActionButton` / `LinkButton`: มี `tooltip` ของตัวเองอยู่แล้ว → ส่ง `tooltip={false}` ให้ `Button` ชั้นใน กันขึ้นสองชั้น (props `tooltip`/`tooltipSide` ของ `ActionButton` ยังเป็นแบบเดิมทุกประการ)
446
- - `DataTableRowActions`: ปุ่ม "…" ใช้ `aria-label` จาก `UiText.actions` แทน `sr-only` อังกฤษ "Open menu"
447
- - **กฎใหม่กันพลาดซ้ำ**: `pnpm check:button` (อยู่ใน `verify` และ CI) ห้ามไฟล์ในแพ็กเกจ import `Button` จาก `components/ui/button` ยกเว้นไฟล์ shadcn เองกับตัวห่อ
448
- - **`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`)
449
-
450
- ## [0.27.0] — 2026-10-05
451
-
452
- ### Added
453
-
454
- - **`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 ให้)
455
- - props ใหม่: `tooltip` (ทับข้อความ, `false` = ปิด), `tooltipSide`, `tooltipDelay` — ไม่ต้องมี `TooltipProvider` ใน tree (ใส่ได้ถ้าอยากคุม delay ร่วมกัน)
456
- - `SimpleTooltip` ปิด tooltip อัตโนมัติของ `Button` ที่ห่ออยู่ให้แล้ว (ไม่ขึ้นสองชั้น) — แอปที่ห่อ tooltip ด้วยกลไกอื่นใช้ `ButtonTooltipSuppress` (export ใหม่) หรือ `tooltip={false}`
457
- - barrel export `Button` จาก `components/shared/button/button.tsx` (ครอบ `components/ui/button.tsx` ที่ยังเป็นไฟล์ shadcn ดิบ ไม่ถูกแก้มือ); `buttonVariants` เหมือนเดิม; props อื่นและหน้าตาไม่เปลี่ยน
458
- - `DataTablePagination`: ปุ่มเลื่อนหน้าทั้ง 4 ใช้ `aria-label` จาก `UiText` (เดิมเป็นข้อความอังกฤษตายตัวใน `sr-only`); `ThemeToggle` ใช้ `aria-label` จาก `UiText.theme.label` แทน `title` (เลิก tooltip ซ้อนของเบราว์เซอร์)
459
- - README หัวข้อ "ปุ่มไอคอน — tooltip มาให้เอง (มาตรฐานทุกโครงการ)" + แถวใหม่ใน `docs/agent-rules.md`
460
- - ใช้ข้อความใน `<span className="sr-only">` (รูปแบบของ shadcn) เป็น tooltip ได้เมื่อไม่มี `aria-label`
461
- - **ปุ่มไอคอนในแพ็กเกจเองได้ tooltip ครบ**: `RowActions`, `ActionMenu`, `SimpleDialog`/`SimpleSheet` (ปุ่มปิด), `SimplePagination`, `DataTablePagination`, `DataTableToolbar`, `CommandPalette`, `ThemeToggle`/`ThemeSwitch`, `Notice`, `UploadList`, `SecretReveal`, `TimePicker`, `MonthStepper`, `RuleBuilder`, `ResourcePropertyFilter`, `AuthParts` (17 ไฟล์เปลี่ยนมา import `Button` ตัวที่มี tooltip)
462
- - `UiText.pagination` เพิ่ม `first` / `last` (หน้าแรก/หน้าสุดท้าย)
463
- - showcase: หน้า `/components/button` เพิ่มแถวปุ่มไอคอน (auto / `tooltip="…"` / `tooltip={false}` / trigger ของ dialog / ห่อ `SimpleTooltip`)
464
-
465
- ## [0.26.1] — 2026-10-03
466
-
467
- แก้ตามผล audit 2026-10-03 (`docs/reports/audit-2026-10-03.md`) — ไม่เปลี่ยน props
468
-
469
- ### Fixed
470
-
471
- - **`RuleBuilder`**: แก้ค่าในเงื่อนไขแล้ว element ถูกสร้างใหม่ทุกครั้ง (key ผูกกับ object) → ช่องที่กำลังพิมพ์หลุดโฟกัสทุกตัวอักษร — object ใหม่ที่มาแทนใช้ key เดิม
472
- - **`RowActions` / `ConfirmActionButton` / `ActionMenu` / `StepForm` / `ChatThread`**: server action ที่ `redirect()`/`notFound()` เคยขึ้น toast "NEXT_REDIRECT" — ถือว่าสำเร็จ (Next นำทางให้แล้ว); `ActionMenu`/`StepForm`/`ChatThread` ถือ `{ ok: false, error }` เป็นล้มเหลวด้วย (ตัวช่วยใหม่ใน `lib/action-result.ts`: `errorMessage`, `failedResult`, `isNavigationError` — `@suphark/ui/next` ยัง export สองตัวแรกจาก confirm-action-button เหมือนเดิม)
473
- - **`RowActions` + `confirm`**: เคยปิด dialog ก่อนรัน action — ตอนนี้ปุ่มยืนยันหมุนระหว่างรอ ล้มเหลวคง dialog ไว้ให้ลองใหม่ สำเร็จค่อยปิด (ตามที่ 0.25.1 ตั้งใจ)
474
- - **`useDialogs().confirm`** รองรับ `requireText` (เดิมข้ามการพิมพ์ยืนยันไปเงียบ ๆ)
475
- - **`DataTable` / `useDataTable`**: เลือกแถวตาม `row.id` เป็นค่าเริ่มต้น (ไม่มี id = index เหมือนเดิม) — เดิมใช้ลำดับแถว ข้อมูลเปลี่ยน/แทรกแถวแล้วแถวที่เลือกกลายเป็นแถวอื่น (`DeleteManyButton` ลบผิดรายการ); การลากเรียงเทียบ id แบบ string
476
- - **`useDataTable` `columnFilters` (regression 0.25.1)**: เทียบด้วยค่าแต่ตั้ง state เป็นค่าจริงจาก prop — เดิม `JSON.parse` ทำให้ `Date` กลายเป็น string; ค่าที่ stringify ไม่ได้ (BigInt/วงอ้างอิง) ไม่ throw ตอน render
477
- - **`DataTable` ช่องค้นหา**: Enter ไม่ส่งซ้ำอีกรอบหลัง 250 ms; ปุ่มล้างตัวกรองล้างข้อความที่พิมพ์ค้างด้วย
478
- - **`NumberInput` / `TagsInput` ใน `<label>` ที่ครอบ** (`LabeledField` ไม่ใส่ `htmlFor`): คลิกข้อความ label เคยกดปุ่ม "ลด"/ลบป้ายแรก — ช่องพิมพ์อยู่ก่อนปุ่มใน DOM แล้ววางตำแหน่งด้วย CSS `order`
479
- - `NumberInput`: ทั้งช่องเคยจางเหมือนปิดใช้งานเมื่อค่าเท่ากับ `min`/`max` (`has-disabled` จับปุ่ม −/+ ที่ปิด) — จางเฉพาะเมื่อช่องปิดจริง; `step` ที่เปลี่ยนหลัง mount ไม่ทำให้ทศนิยมถูกปฏิเสธอีก; `size="sm"` 16px บนมือถือ (iOS ไม่ซูม)
480
- - `TagsInput`: ค่าที่ถูกปฏิเสธค้างในช่อง = ช่อง invalid (ฟอร์มไม่ส่งรายการที่ขาดค่านั้นไปเงียบ ๆ)
481
- - `TimePicker`: ปุ่ม "ตอนนี้" ปัดตาม `roundToStep`
482
- - **`StepForm`**: ดับเบิลคลิก "ถัดไป" ไม่ข้ามขั้นกลาง; `onSubmit` ที่จบเร็ว/sync ไม่ถูกส่งซ้ำ; ตัวควบคุม Base UI ที่ไม่ผ่าน (Select/Checkbox/Switch) โฟกัสตัวมันเอง ไม่ใช่ช่องอื่น
483
- - **`CommandPalette`**: เช็ก Ctrl/⌘ ก่อนอ่าน `e.key` (keydown จาก autofill ไม่มี key ไม่ throw); ไม่เปิดด้วย AltGr (ctrl+alt); แป้นละตินเทียบ `e.key` (Dvorak ฯลฯ) แป้นไทยใช้ `e.code`; 2 ตัวในหน้าเดียวเปิดทีละตัว
484
- - `ChatThread`: ข้อความที่พิมพ์ระหว่างรอส่งไม่ถูกล้างทิ้ง
485
- - `SettingsLayout`: มือถือกด "กลับ" ขณะหัวข้อที่เปิดถูกกรองด้วยคำค้น โฟกัสไปช่องค้นหาแทนหลุดไป body
486
- - **service worker (PWA)**: reload หลังมีเวอร์ชันใหม่เคยเกิดทุก `focusout` (ย้ายช่อง/กดบันทึก = reload กลางฟอร์ม) — รอนิ่ง 2 วินาทีหลังออกจากช่อง ยกเลิกเมื่อมีการคลิก/พิมพ์/โฟกัสใหม่
487
- - `PullToRefresh`: ฟัง scroll แบบ capture ที่ document (root ที่ mount ทีหลัง/หน้าเลื่อนทั้ง document ใช้ได้)
488
- - `ProviderButtons`: ลิงก์ที่ปิดอยู่ดูปิดจริงและถอด `href` (กันคลิกกลาง); `aria-busy` ทั้งสองแบบ
489
- - `useTableSearchParams`: ค่าที่รอส่งไม่ค้างเมื่อ URL ไม่เปลี่ยน; key `__proto__` จาก URL ไม่ทับ prototype
490
- - `MonthStepper`: JSDoc กลับมาแสดงใน editor; `DesignSystemShowcase` กระโดดข้ามแท็บรอบใหม่ได้เวลารอเต็ม
491
- - CSS: scrollbar ทั้งหน้า (`*{scrollbar-width}`) ย้ายเข้า `@layer base` — เดิมทับ `no-scrollbar`/className ของแอปทุกที่ (เช่น คอลัมน์ TimePicker)
492
-
493
- ### Security
494
-
495
- - **`ActionMenu` href + confirm**: นำทางผ่าน `location.assign`/`window.open` ซึ่งไม่กัน `javascript:` แบบ `<a href>` — รับเฉพาะ http(s)/path ภายใน (`lib/safe-url.ts` `safeNavigableUrl` ใช้ร่วมกับ `ClientRedirect`); ลิงก์แท็บใหม่ที่ไม่มี onClick เปิดทันทีในคลิก (ไม่โดน popup blocker)
496
- - `check:barrel` จับ `export *` ทุกรูปแบบ (alias `@suphark/ui/...`, แพ็กเกจ, quote เดี่ยว, comment ท้ายบรรทัด) และรันใน `verify` ของแพ็กเกจ + `prepublishOnly` (publish ไม่ผ่าน CI ก็ยังถูกตรวจ); README สอนขั้นตอนที่ถูก (`fix:barrel`)
497
- - showcase: demo ไม่มีค่าที่หน้าตาเหมือนคีย์จริง (`pk_live_`) แล้ว; หน้า time-picker บอกรูปแบบค่า DateTimePicker ที่มี offset
498
- - tests 12 กรณี (`fixes-0-26-1.test.tsx`, RuleBuilder, service worker)
499
-
500
- - showcase (security): `next` ^16.3.5 → ^16.3.8 — 16.3.5 มี advisory **critical** GHSA-vcvr-r3jv-pc5j (RCE ใน `next/og` ImageResponse; showcase ไม่ได้ใช้ `next/og` จึงไม่ถูกโจมตีได้จริง แต่ `pnpm audit` แจ้ง) — `pnpm audit --prod` เหลือ 0; devDependency `next` ของแพ็กเกจ bump ตาม (peer ยัง `>=15`)
501
- - showcase (security): `@suphark/auth-client` ^0.6.1 → **^0.6.2** (แก้ dot segment ต้นทาง) — ลบตัวกรอง `?next=` ชั่วคราวใน `/auth/[...pi]` ออก; ทดสอบ `/auth/logout` 7 กรณี (`/.//`, `/..//`, `/%2e%2e//`, `/./../..//`, tab, `//`, `/brand`) ด้วย server จริงบนเครื่องแล้ว — ปลายทางไม่ออกนอกโดเมน
502
- - showcase: `@suphark/config-client` ^0.3.0 → ^0.9.2 (CLI `pi-config pull` ตอน build บน Vercel — ข้าม 0.9.0 ที่ pull ค่าหลายบรรทัด CRLF ไม่ได้; 0.9.x บังคับ `PI_CONFIG_URL` เป็น https, ไม่ตาม redirect, timeout 30s; 0.9.2 แก้ crash exit 127 บน Windows) — ทดสอบ pull ด้วย token จาก env (แบบ Vercel) ได้ 6 คีย์; diff/--check บน Windows exit 0/1 ถูกต้อง 10 รอบไม่ crash
503
- - showcase (security): `safeNextPath` ปฏิเสธผลที่ normalize แล้วเป็น `//host` — dot segment `/.//evil.com`, `/..//evil.com`, `/%2e%2e//evil.com` เคยผ่าน (origin ตอน parse ยังเดิม) แล้ว redirect ไป evil.com; `/auth/[...pi]` กรอง `?next=` ด้วยตัวนี้ก่อนส่งให้ handler ของ `@suphark/auth-client` 0.6.1 ที่มีช่องเดียวกัน (แก้ต้นทางใน 0.6.2 — ลบตัวกรองได้หลัง bump) — production ก่อน deploy นี้: `curl /auth/logout?next=/.//evil.com` → 303 https://evil.com/ (รายงานโดย pi-auth และ pi-config)
504
-
505
- ## [0.26.0] — 2026-10-03
506
-
507
- ปิดงานค้างจากผลรีวิว + prop ที่ DueBell ขอ — เพิ่มอย่างเดียว
508
-
509
- ### Added
510
-
511
- - `NumberInput`: `inputRef` (ref ของช่องที่พิมพ์ — focus เองได้) และ `autoFocus` (คำขอจาก DueBell)
512
- - `TimePicker`: `roundToStep` — ปัดนาทีที่พิมพ์ลงตาม `minuteStep` (เช่น step 60 + พิมพ์ 14:30 → 14:00); ค่าเริ่มต้นเก็บตามที่พิมพ์เหมือนเดิม (คำขอจาก DueBell)
513
-
514
- ### Changed
515
-
516
- - **ช่องกรอกใช้ขนาดตัวอักษรตามบทบาท `text-body`** (16px มือถือ / 15px จอกว้าง) — `Input`, `Textarea`, `SelectTrigger`, `NativeSelect` (+ `NumberInput`, `TagsInput`): เดิม `text-base md:text-sm` = 14px บนจอกว้าง เล็กกว่าข้อความรอบข้าง 1px และ `text-body` ที่แอปใส่ให้ช่องได้ผลแค่มือถือ (pi-done ต้องเติม `md:text-body` เอง 19 จุด — ลบได้แล้ว); `SelectTrigger`/`NativeSelect` เดิม 14px แม้บนมือถือ (iOS ซูมเมื่อโฟกัส) — แก้ไฟล์ shadcn 4 ไฟล์เป็นข้อยกเว้น (เพิ่มในรายการคืนไฟล์หลัง `shadcn add --overwrite` ใน CLAUDE.md); `className` ของแอปยังแทนได้ (`cn` รู้จักคลาสตามบทบาท)
517
- - ปุ่ม `ActionButton`/`LinkButton` ที่มี tooltip ไม่สร้าง `TooltipProvider` ต่อปุ่มแล้ว (ตาราง 50 แถว = 100+ provider) — ใช้ `delay={0}` ที่ trigger แทน หน้าตา/จังหวะเหมือนเดิม
518
- - `DesignSystemShowcase`: แท็บอื่นนอกจาก "ภาพรวม" โหลดแบบ lazy เมื่อเปิดแท็บ (เดิม import ทุกแท็บ ~1 MB) — ลิงก์ข้ามแท็บไปหัวข้อรอแท็บโหลดเสร็จก่อนเลื่อน
519
- - ลบ dependency `cn` ที่ไม่ได้ใช้ (แอปไม่ต้องติดตั้งเพิ่ม)
520
- - docs: agent-rules — barrel ที่ client import ห้าม re-export schema ของ zod (ค่า) ให้ export แค่ type (DueBell −18% ต่อหน้า); `ChartConfig` ใช้เฉพาะค่าที่แอปกำหนดเอง (ค่าถูกเขียนลง `<style>` ตรง ๆ)
521
- - CI: `permissions: contents: read` (+ `pull-requests: read` ให้ gitleaks) — GITHUB_TOKEN อ่านได้อย่างเดียว; pin action ด้วย commit SHA (checkout v4.4.0, gitleaks-action v2.3.9, pnpm/action-setup v4.3.0, setup-node v4.4.0)
522
- - showcase: `/components/[slug]` โหลด demo แบบ `next/dynamic` ต่อ slug (`src/docs/demos.tsx` + `DemoView`) — เดิมทุกหน้าโหลด demo ทั้ง 129 ตัว (~409 kB gzip ต่อหน้า เช่น `/components/badge` พก recharts)
523
- - showcase: demo `code-block`/`secret-reveal` ใช้ค่า `example_…` แทน `sk_live_…` (ไม่ให้ secret scanner แจ้งเตือนผิด)
524
- - showcase: `@suphark/auth-client` ^0.1.7 → **^0.6.1** (แก้ open redirect ใน `safeNextPath` ของ handler `/auth/login|callback|logout` — ทดสอบ `/auth/logout?next=/%09/evil.com` แล้วไม่ออกนอกโดเมน); breaking ระหว่างทาง (ลบ Prisma adapter, เปลี่ยนชื่อ `ACCESS_DENIED`) ไม่กระทบ showcase
525
-
526
- ## [0.25.1] — 2026-10-02
527
-
528
- แก้ตามผลรีวิว codebase (bug / security / performance) — ไม่เปลี่ยน props
529
-
530
- ### Fixed
531
-
532
- - **`NumberInput`**: input ซ่อนของ Base UI (ที่ส่งค่าใน FormData) ได้ `step` ไปด้วย → browser ปฏิเสธค่าทศนิยม/ค่าที่ไม่ลง step นับจาก `min` แล้ว **บล็อก submit เงียบ ๆ** (เช่น 1500.5 เมื่อ step=1, 150 เมื่อ min=0 step=100 — DueBell เจอ) → input ซ่อนใช้ `step="any"` (ปุ่ม −/+ ยังเลื่อนตาม `step`; min/max ยังตรวจ)
533
- - **`TimePicker`**: พิมพ์ผิดรูปแบบแล้วกดส่ง ฟอร์มเคยส่งค่าเดิมไปเงียบ ๆ → ช่องเป็น invalid จริง (`setCustomValidity`, ข้อความ `UiText.time.invalid`) ฟอร์มไม่ส่งจนแก้
534
- - **`DateTimePicker`**: แยกไฟล์ (`form/date-time-picker.tsx`); ข้อความวัน/เวลาและค่าใน input ซ่อนคำนวณหลัง hydrate (เดิม server UTC กับ browser ต่างกัน 7 ชม. = hydration mismatch เมื่อมีค่าเริ่มต้น); **`name` ส่ง `"YYYY-MM-DDTHH:mm+07:00"` (มี offset)** แทนค่าที่ไม่มีโซนเวลา → `new Date(formData.get(name))` บน server ได้เวลาที่ถูก (เดิมคลาด 7 ชม.)
535
- - `TimeColumns`: เลื่อนเฉพาะในคอลัมน์ (เดิม `scrollIntoView` เลื่อนทั้งหน้าทุกครั้งที่เลือก)
536
- - **`MonthStepper` / `useMonthCursor`**: "เดือนนี้" คิดตามเวลาไทย (`Asia/Bangkok`) ให้ server กับ browser ตรงกัน — เดิมช่วง 00:00–06:59 ของวันที่ 1 ทุกเดือน server (UTC) เห็นเดือนก่อน = hydration mismatch; `currentMonthKey(now?, timeZone?)` รับโซนเวลาได้ (ไม่ส่ง = เครื่องเหมือนเดิม)
537
- - **`StepForm`**: ดับเบิลคลิกปุ่มส่ง/ถัดไปเคยเรียก `onSubmit` 2 ครั้ง หรือข้ามหน้าตรวจทาน (คลิกที่สองตกที่ปุ่ม "ส่ง" ตำแหน่งเดิม) → กันซ้ำแบบ synchronous + ไม่นับคลิกภายใน 500 ms หลังเปลี่ยนขั้น; input ซ่อนของ Base UI ผิด → โฟกัสช่องที่มองเห็นของมันเอง (เดิมโฟกัสช่องแรกของขั้น)
538
- - **`RowActions` / `ConfirmActionButton`**: action ที่คืน `FormState` `{ ok: false, error }` (ไม่ได้ throw) เคยถูกถือว่าสำเร็จ (ปิด dialog + refresh ไม่มีข้อความ) → toast error + คง dialog ไว้ (`failedResult`)
539
- - **`ActionMenu`**: รายการที่มี `href` + `confirm` เคยไปเลยโดยไม่ถาม → ถามก่อนแล้วค่อยไป; `onClick` ที่ throw/reject แสดง toast และคง dialog ไว้ (เดิม unhandled rejection เงียบ)
540
- - `ChatThread`: `onSend` ล้มเหลว = toast + คงข้อความในช่องไว้ให้ลองใหม่
541
- - `CommandPalette`: คีย์ลัดเทียบ `e.code` — แป้นภาษาไทย Ctrl+K ใช้ได้แล้ว (เดิม `e.key` = "า")
542
- - `SettingsLayout`: มือถือกด "กลับ" แล้วคืนโฟกัสไปหัวข้อเดิม (เดิมหลุดไป body); จอกว้างไม่ย้ายโฟกัสไปหัวข้อ (เดิมย้ายแค่ครั้งแรก)
543
- - `TagsInput`: วางหลายค่าแทรกที่ cursor ของข้อความที่พิมพ์ค้าง (เดิมทิ้งข้อความนั้น)
544
- - `ProviderButtons`: ปุ่มที่ `pendingId` ตรงแสดง spinner และกดซ้ำไม่ได้ (รวมแบบ `href`)
545
- - `useTicker`: subscribe คงที่ข้าม render — เดิมสมัครใหม่ทุก render ทำให้ "x นาทีที่แล้ว" ไม่อัปเดตถ้า parent render บ่อยกว่า interval
546
- - `useTableSearchParams`: setter หลายตัวติดกัน (เช่น setFilter แล้ว setSort) ต่อจากค่าที่เพิ่งตั้ง (เดิมตัวหลังทับตัวแรก)
547
- - `useDataTable`: sync `columnFilters` ตามค่า (เดิมตามตัวอ้างอิง → array literal ทำให้ render ซ้ำ + ทับตัวกรองที่ผู้ใช้เพิ่งเลือก)
548
-
549
- ### Security
550
-
551
- - `ClientRedirect`: รับเฉพาะ http(s)/path — ปฏิเสธ `javascript:`/`data:` (`location.replace` ไม่มีตัวกันแบบ `<a href>`/router)
552
- - showcase: `/login` ใช้ `safeNextPath` (`src/lib/safe-next.ts`) กัน open redirect `next=/%09/evil.com` (WHATWG URL ตัด tab → `//evil.com`); production (`VERCEL_ENV=production`) ที่ไม่มี env ของ Pi Auth หรือตั้ง `PI_AUTH_DISABLED` = build ล้ม แทนเปิดเว็บสาธารณะเงียบ ๆ — ตัว `safeNextPath` ของ `@suphark/auth-client` แก้ใน repo auth แล้ว (0.6.1 รอ publish)
553
-
554
- ### Performance
555
-
556
- - **`DataTable`**: ส่วนลากเรียง (dnd-kit ~16 kB gzip) แยกเป็น `data-table-sortable.tsx` โหลดแบบ lazy เฉพาะเมื่อส่ง `onReorder` — เดิมทุกตารางพกไปด้วย
557
- - **`FormFieldInput`**: variant `autocomplete` (cmdk + Radix Dialog ที่ cmdk พ่วงมา) แยกเป็น `form-field-autocomplete.tsx` โหลดแบบ lazy — เดิมทุกฟอร์มที่ใช้ช่องข้อความธรรมดาพก cmdk
558
- - `DataTable` ช่องค้นหา: ส่งให้ตารางเมื่อหยุดพิมพ์ 250 ms (Enter = ทันที) — เดิมกรอง/เรียง/render ทุกแถว (และ `router.replace` ถ้าผูก URL) ทุกตัวอักษร
559
- - `TimePicker` ไม่พา react-day-picker มาด้วยแล้ว (แยก `DateTimePicker`); import locale ตรงไฟล์ (`date-fns/locale/th`, `date-fns/locale/en-US`, `react-day-picker/locale/th`) — dev server ไม่ต้อง compile locale ทั้ง ~700 ไฟล์
560
- - `PullToRefresh`: ติดตั้ง `touchmove` แบบ non-passive เฉพาะตอนจอมือถือ + หน้าอยู่บนสุด (เดิมติดตลอด → browser ต้องรอ JS ก่อนเลื่อนทุกครั้ง)
561
- - `TreeView`: `expandedIds` แบบ controlled ไม่ทำให้ memo ของแถวคำนวณใหม่ทุก render
562
- - showcase: หน้า number-input อธิบาย step ตามพฤติกรรมใหม่
563
- - tests 12 กรณี (`fixes-0-25-1.test.tsx`, `lazy-parts.test.tsx` + StepForm/NumberInput ใน `inputs-0-25.test.tsx`)
564
-
565
- ## [0.25.0] — 2026-10-02
566
-
567
- input ใหม่ตามแผนจาก shadcnstudio.com — เพิ่มอย่างเดียว
568
-
569
- ### Added
570
-
571
- - **`NumberInput`** — ช่องตัวเลขพร้อมปุ่ม −/+ (Base UI NumberField): `min`/`max`/`step`/`largeStep` (Shift+ลูกศร), `format` (Intl เช่น ทศนิยม 2 ตำแหน่ง/สกุลเงิน), `locale` (ค่าเริ่มต้น th-TH), `prefix`/`suffix` (฿, %, ชิ้น), `hideButtons`, `size` sm|default, `name` ส่งค่าดิบใน FormData — แทน `<Input type="number">` + InputGroup + state
572
- - **`TimePicker`** — ช่องเวลา 24 ชม. ค่า `"HH:mm"`: พิมพ์ได้เลย (`"1430"`, `"9.15"`, `"14:30 น."` → แปลงตอน blur/Enter, ผิดรูปแบบ = `aria-invalid`) หรือกดไอคอนนาฬิกาเลือกจากคอลัมน์ชั่วโมง/นาที (`minuteStep`, ปุ่ม "ตอนนี้"); `name` ส่งใน FormData
573
- - **`DateTimePicker`** — ปฏิทิน (ภาษาไทย) + คอลัมน์เวลาในกล่องเดียว ค่า `Date | null`, `defaultTime` เมื่อเลือกวันครั้งแรก, ปุ่มล้าง; `name` ส่ง `"YYYY-MM-DDTHH:mm"` (เวลาท้องถิ่น แบบ datetime-local)
574
- - `TimeColumns` (คอลัมน์ listbox ใช้ลูกศร/Home/End), `parseTimeInput`, `toLocalDateTimeString`, `type TimeValue`
575
- - **`TagsInput`** — พิมพ์หลายค่าเป็นป้าย: Enter/จุลภาค (+ เว้นวรรคถ้า `splitOnSpace`) เพิ่ม, วางหลายค่าพร้อมกัน, Backspace ลบตัวสุดท้าย, ไม่รับค่าซ้ำ (`allowDuplicates`), `max`, `transform`, `validate` คืนข้อความ error (ค่าที่ไม่ผ่านคงอยู่ในช่องให้แก้), ปลอดภัยกับ IME ไทย; `name` ส่งทุกค่า → `formData.getAll(name)` — redirect URI/โดเมน/อีเมล
576
- - **`StepForm`** — ฟอร์มหลายขั้น (wizard) จาก array `steps` (`title`, `description`, `content`, `validate(formData)`): แถบขั้นตอน (ขั้นที่ผ่านแล้วกดย้อนได้, จอแคบแสดง "ขั้นที่ x จาก y"), ปุ่มย้อนกลับ/ถัดไป/ส่ง, "ถัดไป" ตรวจ required/pattern + `validate` ของขั้นนั้น, ส่ง = ตรวจทุกขั้นอีกครั้งแล้วพากลับไปขั้นที่ผิด, input ตัวแทนที่ซ่อนของ Base UI ผิด = แสดงข้อความใต้ฟอร์ม; ทุกขั้นอยู่ใน form เดียว → `onSubmit(formData)` ได้ค่าครบ (ส่ง server action ต่อได้), throw = แสดงข้อความ, ปุ่มหมุนระหว่างรอ, โฟกัสย้ายไปหัวข้อขั้นใหม่
577
- - `UiText` key ใหม่: `number`, `time`, `tags`, `stepForm`
578
- - showcase: หน้า `/components/{number-input,time-picker,tags-input,step-form}` (129 หน้า); tests 11 กรณี
579
-
580
- ### Performance
581
-
582
- - **barrel ไม่ใช้ `export *` แล้ว** (`index.ts` 66 บรรทัด, `next.ts`, `data-table.ts`, `components/data-table/index.ts` → รายชื่อ export ชัด ๆ แยก `type`; รายชื่อ export เท่าเดิมทุกตัว ตรวจด้วย TypeScript แล้ว) — วัดบน showcase (Next 16.3 Turbopack): server component ที่ import จาก `@suphark/ui` แบบ `export *` ทำให้ client component แทบทั้งแพ็กเกจ (ปฏิทิน, date-fns, cmdk, input-otp, accordion, questionnaire …) ถูกส่งไปทุกหน้า — **root layout ที่ใช้แค่ `ThemeProvider`/`Toaster` ลดจาก 988 kB (gzip 311 kB) เหลือ 61 kB (gzip 18 kB)**, หน้า `/login` 991 → 114 kB; แอปไม่ต้องแก้โค้ด แค่ bump (`optimizePackageImports` / `turbopackRemoveUnused*` ไม่ช่วย — ลองแล้ว)
583
- - `scripts/barrel-exports.mjs`: `pnpm check:barrel` (ใน verify + CI — เจอ `export *` ใน `packages/ui/src` = ล้ม) และ `pnpm fix:barrel` (แปลงให้หลัง `shadcn add`)
584
- - หมายเหตุ: ตัวเลข date-fns 824 kB บน bundlephobia คือทั้งไลบรารี — ใน build จริงมีเฉพาะฟังก์ชันที่ใช้ + locale ไทย/อังกฤษ
585
-
586
- ## [0.24.0] — 2026-10-02
587
-
588
- เติมของเดิมตามที่สำรวจ shadcnstudio.com (หยิบเฉพาะ "หน้าที่" ที่ยังไม่มี ทำเป็นตัวเดียว + props) — เพิ่มอย่างเดียว ค่าเริ่มต้นหน้าตาเดิม
589
-
590
- ### Added
591
-
592
- - **`TextareaField`** — ช่องข้อความหลายบรรทัดพร้อม `label`/`hint`/`error` + **ตัวนับตัวอักษร** เมื่อมี `maxLength` (แดงเมื่อเต็ม, screen reader อ่าน "พิมพ์แล้ว x จาก y ตัวอักษร" ผ่าน `aria-describedby`) + `minRows`/`maxRows` (Textarea ขยายตามเนื้อหาอยู่แล้ว — `maxRows` จำกัดแล้วเลื่อนใน); controlled/uncontrolled; `textareaClassName`
593
- - **`UserAvatars`** — รูปผู้ใช้ซ้อนกัน + "+N" จาก array (`users`, `max`, `size` sm|default|lg, `label` นำหน้ารายชื่อให้ screen reader) ชี้ที่ "+N" เห็นชื่อที่เหลือ; ใช้จาก server component ได้ — แทน AvatarGroup + Avatar×N + AvatarGroupCount
594
- - **จุดสถานะ** `status?: "online" | "away" | "busy" | "offline"` ที่ `Initials`, `UserIdentity` และรายการของ `UserAvatars` (`PresenceDot` + `type Presence` export ด้วย) ใช้ token success/warning/destructive/muted
595
- - **`TableSkeleton`** (`rows`, `columns`, `header`), **`ListSkeleton`** (`rows`, `avatar`, `lines`), **`CardSkeleton`** (`count`, `variant` stat|content) — โครงระหว่างโหลดสำเร็จรูปสำหรับ `loading.tsx` มี `role="status"` + "กำลังโหลด…" ความกว้างสลับแบบคงที่ (ไม่ random → ไม่ hydration mismatch)
596
- - **`Notice`**: `icon` (true = ไอคอนตามโทน หรือส่งเอง) และ `dismissible` + `onDismiss` (ปุ่ม X อ่าน `UiText.close`) — ไม่ส่ง = หน้าตาเดิม; ไฟล์เป็น `"use client"` แล้ว (ใช้จาก server component ได้เหมือนเดิม)
597
- - **`AuthLayout`**: `size?: "sm" | "md" | "lg"` ความกว้างการ์ด (ค่าเริ่มต้น sm = เดิม) — หน้า consent/เลือกองค์กรของ Pi Auth ใช้ md (ขอจาก Pi Auth)
598
- - `UiText` key ใหม่: `loading`, `presence`, `characterCount`
599
- - showcase: หน้า `/components/{textarea-field,user-avatars,loading-skeletons}` (125 หน้า), demo `notice` มี icon + dismissible; tests 10 กรณี
600
-
601
- ## [0.23.0] — 2026-10-02
602
-
603
- ### Added
604
-
605
- - `ProviderButtons`: prop `badgePlacement` — ค่าเริ่มต้น `"corner"` = badge (เช่น "ใช้ล่าสุด") เป็นป้ายเล็กทับขอบบนขวาของปุ่มแบบ Clerk "Last used" ไม่กินที่ข้อความ; `"inline"` = ท้ายข้อความแบบ 0.21.0; ปุ่มเต็มกว้าง `py-2` กันป้ายชนบรรทัดแรก — ขอจากเจ้าของผ่าน Pi Auth (0.22.0 ที่ publish ไปไม่มีส่วนนี้)
606
-
607
- ## [0.22.0] — 2026-10-02
608
-
609
- ### Added
610
-
611
- - `ProviderButtons`: prop `text` ต่อ provider (ข้อความเต็มของปุ่มแทน "ดำเนินการต่อด้วย {label}" เช่น ชื่อ SSO ยาว "Estar (Microsoft 365)"); ปุ่มเต็มกว้าง (1 ตัว / `layout="list"`) **ขึ้นบรรทัดใหม่แทนตัดเป็น …** ; ตาราง 2 คอลัมน์ยังตัด แต่ `aria-label`/`title` เป็นข้อความเต็ม — ขอจาก Pi Auth
612
- - showcase: demo `/components/auth-layout` ปุ่ม SSO ใช้ `text` ยาว
613
-
614
- ## [0.21.0] — 2026-10-02
615
-
616
- ตัวสำเร็จรูปของ primitive ใหม่จาก shadcn (0.20.0) — เพิ่มอย่างเดียว
617
-
618
- ### Added
619
-
620
- - **`ChatThread`** — หน้าแชตจาก array ข้อความ (แทน MessageScrollerProvider/MessageScroller/Viewport/Content/Item/Button + Message/MessageAvatar/MessageContent/MessageHeader/MessageFooter + Bubble/BubbleContent + Marker + InputGroup รวม 15+ ชิ้นตามตัวอย่างทางการของ shadcn): `messages: { id, from: "me" | "other", content, author?, avatar?, meta? }[]`, เลื่อนตามข้อความใหม่ (ข้อความของเรายึดเป็นจุดเลื่อน), ปุ่ม "ไปข้อความล่าสุด", `typing` (แถว "กำลังพิมพ์…" ด้วย Marker), `onSend` = มีช่องพิมพ์ (Enter ส่ง · Shift+Enter ขึ้นบรรทัด · **ไม่ส่งระหว่างประกอบคำด้วย IME** แบบที่ PDF ทำ `submitOnEnter`), คืน Promise = ปุ่มหมุนแล้วล้างช่องเมื่อสำเร็จ, `empty`, `disabled`
621
- - **`SimpleQuestionnaire`** — แบบสอบถามทีละข้อจาก array คำถาม (แทน Questionnaire 15 ชิ้น): `{ name, title, description?, required?, multiple?, choices?: { value, label, description?, disabled? }[], input?: boolean | placeholder }`, คีย์ลัดตัวเลข/ตัวอักษร, ข้อบังคับไม่มีปุ่มข้าม, `onSubmit(values)` ได้ object (ข้อเดียว = string, หลายข้อ = string[], ข้อที่ข้ามไม่มี key) ไม่ต้องอ่าน FormData เอง
622
- - **`AuthLayout`** — หน้าเข้าสู่ระบบ/สมัคร/ยืนยันตัวตนสำเร็จรูปแบบ Clerk (server component ได้): `logo`, `title` (h1), `description`, เนื้อ, `cardFooter` (แถบล่างการ์ดพื้นต่างสี เช่น "ยังไม่มีบัญชี? สมัคร"), `footer` (ใต้การ์ด เช่น "ยืนยันตัวตนโดย Pi Auth · ข้อกำหนด"), `actions` (มุมขวาบน ธีม/ภาษา), `variant` centered (พื้นลายจุดจาง) | split (ฝั่งซ้าย `aside` สีแบรนด์ แบบ shadcn login blocks — มือถือซ่อน), `brandColor`/`brandForeground` (สีแบรนด์ของ tenant), `fullScreen`
623
- - **`ProviderButtons`** (Google/Microsoft/SSO: 1 ตัว = "ดำเนินการต่อด้วย …" เต็มกว้าง, 2+ ตัว = ตาราง 2 คอลัมน์, `badge` เช่น "ใช้ล่าสุด", `href` หรือ `onClick`, `pendingId` กันกดซ้ำ), **`AuthDivider`** ("หรือ"), **`PasswordInput`** (ปุ่มแสดง/ซ่อน + `aria-pressed`, `autoComplete` ค่าเริ่มต้น `current-password` ตามแนวทาง web.dev)
624
- - showcase: หน้า `/login` เปลี่ยนเป็น `AuthLayout` (โลโก้, ปุ่ม "ดำเนินการต่อด้วย Pi Auth", แถบล่าง, "ยืนยันตัวตนโดย Pi Auth", ปุ่มธีม) + หน้า `/components/auth-layout` (ฟอร์มเต็มทั้งสองแบบ)
625
- - `UiText` key ใหม่: `questionnaire` (ปุ่ม/ความคืบหน้า "ข้อ {current} จาก {total}"), `chat`, `auth`
626
- - showcase: หน้า `/components/{chat-thread,simple-questionnaire,marker}` (122 หน้ารวม auth-layout); tests 7 กรณี (รวม 192)
627
-
628
- ## [0.20.1] — 2026-10-01
629
-
630
- ### Fixed
631
-
632
- - **`confirm.requireText` ถูกทิ้งเงียบ ๆ** (รายงานจาก Pi Config): `RowActions`, `ConfirmActionButton` และ `ActionMenu` ส่งให้ `ConfirmDialog` แค่ title/description/label/destructive — ใส่ `confirm: { requireText: slug }` แล้วลบได้ด้วยคลิกเดียวทั้งที่ `ConfirmSpec` ประกาศว่ารองรับ → ส่ง `requireText` + `requireTextLabel` ต่อครบทั้งสามที่; tests 2 กรณี (รวม 185)
633
-
634
- ## [0.20.0] — 2026-10-01
635
-
636
- ขั้น "layout" ของแผนรวม UI ทุกแอป (ดู `todo/todo.md`) — เพิ่มอย่างเดียว ไม่มี prop เดิมเปลี่ยนความหมาย
637
-
638
- ### Added
639
-
640
- - **`MobileTabBar`** (`/next`) + **`AppShell mobileTabs`** — แถบเมนูล่างของมือถือ 3–5 แท็บ (ยกจาก DueBell `MobileNav`): ซ่อนตั้งแต่ `md`, แท็บสูง ≥ 56px, `badge` เป็น `CountBadge` บนไอคอน (รวมใน aria-label), `onClick` แทนการไปหน้า, ไฮไลต์ตาม path (`/` ตรงตัว, อื่น ๆ prefix หรือ `exact`); `icon` เป็น **element** (`<Home />`) จึงส่งจาก server layout ได้เหมือน `AppNav`; `AppShell` ทั้ง layout sidebar/top เว้นที่ใต้เนื้อหาให้เอง
641
- - utility **`pb-safe`** (safe-area ล่างของ iPhone) และ **`pb-tab-bar`** (เว้นที่ให้พ้นแถบล่าง) ใน `styles.css`
642
- - **`SettingsLayout`** — หน้าตั้งค่าหลายหัวข้อ (ยกจาก DueBell master-detail + PDF ค้นหาได้): จอกว้างสารบัญซ้าย sticky + เนื้อหา, มือถือรายการหัวข้อ → หน้าหัวข้อ + ปุ่ม "กลับ" (ย้ายโฟกัสไปหัวข้อให้ screen reader), `searchable` ค้น label/description/`keywords` (ไทย+อังกฤษ), `badge` ท้ายรายการ, คุมเองด้วย `value`/`onValueChange` หรือ `initialSection`
643
- - **`FilterBar`** — แถบค้นหา + select ตัวกรองเหนือรายการ client-side (Pi Auth เขียนซ้ำ 14 หน้า): ปุ่ม "ล้างตัวกรอง" โผล่เมื่อมีค่าค้าง (เทียบกับ `allValue` ของแต่ละตัว) และตั้งกลับให้เองถ้าไม่ส่ง `onClear`; ตัวกรองแบบอื่นใส่เป็น `children` + `active`
644
- - **`SetupChecklist`** — รายการ "ต้องตั้งอะไรให้ครบ" (ยกจาก Pi Auth `SetupSteps`): ✓ เมื่อทำแล้ว, ขั้นบังคับถัดไปถูกเน้น (`aria-current="step"`), ขั้น `optional` ไม่นับ, ครบแล้วซ่อนทั้งกล่อง (`hideWhenComplete`); `action` รับ `{ current, done }` ให้แอปส่ง `LinkButton` เอง (barrel ไม่พึ่ง next)
645
- - **`MonthStepper`** + **`useMonthCursor`** — เลือกเดือน ‹ เดือน › กดชื่อเดือนกลับเดือนนี้, `min`/`max`, `locale` (`th-TH` = พ.ศ., `th-TH-u-ca-gregory` = ค.ศ.), `format` long|short, `formatLabel` (ยกจาก DueBell `MonthSelector`)
646
- - **`lib/month.ts`** (barrel): `MonthKey` (`"YYYY-MM"` แบบเดียวกับ `CalendarMonthGrid`), `parseMonthKey`, `isMonthKey`, `toMonthKey`, `currentMonthKey`, `shiftMonth` (ข้ามปีให้), `compareMonth`, `formatMonthKey` — แทน `CycleMonth` ของ DueBell
647
- - `UiText` key ใหม่ (optional ทั้งหมด มีค่าไทย/อังกฤษใน `UI_TEXT_TH`/`UI_TEXT_EN`): `month`, `setup`, `filters`, `settings`, `mobileTabs`
648
- - **ชุด component สำเร็จรูป (Simple\*)** — ทิศทางของแพ็กเกจ: แอปเขียน component เดียว + props แทนการประกอบ primitive หลายชิ้นซ้อนกันแบบ shadcn (ดู `docs/agent-rules.md` "ใช้ตัวสำเร็จรูปก่อน"):
649
- - `SimpleDialog` (แทน Dialog 7 ชิ้น) / `SimpleSheet` (+ `side`) — trigger, title, description, children, `footer` (ReactNode หรือ `(close) => …`), `size`, controlled ได้; **ปุ่ม X อ่าน `UiText.close`** แก้ปัญหา "Close" แปลภาษาไม่ได้โดยไม่แตะไฟล์ shadcn
650
- - `SimpleTabs` (items: value/label/content/icon/badge/disabled, `variant` default|line, `fullWidth`) / `SimpleAccordion` (`multiple`, `initialOpen`)
651
- - `SimpleTooltip` (ห่อ element เดียว, content ว่าง = ไม่มี tooltip) / `SimplePopover` (trigger + title/description + เนื้อหา)
652
- - `ActionMenu` — เมนูคำสั่งจาก array: icon, shortcut, separator, label, href/external, destructive, **`confirm` เปิด `ConfirmDialog` ก่อนเรียก onClick**; trigger ไม่ส่ง = ปุ่ม …
653
- - `SimplePagination` + `paginationRange` — page/pageCount/onPageChange, จำนวนปุ่มคงที่, มือถือเหลือ "x / y"
654
- - `SimpleTable<T>` — columns (key/header/cell/align/className) + rows + getRowKey + empty + onRowClick; ค่าจาก key ตรง ๆ ถ้าไม่ส่ง cell
655
- - `SegmentedControl<T>` — เลือกทีละหนึ่งและต้องมีค่าเสมอ (กดตัวเดิมไม่หลุด), `count`, `icon`, `size`, `fullWidth`
656
- - `UiText` key ใหม่: `close`, `pagination`
657
- - **shadcn/ui base-nova ล่าสุดทั้งชุด** (`shadcn add --all --overwrite`, CLI 4.21.0): primitive ใหม่ `bubble`, `marker`, `message`, `message-scroller`, `questionnaire`, `direction` (export ใน barrel) และ `toast` (Base UI — **ไม่อยู่ใน barrel** เพราะชื่อ `toast`/`Toaster` ชนกับ sonner ที่แพ็กเกจใช้; import จาก `@suphark/ui/components/ui/toast`); dep ใหม่ `@shadcn/react`
658
- - ไฟล์เดิมได้ class/ลำดับจาก upstream (ส่วนใหญ่เรียง class ใหม่) + sidebar deps ของ useEffect/useMemo ครบขึ้น
659
- - **คงของที่แก้ไว้เอง** (ติดมาจาก supplychain ตั้งแต่ 0.1.0 — upstream ยังไม่มี): `scroll-area.tsx` (`data-[orientation=…]` ของ Base UI — ไม่งั้นแถบเลื่อนกว้าง 0 มองไม่เห็นทั้งแอป), `slider.tsx`, `tabs.tsx` (orientation แบบเดียวกัน), `table.tsx` (prop `containerClassName`)
660
- - CLI รุ่นนี้เขียน `import { cn } from "cn"` — เปลี่ยนกลับเป็น `@suphark/ui/lib/utils` ทุกไฟล์ เพราะ `cn` ของแพ็กเกจรู้จักคลาสตามบทบาท (`text-body` ฯลฯ, 0.13.1) ส่วน `cn` ทั่วไปจะลบทิ้งเงียบ ๆ
661
- - `hooks/use-mobile.ts` (CLI เขียนทับ) เติม `"use client"` คืน; biome ปิด `noDoubleEquals` เฉพาะ `components/ui/**` (`field.tsx` ของ upstream ใช้ `==`)
662
- - showcase: หน้า `/components/{month-stepper,filter-bar,setup-checklist,settings-layout,mobile-tab-bar,simple-dialog,simple-sheet,simple-tabs,simple-accordion,simple-tooltip,simple-popover,action-menu,simple-pagination,simple-table,segmented-control}` (118 หน้า) + `/shell` มี `mobileTabs` ให้ลองบนจอมือถือ; tests 20 กรณีเพิ่ม (รวม 183)
663
-
664
- ## [0.19.0] — 2026-09-30
665
-
666
- ปิดรายการ "component เล็กที่ใช้ร่วม" ของแผนรวม UI ทุกแอป (ดู `todo/todo.md`) — เพิ่มอย่างเดียว ไม่มี prop เดิมเปลี่ยนความหมาย
667
-
668
- ### Added
669
-
670
- - **`Stat` เพิ่ม `icon` / `badge` / `delta` / `tone`** + **`StatGrid`** (Pi Auth มี 17 การ์ดแบบนี้, DueBell มี DeltaBadge เอง): `delta` = `{ value, direction: "up"|"down"|"flat", tone?, hint? }` เลือกไอคอนเทรนด์และสีให้ (ขึ้น = success, ลง = destructive — ทับด้วย `tone` ได้เพราะบางตัวชี้วัด "ลง" คือดี), `icon` มีกรอบพื้นตามโทน, `tone` เปลี่ยนสีตัวเลข (`default|success|warning|destructive|info`); `StatGrid` คอลัมน์ 2|3|4 (มือถือ 1 → sm 2)
671
- - **`SettingsRow`** / **`SwitchRow`** / **`DangerZone`** (barrel) — แถวตั้งค่า: ชื่อ+คำอธิบายซ้าย control ขวา (จอแคบตกลงมา), `htmlFor` ผูกชื่อกับ control, `tone="destructive"`; `SwitchRow` = แถว + `Switch` ที่ผูก id ให้แล้ว (คลิกชื่อแล้วสลับ) รับ props ของ `Switch` ทั้งหมด; `DangerZone` = กรอบ `border-destructive/40` + พื้น subtle + เส้นคั่นแถว (Pi Auth/Pi PDF เขียน layout นี้ซ้ำ)
672
- - **`ConfirmDialog` / `useConfirm` `requireText`** — ต้องพิมพ์ข้อความให้ตรงก่อนปุ่มยืนยันจะกดได้ (ลบของที่กู้ไม่ได้ แบบ GitHub); `requireTextLabel` ทับข้อความเหนือช่อง, ค่าเริ่มต้นจาก `UiText.confirmTypeToConfirm`/`…Suffix` (key ใหม่); ปิดกล่องแล้วล้างสิ่งที่พิมพ์เสมอ (ยืนยันรายการถัดไปทันทีไม่ได้) — ใช้ผ่าน `confirm({ requireText: app.name, destructive: true })` ได้เลย
673
- - **`UserIdentity`** (barrel) — avatar (`Initials` สีพื้นคงที่ต่อชื่อ) + ชื่อ + อีเมล + `badge`/`hint`, `size` sm|default, `hideAvatar`; **ไม่มีชื่อ = ใช้อีเมลเป็นบรรทัดหลัก** (ผู้ใช้ที่ยังไม่ตั้งชื่อ) — Pi Auth เขียนซ้ำ ~15 จุด
674
- - **`CountBadge`** (barrel) — ป้ายจำนวนท้ายเมนู/แท็บ: เกิน `max` เป็น `99+`, `0` ซ่อนอัตโนมัติ (`showZero` บังคับแสดง), `tone` muted|primary|destructive
675
- - **`SaveStatus`** (barrel, ยกจาก Pi PDF) — สถานะบันทึกอัตโนมัติบรรทัดเดียว: `saving` (spinner) / `saved` (✓ เขียว) / `error` (`role="alert"`, ข้อความของแอปหรือ `UiText.actionFailed`) / `idle` + `savedAt` = เวลาสัมพัทธ์ผ่าน `ClientDateFormatter` (อัปเดตเองหลัง mount ไม่ทำ hydration mismatch); `language`/`text`
676
- - **`SliderField`** (barrel, ยกจาก Pi PDF) — ช่องเลื่อน + ป้ายชื่อ + ค่าปัจจุบันมุมขวา: `value`/`onValueChange` เป็น number เดี่ยว, `unit` หรือ `formatValue`, `description`, `size` sm|default, `disabled`
677
- - **`SkipLink`** (barrel) — ลิงก์ "ข้ามไปเนื้อหา" สำหรับหน้าที่ไม่ได้ใช้ `AppShell` (Pi PDF มีของตัวเอง); `AppShell` เปลี่ยนมาใช้ตัวเดียวกันนี้ภายใน
678
- - **`FormDialog` `revealSuccess`** — แบบ **ไม่ต้องส่งฟังก์ชัน** จึงใช้จาก **server component** ได้ (รายงานจาก Pi Auth: `renderSuccess` เป็นฟังก์ชัน ข้าม server/client boundary ไม่ได้ → `Functions cannot be passed directly to Client Components`): server action คืน `{ ok: true, data: { values: { "Signing secret": "whsec_…" } } }` แล้ว dialog แสดง `SecretReveal` ให้เอง + ปุ่ม "เสร็จแล้ว" (ไม่ปิดเอง); `successTitle` (ค่าเริ่มต้น `UiText.showOnce`), `successDescription` แทน description ของฟอร์ม — เคส "สร้างแล้วโชว์ secret ครั้งเดียว" (webhook secret / SCIM token / service account key / client secret) ซ้ำกันทุกแอป; `renderSuccess` เดิมยังใช้ได้จาก client component
679
- - showcase: หน้า `/components/settings-row`, `/components/user-identity` + หน้า `stat` ใหม่ (ไอคอน/delta/StatGrid) — 104 หน้า; tests 9 กรณีเพิ่ม (รวม 163)
680
-
681
- ## [0.18.0] — 2026-09-30
682
-
683
- > 0.17.0 ถูก publish ตอนที่ `LanguageSwitch` ยังไม่เข้า (commit ถัดไป) — ของชิ้นนี้จึงออกเป็น 0.18.0
684
-
685
- ### Added
686
-
687
- - **`LanguageSwitch`** + **`LanguageMenuItems`** (barrel) — สลับภาษาที่ Pi PDF (`language-selector.tsx`) / Pi Config (`locale-toggle.tsx` + `locale-menu-items.tsx`) / Pi Auth เขียนซ้ำ; แพ็กเกจไม่รู้จัก i18n ของแอป → แอปส่ง `locales[{ value, label, hint?, flag? }]` + `value` + `onValueChange` เอง (เขียน cookie/เรียก server action/`router.refresh()` ฝั่งแอป):
688
- - `LanguageSwitch` (นอกเมนู เช่น header/หน้าก่อนล็อกอิน — คู่กับ `ThemeToggle`): `mode` `auto` (ค่าเริ่มต้น: ≤2 ภาษา = ปุ่มเดียววนรอบแสดงชื่อภาษาถัดไป, >2 = เมนูพร้อมธง/บรรทัดรอง) / `toggle` / `menu`, `showLabel` (menu = รหัสภาษาตัวใหญ่, false = ปุ่มไอคอน), `label` (ค่าเริ่มต้น **`UiText.language.label`** — key ใหม่), `align`, props ของ `Button` ส่งผ่านหมด; `locales` ว่าง = ไม่ render
689
- - `LanguageMenuItems` (ในเมนูผู้ใช้ — คู่กับ `ThemeMenuItems`): `DropdownMenuRadioGroup` + หัวกลุ่มจาก `UiText.language`, `disabled` ปิดระหว่าง pending
690
- - `useCopyToClipboard`: `copyToClipboard(text, key?, labels?)` — ทับข้อความ toast เฉพาะครั้งนั้นได้ (ข้อสังเกตจาก Pi Config: `RevealedValues` มีข้อความต่างกันทุกปุ่ม "คัดลอกคีย์ X / ค่าของ X / ทั้งชุด N ค่า" แต่เดิม hook รับข้อความเดียวตอนสร้าง จึงต้องปิด toast แล้วเรียก `toast.success()` เอง)
691
- - `SimpleSelect`: **`emptyValue`** — บอกว่า option ไหนหมายถึง "ไม่เลือก/ไม่มี" แล้วคอมโพเนนต์ส่ง **ค่าว่าง** ใน FormData แทน sentinel (Base UI ไม่รับ `value=""`) — เดิม `name` ใช้ได้เฉพาะเคสที่ค่าที่เลือก = ค่าที่ส่ง 1:1 แอปที่มี "ไม่มีกลุ่ม" ต้องทำ hidden input แปลงค่าเอง (รายงานจาก Pi Config `SectionSelect`: `__none__` หลุดเข้า server action); ใช้คู่ `required` ไม่ได้ (hidden input ไม่เข้า constraint validation) ให้ตรวจที่ schema
692
- - `SearchInput`: ส่ง `aria-label` ตรง ๆ ได้แล้ว (เดิมคอมโพเนนต์เขียนทับเงียบ ๆ หลัง `{...props}` ต้องใช้ `label` เท่านั้น — รายงานจาก Pi PDF)
693
- - showcase: หน้า `/components/language-switch` — 102 หน้า; tests 8 กรณีเพิ่ม (รวม 154)
694
-
695
- ## [0.17.0] — 2026-09-30
696
-
697
- ### Added
698
-
699
- - **`CopyButton`** (barrel) — ปุ่มคัดลอกข้อความ: ไอคอน Copy → ✓ (`text-success`) ชั่วครู่แล้วกลับ, ใช้ `useCopyToClipboard` (มี fallback `execCommand` สำหรับ webview ที่บล็อก clipboard API) แอปไม่ต้องเขียน state เอง (Pi Auth เขียนซ้ำ 12 ไฟล์, Pi Config ใน `RevealedValues`): `value`, `children` (ไม่ส่ง = ปุ่มไอคอนล้วน), `label`/`copiedLabel` (ค่าเริ่มต้น `UiText.copy`/`copied` — เป็น aria-label เมื่อไม่มี children และเป็น `title` เสมอ), `toast` (ค่าเริ่มต้น **false** เพราะปุ่มแสดง ✓ อยู่แล้ว — ส่ง `true` หรือข้อความของแอปได้), `timeout`, `onCopied`, props อื่นของ `Button` (variant/size/className) ส่งผ่านหมด
700
- - **`CodeBlock`** (barrel, สเปกจาก Pi Config — ของเดิมอยู่ `features/docs/code-block.tsx` ใช้ 2 ไฟล์; Pi Auth มีแบบเดียวกัน ~9 จุด) — บล็อกโค้ด/คำสั่ง: หัวแถบ `title ?? lang ?? "shell"` (mono, `title={false}` = ไม่มีหัวแถบ) + `CopyButton`, เนื้อเป็น `<pre><code>` mono เลื่อนแนวนอน; `copyText` เมื่อคัดลอกไม่เท่ากับที่แสดง (เช่นคัดลอกเฉพาะค่า token ไม่เอา `KEY=`), `copyLabel`, `showCopy`, `wrap` (ตัดบรรทัดแทนเลื่อน), `maxHeightClassName`; ไม่มี syntax highlight โดยตั้งใจ (ไม่เพิ่ม dependency)
701
- - `SearchInput`: ใช้แบบ **uncontrolled** ได้แล้ว — ไม่ส่ง `value` = คุมค่าในตัวเองจาก `defaultValue` (ปุ่มล้าง/Escape ยังทำงาน) จึงวางใน `<form>` ที่ submit ไป server action/URL param พร้อม `name` ได้ (คำถามจาก Pi Auth: `/console/logs`, `/platform/users`); `onValueChange` ยังเรียกทุกตัวอักษรทั้งสองโหมด — **ไม่มี debounce ในตัว** (ให้แอป debounce เอง) จดไว้ใน docstring แล้ว
702
- - showcase: หน้า `/components/copy-button`, `/components/code-block` — 101 หน้า; tests 5 กรณีเพิ่ม (รวม 146)
703
- - test: `date-picker.test.tsx` ตั้ง timeout 20s (เปิด popup = render ปฏิทินทั้งเดือนใน jsdom ช้าเป็นครั้งคราวเกิน 5s ตอนรันทั้งชุด — ทำให้ `pnpm verify`/CI แดงสุ่ม ๆ)
704
-
705
- ## [0.16.0] — 2026-09-30
706
-
707
- ### Added
708
-
709
- - **`SearchInput`** (barrel) — ช่องค้นหามาตรฐานที่ Pi Auth (~15 จุด) / Pi PDF (5) / DueBell (8) เขียนซ้ำ: ไอคอนแว่นขยายซ้าย, ปุ่มล้าง (X) ขวาเมื่อมีข้อความ (กดแล้ว focus กลับที่ช่อง), **Escape ล้างค่า** (`clearOnEscape`), `type="search"` แต่ซ่อนปุ่ม X ของ WebKit; controlled ล้วน `value` + `onValueChange(next)` (แอปคุม state/debounce เอง), `onClear`, `size` xs|sm|default (h-7/h-8/h-9), `label`/`clearLabel`/`placeholder` (ค่าเริ่มต้นจาก **`UiText.search`** `{ placeholder, label, clear }` — key ใหม่), `groupClassName` (กรอบ) แยกจาก `className` (ตัว `<input>`), ส่ง `ref` ถึง input ได้; props อื่นของ `<input>` ส่งผ่านหมด
710
- - showcase: หน้า `/components/search-input`; **เมนูซ้ายของ `/components` มีช่องค้นหาแล้ว** — กรองตามชื่อ/slug/ชื่อ export/คำอธิบาย, บอกจำนวนที่พบ (`aria-live`), กด `/` ที่ไหนก็ได้เพื่อโฟกัสช่องค้นหา (ไม่ดักเมื่อกำลังพิมพ์ในช่องอื่น)
711
-
712
- ## [0.15.0] — 2026-09-30
713
-
714
- ### Added
715
-
716
- - **`InfoHint`** (ขอจาก Pi Auth): ไอคอน (i) ท้ายหัวข้อ ที่เก็บคำอธิบายยาว ๆ ไว้ใน tooltip แทนการวาง
717
- เป็น `description` ใต้หัวข้อถาวร — ใช้กับคำอธิบายที่คนซึ่งรู้อยู่แล้วไม่ต้องอ่านซ้ำทุกครั้ง
718
- props: `children` (เนื้อหา), `label` (ชื่อที่ screen reader อ่าน — บังคับ), `side`, `className`
719
- trigger เป็น **ปุ่มจริง** ไม่ใช่ `<span>` จึงเปิดได้ด้วยคีย์บอร์ด (Tab) และจอสัมผัส ไม่ใช่ hover อย่างเดียว
720
- - showcase: หน้า component `/components/info-hint`
721
-
722
- ## [0.14.0] — 2026-09-29
723
-
724
- > **0.13.0 และ 0.13.1 ถูก publish ขึ้น npm เมื่อ 2026-09-25 จากโค้ดที่ไม่เคยเข้า git** (สเกลตัวอักษรตามบทบาท ขอจาก Pi Done) — รุ่นนี้กู้โค้ดจาก tarball 0.13.1 กลับเข้า `main` แล้ว (3-way merge กับ 0.12.0) บันทึกของสองรุ่นนั้นอยู่ข้างล่างตามที่ publish จริง
725
-
726
- รุ่นแรกของแผนรวม UI จากทุกแอป (สำรวจ Pi Auth / DueBell / Pi PDF / Pi Config — ดู `todo/todo.md`) — เพิ่มอย่างเดียว ไม่มี prop ถูกเปลี่ยนความหมาย
727
-
728
- ### Added
729
-
730
- - **token สีโทนใน `styles.css`**: `--info` / `--info-foreground` (`bg-info`, `text-info`) และพื้นอ่อนครบ 4 โทน `--{info|success|warning|destructive}-subtle` / `-subtle-foreground` / `-border` (`bg-info-subtle text-info-subtle-foreground border-info-border` …) สลับ dark ให้เอง — แทน `sky-*` ที่ PDF ใช้ ~20 จุด และ `--info` ที่ DueBell ตั้งเอง (ถ้าแอปตั้ง `--info` ไว้เองให้ลบออก ค่าของแพ็กเกจ = sky-600 / dark sky-400)
731
- - `EmptyState`: `variant` (`dashed` ค่าเริ่มต้น · `card` · `plain`), `size="compact"`, `tone="destructive"`, `secondaryAction` (ขอจาก PDF ที่มีสำเนาของตัวเอง)
732
- - `CheckboxField`: `hint` — คำอธิบายใต้ label (Config เขียน `<p>` ต่อท้ายเอง ~8 จุด)
733
- - `SimpleSelect`: `name` + `required` — ส่งค่าใน FormData ของ server action ได้ (Config เคยใช้ hidden input เอง)
734
- - `FormDialog`: `renderSuccess(state)` — สำเร็จแล้วแสดงผลแทนฟอร์ม (secret/token ที่ออกครั้งเดียว) พร้อมปุ่ม "เสร็จแล้ว"; Pi Auth `SecretDialog` ใช้แทนได้
735
- - `ConfirmActionButton`: `redirectTo` — สำเร็จแล้ว `router.push` แทน refresh (Pi Auth `uninstall-button.tsx` ใช้แทนได้)
736
- - `useCopyToClipboard({ toast, timeout })`: คืน `copiedKey` (`copyToClipboard(text, key)` — หลายปุ่มในหน้าเดียวรู้ว่าปุ่มไหนเพิ่งคัดลอก), `toast: false` หรือข้อความของแอป (แปลภาษาได้), ใช้ `copyText` ที่มี fallback; `copyToClipboard` คืน `Promise<boolean>` — ของเดิม (`isCopied`, `copyToClipboard(text)`) ใช้ได้เหมือนเดิม (Pi Auth เขียน copy เอง 12 ไฟล์)
737
- - `formatBytes(bytes, locale, { minUnit })` — บังคับหน่วยเล็กสุด เช่น `"MB"` (PDF มีสำเนาเพราะต้องการแสดงเป็น MB)
738
-
739
- ### Changed
740
-
741
- - `Notice` / `StatusBadge` ใช้ token โทนแทน palette ดิบ (ถอด `ui-audit-ok-file`) — สีเท่าเดิม ยกเว้นตัวอักษรของ `Notice` จาก 900→800 (dark 100→200) ให้ตรงกับ `StatusBadge`
742
- - `FormDialog`: state ของ action อยู่ในเนื้อ dialog — ปิดแล้วเปิดใหม่ได้ฟอร์มรอบใหม่เสมอ (เดิม `keepOpen` เปิดใหม่ยังเห็นผลรอบก่อน และ state ที่มี secret จะถูกส่งเป็น `prev` ไปกับ submit ถัดไป)
743
- - `useCopyToClipboard`: ไม่มี `navigator.clipboard` แล้วลอง fallback แทนการเตือน "Clipboard not supported"
744
-
745
- ### ตรวจแล้วไม่แก้
746
-
747
- - hydration mismatch ของ trigger ที่ render ผ่าน Button (PDF รายงาน, ห่อ wrapper `slot-props.ts`): showcase ใช้ `render={<Button/>}` กับ Dialog/Sheet/Popover/DropdownMenu/Tooltip ไม่มี hydration error ใน dev — น่าจะมาจาก wrapper `asChild` ของ PDF เอง ให้ PDF ลองเปลี่ยนเป็น `render=` ตรง ๆ ก่อน
748
- - ข้อความ sr-only "Close" ใน `DialogContent`/`SheetContent` ยังแปลไม่ได้ — อยู่ในไฟล์ shadcn (ห้ามแก้มือ) รอตัดสินใจ
749
-
750
- ### Fixed
751
-
752
- - showcase: เมนู "ออกจากระบบ" ที่หัวเว็บดูเหมือนกดไม่ได้ — ออกแล้วกลับหน้าเดิม middleware ส่งไปล็อกอิน และ Pi Auth ยังมี session จึงล็อกอินกลับทันที → `UserButton afterSignOutUrl="/login"` จอดหน้าเข้าสู่ระบบ
753
-
754
- ## [0.13.1] - 2026-09-25
755
-
756
- ### Fixed
757
-
758
- - **`cn()` ลบคลาสตามบทบาททิ้งเงียบ ๆ** (พบตอน Pi Done ย้ายไปใช้ 0.13.0): tailwind-merge รู้จักเฉพาะ
759
- `text-xs … text-9xl` ว่าเป็นขนาดตัวอักษร `text-body`/`text-caption` จึงถูกเดาว่าเป็น "สี" แล้วโดน
760
- `text-foreground/80` หรือ `text-muted-foreground` ที่อยู่ในสตริงเดียวกันลบทิ้ง — build ผ่าน ไม่มี
761
- error ใด ๆ แต่ตัวอักษรกลับไปใช้ขนาดที่สืบทอดมา (ชื่องานใน Pi Done ควรเป็น 16px บนมือถือ กลายเป็น
762
- ค่า root) ตอนนี้ `cn()` ใช้ `extendTailwindMerge` ประกาศคลาสทั้ง 6 ไว้ในกลุ่ม `font-size` จาก
763
- `TYPOGRAPHY_ROLES` โดยตรง เพิ่มบทบาทใหม่ในอนาคตจึงไม่ต้องแก้ที่นี่อีก
764
-
765
- ## [0.13.0] — 2026-09-25
766
-
767
- ### Added
768
-
769
- - **สเกลตัวอักษรตามบทบาท + ตอบสนองจอ** (ขอจาก Pi Done — บนมือถืออ่านเล็กเกินไป): คลาสใหม่ `text-title-lg` `text-title` `text-subtitle` `text-body` `text-caption` `text-label` ค่าเป็น **mobile-first แล้วแน่นขึ้นบนจอ ≥ 48rem** (body 16→15px, caption 14→13px, subtitle 17→16px) — เหตุผลเดียวกับที่ iOS ใช้ body 17px ส่วน macOS ใช้ 13px: มือถือถือห่างและใช้นิ้ว เดสก์ท็อปอ่านใกล้และชี้ด้วยเมาส์
770
- - ทำได้โดย `@theme` ชี้ `--text-*` ไปที่ `--fs-*` แล้วเปลี่ยน `--fs-*` ใน media query — utility ยังเป็นคลาสเดียวและยังใช้ variant ได้ (`md:text-body`); **ไม่แตะ root font-size** ซึ่งจะทับค่าที่ผู้ใช้ตั้งไว้ในเบราว์เซอร์
771
- - `text-xs … text-2xl` ยังใช้ได้เหมือนเดิม ไม่ใช่ breaking — แต่ `text-xs` (12px) ไม่ควรเป็นขนาดเนื้อหาอีกต่อไป
772
- - `TYPOGRAPHY_ROLES` export จาก `@suphark/ui` เป็นแหล่งความจริงเดียวของตาราง (showcase `/design-system#tokens-typography` render จากค่านี้ ไม่เขียนซ้ำ)
773
- - **ความสูงขั้นต่ำของรายการเมนูบนอุปกรณ์สัมผัส**: `@media (pointer: coarse)` ตั้ง `min-height: 44px` ให้ `dropdown-menu-item` / `checkbox-item` / `radio-item` / `sub-trigger` / `select-item` / `command-item` — เดิมไม่มีความสูงเลย (`py-1` ≈ 26px) ต่ำกว่าเกณฑ์ Apple 44pt / Material 48dp; ใช้ `pointer: coarse` ไม่ใช่ความกว้างจอ เพราะหน้าต่างแคบบนเดสก์ท็อปยังใช้เมาส์
774
- - audit: กฎ `raw-text-scale` (warning, opt-in ด้วย `--semantic-text`) จับ `text-xs/text-sm/…` ที่ยังไม่ย้ายไป role class — ให้แอปทยอยย้ายทีละส่วนแล้วเปิดกฎกันถอยหลัง
775
-
776
- ### Fixed
777
-
778
- - **`arbitrary-text` ของ audit ไม่เคยจับ `text-[0.8rem]`**: regex เดิม `\d+` ตามด้วยหน่วยทันที จึงข้ามค่าทศนิยมทั้งหมด (ทั้งใน `audit.mjs` และ `audit-tailwind-typography.mjs`) — แก้เป็น `\d*\.?\d+` แล้วพบของค้าง 15 จุดที่ผ่าน CI เขียวมาตลอด
779
- - แทนขนาด arbitrary 12 จุดด้วย role class: `text-[0.7rem]`/`text-[0.625rem]`/`text-[0.6rem]` → `text-label` (HeaderCrumb badge, SubNav, TopTabs, IconPicker, Initials, Stepper, SecretReveal, PrimarySecondaryText, showcase site-header) และข้อความ error ของ `FormFieldInput` → `text-caption` (บนมือถือขึ้นเป็น 14px อ่านออกและพ้นเกณฑ์ iOS zoom)
780
- - ที่ยังเหลือคือใน `components/ui/**` (`button.tsx` `toggle.tsx` `calendar.tsx` = `text-[0.8rem]`) ซึ่งเป็นไฟล์ที่ shadcn generate — ห้ามแก้มือ รอรอบ regenerate
781
-
782
- ## [0.12.0] — 2026-09-23
783
-
784
- ### Added
785
-
786
- - **`OrganizationSwitcher` prop `itemAction(org)`** (ขอจาก Pi Auth) — ปุ่มท้ายแถวของแต่ละองค์กรในเมนู เช่น ⚙ "ตั้งค่าองค์กร" แบบ Vercel/Clerk: แอปส่ง element ที่กดได้มาเอง (พร้อม aria-label/title) component ห่อให้และ **หยุด click/keydown ไม่ให้สลับองค์กร**; คืน `null` = แถวนั้นไม่มีปุ่ม; มี `itemAction` แล้วองค์กรเดียวก็ยังมีเมนู (เดิมยุบเป็นป้ายเฉย ๆ เมื่อไม่มี `footer`) — เดิมแอปต้องวาง "ตั้งค่าองค์กร" เป็นรายการแยกใน `footer`
787
- - **`OrganizationSwitcher` prop `selectableActive`** (ขอจาก Pi Auth) — แถวที่ active กดได้ด้วย เรียก `onSwitch` เหมือนแถวอื่น (ค่าเริ่มต้น false = คงพฤติกรรมเดิม กดแล้วไม่ทำอะไร); สำหรับเมนูที่ "เลือก" = ไปหน้าของสิ่งนั้นไม่ใช่แค่สลับ เช่น crumb บัญชีของ Pi Auth ที่กดชื่อบัญชีแล้วเข้าคอนโซลบัญชีนั้น — ตัดรายการ "คอนโซลบัญชี" ท้ายเมนูที่ซ้ำช่องทางออกได้
788
- - showcase: หน้า `organization-switcher` ตัวแรกใส่ ⚙ ท้ายแถว; tests 2 กรณี (รวม 130)
789
-
790
- ## [0.11.2] — 2026-09-22
791
-
792
- ### Fixed
793
-
794
- - **`HeaderCrumbs` บนจอแคบใช้ย้อนกลับ/สลับไม่ได้** (ผู้ใช้ชี้ใน `/shell-top` มือถือ): เดิมซ่อน crumb ก่อนสุดท้ายทั้งก้อนแต่ยังเห็นตัวคั่น `/ /` และไม่มีทางเข้าถึงตัวสลับองค์กร/แอป → บน `< sm` ยุบเป็นปุ่ม `…` (aria-label `collapsedLabel` ค่าเริ่มต้น "แสดงเส้นทางทั้งหมด") เปิด `Popover` แสดง crumb ที่ซ่อนเรียงลงมา — แต่ละตัวยังเป็น switcher ของมันเอง (DropdownMenu ซ้อนใน Popover ทำงานได้ ทดสอบสลับองค์กรใน popover แล้ว); ตัวคั่นซ่อนตามตัวที่ซ่อน
795
- - `HeaderCrumb`: `overflow-hidden` + label `min-w-0` และ wrapper ตัวสุดท้ายบังคับ `max-w-full` — ตัวสุดท้ายหดตามช่องแทนบานทับ `header.actions`; `badge` ซ่อนบนจอแคบ (`hidden sm:inline`) ให้ที่กับชื่อ
796
- - `SubNav`: key ของกลุ่มใช้ชื่อกลุ่มแทน index (biome `noArrayIndexKey` — suppression เดิมหลุดตอน 0.11.1 จัดรูปแบบใหม่ ทำให้ `biome ci` ใน CI ไม่ผ่าน)
797
- - showcase: `/shell-top` header actions ซ่อน LayoutSwitch + กระดิ่งบนจอแคบ (ก่อนหน้ากินที่จน crumb เหลือ 25px); tests 2 กรณี (รวม 128)
798
-
799
- ## [0.11.1] — 2026-09-22
800
-
801
- ### Changed
802
-
803
- - showcase: เพิ่มหน้า stub ให้ลิงก์เมนู mock ที่ไม่มีหน้าจริง (`/shell-top/roles`, `/shell-top/security`, `/shell/bidding`, `/shell/orgs`) — Next prefetch เคย 404 ใน console ทุกครั้งที่เข้า `/shell`, `/shell-top`
804
-
805
- - `SubNav` (AppShell layout top): หัวกลุ่มกับรายการเคยเป็นสี muted เท่ากันจนแยกไม่ออก (ผู้ใช้ Pi Auth ชี้ในหน้า Settings) → หัวกลุ่มเป็น `text-[0.7rem] font-semibold uppercase tracking-wider text-muted-foreground/70`, รายการที่ไม่ active เป็น `text-foreground/80` (hover `text-foreground`), กลุ่มที่ 2 ขึ้นไปมีเส้นคั่น `border-t border-border/60` + `pt-3`; active และ `<select>` บนจอแคบคงเดิม — ไม่มี prop ใหม่ (ขอจาก Pi Auth)
806
-
807
- ## [0.11.0] — 2026-09-21
808
-
809
- ### Added
810
-
811
- - `AppShell` (layout sidebar): slot **`sidebarContentBefore`** / **`sidebarContentAfter`** วางใน `SidebarContent` ก่อน/หลังเมนู — เลื่อนไปกับเมนู (ต่างจาก `sidebarHeader` ที่ตรึงบน) ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน; สำหรับต้นไม้โฟลเดอร์ + ถังขยะ + มิเตอร์พื้นที่แบบ Google Drive (ขอจาก Pi Files — เดิมต้องยัดใน `sidebarHeader` แล้วจำกัด max-h เอง)
812
- - showcase: `/shell` ใส่ `TreeView` + `ProgressBar` ใน `sidebarContentAfter` เป็นตัวอย่าง (`app/shell/sidebar-drive.tsx`)
813
-
814
- ## [0.10.0] — 2026-09-21
815
-
816
- ### Added
817
-
818
- - **`SectionNav`** (`@suphark/ui/next`, สเปกจาก Pi Auth — ชิ้นสุดท้ายที่แอปยังประกอบเอง) — หัวหมวด + แถบแท็บย่อยใต้หัวข้อสำหรับ layout ของหมวดที่มีหลายหน้า (directory/access/security/integrations): `title`/`description`/`actions` ส่งต่อ `PageHeader`, `tabs[{ href? | slug?, label, exact?, badge? }]` (href หรือ `${base}/${slug}`), แต่ละแท็บเป็น `NavLink` (`-mb-px border-b-2` ไฮไลต์ `border-primary` ตาม pathname, `aria-current=page`), badge ต่อท้าย label, `navLabel` (ค่าเริ่มต้น `UiText.sectionNav.tabs` — "แท็บย่อย"/"Section tabs"), ไม่มี tabs = ไม่มี nav; ไม่มี state ใช้จาก server component ได้ (เฉพาะแถบ `<nav>` เป็น client component เล็ก ๆ เพื่ออ่าน UiText); types `SectionNavProps`, `SectionNavTab`; `UiText.sectionNav` (optional)
819
- - showcase: หน้า `section-nav` (แท็บชี้หน้าจริงในหมวดนำทาง — Link prefetch จะไม่ 404) — 97 หน้า; tests 3 กรณี (รวม 126)
820
-
821
- ## [0.9.0] — 2026-09-21
822
-
823
- ### Added
824
-
825
- - **`RuleBuilder<F extends string>`** (barrel, สเปกจาก Pi Files — แทน rule-builder ชั่วคราว 390 บรรทัดของกฎ storage/lifecycle) — ตัวสร้างเงื่อนไขแบบต้นไม้ AND/OR/NOT ซ้อนได้ generic ไม่ผูก schema: แอปส่ง `fields[{ key, label, operators[{ value, label }], defaultLeaf(), renderValue({ leaf, onChange, disabled }), describe?, group?, disabled?: boolean | string }]` (ตัวแก้ค่าเป็นของแอป — ToggleGroup/Input/number+หน่วย), component จัดโครง: กลุ่ม = กรอบ (root border ปกติ, ลูก border-dashed bg-muted/30) + ToggleGroup and/or + ปุ่ม "เงื่อนไข"/"กลุ่มย่อย" (ซ่อนเมื่อถึง `maxDepth` ค่าเริ่มต้น 3 รวม root; กลุ่มใหม่สลับ op จากแม่), leaf = grid `[ฟิลด์ 11rem | operator (เมื่อ >1) | ค่า | NOT]` มือถือเรียงลง (เปลี่ยนฟิลด์ → `defaultLeaf()` ของฟิลด์ใหม่), NOT = กรอบ border-destructive/40 + ป้าย "ไม่ใช่" + ปุ่ม "เอา NOT ออก" (`allowNot`), ปุ่มลบทุก item; ฟิลด์ใช้ `NativeSelect` (`label` เป็น string, `group` = optgroup, `disabled` string = title เหตุผล, ฟิลด์ที่เลือกอยู่แล้วไม่ถูกปิด), key ของ item คงที่ต่อ object (WeakMap) แก้ item หนึ่งไม่ re-mount พี่น้อง, `onChange` ต้นไม้ใหม่ทั้งก้อน immutable, `role=group` aria-label "กลุ่ม AND ระดับ n", `disabled` ทั้ง builder, `showSummary` ประโยคสรุปใต้ builder, `defaultField`, `language`/`text` (and/or/not/wrapNot/addCondition/addGroup/removeCondition/removeNot/emptyAnd/emptyOr/summary/field/operator/group()/andJoin/orJoin/notPrefix/unknownField()); data-slot `rule-builder`/`rule-group[data-op][data-depth]`/`rule-item`/`rule-leaf`/`rule-not`/`rule-builder-summary`
826
- - types `RuleLeaf<F>`/`RuleGroup<F>`/`RuleNot<F>`/`RuleCondition<F>`/`RuleFieldDef<F>`/`RuleBuilderProps<F>`/`RuleBuilderText`; helpers **`describeCondition(cond, fields, text?, language?)`** (AND " และ ", OR " หรือ ", NOT "ไม่ใช่ …", กลุ่มย่อย >1 ข้อครอบวงเล็บ, leaf ใช้ `describe` ของฟิลด์หรือ "label operator value", ฟิลด์ไม่รู้จัก → "ฟิลด์ไม่รู้จัก: key"), `isRuleGroup`/`isRuleNot` type guards, `emptyGroup(op = "and")`
827
- - showcase: หน้า `rule-builder` (ชุดกฎ storage ตัวอย่าง + JSON สด) — 96 หน้า; tests 4 กรณี (รวม 123)
828
-
829
- ### Fixed
830
-
831
- - **`CommandItem` ที่ active มองไม่ออกว่าตัวไหน** (รายงานจาก Pi Done ใน dark mode): shadcn ใช้ `data-selected:bg-muted` แต่ Tailwind `data-selected:` = `[data-selected]` (มีแอตทริบิวต์) ขณะที่ cmdk ใส่ `data-selected="false"` ให้รายการอื่นด้วย → ทุกแถวได้ bg-muted เท่ากัน; `styles.css` override `[data-slot=command-item][data-selected="true"]` เป็น `color-mix(muted, foreground 8%)` (dark 14%) — ไม่แตะ `command.tsx` ของ shadcn (CommandPalette, Combobox, SearchableSelect ได้ผลด้วย)
832
-
833
- ## [0.8.0] — 2026-09-21
834
-
835
- รอบ feedback หลังทุกแอปย้ายมา 0.7.0 ครบ 6/6 — สเปกจาก Pi Done (ColorPicker, CommandPalette client-side), Pi Mail (ActionForm), Pi Files (file explorer 4 ตัว) ทำรวดเดียว; minor ทั้งหมด ไม่เปลี่ยน API เดิม
836
-
837
- ### Added
838
-
839
- - **`ColorPicker<T>`** + **`ColorSwatch`** (barrel) — กริดเลือกสีจากชุดที่แอปส่ง `colors[{ value, label, swatchClassName? | swatch? (CSS) }]` (แพ็กเกจไม่ bundle palette): ปุ่ม `aria-pressed` + `aria-label`/`title`, ที่เลือก = `ring-2 ring-primary ring-offset-1` + ✓ ขาว, hover scale-105, roving tabindex + ลูกศร ↑↓←→/Home/End (จำนวนคอลัมน์อ่านจาก grid จริง), `columnsClassName` (grid-cols-5), `size` sm|md (size-7/size-8), `language`/`text`; `ColorSwatch` จุดสี size-3.5 สำหรับ trigger/badge (`color` CSS หรือ class) — ไม่มี popover ในตัว แอปครอบเองแบบ IconPicker (สเปกจาก Pi Done แทน metadata-color-picker 9 จุด)
840
- - **`CommandPalette` โหมด client-side / controlled** (ขอจาก Pi Done): `filter(query) => CommandPaletteSection[]` คำนวณฝั่งแอปทุกครั้งที่ query เปลี่ยน (เรียกแม้ query ว่าง — ใช้แสดง action/ล่าสุด, ไม่ debounce) แสดงก่อนผลจาก `search`; item ใน section รับ `onSelect()` (action ล้วน `href` ไม่บังคับแล้ว; มีทั้งคู่เรียก onSelect ก่อนแล้วไป href) + `shortcut` (ป้ายท้ายรายการ); `open`/`onOpenChange` คุมจากภายนอก, `showTrigger={false}` ซ่อนปุ่มของแพ็กเกจ, `shortcut={null}` ไม่ดักคีย์ลัด (แอปดักเอง); type `CommandPaletteSectionItem`
841
- - **`ActionForm`** (ข้อสังเกตจาก Pi Mail): `hideSuccessMessage` ซ่อนข้อความสำเร็จเมื่อแอปแจ้งผลเอง (ใน compact ข้อความดันปุ่มตกบรรทัด — ยังแสดง error), `submitDisabled` + `submitProps` (variant/size/className ของปุ่มส่ง); **`FormState<TData>`** — `ok: true` มี `data?: TData` และ `ActionForm<TData>`/`FormDialog<TData>`/`FormAction<TData>` generic → `onSuccess(s)` ได้ `s.data` ไม่ต้อง cast (`TData` ค่าเริ่มต้น `undefined` — โค้ดเดิมไม่ต้องแก้)
842
- - **`TreeView`** (barrel, สเปกจาก Pi Files) — ต้นไม้ทั่วไป `role=tree` ตาม WAI-ARIA (treeitem มี aria-level/expanded/selected, roving tabindex): `nodes[{ id, label, icon?, hasChildren?, children?, disabled?, badge? }]`, `selectedId`/`onSelect(id, node)`, `expandedIds`/`defaultExpandedIds`/`onExpandedChange` (controlled ได้), `loadChildren(id, node)` โหลดลูก lazy (Spinner ระหว่างโหลด, cache ในตัว, error = ลูกว่าง), `onDrop(targetId, e, node)`/`canDrop` HTML5 dnd ไฮไลต์ node (`data-drop-target`), `renderLabel(node, { expanded, selected, level })`, คีย์บอร์ด ↑↓ เลื่อน → ขยาย/เข้าไปลูกแรก ← ยุบ/กลับแม่ Home/End Enter/Space เลือก, `indent` (12px/ระดับ), `emptyState`, `label`; สีตาม token ของ Sidebar (เลือก = `bg-sidebar-accent`) — ลูกศรยุบ/ขยายไม่ใช่ button (ไม่ซ้อน focusable ใน treeitem)
843
- - **`Dropzone`** (barrel, สเปกจาก Pi Files) — พื้นที่ลากวางไฟล์: การ์ดเดี่ยว (เส้นประ + ไอคอน + ข้อความ + ปุ่มเลือกไฟล์; คลิกการ์ด/ปุ่ม/Enter เปิด picker) หรือ `overlay` ห่อ `children` แสดงชั้น "วางไฟล์ที่นี่" เฉพาะตอนลากไฟล์เข้าหน้า (ฟัง window + กัน browser เปิดไฟล์เมื่อวางนอกโซน); `onFiles(File[])`, `multiple` (true), `accept` (นามสกุล/กลุ่ม/mime — export `matchesAccept`), `maxBytes`, `onRejected(files, "size" | "type" | "multiple")`, `disabled`, `directory` (`webkitdirectory` + วางโฟลเดอร์ไล่อ่านไฟล์ข้างในผ่าน `webkitGetAsEntry`), `hint` (ค่าเริ่มต้นแสดง maxBytes), `icon`, `language`/`text`; data-attribute `data-active`/`data-disabled`
844
- - **`UploadList`** (barrel, สเปกจาก Pi Files) — รายการอัปโหลด `items[{ id, name, sizeBytes, progress 0–1, status queued|uploading|done|error|aborted, error?, mime? }]` พร้อม `ProgressBar` ต่อแถว (queued จาง), `FileTypeIcon`, สรุป "x/y เสร็จ"/"เสร็จทั้งหมด" (aria-live), `onCancel`/`onRetry`/`onClearDone`/`onClose`, ยุบ/ขยาย (`defaultCollapsed`), `floating` มุมล่างขวาแบบ Drive (w-80 ไม่เกินจอ), `title`, `language`/`text`; `items` ว่าง = ไม่ render
845
- - **`FileTypeIcon`** + **`getFileKind(mime, filename)`** (barrel) — ไอคอน lucide ตามชนิดไฟล์ (folder/image/video/audio/pdf/doc/sheet/slides/zip/code/text/default) จาก mime ก่อน นามสกุลสำรอง สีตาม token (pdf destructive, sheet success, folder/slides warning, สื่อ primary), `size` px, `data-kind`; **`formatBytes(bytes, locale)`** (lib/formatters) ฐาน 1024 "1.5 KB"/"12 MB"
846
- - showcase: หน้า `color-picker` (กริด + ครอบ Popover), `tree-view` (lazy + drop ไฟล์จริง), `dropzone` (การ์ด + overlay), `upload-list` (mock progress + ลอย), `file-type-icon`; `command-palette` เพิ่มแบบ client-side คุม open เอง; `action-form` เพิ่ม hideSuccessMessage/submitDisabled/data (SecretReveal) — 95 หน้า; tests 17 กรณีเพิ่ม (รวม 119)
847
-
848
- ## [0.7.0] — 2026-09-20
849
-
850
- component ระดับแอปที่ Pi Auth และ Pi Done เขียนซ้ำกัน — สเปกจาก Pi Auth ออกแบบให้ทั้งสองแอปใช้ร่วม (ไม่พึ่ง i18n/auth ของแอป ทุกข้อความรับเป็น prop หรือ `UiText`)
851
-
852
- ### Added
853
-
854
- - **`CommandPalette`** (`@suphark/ui/next`) — ค้นหาแบบ ⌘K/Ctrl+K ทั้งแอป: `pages` (หน้าในเมนูที่แอปกรองสิทธิ์แล้ว — กรองฝั่ง client ตาม label/group/href/keywords) + `search?(query) => Promise<CommandPaletteSection[]>` (server action ของแอป เรียกเมื่อ ≥ `minQuery` ตัว, debounce `debounceMs` 150, เก็บ query คู่ผล ผลล่าสุดเท่านั้นที่แสดง), `compact` (ปุ่มไอคอนใน header) หรือปุ่มเต็มกว้าง + `Kbd` (⌘K/CtrlK ตาม platform หลัง mount) สำหรับ `sidebarHeader`, `shortcut` (ค่าเริ่มต้น k), `text` (fallback `UiText.palette`), `onNavigate` (ค่าเริ่มต้น router.push + ปิด); `Command shouldFilter={false}`, `CommandEmpty` แสดง "กำลังค้นหา…" ขณะ pending; types `CommandPaletteItem`, `CommandPaletteSection`, `CommandPaletteText`
855
- - **`OrganizationSwitcher`** (`@suphark/ui/next`) — ตัวสลับองค์กร/tenant แบบ Clerk บน `HeaderCrumb` + `DropdownMenu`: `organizations[{ id, name, slug?, logoUrl? }]`, `activeId`, `onSwitch(id)` (server action — component ทำ useTransition + `router.refresh()` ปิดได้ด้วย `refresh={false}`), `placement="header"` (HeaderCrumb ใน layout top) / `"sidebar"` (ปุ่มเต็มกว้าง + SwitcherChevron ใน `sidebarHeader`), `fallbackLabel`/`heading` (ค่าเริ่มต้น `UiText.switcher`), `hint(org)` (ค่าเริ่มต้น `/slug`), `footer` (slot ท้ายเมนู เช่น สร้างองค์กร); องค์กรเดียว = ไม่มีเมนู, active มี ✓ และกดไม่ทำอะไร, pending ปิดปุ่ม, โลโก้ `<img size-5>` หรือ Building2; types `OrganizationOption`, `OrganizationSwitcherProps`
856
- - `UiText.palette` / `UiText.switcher` (optional, มีค่าเริ่มต้นไทย/อังกฤษ)
857
- - **`IconPicker`** (barrel, generic `T extends string`) — เลือกไอคอนจากชุดที่แอปส่ง `icons[{ name, icon: LucideIcon, labelTh, labelEn, keywords? }]` (แพ็กเกจไม่ bundle ชุดไอคอน): ค้นหา name/labelTh/labelEn/keywords ผ่าน `useDeferredValue`, preview ไอคอน+ชื่อด้านบน, กริด `SelectionButton` (aria-pressed, title/aria-label ตาม `language` th/en), `accentColor` (inline style สี dynamic ของผู้ใช้), `heightClassName`/`columnsClassName`, `text` ทับข้อความ — สเปกจาก DueBell ให้ Pi Done (metadata icon) ใช้ร่วม
858
- - **`SelectionButton`** (barrel) — ปุ่ม toggle `aria-pressed` + สีเน้นเมื่อเลือก (Button outline) — chip ประเภท/ตัวกรอง, ช่องใน IconPicker (ยกจาก DueBell)
859
- - **`CalendarMonthGrid<T>`** (barrel) — ตารางเดือนเลือกวันที่แอปคุมเนื้อหาต่อวัน: `month "YYYY-MM"`, `selectedDay`, `onSelectDay(day, date)`, `itemsByDay`, `renderDay(ctx)` (คืน null = ✓ ของแพ็กเกจเมื่อ complete), `dayState(items, ctx) → { complete?, hasItems? }` → data-attribute `data-selected/today/has-items/complete/weekend` ให้ CSS ของแอปแต่ง, (data-attribute เป็น boolean มี/ไม่มี — CSS ใช้ `[data-selected]`), `weekStartsOn 0|1`, `weekdayLabels` (export `WEEKDAYS_TH`/`WEEKDAYS_EN`), `today` (ไม่ส่ง = browser หลัง mount ผ่าน `useMounted` — SSR ได้ไม่ mismatch), `dayLabel` (aria-label "วันที่ N มี M รายการ"), `legend`, `dayClassName`; ช่องว่างก่อนวันที่ 1 aspect-square; ปุ่มวัน `aria-pressed` — สเปกจาก DueBell + Pi Done
860
- - **`ProgressBar`** (barrel) — แถบความคืบหน้าตั้ง `tone` (default/success/warning/destructive), `size` sm/md/lg, `label`, `showValue`/`format`, `trackClassName`/`indicatorClassName`/`indicatorStyle` (สี dynamic) จาก props — `Progress` ของ shadcn ต่อท้าย Track+Indicator ให้เสมอ ส่ง children เข้าไปจะได้ 2 แถบ (รายงานจาก DueBell) จึงประกอบจาก `ProgressPrimitive.Root` โดยตรง
861
- - showcase: หน้า `icon-picker`, `selection-button`, `calendar-month-grid`, `progress-bar`; tests 6 กรณีเพิ่ม (รวม 100)
862
- - **error รายช่องในฟอร์ม server action** (ขอจาก Pi Mail): `FormState` แบบ `ok: false` รับ `errors?: Record<string, string>`; `ActionForm` และ `FormDialog` รับ `children` เป็นฟังก์ชัน `({ state, pending, errors, fieldError }) => ReactNode` (type `FormRenderContext`) → `<LabeledField error={fieldError("email")}>` ไม่ต้องมี hook ของแอป; `FormMessage` ยังแสดง `error` รวมเหมือนเดิม
863
- - `UserMenu`: prop `menuLabel` + `UiText.userMenu`/`signOut` (ขอจาก Pi PDF — แอปอังกฤษได้ label ไทย); `ThemeMenuItems`: prop `onThemeChange(theme)` สำหรับบันทึกธีมเป็น preference ของบัญชี + ข้อความจาก `UiText.theme`
864
- - `LabeledField`: prop `htmlFor` — ส่งมาแล้ว label ไม่ครอบ control (ผูกด้วย id) ให้ tooling ที่ไม่คำนวณ nested label เห็น accessible name (ข้อสังเกตจาก Pi Files)
865
- - `DataTable`: `showToolbar` / `showPagination` (ค่าเริ่มต้น true) ปิดแถบค้นหา/ตัวกรอง/View และ pagination สำหรับตารางตั้งค่าเล็ก ๆ (ขอจาก Pi Files)
866
-
867
- ### Fixed
868
-
869
- - **`AppShell` (layout sidebar) มี `<main>` ซ้อน 2 ชั้น** (รายงานจาก DueBell): `SidebarInset` ของ shadcn เป็น `<main>` อยู่แล้ว → ใส่ `id={APP_MAIN_ID}`/`tabIndex` ที่ SidebarInset และเลิกสร้าง `<main>` ชั้นใน (เป็น `<div>`); layout top ยังมี `<main>` ตัวเดียวเหมือนเดิม
870
- - showcase: หน้า `command-palette`, `organization-switcher`; `/shell` (sidebarHeader + ยุบเป็นไอคอน) และ `/shell-top` (crumb องค์กร) ใช้สองตัวนี้จริงแทน mock เดิม; tests 5 กรณี (คีย์ลัด toggle + กรอง keywords, search debounce/ผลล่าสุด/push+ปิด, org เดียวไม่มีเมนู, active ไม่เรียก onSwitch + refresh, fallback label)
871
-
872
- ## [0.6.1] — 2026-09-20
873
-
874
- ### Added
875
-
876
- - **`npx suphark-ui-audit`** (`bin` ของแพ็กเกจ, `scripts/audit.mjs`) — ตรวจโค้ดแอปปลายทางตามกติกา design system ในคำสั่งเดียว: error = `window-dialog` (window.confirm/alert/prompt), `radix-state` (`data-[state=…]` ที่ Base UI ไม่มี), `arbitrary-text` (`text-[Npx]`), `base-ui-props` (สคริปต์เดิม); warning = `raw-palette` (bg-red-500 …), `important` (utility ลงท้าย `!` — เฉพาะ `--important` สำหรับโค้ดแพ็กเกจ); `--strict` ให้ warning เป็น error; ยกเว้น `// ui-audit-ok: เหตุผล` / `/* ui-audit-ok-file: เหตุผล */`; ข้าม `components/ui/**`, tests, generated; README + `docs/agent-rules.md` มีวิธีใช้ — repo นี้รัน `pnpm audit:ui` ใน `verify` และ CI (0 error 0 warning)
877
- - `FormFieldRadioCards`: prop `aria-label` ส่งต่อไป RadioGroup เมื่อไม่แสดง `label` (ข้อสังเกตจาก Pi Auth หลังย้ายมาใช้ — ของเดิมมี aria-label)
878
-
879
- ### Changed
880
-
881
- - ล้าง raw palette ในแพ็กเกจเป็น semantic token: labs badge ใน `AppNav` → `text-warning-muted-foreground`, `FloatingSaveButton` สำเร็จ → `bg-success`, `FormMessage` สำเร็จ → `text-success`, ดอกจัน/ข้อความ error ใน `FormField*` → `text-destructive`, ไอคอน `ThemeToggle` → `text-warning`/`text-primary`/`text-muted-foreground`, ตัวอย่างใน design-system; ที่ยังใช้ palette โดยตั้งใจและจดไว้ (`ui-audit-ok-file`): `Notice`/`StatusBadge` (โทน 50/900 คู่ dark — รอตัดสินใจ token ระดับ tone), `lib/status-colors.ts` (ชุดสีให้ผู้ใช้เลือก)
882
-
883
- ## [0.6.0] — 2026-09-20
884
-
885
- component สำเร็จรูปสำหรับฟอร์ม server action ที่ Pi Auth ประกอบเองอยู่ (DueBell/Mail/Files ใช้ได้เหมือนกัน) — ไม่เปลี่ยน API เดิม; **รวม 0.5.2 ที่ยังไม่ได้ publish**
886
-
887
- ### Added
888
-
889
- - **`LabeledField`** + **`CheckboxField`** (barrel) — ช่องฟอร์มสำหรับ server action ธรรมดา (ไม่ใช่ react-hook-form): label ครอบ control + `hint` หรือ `error` (ตั้ง `data-invalid`) ประกอบจาก shadcn `Field/FieldLabel/FieldDescription/FieldError`; `CheckboxField` = Checkbox + label บรรทัดเดียว ใส่ `name` แล้วส่ง "on" ใน FormData — ยกจาก Pi Auth (42 ไฟล์) คง API เดิมเป๊ะ; ใช้จาก server component ได้ (ชื่อ `LabeledField` เพราะ `Field` ชนกับ shadcn และ `FormField` ชนกับ react-hook-form)
890
- - **`ActionForm`** (barrel — ไม่พึ่ง next) — ฟอร์ม inline คู่กับ `FormDialog`: `useActionState` + `FormMessage` + ปุ่มส่ง (ป้ายค่าเริ่มต้น `UiText.save`), ไม่ใช้ `action=` บน `<form>` จึงคงค่าที่พิมพ์เมื่อ action คืน error; props `action`, `title?`, `submitLabel?`, `compact?` (ไม่มี Card, ปุ่ม sm, จัดแนวนอน wrap สำหรับแถวตาราง), `className?`, `onSuccess?(state)`; export type **`FormAction`** จาก barrel (FormDialog ใช้ชนิดเดียวกัน)
891
- - **`Stepper`** (`@suphark/ui/next` — ใช้ next/link) — แถบขั้นตอน wizard ทุกขั้นกดเข้าได้: `steps[{ key, href, label, status: done|todo|problem, statusLabel? }]`, `current`, `ariaLabel?`; แนวนอนเลื่อนได้บนจอแคบ แนวตั้งบน lg; วงกลม เลข/✓/⚠ + `StatusBadge` ใต้ชื่อ; `aria-current="step"`; server component ได้; types `StepperStep`, `StepStatus`
892
- - **`FormFieldRadioCards`** (backward-compatible): ใช้ **โดยไม่มี `control`** ในฟอร์ม server action ได้ — `name` ส่งต่อไป `RadioGroup` (มี hidden input ใน FormData) + `defaultValue` (uncontrolled) หรือ `value`+`onValueChange` (controlled); **`layout="row"`** = การ์ดแนวนอน ปุ่มวิทยุจริงซ้าย ไอคอน+ชื่อบรรทัดบน คำอธิบายบรรทัดล่าง (`grid sm:grid-cols-2`, checked = `border-primary bg-primary/5`); layout `grid` เดิม จุดเลือกมุมขวาบนตอนนี้ตามสถานะจริงของปุ่มวิทยุ (CSS `peer-data-checked`) จึงถูกทั้ง controlled/uncontrolled
893
- - showcase: หน้า `labeled-field`, `action-form`, `stepper`, `form-field-radio-cards`; tests 8 กรณี (labeled-field, action-form ส่ง FormData/คงค่า/onSuccess/compact, stepper, radio-cards FormData + row)
894
-
895
- ## [0.5.2] — 2026-09-20 (ไม่ได้ publish แยก — รวมใน 0.6.0)
896
-
897
- รอบรีวิวประสิทธิภาพ + บั๊กที่เหลือ — ไม่เปลี่ยน props
898
-
899
- ### Fixed
900
-
901
- - **`PullToRefresh`** (`@suphark/ui/pwa`): effect ผูก `pullDistance`/`isRefreshing` เป็น deps → ถอด/ใส่ listener 4 ตัวบน `window` ใหม่ทุก `touchmove` และ `touchend` หลุดได้ระหว่างนั้น (ดึงแล้วไม่รีเฟรช/ป้ายค้าง) — อ่านค่าผ่าน ref ลงทะเบียนครั้งเดียว
902
- - **`ThemeToggle`** เมื่อส่ง `persistedTheme`: effect ผูก `theme` ด้วย → ผู้ใช้กดเปลี่ยนแล้วถูก set กลับเป็นค่าที่บันทึกไว้ทันที (server action ยังไม่กลับ) — sync เฉพาะเมื่อ `persistedTheme` เปลี่ยน (รวมครั้งแรก)
903
- - **`DataTableRowActions`**: `EditDialog` ถูก mount ทุกแถวตลอดเวลา (50 แถว = 50 Dialog+ฟอร์ม และ `initialData` stale) — mount เฉพาะตอนเปิด
904
- - **`useDataTable`**: `onGlobalFilterChange` อ่านค่าเก่าจาก closure (พิมพ์เร็วใน tick เดียวหายตัวอักษร) → อ่านจาก `table.getState()`; `getPaginationRowModel` ไม่ใส่เมื่อ `manualPagination` (server แบ่งหน้าแล้ว ไม่ตัดซ้ำ); ตัด `useMemo` ที่ไม่มีผล
905
- - **`FormFieldCombobox`**: cache ตัวเลือก key ด้วย function identity ใน `Map` module-level → inline fetcher ทำให้โตไม่จำกัด (memory leak) และไม่เคย hit — key ที่เป็นฟังก์ชันเก็บใน `WeakMap` (ส่ง `fetcherKey` ยังดีที่สุด)
906
-
907
- ### Changed (ประสิทธิภาพ)
908
-
909
- - **`ClientDateFormatter`**: ทุก instance เคยมี `setInterval` ของตัวเอง + render เพิ่ม 1 รอบตอน mount → ใช้ **`useTicker()`** นาฬิกากลางแชร์ timer เดียวต่อ interval + `useMounted()` (ไม่มี render เพิ่ม) + cache `Intl.DateTimeFormat`
910
- - **`FormattedNumber`**: จาก state + 2 effects (3 renders/เซลล์) และ `new Intl.NumberFormat` ทุกครั้ง → `useMounted()` + `useMemo` + cache formatter ต่อ locale+options (ตารางหลายร้อยเซลล์เห็นผล)
911
- - **`DataTableFacetedFilter`**: `selectedValues` Set ใหม่ทุก render ทำให้ `useMemo` เรียง options ไม่เคย hit → memo จาก `filterValue` จริง
912
- - **`AppNav`**: กลุ่มเมนูที่กางได้เปลี่ยนจาก remount ทั้ง subtree ด้วย `key` เมื่อลูก active/ค้นหา → Collapsible แบบ controlled (`NavCollapsible`) เปิดอัตโนมัติเมื่อสถานะเปลี่ยน ผู้ใช้กาง/หุบเองได้ ไม่ mount ใหม่ มี animation
913
- - **`RelativeDate`** ใช้ `useMounted()` กลาง
914
-
915
- ### Added
916
-
917
- - **`useMounted()`** (barrel) — true เฉพาะฝั่ง client หลัง hydrate ผ่าน `useSyncExternalStore` (ไม่มี render เพิ่ม) — ใช้กับค่าที่ server/browser ต่างกัน (เวลา, โซนเวลา, localStorage)
918
- - **`useTicker(intervalMs)`** (barrel) — ตัวนับที่เพิ่มทุก interval, ทุก instance แชร์ `setInterval` เดียวต่อ interval (เริ่ม/หยุดตามผู้ใช้ตัวแรก/สุดท้าย); 0 = ไม่ tick
919
- - **`getNumberFormat` / `getDateTimeFormat`** (`@suphark/ui/lib/intl-cache`, barrel) — Intl formatter ที่ cache ต่อ locale+options
920
- - tests: `use-ticker` (แชร์ timer/หยุดเมื่อถอดหมด), `intl-cache`
921
-
922
- ### ไม่ได้แก้ตามที่ประเมินไว้ (0.5.2)
923
-
924
- - `DataTableToolbar` render ชุดตัวกรอง 2 ครั้ง (desktop + Sheet มือถือ): ตรวจแล้ว `SheetContent` ของ Base UI ไม่ mount ลูกเมื่อปิด ชุดใน Sheet จึงมีเฉพาะตอนเปิด — ต้นทุนจริงมีแค่ชุด desktop ที่ `hidden` บนมือถือ (ปุ่ม + trigger ไม่มี popup) ไม่คุ้มเสี่ยง hydration flash จากการ gate ด้วย `useIsMobile()`
925
-
926
- ### Added (showcase/เครื่องมือ)
927
-
928
-
929
- - showcase: หน้า **`/changelog`** render `packages/ui/CHANGELOG.md` ตอน build (parser เล็ก ๆ ไม่เพิ่ม dep: รุ่น/หมวด Added-Changed-Fixed/รายการ/inline code-bold-link) + ลิงก์ในเมนูบน
930
- - **`pnpm check:changelog`** (`scripts/check-changelog.mjs`) — ทุกการเปลี่ยนแปลงใน `packages/ui/src`, `package.json`, `README.md`, `apps/showcase/src` ต้องมีบรรทัดใน CHANGELOG; ตรวจ working tree ก่อน commit (อยู่ใน `pnpm verify`) และช่วง commit ใน CI (`CHANGELOG_RANGE`)
931
-
932
- ## [0.5.1] — 2026-09-20
933
-
934
- ### Fixed
935
-
936
- - **`RelativeDate` hydration mismatch (React #418) บน production** — เดิม render "วันนี้/เมื่อวาน" บน server ด้วย "ตอนนี้" ของ server (UTC และ module ที่โหลดไว้ก่อน) ซึ่งต่างจาก browser ได้ทั้งโซนเวลาและข้ามวัน → บน Vercel หน้า `/shell` (ตารางมี RelativeDate) hydrate พังทั้งหน้าและ React render ใหม่ทั้ง tree (เห็น theme script ซ้ำ 2 ตัว) — ตอนนี้คำนวณเฉพาะฝั่ง client หลัง mount (`useSyncExternalStore` guard แบบเดียวกับ `ClientDateFormatter`) ก่อน mount แสดงไอคอน + ช่องว่างขนาดเดิม
937
- - `RelativeDate`: เอาข้อความเฉพาะ supplychain ("วันที่อ้างอิงราคาจริง") ออกจาก tooltip → prop ใหม่ `titlePrefix` (ค่าเริ่มต้น "วันที่") และ `emptyText`; สีเป็น semantic token (`text-muted-foreground`) แทน `text-slate-*` ตามกฎ design system (className/iconClassName ทับได้เหมือนเดิม)
938
- - showcase: launch config `showcase-prod` (:3202, `next start` หลัง build) สำหรับตรวจ production build ในเครื่อง
939
-
940
- ## [0.5.0] — 2026-09-19
941
-
942
- ยกของที่พิสูจน์แล้วจาก Pi Done (todo/kanban) เข้าแพ็กเกจ — ไม่เปลี่ยน props เดิม
943
-
944
- ### Added
945
-
946
- - **`DialogsProvider` + `useDialogs()`** (`@suphark/ui`): กล่องโต้ตอบแบบ promise ทั้งแอปบน AlertDialog — `alert / confirm / prompt / choose / checklist` ครอบครั้งเดียวที่ root แล้วเรียกได้ทุกที่โดยไม่ต้อง render อะไรในผู้เรียก (ต่างจาก `useConfirm()` ที่ยังอยู่เหมือนเดิม); เปิดคำขอใหม่ขณะกล่องเปิด → คำขอเดิม resolve เป็นค่ายกเลิก; `UiText` เพิ่ม optional `ok`, `alertTitle`, `promptTitle`, `chooseTitle`, `checklistTitle` (มีค่าใน `UI_TEXT_TH`/`UI_TEXT_EN`)
947
- - **`createKanbanCollisionDetection()`** (`@suphark/ui/kanban` — entry แยกเพราะ @dnd-kit/core สร้าง context ตอน import ใช้จาก server component ไม่ได้): collision detection ของ dnd-kit สำหรับบอร์ดหลายคอลัมน์ — pointer ในช่องว่างระหว่างการ์ด/ใต้การ์ดสุดท้ายแปลงเป็น "การ์ดที่ใกล้ที่สุดในคอลัมน์" (ไม่นับการ์ดที่ลาก) แทนการรายงานคอลัมน์ซึ่งทำให้ dnd-kit ล้าง transform (การ์ดเด้งกลับ, drop ไปท้ายคอลัมน์); คอลัมน์ว่างรายงาน droppable ของรายการ; รองรับ droppable อื่นของคอลัมน์ผ่าน `resolveColumnId` (เช่น handle ลากเรียงคอลัมน์)
948
- - **`useTrackedState()`** (`@suphark/ui`): `useState` + `getLatest()` ที่อัปเดต synchronous — mutation ติดกันใน tick เดียว (drag-drop, automation, bulk) อ่านค่าล่าสุดแทน closure ตอน render
949
- - **`@suphark/ui/pwa`** (entry ใหม่): `PwaServiceWorkerRegistration` (ลงทะเบียน SW + เก็บ `beforeinstallprompt` + `watchServiceWorkerUpdates`: เช็คอัปเดตตอนโหลด/กลับมาที่แท็บ/ทุก 15 นาที แล้ว reload ครั้งเดียวเมื่อ worker ใหม่คุมหน้า เลื่อนขณะพิมพ์ ไม่ reload ตอนติดตั้งครั้งแรก), `PwaInstallCard` (`appName`, `text` ค่าเริ่มต้นไทย + `PWA_INSTALL_CARD_TEXT_EN`), `PullToRefresh` (มือถือ, `router.refresh()`, `PULL_TO_REFRESH_TEXT_EN`), install-prompt store; `PullToRefresh` ใช้ next/navigation จึงอยู่ entry นี้ไม่ใช่ barrel หลัก
950
- - showcase: หน้า `dialogs`, `kanban-board` (บอร์ด 3 คอลัมน์ลากได้จริง), `pwa-install-card`
951
-
952
- ## [0.4.4] — 2026-09-19
953
-
954
- รอบแก้บั๊กจาก code review ทั้ง codebase + audit Web Interface Guidelines — ไม่เปลี่ยน props เดิม
955
-
956
- ### Fixed
957
-
958
- - **`DeleteManyButton`**: `AlertDialogAction` ของ Base UI เป็นปุ่มธรรมดา (ไม่ใช่ Close) → ลบสำเร็จแล้วกล่องค้างเปิด กด Delete ซ้ำได้ด้วย ids ที่ลบไปแล้ว — คุม `open` เอง ปิดหลังสำเร็จ + `table.resetRowSelection()`; ปิดไม่ได้ระหว่าง pending
959
- - **`useDataTable`**: effect sync `initialPageSize` ทับ `pagination.pageSize` ที่ควบคุมจาก prop/URL ตอน mount (URL=50 แต่ตารางกลายเป็น 25 และตัดแถวที่ server ส่งมา) — ข้ามเมื่อ `pagination` ถูกส่งมา (controlled)
960
- - **`FormFieldSelect`**: `onValueChange` ไม่ถูกเรียกเลยเมื่อไม่ส่ง `variant` (ค่าเริ่มต้น select)
961
- - **`FormFieldInput isNumeric`**: แปลงเป็น number ทันทีทำให้ศูนย์ท้ายทศนิยมหายขณะพิมพ์ ("1.0" → "1" พิมพ์ 1.05 ไม่ได้) — คงเป็น string จนทศนิยมจบ
962
- - **`ErrorState`**: hardcode `/api/auth/get-session` + `/sign-in` ของ supplychain → แอปอื่นได้ 404 แล้วถูกตีความว่า "เซสชันหมดอายุ" พร้อมลิงก์ที่ไม่มี — ตรวจเซสชันเฉพาะเมื่อส่ง prop ใหม่ `sessionCheckUrl` (และ `signInHref` ค่าเริ่มต้น `/sign-in`); response ที่ไม่ใช่ 401/ok ไม่ถือว่าหมดอายุ (supplychain ส่ง `sessionCheckUrl="/api/auth/get-session"` เมื่อย้ายมาใช้)
963
- - **`formatRelativeThaiDate`**: `Math.abs` ทำให้วันพรุ่งนี้เป็น "เมื่อวาน" — ใช้ `differenceInCalendarDays` คงเครื่องหมาย เพิ่ม "พรุ่งนี้"; ไกลกว่านั้น date-fns บอกทิศทางเอง
964
- - **data attribute ของ Radix ที่หลงมาบน Base UI** (ไม่เคยทำงาน): `AppNav` ลูกศรกลุ่มเมนูไม่หมุน (`data-[state=open]` → `data-open`), `FormFieldRadioCards` การ์ดที่เลือกไม่ไฮไลต์ (`peer-data-[state=checked]` → `peer-data-checked`), trigger เปิดเมนูไม่มีพื้นหลัง (`UserMenu`, `DataTableColumnHeader`, `DataTableRowActions`, `HeaderCrumb` → `data-popup-open`), แท็บ `DesignSystemShowcase` ไม่ไฮไลต์ (`data-[state=active]` → `data-active`)
965
- - **`ConfirmActionButton` / `RowActions`**: action โยน error → เด้ง error boundary ทั้งหน้าและ dialog ค้าง — จับแล้ว `toast.error(message)` (ข้อความจาก `UiText.actionFailed` optional) คง dialog ไว้ให้ลองใหม่
966
- - **`HeaderBreadcrumbs hideIds`**: regex `[0-9a-z]{16,}` ซ่อน segment คำธรรมดายาว ≥16 (เช่น `organizationmembers`) — ต้องมีตัวเลขปนจึงถือเป็น id
967
- - **`RowActions` รายการ `href`** render เป็น `<Link>` จริง (Ctrl/⌘+click, คลิกกลาง, เปิดแท็บใหม่ได้) ยกเว้นมี `confirm`; **`TransitionLink`** ไม่ดัก modifier keys/คลิกกลางแล้ว
968
- - **`RefreshButton`**: `{...props}` ทับ `isLoading` ที่รวม pending ของ `router.refresh()` เมื่อผู้เรียกส่ง `isLoading={false}`
969
- - **cmdk value ชนกันเมื่อ label ซ้ำ** (`Combobox`, `FormFieldCombobox`, `FormFieldSelect variant="combobox"`, `FormFieldInput variant="autocomplete"`): ต่อ `value` ท้าย label ให้ระบุรายการได้ (ค้นด้วยรหัสได้ด้วย)
970
-
971
- ### Added (Web Interface Guidelines)
972
-
973
- - **`AppShell`**: กล่องเนื้อหาเป็น `<main id="app-main" tabIndex={-1}>` (export `APP_MAIN_ID`) + skip link "ข้ามไปเนื้อหา" (มองเห็นเมื่อกด Tab ตัวแรก) ทั้ง layout sidebar และ top
974
- - `styles.css`: `@media (prefers-reduced-motion: reduce)` ตัด animation/transition ทุก component (tw-animate, animate-spin, dialog zoom)
975
- - `SubNav`/`TopTabs`: `focus-visible:ring-2` ให้เท่ากับ HeaderCrumb; `<select>` ของ SubNav ใส่ `text-foreground` (Windows dark mode); `ConfirmDialog` ปุ่มยืนยัน `aria-busy` ตอน pending; ข้อความ "ไม่พบ" ใช้ curly quotes
976
- - showcase: `/login` กัน `next=//host` (protocol-relative) ; `LayoutSwitch` ป้ายเป็น `sr-only` บนจอแคบ; demo `form-fields` เพิ่มช่อง isNumeric + `onValueChange`
977
-
978
- ## [0.4.3] — 2026-09-19
979
-
980
- ### Added
981
-
982
- - **`useConfirm()`** (barrel) — `window.confirm` แบบ promise บน ConfirmDialog สำหรับโค้ดฝั่ง client ที่ไม่ใช่ server action (ยกมาจาก DueBell ที่ย้าย window.confirm 8 จุดด้วย hook นี้): `const { confirm, confirmDialog } = useConfirm(); if (!(await confirm({ description, destructive: true }))) return;` แล้ว render `{confirmDialog}`; เรียกซ้ำขณะเปิดอยู่ → คำขอเดิม resolve(false)
983
- - `ConfirmSpec` object รับ `destructive?` (RowActions/ConfirmActionButton ใช้เป็นตัวทับค่าเดิมของรายการ/variant)
984
- - README: ตาราง "ยืนยันก่อนทำ — ห้ามใช้ window.confirm" (เลือก RowActions.confirm / ConfirmActionButton / useConfirm / ConfirmDialog) และหมายเหตุ Turbopack ค้าง `Package path ./styles.css is exported … but no valid target file` หลังอัปเดตแพ็กเกจ = cache ของ dev server ให้ลบ `.next`
985
-
986
- ## [0.4.2] — 2026-09-19
987
-
988
- ### Added
989
-
990
- - **`ConfirmDialog`** (barrel) — กล่องยืนยันมาตรฐานบน `AlertDialog` ของแพ็กเกจ ใช้แทน `window.confirm` (webview/automation บางตัวปิด window.confirm ทิ้ง กดแล้วไม่เกิดอะไร และไม่เข้า design system): controlled `open`/`onOpenChange`, `title?` (ค่าเริ่มต้น `UiText.confirmTitle`), `description`, `confirmLabel?` (ค่าเริ่มต้น `UiText.confirm`), `cancelLabel?`, `destructive` (ปุ่มยืนยันสีแดง), `pending` (ปุ่มหมุน + ปิดไม่ได้ชั่วคราว), `onConfirm`; พร้อม type `ConfirmSpec` (= `string | { title?, description, confirmLabel?, cancelLabel? }`) และ `normalizeConfirm()`
991
- - **`ConfirmActionButton`** (`@suphark/ui/next`) — ปุ่มเดี่ยว + ConfirmDialog + server action ในตัว (`confirm`, `action`, `data`, `onDone?` + props ของ ActionButton เช่น `variant`/`icon`/`permission`) รอ action เสร็จแล้ว `router.refresh()` — ใช้แทน `<form action>` + window.confirm ที่ Pi Auth มี (ชื่อไม่ใช้ `ConfirmButton` เพราะมีปุ่มธรรมดาชื่อนั้นอยู่แล้วใน standard buttons)
992
- - `UiText.confirm` / `UiText.confirmTitle` (optional มีค่าเริ่มต้นไทย/อังกฤษ) — แอปที่ส่ง UiText เองไม่ต้องเพิ่ม
993
- - export type `RowActionItem` (รายการที่ไม่ใช่ separator)
994
-
995
- ### Changed
996
-
997
- - **`RowActions` / `RowMenu` prop `confirm`** เปิด `ConfirmDialog` แทน `window.confirm` (ขอจาก Pi Auth ผ่าน Pi Mail) — รับ `ConfirmSpec`: string เดิมยังใช้ได้ (= description, title/ปุ่มค่าเริ่มต้น) หรือ object กำหนด `title`/`confirmLabel`/`cancelLabel`; ปุ่มยืนยันสีแดงเมื่อ `destructive`; dropdown ปิดก่อน dialog เปิด และ dialog render นอก DropdownMenu (กัน focus trap ของ Base UI ชนกัน); กดยืนยันแล้วค่อยรัน `href`/`onSelect`/`action` ใน transition เหมือนเดิม — โค้ดเรียกเดิมไม่ต้องแก้
998
- - ไม่มี `window.confirm` เหลือในแพ็กเกจ
999
- - showcase: หน้า `/components/confirm-dialog` (ConfirmDialog + ConfirmActionButton) และ `/components/row-actions` ใช้ confirm แบบ object
1000
-
1001
- ## [0.4.1] — 2026-09-19
1002
-
1003
- ### Added
1004
-
1005
- - **`SwitcherChevron`** (barrel) — ลูกศร ⇅ มาตรฐานท้าย trigger แบบ switcher/select: `ChevronsUpDown` ขนาด `size-3` (12px) `shrink-0 text-muted-foreground opacity-70` aria-hidden (เล็กบางแบบ Clerk) รับ `className` ทับได้ — ใช้ตัวเดียวทั้งแพ็กเกจ/แอป (`OrganizationSwitcher` ใน @suphark/auth-ui จะย้ายมาใช้ด้วย)
1006
-
1007
- ### Fixed
1008
-
1009
- - `HeaderCrumb`: ลูกศรใหญ่ 24px ในแอปปลายทาง (Pi Auth) — เดิมใช้ `size-3.5!` ซึ่ง Tailwind ของแอปที่ scan ผ่าน `@source` ไม่ generate class แบบ `!` → svg ไม่มีขนาดจึงตกไป 24px ของ lucide; เปลี่ยนเป็น `SwitcherChevron` (ไม่ใช้ `!`)
1010
-
1011
- ### Changed
1012
-
1013
- - ลูกศรของ `UserMenu`, `Combobox`, `MultiSelectCombobox`, `FormFieldCombobox`, `FormFieldSelect` เปลี่ยนมาใช้ `SwitcherChevron` (เดิม h-4 w-4 opacity-50 ปนกันหลายขนาด) — ทุกจุดได้ 12px จางเท่ากัน; ไอคอน sort ใน `DataTableColumnHeader` ไม่ใช่ switcher คงเดิม; `components/ui/searchable-select.tsx` เป็นไฟล์ shadcn ไม่แตะ
1014
- - นโยบาย: หลีกเลี่ยง `!` บน utility ในแพ็กเกจถ้าไม่จำเป็น (เหลือเฉพาะ `group-data-[collapsible=icon]:p-0!` ใน `AppBrand` ซึ่ง `sidebar.tsx` ของ shadcn generate class เดียวกันอยู่แล้ว)
1015
-
1016
- ## [0.4.0] — 2026-09-19
1017
-
1018
- ### Added
1019
-
1020
- - **`AppShell layout="top"`** — โครงแบบ Clerk dashboard (ขอจาก Pi Auth): header 56px มีโลโก้ + **crumb switchers** คั่น `/` (`crumbs` = array หรือ component ที่ render `<HeaderCrumbs>`) + `header.actions`/`banner`/`center`; **แถบแท็บหลัก** ใต้ header จาก `nav` ชั้นแรก (active underline ตาม prefix, badge, เลื่อนแนวนอนบนมือถือ); แท็บที่มี `items` → **sub-nav ซ้าย** 224px (รองรับหัวกลุ่ม 2 ชั้น, `searchable`, มือถือเป็น select); `contentMaxWidth`, `brandColor` ใช้ได้เหมือนเดิม; breadcrumb ของหน้าปิดเป็นค่าเริ่มต้นในโหมดนี้ — โหมด `sidebar` เดิมไม่เปลี่ยน
1021
- - component ประกอบ (export จาก `@suphark/ui/next`): `HeaderCrumb` (ปุ่ม switcher: icon/label/badge/hint + ⌄ ใช้เป็น trigger ของ DropdownMenu ใดก็ได้), `HeaderCrumbs`, `TopTabs`, `SubNav`, helpers `topLevelTabs`/`isTabActive`
1022
- - showcase: `/shell-top` ตัวอย่างเต็ม (องค์กร → แอป → Production [Invite-only], แท็บ 6 ตัว, "ตั้งค่า" มี sub-nav 2 กลุ่ม)
1023
-
1024
- ## [0.3.3] — 2026-09-19
1025
-
1026
- ### Fixed
1027
-
1028
- - `FormDialog`: จำกัดความสูงไว้ในจอ (`max-h-[calc(100dvh-2rem)]`) header/footer ตรึง เฉพาะเนื้อฟอร์มเลื่อน — ฟอร์มยาวไม่ล้นในหน้าต่างเตี้ย (รายงานจาก Pi Auth หน้าลงทะเบียนแอป); `contentClassName` ยังใช้กำหนดความกว้างได้เหมือนเดิม
1029
-
1030
- ### Added
1031
-
1032
- - export type `DateRange` (react-day-picker) จาก barrel — แอปใช้กับ `DateRangePicker` ได้โดยไม่ต้องติดตั้ง react-day-picker
1033
-
1034
- ### Changed (showcase เท่านั้น — ไม่กระทบ API)
1035
-
1036
- - หน้า **/components** แบบ ui.shadcn.com/docs/components: 74 หน้า (ทะเบียน `apps/showcase/src/docs/registry.ts` + demo ต่อไฟล์ใน `src/docs/demos/`) แต่ละหน้ามีตัวอย่างจริง/โค้ด (อ่าน source ของ demo ตอน build)/import line/หมายเหตุ/ลิงก์ shadcn+source, เมนูซ้ายตามหมวด, prev/next
1037
-
1038
- - `DesignSystemShowcase` รับ prop `links?: { shell }` และตัดหัวใหญ่/badge ออก (แอปเป็นคนมีหัวเดียว); แท็บป้ายสั้น (จอแคบเลื่อนแนวนอน 1 แถว — แก้แท็บทับหัวเรื่องเพราะ `group-data-horizontal/tabs:h-8`); hero ไม่มีกรอบ + `HeroSpecimen` (component จริง + สวิตช์ --brand); CardTitle/CardDescription/หัว section ใหญ่ขึ้น 1 ขั้น; h2 ภาษาไทยอย่างเดียว; ปุ่ม "ตัวอย่างหน้าจอ" ชี้ไป `/shell`
1039
- - ช่องค้นหาในหน้า Overview ค้นได้จริง: `search-index.ts` (26 หัวข้อ) → สลับแท็บ + เลื่อนไป anchor (`id` บนทุก section หลัก) + empty state
1040
-
1041
- ## [0.3.2] — 2026-09-18
1042
-
1043
- ### Fixed
1044
-
1045
- - `copyText`/`selectContents` export จาก barrel หลักด้วย (แอปใช้ทำปุ่มคัดลอกเองได้)
1046
- - `EnvSnippet`: ค่ายาว (client secret) ล้นออกนอก dialog — ตัดบรรทัดในกรอบ (`whitespace-pre-wrap break-all` + `min-w-0`) แทน scroll แนวนอน (รายงานจากหน้า Register app ของ Pi Auth)
1047
- - `EnvSnippet` / `SecretReveal`: ปุ่มคัดลอกเงียบเมื่อ `navigator.clipboard` ถูกบล็อก (webview/embedded browser เช่น browser ใน Claude desktop) — เพิ่ม `copyText()` (`@suphark/ui/lib/copy-text`) ที่ fallback ไป `execCommand("copy")`; ถ้ายังไม่ได้ EnvSnippet จะเลือกข้อความให้และบอกให้กด Ctrl+C (`UiText.copyFailed` optional)
1048
-
1049
- ## [0.3.1] — 2026-09-18
1050
-
1051
- ### Changed
1052
-
1053
- - `AppShell`: slot `sidebarHeader` ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน (เดิมล้นทับเนื้อหา — รายงานจาก Pi Auth) + prop ใหม่ `sidebarHeaderCollapsed` สำหรับไอคอนแทนตอนยุบ
1054
- - `HeaderBreadcrumbs`: `labels` รับฟังก์ชัน `(segment, href) => ReactNode` ได้ (ใส่ `t()` ของ next-intl) และ map รับ ReactNode — ฟังก์ชันต้องมาจาก client component (ส่งจาก server layout จะได้ 500 "Functions cannot be passed directly to Client Components"; showcase สาธิตด้วย `breadcrumbs: <ShellBreadcrumbs />`)
1055
- - README: อธิบายว่า `ThemeMenuItems` มี group + หัวในตัว ไม่ต้องห่อเอง
1056
-
1057
- ## [0.3.0] — 2026-09-18
1058
-
1059
- ### Added
1060
-
1061
- - **AppShell** (`@suphark/ui/next`) — โครงแอปกลางแทน ConsoleShell ของ Pi Auth และ main layout ของ supplychain: shadcn Sidebar (ยุบเป็นไอคอน/offcanvas/none, Sheet บนมือถือ, ⌘B, cookie `sidebar_state`), โลโก้ `AppBrand` (name/logo/subtitle/version), ค้นหาเมนู, เมนู 2 ชั้น `AppNav` (ไฮไลต์ตาม pathname, กางอัตโนมัติเมื่อลูก active, dropdown เมื่อยุบ, badge, labs, pin ผ่าน `pin` prop), slot `sidebarHeader`/`sidebarFooter`, header (SidebarTrigger + `HeaderBreadcrumbs` อัตโนมัติ + `center`/`actions`/`banner`), `brandColor` ต่อ tenant, `contentMaxWidth`
1062
- - `UserMenu` (variant `sidebar` | `header`): หัวผู้ใช้ + children (ใส่ DropdownMenuItem/ThemeMenuItems เอง) + `onSignOut`
1063
- - `HeaderBreadcrumbs` (labels/skip/hideIds/homeHref), `NavLink` (จาก Pi Auth)
1064
- - โมเดลเมนู `AppNavGroup`/`AppNavItem` (icon เป็น ReactNode — ประกาศใน server component ได้) + helpers `filterNav` (ตัดตามสิทธิ์ทุกชั้น), `searchNav`, `flattenNav`, `isNavActive` (+tests รวม 40)
1065
- - barrel หลัก export `toast` จาก sonner (แอปไม่ต้องติดตั้ง sonner เอง)
1066
- - showcase: `/shell` ตัวอย่าง layout ที่ใช้ AppShell + UserMenu + ThemeMenuItems + filterNav + cookie defaultOpen
1067
-
1068
- ## [0.2.0] — 2026-09-18
1069
-
1070
- ### Added
1071
-
1072
- - entry **`@suphark/ui/data-table`** (ยกจาก supplychain `components/data-table`): `DataTable`, `DataTableColumnHeader`, `DataTableToolbar`, `DataTablePagination`, `DataTableFacetedFilter`, `DataTableDateRangeFilter`, `DataTableViewOptions`, `DataTableRowActions`, `useDataTable`, types (`DataTableProps`, `FacetedFilterConfig`) + `DeleteManyButton`, `ResourcePropertyFilter` (+helpers) — ต้องมี peer `@tanstack/react-table` (optional; แอปที่ไม่ใช้ตารางไม่ต้องติดตั้ง); ลากเรียงแถวใช้ `@dnd-kit/*` เป็น dependency ของแพ็กเกจ
1073
- - `useTableSearchParams` / `TABLE_PARAM` (state ตารางใน URL) ที่ `@suphark/ui/next`
1074
- - tests: data-table, data-table-faceted-filter, resource-property-filter (รวม 34)
1075
- - exports `./package.json`, `./components/data-table`
1076
- - showcase: การ์ด DataTable จริงในแท็บ Data Display; code recipe ใช้ import path ของแพ็กเกจ
1077
- - `apps/showcase/vercel.json` + `docs/ops/deploy-vercel.md`
1078
-
1079
- ## [0.1.1] — 2026-09-18
1080
-
1081
- ### Added
1082
-
1083
- - `NativeSelect` / `NativeSelectOption` (shadcn `native-select` — `<select>` ธรรมดาสำหรับฟอร์ม server action/FormData) ตามที่ Pi Auth ใช้ 17 ไฟล์
1084
- - dependency `cn` (^0.3.0) — shadcn CLI รุ่นใหม่สร้างไฟล์ที่ `import { cn } from "cn"` แทน `lib/utils`; ทั้งสองทางใช้ได้ ไฟล์เดิม 59 ตัวยังใช้ `@suphark/ui/lib/utils`
1085
-
1086
- ### Changed
1087
-
1088
- - ตัด `__tests__`/`*.test.*` ออกจาก tarball (`files` ใน package.json) — test ยังอยู่ใน repo
1089
-
1090
- ### Notes
1091
-
1092
- - `ThemeToggle` props `persistedTheme` / `onThemeChange` เป็น optional อยู่แล้ว — ไม่ส่ง = ใช้ next-themes อย่างเดียว (ใช้แทน ThemeToggle ของ @pi/ui ได้โดยไม่ต้องเปลี่ยน call site)
1093
-
1094
- ## [0.1.0] — 2026-09-18
1095
-
1096
- รุ่นแรก — ยกจาก `supplychainnextjs-1` (source of truth) + ชุด generic จาก Pi Auth `@pi/ui`
1097
-
1098
- ### Added
1099
-
1100
- - **shadcn/ui base-nova 59 primitives** (`components/ui/*`) + tests (combobox, date-picker, slider, tabs)
1101
- - **token/ฟอนต์** `styles.css`: slate + success/warning/sidebar/chart, radius, scrollbar, Noto Sans Thai Looped, dark variant
1102
- - **shared/button**: AddNew, Save, Submit, Cancel, Confirm, Delete, FloatingSave, Print, ActionButton (base)
1103
- - **shared/form** (react-hook-form): FormFieldInput/Select/Combobox/Date/Boolean/RadioGroup/RadioCards/ColorSelect/FormattedNumber, SimpleSelect
1104
- - **shared**: Combobox, MultiSelectCombobox, DateRangePicker, FormattedNumber, PrimarySecondaryText, ClientDateFormatter, ErrorState, PageSkeleton, ClientRedirect
1105
- - **จาก Pi Auth**: FormDialog, FormMessage/FormState, NameIdFields/toIdentifier (+test), StatusBadge/statusTone, RowMenu, RowActions, SecretReveal, EnvSnippet/randomSecret, Notice, EmptyState, EmptyRow, DataList/DataRow, Stat, DescriptionList, PageHeader, SectionCard, Initials/initialsOf, UiText (TH/EN provider)
1106
- - **theme** บน next-themes: ThemeProvider, ThemeToggle (เมนู 3 ตัวเลือก), ThemeSwitch (ปุ่มเดี่ยว), ThemeMenuItems (ใส่ใน DropdownMenu), useTheme
1107
- - **lib**: `cn`, `formatThaiFullDate`/`formatRelativeThaiDate`, status-colors, `UiPermissionProvider`/`usePermission` (แอป inject ตัวตรวจสิทธิ์เอง)
1108
- - **hooks**: `useIsMobile`, `useCopyToClipboard`
1109
- - **สีแบรนด์ต่อ tenant**: rule `[style*="--brand"], [data-brand]` ใน `styles.css` ให้ตั้ง `--brand`/`--brand-foreground` บน wrapper แล้ว primary/ring/sidebar-primary ตาม (แก้ตามบั๊ก @pi/ui `4cd39d8` ที่ `--primary: var(--brand)` บน :root ไม่สืบทอดไปยัง wrapper)
1110
- - entry `@suphark/ui/next` สำหรับ component ที่ใช้ next/navigation (LinkButton, RefreshButton, TransitionLink, PageBreadcrumb, PageShell, FormDialog, RowActions, RowMenu)
1111
- - showcase `components/design-system/*` (8 sections) + `docs/design-system.md`
1112
- - audit scripts `scripts/audit-base-ui-props.mjs`, `scripts/audit-tailwind-typography.mjs` (รับ path เป็น argument ให้แอปปลายทางใช้ได้)
1113
-
1114
- ### Changed (เทียบต้นทาง)
1115
-
1116
- - `NameIdFields` เขียนใหม่บน `ui/field.tsx` (Field/FieldLabel/FieldDescription) แทน `pi/Field`
1117
- - `ActionButton`/`LinkButton` ใช้ `usePermission` จาก `lib/permission` (context) แทน `@/lib/auth`
1118
- - `FormattedNumber` รับ `DecimalLike` (`{ toString(): string }`) แทน `Prisma.Decimal`
1119
- - `FormFieldInput` แยก `CopyableInput` เพื่อไม่เรียก hook ในเงื่อนไข
1120
- - `ui/form.tsx`, `hooks/use-mobile.ts` เพิ่ม `"use client"` (จำเป็นเมื่อ import ผ่าน barrel จาก server component)
1121
- - `ui/date-picker.tsx` แก้ `parts[0] ?? ""` ให้ผ่าน `noUncheckedIndexedAccess` ของแอปปลายทาง
1122
-
1123
- ### Not included (รุ่นถัดไป)
1124
-
1125
- - data-table (tanstack) + DeleteManyButton, sidebar/app shell, rich-text-editor (lexical), form-field-image-upload, pdf-export/print, rate, view-tracker, icon-picker, resource-property-filter
1
+ # Changelog
2
+
3
+ รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
+
5
+ ## [0.60.0] — 2026-10-24
6
+
7
+ ### Added
8
+ - `DataTable variant="quiet"` บนมือถือ (ตาม mockup รอบ 2): **กดค้างที่แถว (500 ms) เข้าโหมดเลือกหมู่** — ช่องเลือก (คอลัมน์ `select`) ซ่อนจากสายตาจนกว่าจะเลือก (คีย์บอร์ด/โปรแกรมอ่านจอยังโฟกัสได้ตลอด), หัว "เลือก N รายการ · เลือกทั้งหมด" (`role="status"`), แตะแถวสลับการเลือก, เอาออกหมด = ออกจากโหมด, สั่นเบา ๆ ถ้าอุปกรณ์รองรับ · บรรทัดบอกใบ้ "กดค้างที่แถวเพื่อเลือกหมู่" แสดงครั้งแรกครั้งเดียว (หายเองใน 6 วินาที จำใน localStorage)
9
+ - `DataTable variant="quiet"` แผ่นตัวกรอง: ตัวกรองแบบรายการติ๊ก (`DataTableFilterSections`) หัวข้อเปิดไว้ แถวสูง 44px มีจำนวนต่อค่า (ใช้ `getFacetedUniqueValues` ของแอปถ้าส่งมา ไม่งั้นนับจากแถวที่ผ่านตัวกรองคอลัมน์อื่น) + ปุ่มล่าง "ล้าง / ดู N รายการ" แทนปุ่ม popover ต่อคอลัมน์
10
+ - `DataTable densityStorageKey` (`quiet`): จำความหนาแน่นที่ผู้ใช้เลือกใน localStorage — ไม่ส่ง = ไม่จำ; localStorage ใช้ไม่ได้ก็ทำงานต่อ
11
+ - `UiText.table.selectedCount` / `selectedPill` / `selectAll` / `longPressHint` / `clearFilters` / `showResults`
12
+
13
+ ### Changed
14
+ - แคปซูลเลือกหมู่ของ `quiet` แสดง "N รายการ" (สั้นลง เพราะหัวรายการมือถือบอก "เลือก N รายการ" แล้ว)
15
+
16
+ ### Fixed
17
+ - `SelectionBar` แบบลอย (`floating`/`pill`): กว้างตามเนื้อหา (`w-max`) — เดิมถูกบีบเหลือครึ่งจอจากการยึด `left-1/2` ทำให้ปุ่มตัดคำและทับกันบนมือถือ · `pill` เป็นบรรทัดเดียวเสมอ ปุ่มที่แอปส่งมาใช้สีตัวอักษรบนพื้นสีหลัก และสูง 44px บนจอสัมผัส · ปุ่มปิดแผ่นตัวกรองสูง 44px บนจอสัมผัส
18
+
19
+ ## [0.59.1] — 2026-10-24
20
+
21
+ ### Fixed
22
+ - `DataTable variant="quiet"` บนจอสัมผัส (`pointer-coarse`): ปุ่ม "ตัวกรอง", ช่องค้นหา, ปุ่มก่อนหน้า/ถัดไป สูง 44px · พื้นที่แตะช่องเลือกของรายการมือถือขยายเป็น 44px (แตะส่วนที่เกินช่องก็สลับการเลือก) · ปุ่มลบชิปตัวกรองมีพื้นที่แตะกว้างขึ้น · วัด contrast ข้อความรอง/สถานะแล้ว ≥ 4.5:1 ทั้งโหมดสว่างและมืด (ไม่ต้องแก้)
23
+
24
+ ## [0.59.0] — 2026-10-24
25
+
26
+ ### Added
27
+ - `DataTable variant="quiet"` (opt-in, ค่าเริ่มต้น `default` เหมือนเดิม — ตาม mockup "Calm toolbar, dense rows"): แถบเครื่องมือบรรทัดเดียว — ช่องค้นหา + ชิปตัวกรองที่ลบได้ + ปุ่ม "ตัวกรอง" ปุ่มเดียว (เปิดแผ่นตัวกรอง: ขวาบนเดสก์ท็อป · ล่างบนมือถือ) + ตัวนับ "N จาก M รายการ" + สลับความหนาแน่น (`defaultDensity`) + ไอคอนคอลัมน์ · หัวตารางเรียบ (ลูกศรเรียงของคอลัมน์ที่ไม่ได้เรียงโผล่เมื่อชี้) · ช่องเลือก (`[role=checkbox]`) และปุ่มท้ายแถว (`DataTableRowActions`) โผล่เมื่อชี้/โฟกัส/เลือก (จอสัมผัสเห็นตลอด) · `StatusBadge` ในตารางเป็นจุดสี · แถวที่เลือกมีแถบสีซ้าย · เลือกแล้วขึ้นแคปซูล `SelectionBar variant="pill"` (`selectionActions` ใส่ปุ่มคำสั่ง) แทนข้อความ "เลือก N จาก M แถว" · เลขหน้าย่อ "1–10 จาก 128 · ‹ 1 / 13 ›"
28
+ - มือถือ (< sm) ของ `variant="quiet"`: คอลัมน์ที่ใส่ `columnDef.meta = { mobile: "title" | "end" | "sub" | "status" | "meta" | "hidden" }` (type `DataTableMobileSlot`) แสดงเป็นรายการแถวละ 3 บรรทัด (ชื่อ + ยอด / รอง / สถานะ · meta) แทนตารางเลื่อนแนวนอน · ไม่ใส่ slot ใดเลย = ตารางเหมือนเดิม · คอลัมน์ `select` แสดงที่ซ้ายของรายการ
29
+ - `DataTablePagination variant="quiet"`, `DataTableViewOptions iconOnly`, `UiText.table.rangeOf` / `shownOf` / `density`
30
+ - `NotificationCenter headerAction` (ขอโดย pi-done): ปุ่มคำสั่งของทั้งกล่อง เช่น "อ่านทั้งหมดแล้ว" ที่หัว ข้างปุ่ม "ดูทั้งหมด" — ไม่ต้องยืม `viewAll`
31
+ - `SwitchField labelClassName` (ขอโดย pi-done): ทับสไตล์ข้อความป้าย (ค่าเริ่มต้น `text-sm`)
32
+
33
+ ### Fixed
34
+ - `useIsMobile` ไม่พังเมื่อไม่มี `window.matchMedia` (jsdom/เบราว์เซอร์เก่า) — ใช้ค่าตอน mount อย่างเดียว (กระทบ `DataTableToolbar` ที่เรียก hook นี้ แอปที่เทสต์ DataTable ใน jsdom ไม่ต้อง mock matchMedia)
35
+ - showcase: `/components/data-table` ตัวอย่างใหม่ (สลับ default/quiet, เลือกแถว, ใบสั่งซื้อสมมติ)
36
+
37
+ ## [0.58.0] — 2026-10-24
38
+
39
+ ### Added
40
+ - `PullToRefresh variant="circle"` (`@suphark/ui/pwa`, ขอโดย pi-done — เจ้าของเลือกแบบวงกลม): วงกลม 40px ไอคอนล้วน (ลูกศร → หมุนตอนรีเฟรช) กลางแนวนอน พักที่ 16px ใต้ขอบบนของ root · เริ่มทำงานที่ 64px สูงสุด 104px แรงต้าน 0.5 · `role="status"` + ข้อความ `sr-only` จาก `text` (ปกติ `PULL_TO_REFRESH_TEXT_*`) · ค่าเริ่มต้นยัง `variant="pill"` (แคปซูลมีข้อความ) เหมือนเดิม
41
+
42
+ ## [0.57.0] — 2026-10-24
43
+
44
+ ยกจาก pi-done (รอบสองของคำขอย้าย component ที่แอปทำเอง)
45
+
46
+ ### Added
47
+ - `RowTable` / `RowTableHeader` / `RowTableRow` / `RowTableCell` / `RowTableActions` / `RowTableGroup` — ตาราง grid ที่หัว/แถว/แถวกลุ่มใช้คอลัมน์ชุดเดียว (`columns` + `mobileColumns` เป็น `grid-template-columns` ผ่านตัวแปร CSS) หัวซ่อนบนมือถือ ช่อง `hideOnMobile` ซ่อนบนมือถือ ปุ่มท้ายแถวโผล่ตอนชี้/โฟกัส (เห็นตลอดบนจอสัมผัส) — แทนที่ pi-done เขียนซ้ำ 4 ที่
48
+ - `ListSection` / `ListSectionHeader` — กลุ่มรายการแบบไม่มีกล่อง (หัว + จำนวน + ปุ่มขวา + เส้นบาง + แถวคั่นเส้น) = `SectionPanel plain` + `TaskGroupHeader`
49
+ - `SidebarNavItem` / `SidebarCountBadge` — แถวเมนูข้างแบบเรียบ (active = `bg-primary/10 text-primary`, ตัวเลขเป็นข้อความเทา ไม่ใช่ pill, 44px บนจอสัมผัส, `draggingOver`)
50
+ - `ProgressBar tone="auto"` — เขียวเมื่อครบ (value ≥ max) นอกนั้นสีหลัก (ครอบคลุม `ProgressBar` ของ pi-done: ใช้ `<ProgressBar value={percent} tone="auto" className="w-10" />`)
51
+ - showcase: หน้า `/components/row-table`, `/components/list-section`, `/components/sidebar-nav-item`
52
+
53
+ ## [0.56.0] — 2026-10-24
54
+
55
+ ### Added
56
+ - `DatePicker` `minDate` / `maxDate` (`"YYYY-MM-DD"` | Date, รวมวันนั้น) ทั้งวันเดียวและ `mode="range"` (ขอโดย DueBell แทนการกันที่ `onChange`): ปฏิทินกดวันนอกช่วงไม่ได้ · เดือน/ปีเลื่อนไปนอกช่วงไม่ได้ (ลูกศรเดือน + ตารางเลือกเดือน) · ค่าที่พิมพ์แล้วอยู่นอกช่วง = ไม่ยืนยัน (Enter/Tab/ปุ่มผล) และกล่องผลแสดงเหตุผลสีแดงแทน · ค่าที่ส่งเข้ามาเองนอกช่วงไม่ถูกแก้ ช่องขึ้น `aria-invalid` · ข้อความ `UiText.datePicker.beforeMin` / `afterMax` (`{date}` ตาม `locale` ของช่อง) แอปทับได้
57
+
58
+ ## [0.55.0] — 2026-10-24
59
+
60
+ ยกจาก pi-done (รอบแรกของคำขอย้าย component ที่แอปทำเอง)
61
+
62
+ ### Added
63
+ - `ResponsivePopover` / `ResponsivePopoverTrigger` / `ResponsivePopoverContent` / `ResponsivePopoverItem` / `useIsResponsiveSheet()` — Popover บนจอ ≥ 768px · แผ่นล่าง (Drawer) บนมือถือ: สูงตามเนื้อหา, `footer` ค้างล่าง, ปัดลงปิด; `useIsResponsiveSheet()` ให้ picker ขยายพื้นที่แตะ (`ResponsivePopoverItem` = 44px ในแผ่น) · `sheetContentClassName` ทับสไตล์ตัวแผ่น
64
+ - `SwitchField` — สวิตช์ + ป้ายในตัว (คลิกป้ายสลับ, `hint` ได้, ป้ายสูง 44px บนจอสัมผัส) แทน `<label><Switch/>…</label>` ที่ต้อง biome-ignore เพราะ Switch เป็น span
65
+ - `PullToRefresh` (`@suphark/ui/pwa`) prop `onRefresh` — รีเฟรชเองได้ (แอป local-first ที่ `router.refresh()` ไปไม่ถึง) · คืน promise = หมุนจนเสร็จ ขั้นต่ำ 500ms (ไม่ส่ง = `router.refresh()` แล้วหมุน 700ms เหมือนเดิม)
66
+
67
+ ### Fixed
68
+ - `PullToRefresh`: วงโผล่ใต้ขอบบนของ root (เดิมติดขอบจอ จมใต้ header ที่ sticky) และ `z-50` · เริ่มดึงได้เฉพาะเมื่อแตะ "ในส่วน root" ที่เลื่อนอยู่บนสุด — sheet/dialog ที่ portal ออกไปนอก root จึงปัดลงของตัวเองได้ ไม่ถูกแย่งไปรีเฟรชหน้า (root เป็น `<html>`/`<body>` ทำงานเหมือนเดิม)
69
+ - showcase: หน้า `/components/responsive-popover` ใหม่ · labeled-field มีตัวอย่าง `SwitchField`
70
+
71
+ ## [0.54.0] — 2026-10-24
72
+
73
+ ### Added
74
+ - `AccessibleCalendar` — `Calendar` ที่ลูกศร/Home/End/PageUp/PageDown เลือกวันด้วยคีย์บอร์ดได้จริง (props เหมือน `Calendar` ทุกอย่าง) · ใช้แทน `Calendar` ดิบเมื่อวางปฏิทินเอง (ปุ่มวันของ shadcn ไม่ผูก `ref` ลูกศรจึงเลื่อนได้แค่สถานะภายใน) · `FocusableDayButton` ย้ายจาก `date-picker.tsx` มาอยู่ `shared/date/accessible-calendar.tsx`
75
+
76
+ ### Fixed
77
+ - `DateRangePicker` (ตัวกรอง) และ `DateTimePicker`: ปฏิทินใช้ `AccessibleCalendar` — เลือกวันด้วยคีย์บอร์ดได้จริง (เดิมโฟกัสค้างที่เดิม)
78
+ - `DateRangePicker`: ข้อความ placeholder/"ล้างช่วงวันที่" มาจาก `UiText.datePicker` (`rangePlaceholder`, ใหม่ `clearRange`) — แอปที่ไม่ส่ง `placeholder` และใช้ UiText ไทยจะเห็นข้อความไทยแทนอังกฤษ; ส่ง `placeholder` เองยังชนะเสมอ
79
+ - showcase: `/components/calendar` ใช้ `AccessibleCalendar`
80
+
81
+ ## [0.53.0] — 2026-10-23
82
+
83
+ ### Changed
84
+ - showcase: `/components/sortable-list` ปรับให้ตรง mockup — หัวตาราง "ตัวแปรสภาพแวดล้อม · N คีย์" + ปุ่ม ? ด้านบน, ตัวเลขจำนวนคีย์ในหัวกลุ่ม, เส้นนำแนวตั้งของคีย์ในกลุ่ม, สีตอนชี้แถว, เส้นแทรกสีหลักที่แถวที่กำลังลาก, แคปซูลเลือกหมู่อยู่ใต้ตาราง
85
+ - `DatePicker` ปรับให้ตรง mockup "Type first" (0.49.0 วาดไม่ตรง): ไอคอนปฏิทินอยู่ซ้ายของช่อง · ป้าย `Enter` อยู่ในช่องขวา (เดิมอยู่ในกล่องผล) · กล่องผลที่อ่านออก (เขียว) แยกเป็นกล่องของตัวเองเหนือการ์ดปฏิทิน · หัวปฏิทินเป็นปุ่มเดียว "ตุลาคม 2569 ▾" กดแล้วเป็นตารางเลือกเดือน/ปี (แทนเมนูเดือน + เมนูปีสองอัน) · แผ่น "ทั้งหมด" มีหัว "พิมพ์ลัดทั้งหมด · กดเพื่อเติมในช่อง" · `UiText.datePicker.allShortcuts`/`tapToFill`
86
+
87
+ ## [0.52.0] — 2026-10-23
88
+
89
+ ### Changed
90
+ - `variant="quiet"` ของ `RecordCard`/`NotificationCenter` ปรับให้ตรงแบบ mockup จริง (0.51.0 วาดไม่ตรง): ไอคอนอยู่ในกรอบ 36px พื้น muted สีตาม accent · `StatusBadge` ที่อยู่ในแถว quiet กลายเป็น "จุดสี + ข้อความสี" ไม่มีกรอบ/พื้นอัตโนมัติ (CSS `in-data-[variant=quiet]`, นอก quiet เหมือนเดิม) · ปุ่มรองเป็น outline (เดิม ghost) ปุ่มหลักใช้แถวเดียว
91
+
92
+ ### Added
93
+ - `RecordCard`/`NotificationEntry` `highlight` (quiet: พื้น accent จาง ๆ เน้นแถวที่ต้องจัดการก่อน) และ `progress` (quiet: แถบความคืบหน้า 0–100 ใต้ยอด)
94
+ - showcase: notification-center โหมด quiet ตามแบบ — ยอดไม่มีสตางค์, แถวแรกเน้น + ปุ่มหลักตัวเดียว, แถบ 35/60 รอบ, เมนู … ทุกแถว
95
+
96
+ ## [0.51.0] — 2026-10-23
97
+
98
+ ### Added
99
+ - `useSortableRow().toggleSelected()` — เลือก/ไม่เลือกแถวจากช่องติ๊กในแถว (ช่องติ๊กโผล่เมื่อชี้ เหมือนที่จับ)
100
+ - `SelectionBar variant="pill"` — แคปซูลสีหลักลอยกลางล่างจอ เด่นกว่า `floating` (ใช้กับแถบเลือกหมู่ของ Sortable)
101
+ - `RecordCard variant="quiet"` (opt-in, ค่าเริ่มต้น `card` เหมือนเดิม) — แถวเรียบแบบสมุดบัญชี "Quiet ledger": ไม่มีกรอบ/พื้นสี สีของ `accent` เหลือแค่ไอคอน, ชื่อ + (สถานะ · meta) ซ้าย, ยอดเงินขวา, ปุ่มท้ายแถว — เห็นได้มากกว่าเท่าตัวในที่เท่ากัน · props เดิมทั้งหมดใช้ต่อได้ ไม่ใช่ major
102
+ - `NotificationCenter variant="quiet"` + `NotificationEntry.section` — ชิปนับเป็นแท็บขีดเส้นใต้ (ตัวเลข danger เป็นสีแดง) รายการเป็นแถว quiet ในกรอบเดียว มีหัวกลุ่มคั่นตาม `section`
103
+ - showcase: sortable-list มีช่องติ๊กโผล่เมื่อชี้ + แคปซูลเลือกหมู่ + กรอบบอกตำแหน่งเดิมตอนลาก; record-card / notification-center มีตัวอย่าง quiet (notification-center สลับ card/quiet ได้)
104
+
105
+ ## [0.50.0] — 2026-10-23
106
+
107
+ ### Added
108
+ - `@suphark/ui/sortable`: `DragHandle reveal` (opt-in, ค่าเริ่มต้นเหมือนเดิม) — "สงบจนกว่าจะแตะ": ซ่อนที่จับ โผล่เมื่อชี้แถว / โฟกัสในแถว / แถวถูกเลือก (จอสัมผัสเห็นตลอด, Tab ถึงที่จับได้เสมอ) · แอปใส่ `group/row` ที่แถว
109
+ - showcase: `/components/sortable-list` ใช้ `reveal` และย้ายข้อความสอนยาวไว้หลังปุ่ม ? (แผ่นลัดแป้นพิมพ์)
110
+
111
+ ## [0.49.0] — 2026-10-23
112
+
113
+ ### Changed
114
+
115
+ - **`DatePicker` redesign "Type first"** (ทั้งวันเดียวและ `mode="range"`; API/props/ค่าที่ส่งในฟอร์ม `name`/`defaultValue`/`required` เหมือนเดิมทุกตัว — แอปที่ใช้อยู่ไม่ต้องแก้):
116
+ - **ช่องคือ input ที่พิมพ์ได้โดยตรง** (เดิมเป็นปุ่มที่ต้องเปิดป๊อปอัปก่อนถึงมีช่องพิมพ์ซ้อนอยู่ข้างใน) — คลิกช่อง = เลือกทั้งข้อความ พิมพ์ทับได้เลย · `<label>` ที่ครอบ/ชี้ (`LabeledField`) โฟกัสที่ช่องพิมพ์โดยตรง
117
+ - **เห็นผลที่อ่านออกใต้ช่องทันที**: "จ. 26 ต.ค. 2569 · ในอีก 3 วัน" / ช่วง "16–23 ต.ค. 2569 · 8 วัน" (กดผลนั้นหรือ Enter เพื่อยืนยัน) — แก้ปัญหา "10/02 อ่านได้สองแบบ"; อ่านไม่ออก = "อ่านไม่ออก — ลองพิมพ์ t+3" ไม่ทิ้งค่าที่พิมพ์ทิ้งเงียบ ๆ; พิมพ์วันเดียวในช่องช่วง = ช่วงหนึ่งวัน
118
+ - ปฏิทินเลื่อนตามข้อความที่พิมพ์และไฮไลต์วันที่อ่านได้ทันที · แถวพิมพ์ลัดบรรทัดเดียวใต้ปฏิทิน (กดเติมลงช่อง) + "ทั้งหมด ›" เปิดแผ่นตัวอย่างวิธีพิมพ์ในกล่องเดียวกัน — **เลิกปุ่ม (i) / ตารางวิธีพิมพ์ที่กินที่ / ปุ่ม "ตกลง"**
119
+ - ปุ่ม ✕ ล้างค่าและปุ่มเปิดปฏิทินเป็น `<button>` จริง (เดิม ✕ เป็น `<span role="button">`) · วันที่สัมพัทธ์ "(พรุ่งนี้)" ยังแสดงท้ายช่องตาม `showRelative`
120
+ - **ปฏิทินใช้ภาษา/ปีเดียวกับช่อง** (เดิมไม่ส่ง `locale` แล้วขึ้น "Oct 2026" อังกฤษ ทั้งที่ช่องแสดง พ.ศ.) · หัวคอลัมน์วันไทย 2 ตัว (อา จ อ พ พฤ ศ ส) แทนตัวเดียวที่ "อ"/"พ" กำกวม
121
+ - คลิกวันที่ที่เลือกอยู่แล้วในปฏิทิน = ยืนยันวันนั้น (เดิมล้างค่าแบบ toggle — ล้างใช้ปุ่ม ✕)
122
+ - `showSmartInput={false}` = ช่องอ่านอย่างเดียว กดแล้วเปิดปฏิทินอย่างเดียว (แบบเดิม ไม่มีแถบพิมพ์ลัด) · `showPresets` ยัง deprecated ไม่มีผล
123
+ - ป๊อปอัปไม่มี animation ตอนปิด (Base UI ถอดตัวดักโฟกัส `<span tabindex=0>` ที่วางต่อท้ายช่องก็ต่อเมื่อ animation จบ ระหว่างนั้น Tab ตกไปที่มัน)
124
+ - `UiText.datePicker` เพิ่ม `cannotRead` (`{example}`), `shortcutsLabel`, `showAll`, `backToCalendar`, `openCalendar`, `dayCount` (`{count}`) — ไทย/อังกฤษในตัว
125
+
126
+ ### Fixed
127
+
128
+ - **คีย์บอร์ด: ลูกศรเลือกวันในปฏิทินไม่ย้ายโฟกัสจริง** — `CalendarDayButton` ใน `components/ui/calendar.tsx` (ไฟล์ shadcn) มี effect สั่ง `ref.current?.focus()` แต่ **ไม่เคยผูก `ref` กับปุ่ม** ลูกศรจึงเลื่อนแค่สถานะภายในของ react-day-picker โฟกัสจริงค้างที่เดิม (กระทบ `Calendar` ทุกที่ รวม DatePicker แบบเดิม) · `DatePicker` แก้ที่ฝั่งตัวเองด้วย `components={{ DayButton }}` ที่ผูก ref ให้ · **`Calendar` ดิบที่แอปใช้เองยังมีบั๊กนี้** (ห้ามแก้ไฟล์ shadcn — จดใน todo)
129
+ - ป๊อปอัป DatePicker สูง ~400px ไม่พอทั้งบนและล่างแล้ว Base UI ย้ายไปวางข้างช่อง (ทับเนื้อหา) → พลิกได้เฉพาะบน↔ล่าง
130
+ - Tab ออกจากช่องที่เปิดป๊อปอัปอยู่ แล้วโฟกัสหลุดไป `body` แทนที่จะไปช่องถัดไป (ตัวดักโฟกัสของ Base UI)
131
+
132
+ ### Security
133
+
134
+ - อัป `next` (devDependency ของแพ็กเกจ + showcase ui.suphark.com) เป็น `^16.4.2` — ปิดช่องโหว่ใน `next >=16.0.0 <=16.4.0`: **critical** RCE ไม่ต้องล็อกอินใน Image Optimization API เมื่อใช้ไฟล์ SVG (GHSA-2464-fhr4-w9xw) · **critical** RCE ไม่ต้องล็อกอินใน `use cache` ที่คืน typed array (GHSA-673q-mjm6-r5xr) · high DoS ใน App Router ที่ใช้ Server Actions · override `source-map-js >=1.2.2` (high DoS ที่ติดมากับ postcss) · `pnpm audit --prod --audit-level=high` สะอาด · **peerDependency `next >=15` ไม่เปลี่ยน** — แอปที่ใช้ Next 16 อัปเองเป็น `^16.4.2` (แพ็กเกจส่ง source ไม่ได้บังคับเวอร์ชัน next)
135
+
136
+ ### Added
137
+
138
+ - `HeaderBreadcrumbs` (และ `AppShell` `header.breadcrumbs: { upButton }`): **`upButton`** + `upLabel` — ปุ่ม X ท้าย breadcrumb ถอยขึ้นหนึ่งระดับแบบ Vercel (ไปที่ segment ก่อนหน้า หรือ `homeHref` ถ้ามี segment เดียว · ไม่แสดงหน้าแรก) · ปิดไว้เป็นค่าเริ่มต้น แอปเดิมไม่เปลี่ยน (ขอจาก Pi Config ซึ่งทำเองใน `breadcrumbs.tsx` แล้ว) · showcase `/shell` เปิดใช้
139
+
140
+ ## [0.48.0] — 2026-10-10
141
+
142
+ > ประกาศพร้อม publish แล้วแต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.49.0
143
+
144
+ ### Added
145
+
146
+ - showcase: หน้า `/components/web-push-row` (demo WebPushRow)
147
+ - WebPushRow (@suphark/ui/pwa): แถวตั้งค่าเปิด/ปิดการแจ้งเตือน Web Push บนเบราว์เซอร์ พร้อมจัดการ Service Worker registration, PushManager subscription, permission check, และ Feedback toast อัตโนมัติ
148
+
149
+ ## [0.47.0] — 2026-10-08
150
+
151
+ ### Changed
152
+
153
+ - **`@suphark/ui/sortable` รองรับโครงเดียว: หัวกลุ่มกับแถวรายการเป็นพี่น้องกัน** (`<tr>` ใน `<tbody>` เดียวกัน หรือการ์ดในลิสต์เดียวกัน) — เจ้าของเลือกให้เหลือรูปแบบเดียวเพื่อดูแลง่าย หลัง Pi Config ย้ายจากโครง "`<tbody>` ต่อกลุ่ม" มาแบบนี้แล้ว (71735f0) · **ห้ามให้ node ของกลุ่มครอบแถวรายการไว้** (เช่นแปะ `ref` ของกลุ่มที่ `<tbody>` ที่ห่อแถวคีย์) ไม่งั้นแถวลูกจะเลื่อนสองชั้นและกรอบของก้อนถูกนับซ้ำ · ถอดโค้ดที่เคยรองรับโครงนั้นออก (`insideGroup` ใน context ภายใน, การกันนับซ้ำใน `blockRect`) — ไม่มี props ใดเปลี่ยน แอปที่ทำตามตัวอย่างใน `/components/sortable-list` ไม่ต้องแก้อะไร
154
+
155
+
156
+ ### Added
157
+
158
+ - type `DistributiveOmit<T, K>` / `DistributivePick<T, K>` (ขอจาก Pi Auth): `Omit` ปกติยุบ union ของ props ที่มี discriminant — `Omit<DatePickerProps, "locale">` ทำให้ `name` กลายเป็น `string | [string, string]` แยกตาม `mode` ไม่ได้อีก ตัวห่อของแอปจึงพัง · ใช้ `DistributiveOmit<DatePickerProps, "locale">` แทนได้เลย ไม่ต้อง cast (มีเทสต์ type ที่พิสูจน์แล้วว่าแดงเมื่อถอดตัวแก้)
159
+
160
+
161
+ ### Fixed
162
+
163
+ - `@suphark/ui/sortable`: ลากรายการเข้ากลุ่มอื่นไปวาง "ใต้" แถวหนึ่ง ระหว่างลากอยู่ถูกที่ แต่ปล่อยแล้วกระโดดไปอยู่ "เหนือ" แถวนั้น (เจ้าของเจอในตัวอย่างโครง `<tbody>`) — ตอนข้ามกลุ่มจัดตำแหน่งก่อน/หลังไว้แล้ว แต่ตอนปล่อย `reorderItems` ย้ายไปแทนที่แถวที่ชี้ซ้ำอีกรอบ · ตอนนี้ถ้าปล่อยตอนยังชี้แถวเดิมที่ข้ามกลุ่มเข้ามา ใช้ตำแหน่งที่จัดไว้เลย
164
+ - showcase: `/components/sortable-list` **เอาตัวอย่างโครง `<tbody>` ต่อกลุ่มออก** — เจ้าของเลือกให้มีรูปแบบเดียว (หัวกลุ่ม + แถวรายการเป็น `<tr>` ใน `<tbody>` เดียวกัน) เพื่อดูแลง่าย
165
+
166
+
167
+ ### Fixed
168
+
169
+ - `AuthLayout`: โลโก้แนวนอนถูกบีบในกรอบ 44×44 จนอ่านไม่ออก (Pi Auth — wordmark ของลูกค้า 315×70 เหลือสูง ~10px) · ตอนนี้ `<img>` สูง 44px กว้างตามสัดส่วน (ไม่เกิน 220px) ส่วนไอคอน/ตัวอักษร (svg, span) ยังเป็นกรอบจัตุรัสมนเหมือนเดิม — เลือกเองตามชนิดลูก แอปไม่ต้องรู้สัดส่วนรูป
170
+ - showcase: `/components/auth-layout` เพิ่มแท็บ "โลโก้แนวนอน"
171
+
172
+ ## [0.45.0] — 2026-10-08
173
+
174
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.45.1
175
+
176
+
177
+ ### Added
178
+
179
+ - `RecordCard iconSize="sm" | "md"` (ขอจาก DueBell — การ์ดบิลโหมดกะทัดรัด): `sm` = กรอบ 36px (ไอคอน 16px) เยื้องรายละเอียด/แถบล่าง `ml-12` · `md` (ค่าเริ่มต้น) = 44px / `ml-14` เหมือนเดิม · ใช้คู่ `iconTile={false}` ได้เพื่อตั้งระยะเยื้องให้ตรงไอคอนของแอปเอง
180
+ - showcase: `/components/record-card` เพิ่มการ์ดแบบ `iconSize="sm"`
181
+
182
+ ## [0.44.0] — 2026-10-08
183
+
184
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.45.0
185
+
186
+
187
+ ### Added
188
+
189
+ - **`Button tone`** (สเปกจาก DueBell — `ToneButton`): ปุ่มขอบโครงเดียวกัน ต่างแค่สีตามความหมายของรายการ — `income` เงินเข้า/รับเงิน · `expense` เงินออก/จ่าย · `reminder` รายการเตือน (ม่วง) · `warning` ย้อนกลับได้ (พักรายการ, รอยืนยัน) · `revert` เลิกทำ/ย้อนสถานะ (สีเดียวกับ expense) — ลบถาวรยังเป็น `variant="destructive"` · type `ButtonTone`
190
+ - token `--tone-{income,expense,reminder,warning}` / `-border` / `-hover` ทั้งสองธีม — **แอป override เป็นสีประเภทรายการของตัวเองได้** (ค่าเริ่มต้นผูกกับตระกูล success/destructive/warning + ม่วงใหม่) · วัดบนเบราว์เซอร์แล้ว: ตัวอักษร 7.1–8.4:1 (สว่าง) / 12.5–16:1 (มืด), ขอบ 3.2–3.8:1 / 3.4–4.0:1
191
+ - showcase: `/components/button` เพิ่มแถวปุ่มโทน
192
+
193
+ ### Changed
194
+
195
+ - `RecordCard` กรอบไอคอน: ขอบ `color-mix(accent 25%)` (เดิม 35%) ให้ตรงสูตร `CategoryIcon` ของ DueBell (พื้น 12% / ขอบ 25%) — แอปส่งไอคอนเปล่าได้เลย
196
+
197
+ ## [0.43.0] — 2026-10-08
198
+
199
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.44.0
200
+
201
+
202
+ ### Added
203
+
204
+ - **`RecordCard` — การ์ดรายการมาตรฐาน** (ยกจาก `LedgerCard` ของ DueBell ที่ใช้ 3 ที่: รายการบิล, การ์ดแค็ตตาล็อก, ศูนย์แจ้งเตือน): หัว = ไอคอน + ชื่อ/สถานะ + meta → รายละเอียด → แถบล่าง (`footerSummary` ซ้าย, `actions` ขวา) โดยรายละเอียดและแถบล่างเยื้องตรงคอลัมน์ข้อความ · `accent` รับ tone (`neutral/info/success/warning/danger` + ชื่อเดิม `default/destructive`) **หรือสี CSS ตรง ๆ** (`var(--type-income)`) → ขอบซ้าย 3px + ขอบจาง 30% + พื้นไล่สี 11% + กรอบไอคอนตามสี · ชื่อยาวตัดที่ 2 บรรทัด ไม่ดันป้ายสถานะตก · `unread`, `onSelect` (ปุ่มในแถบล่างไม่ไปโดน), `as="li"`, `titleAs` · `recordAccentColor()` · `UiText.record.details`
205
+
206
+ ### Changed
207
+
208
+ - **`NotificationCenter` ประกอบจาก `RecordCard` + `StatusBadge`** (DueBell: แถวเดิมเขียนเลย์เอาต์ซ้ำกับการ์ดของแอปและเป็น private) — เพิ่มอย่างเดียว ของเดิมใช้ต่อได้:
209
+ - แถวคือ `RecordCard` · `NotificationEntry.accent` (สี CSS สำหรับสีของประเภทรายการ เช่น รายรับ/รายจ่าย/เตือน) + `details`
210
+ - `renderItem(item, { busy })` เรนเดอร์แถวเองได้
211
+ - ชิปนับเป็น `StatusBadge` (ชิปทั้งระบบชุดเดียว) · tone รับ `danger`/`neutral` แบบ `StatusBadge` (`destructive`/`default` ยังได้)
212
+ - `NotificationAction.render` ใส่ปุ่มของแอปเองได้ (เช่น `ToneButton`) · `onSelect` ไม่บังคับเมื่อใช้ `render`
213
+ - `headingAs` / `headingClassName` (หัวข้อแบบ label ตัวเล็กของแอป)
214
+
215
+ ### Fixed
216
+
217
+ - **`@suphark/ui/sortable` ลากกลุ่มแล้วกลุ่มอื่นไม่เลื่อนหลบ เมื่อแอปแปะ ref ของกลุ่มที่ `<tbody>` ทั้งก้อน** (Pi Config รายงานหลัง bump 0.42.1): ตัดสินก่อน/หลังจากกลางของสิ่งที่ลาก — ก้อน `<tbody>` สูงจนกลางไม่เคยเลยครึ่งของกลุ่มเป้าหมาย → ได้ "ที่เดิม" เสมอ · ตอนนี้ใช้ตำแหน่งเมาส์ · และแถวที่อยู่ใน node ของกลุ่มเองไม่ได้ transform ซ้ำอีกชั้น (เดิมเลื่อนสองเท่า) และไม่ถูกนับซ้ำตอนวัดขนาดก้อน
218
+ - showcase: `/components/record-card` ใหม่ · `/components/notification-center` เพิ่มรายการเตือนสีม่วง (accent สี CSS) + รายละเอียด · `/components/sortable-list` เพิ่มตัวอย่างโครง `<tbody>` ต่อกลุ่ม (แบบ Pi Config)
219
+
220
+
221
+ ### Fixed
222
+
223
+ - **วันที่สัมพัทธ์นับผิด 1 วัน** (เจอตอนตรวจ 0.42.0 บน production: พิมพ์ `t+3` ได้ 10 ต.ค. ถูก แต่วงเล็บบอก "(ใน 2 วัน)") — `formatRelativeThaiDate` ใช้ `formatDistanceToNow` ซึ่งนับเวลาจาก "ตอนนี้" ถึงเที่ยงคืนของวันนั้น (บ่าย/ค่ำเหลือ ~2.4 วัน → 2) และมีคำว่า "ประมาณ 1 เดือน" · ตอนนี้นับเป็นวันปฏิทินด้วย `Intl.RelativeTimeFormat`: "ในอีก 3 วัน", "10 วันที่ผ่านมา", "เดือนหน้า", "ปีที่แล้ว" (< 31 วัน = วัน, < 12 เดือน = เดือน, ที่เหลือ = ปี) — กระทบ `DatePicker` และ `RelativeDate` · `DatePicker locale` ใช้สูตรเดียวกัน (เดิมนับเป็นวันอย่างเดียว "in 45 days")
224
+ - `formatRelativeCalendarDate(date, locale)` ใน `lib/formatters` (ใช้ร่วมกันข้างใน)
225
+
226
+
227
+ ### Added
228
+
229
+ - **`DatePicker` พิมพ์ลัด** (เจ้าของขอ): `t` = วันนี้, `t+3` / `t+3d` = อีก 3 วัน, `t-2` = 2 วันก่อน, `t+2w` สัปดาห์, `t+1m` เดือน, `t+1y` ปี (`today`/`วันนี้` และหน่วยไทย วัน/สัปดาห์/เดือน/ปี ก็ได้; ปลายเดือนตัดให้อยู่ในเดือนถัดไปแบบ date-fns) · ช่วง: `t-7..t`, `t ~ t+1m`, `t ถึง t+3` — ใช้ได้กับ `parseSmartDateInput` / `parseSmartDateRange` ทุกที่ (รวม `FormFieldDate`)
230
+ - ปุ่ม (i) ท้ายช่องพิมพ์ เปิดตารางวิธีพิมพ์ในกล่องเดียวกัน (ไม่ใช่ tooltip — popup ซ้อน popup และจอสัมผัสเปิด tooltip ไม่ได้) · ข้อความจาก `UiText.datePicker.help*`
231
+
232
+ ### Changed
233
+
234
+ - **เลิกปุ่มทางลัด** (วันนี้/พรุ่งนี้/+1 สัปดาห์ …) ทั้งแบบวันเดียวและช่วง — ใช้การพิมพ์ลัดแทนเพื่อลดพื้นที่ · `showPresets` ยังรับได้แต่ไม่มีผล (deprecated)
235
+ - ช่วงวันที่: **ไม่ใส่ปี = ปีปัจจุบันเสมอ** (0.41.0 ยืมปีจากฝั่งขวา)
236
+ - placeholder ช่องพิมพ์: "เช่น 15/3, t+3, t+1m" / ช่วง "เช่น 1/7-5/7, t-7..t"
237
+
238
+ ## [0.41.0] — 2026-10-07
239
+
240
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.42.0
241
+
242
+
243
+ ### Added
244
+
245
+ - **`DatePicker mode="range"` — เลือกช่วงวันที่** (เจ้าของขอ): คลิกวันเริ่มแล้ววันสุดท้าย (มีบรรทัดบอก "เลือกวันสุดท้ายของช่วง" ระหว่างเลือก) หรือพิมพ์ **"1/7-5/7" = 1–5 ก.ค.** · รองรับ "1-5/7", "1/7 - 5/8/68", "1/7 ถึง 5/7", "1/7 to 5/7", "1/7–5/7" · ปีที่ไม่ได้พิมพ์ฝั่งซ้ายใช้ปีฝั่งขวา · ใส่กลับด้านแล้วสลับให้ · "1-7-2568" ยังเป็นวันเดียวเหมือนเดิม (`-` เป็นตัวคั่นช่วงเมื่อมี `/` หรือ `.`)
246
+ - แสดงด้วย `Intl.DateTimeFormat#formatRange` ตาม `locale`: "1–5 ก.ค. 2569" / en-GB "1–5 Jul 2026"
247
+ - ทางลัดของช่วง: วันนี้, 7/30 วันล่าสุด, เดือนนี้, เดือนที่แล้ว, 7/30 วันข้างหน้า
248
+ - ฟอร์ม: `name={["from", "to"]}` ส่ง `YYYY-MM-DD` สองช่อง (ใช้กับ `<form method="get">` ของตัวกรองได้ตรง ๆ) + `required` · `value`/`defaultValue` = `{ from, to }` (Date หรือ "YYYY-MM-DD") · `onChange(range | null)`
249
+ - `parseSmartDateRange(input)` · types `DatePickerRange`, `DatePickerSingleProps`, `DatePickerRangeProps` (`DatePickerProps` เป็น union — ของเดิมที่ไม่ส่ง `mode` ใช้ได้เหมือนเดิม)
250
+ - `UiText.datePicker`: `rangePlaceholder`, `rangeSmartPlaceholder`, `pickEnd`, ทางลัดของช่วง
251
+ - showcase: `/components/date-picker` เพิ่มตัวอย่างช่วงวันที่
252
+
253
+
254
+ ### Fixed
255
+
256
+ - `DatePicker`: ปฏิทินใน popup มีที่ว่างซ้ายขวามาก (เจ้าของแจ้ง) — กล่องกว้างตายตัว 320–340px แต่ช่องวันที่ 28px ปฏิทินจึงกว้างแค่ ~200px · ตอนนี้ช่องวันที่ 40px (กดง่ายขึ้นด้วย) และกล่องกว้างพอดีปฏิทิน 306px (commit แรกหลุด format ของ biome — แก้ในรุ่นเดียวกัน)
257
+
258
+ ## [0.40.0] — 2026-10-07
259
+
260
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.40.1
261
+
262
+
263
+ ### Added
264
+
265
+ - **`DatePicker` ใช้ในฟอร์มและหลายภาษาได้** (ขอจาก Pi Auth — `<input type="date">` แสดง "10/02/2027" อ่านได้ทั้ง 2 ต.ค. และ 10 ก.พ.):
266
+ - `locale` (BCP 47): แสดงผ่าน Intl `{ day, month: "short", year }` ทั้งในช่อง ปฏิทิน และวันที่สัมพัทธ์ — `"en-GB"` = "2 Oct 2027", `"en"` = "Oct 2, 2027", `"th"` = "2 ต.ค. 2570" · ไม่ส่ง = รูปแบบไทยเดิม
267
+ - `name` (+ `defaultValue` แบบไม่ควบคุม, `value`/`onChange` ไม่บังคับแล้ว): ส่ง `YYYY-MM-DD` ตามวันที่ local ไปกับ FormData ของ `<form action>` · `required` ทำงานกับการตรวจของเบราว์เซอร์และตาข่ายฟอร์ม (ชื่อช่องจาก `LabeledField`)
268
+ - `showRelative` (ค่าเริ่มต้น true) · `toDateInputValue(date)` export ให้ใช้เอง · type `DatePickerProps`
269
+ - `UiText.datePicker` (placeholder, ทางลัด, ปุ่ม) ไทย/อังกฤษในตัว
270
+
271
+ ### Changed
272
+
273
+ - `DatePicker` ย้ายไป `components/shared/date/date-picker.tsx` — ไม่ได้มาจาก shadcn CLI (registry ของ base-nova ไม่มี date-picker) แต่ยกมาจาก supplychain; `components/ui/date-picker` ยัง re-export ให้ import path เดิมใช้ได้
274
+ - ข้อความเดิมที่ฝังไว้ย้ายไป UiText: placeholder ค่าเริ่มต้นจาก "เลือกวันที่อ้างอิงราคา..." (ของ supplychain) เป็น "เลือกวันที่", ทางลัดเป็นภาษาไทย ("วันนี้", "+1 สัปดาห์" …) แทนอังกฤษปนไทย, หัวข้อ "Quick Preset" → "ทางลัด"
275
+ - สีดิบ (slate/emerald) → semantic token · ปุ่ม "ตกลง" ของช่องพิมพ์วันที่เป็น `type="button"` (เดิม submit — ส่งฟอร์มแม่ได้ถ้าไม่ได้กด Enter) · ปุ่มล้างมี `aria-label` และกดด้วยคีย์บอร์ดได้
276
+
277
+ ### Fixed
278
+
279
+ - `value="YYYY-MM-DD"` อ่านเป็น UTC แล้วเลื่อนวันในโซนเวลาตะวันตก → อ่านเป็นวันที่ local
280
+ - วันที่สัมพัทธ์ ("(พรุ่งนี้)") แสดงหลัง mount — ขึ้นกับวันของเครื่องผู้ใช้ จึง hydration mismatch ได้ข้ามวัน/โซนเวลา (ค้างใน todo)
281
+ - showcase: `/components/date-picker` เพิ่มตัวอย่างในฟอร์ม server action (`name` + `locale="en-GB"` + `required`)
282
+
283
+
284
+ ### Added
285
+
286
+ - **`SortableProvider freeLayout` — ชั้นบนสุดจัดได้อิสระ** (เจ้าของแจ้งว่าเดิม "ล็อก" ให้รายการไม่มีกลุ่มอยู่บนเสมอ กลุ่มอยู่ล่างเสมอ): กลุ่มกับรายการไม่มีกลุ่มสลับที่กันได้ทุกแบบ — กลุ่มขึ้นไปเหนือรายการ, รายการไปอยู่ระหว่างกลุ่ม, เอารายการออกจากกลุ่มไปวางก่อนกลุ่มไหนก็ได้ · หัวกลุ่ม: **ครึ่งบน = วางก่อนกลุ่ม, ครึ่งล่าง = เข้ากลุ่มเป็นตัวแรก** · ระหว่างลากทุกอย่างในชั้นบนสุดเลื่อนหลบเป็นก้อน (กลุ่ม = หัว + รายการ) · โซนท้ายตาราง = วางท้ายสุด · ไม่เปิด = แบบเดิม (แอปที่ยังเรนเดอร์รายการไม่มีกลุ่มก่อนไม่เปลี่ยนพฤติกรรม)
287
+ - `SortableOrder.root` (optional) + `rootEntries(order)` (ลำดับชั้นบนสุดสำหรับวนเรนเดอร์), `moveRootEntry`, `moveItemToRoot`, type `SortableRootEntry` · `sortableOrderToPayload(order).root` + `sortableOrderOf(..., { root })` สำหรับบันทึก/โหลด — `groups` / `items[UNGROUPED]` ยังตรงกับ root เสมอ (payload เดิมใช้ต่อได้)
288
+ - showcase: `/components/sortable-list` เปิด `freeLayout` และเรนเดอร์ตาม `rootEntries`
289
+
290
+ ### Fixed
291
+
292
+ - (จาก 0.38.1 ที่ไม่ได้ขึ้น npm แยก) ลากรายการเข้ากลุ่มแล้ววางเป็นรายการแรกไม่ได้ — ดูด้านล่าง
293
+ - โซน "ท้ายตาราง" (`useSortableDropZone(UNGROUPED)`) ถูก collision เปลี่ยนเป้าหมายเป็นรายการไม่มีกลุ่มตัวแรก → วางแล้วไปโผล่บนสุดแทนท้ายสุด
294
+
295
+ ## [0.38.1] — 2026-10-07
296
+
297
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.39.0
298
+
299
+ ### Fixed
300
+
301
+ - **`@suphark/ui/sortable` ลากรายการเข้ากลุ่มแล้ววางเป็นรายการแรกไม่ได้** (เจ้าของแจ้ง): ระหว่างลากขึ้นเป็นตัวแรกถูก แต่พอปล่อยกลับไปเป็นตัวที่สอง — ตอนชี้หัวกลุ่ม collision เลือก "รายการที่ใกล้ที่สุดที่ไม่ใช่ตัวที่ลาก" (= ตัวแรกเดิม) แล้ว `onDragEnd` ย้ายไปแทนที่ตัวนั้น · ตอนนี้ชี้หัวกลุ่ม = รายการแรกที่แสดงอยู่ **รวมตัวที่ลากเอง** · ยืนยันบน demo: ลากจากนอกกลุ่ม / จากกลุ่มอื่น / จากท้ายกลุ่มเดียวกันขึ้นหัวกลุ่ม → เป็นตัวแรกทุกกรณี, วางครึ่งล่างของแถวยังไปอยู่ "หลัง" แถวนั้นตามเดิม (ทำซ้ำบน production v0.38.0 ได้ก่อนแก้)
302
+
303
+
304
+ ### Fixed
305
+
306
+ - **`@suphark/ui/sortable` ลากกลุ่มแล้วไม่เห็นอะไรขยับ** (เจ้าของแจ้ง): หัวกลุ่มมักอยู่ใน `SortableGroup` ซึ่งเป็น context ของรายการ dnd-kit จึงหากลุ่มในลำดับไม่เจอ ไม่มีแถวไหนได้ transform เลยจนปล่อย · ตอนนี้ provider คำนวณระยะเลื่อนของกลุ่มเอง และเลื่อน **ทั้งก้อน** (หัวกลุ่ม + แถวรายการ) — กลุ่มอื่นหลบเท่าความสูงทั้งก้อน, ช่องจาง ๆ ของกลุ่มที่ลากเลื่อนไปจุดวาง (ไม่มี overlay = ทั้งก้อนลอยตามเมาส์) · ที่วางของกลุ่มวัดรวมแถวรายการ ใช้ได้ทุกโครงสร้างโดยไม่ต้องแก้ call site · วัดบนหน้า demo: ลาก "ฐานข้อมูล" ลงเลย "อีเมล" → ฐานข้อมูลทั้งก้อน +134px, อีเมลทั้งก้อน −135px, ปล่อยแล้วไม่มี style ค้าง
307
+ - แถวต้นทางจางค้างหลังปล่อย (เจอกับหัวกลุ่ม): side effect ของ drop animation ใน dnd-kit จำ opacity ที่เราตั้งไว้เป็นค่าเดิมแล้วเขียนคืน — ปิด side effect นั้น (opacity คุมผ่าน style ของ React อยู่แล้ว)
308
+
309
+ ### Added
310
+
311
+ - `SortableProvider multiSelect` (+ `selected`/`onSelectedChange` ถ้าจะคุมเอง): เลือกหลายรายการแล้วลากไปพร้อมกัน — ⌘/Ctrl+คลิก ทีละตัว, Shift+คลิก เป็นช่วง (เฉพาะแถวที่แสดง), คลิกเฉย ๆ ล้าง (คลิกปุ่ม/ช่องกรอกในแถวไม่นับ) · ที่จับ: `x` เลือก/ไม่เลือก, ⌘/Ctrl+A ทั้งหมด, Esc ล้าง · ลากตัวที่เลือก = ทุกตัวที่เลือกจางลงตรงที่เดิมแล้วไปรวมที่จุดวาง เรียงตามลำดับเดิม (ข้ามกลุ่มได้) · ลากตัวที่ไม่ได้เลือก = ลากตัวเดียวและล้างการเลือก
312
+ - `useSortableRow` คืน `selected` + `selectProps` (กระจายลงแถว มี `data-selected` ไว้ทำสี) · `useSortableState` คืน `selected`/`clearSelection` (ใช้คู่ `SelectionBar`)
313
+ - `renderOverlay` ได้ `SortableDrag` = `{ kind, id, ids }` (`ids` = ทุกตัวที่ลากไปด้วยกัน — ทำการ์ดซ้อน + จำนวน) · `SortableAnnouncementContext.count` + `UiText.sortable.startedMany` ("เริ่มลาก {count} รายการที่เลือกไว้")
314
+ - helper pure: `flatItems(order)`, `gatherItems(order, activeId, ids)`
315
+ - showcase: `/components/sortable-list` เปิด `multiSelect` + `SelectionBar` + การ์ดซ้อนตอนลากหลายคีย์
316
+
317
+
318
+ ### Added
319
+
320
+ - **`FormInvalidNet`** (ข้อเสนอจาก Pi Auth — มี `<form action>` เขียนเอง ~60 จุด): ตาข่ายฟอร์ม invalid ระดับแอป วางครั้งเดียว ฟัง `invalid` ทั้ง document แล้วแจ้งเป็น toast ข้อความเดียวกับ `UiText.invalidForm` + พาไปช่องแรกที่โฟกัสไม่ได้ · ข้ามฟอร์มที่มีตาข่ายของตัวเอง (`FormDialog`/`ActionForm`/`useFormInvalidNotice` ใส่ `data-invalid-notice` ให้; ใส่เองที่ฟอร์มเพื่อปิดได้) · วางซ้ำหลายที่ก็ไม่ขึ้น toast ซ้อน
321
+ - `AppShell` วาง `FormInvalidNet` ให้เอง — หน้านอก AppShell (เช่น /login) วางใน root layout ใต้ `Toaster`
322
+ - `useFormInvalidNotice().formProps` มี `data-invalid-notice` แล้ว
323
+ - showcase: root layout วาง `FormInvalidNet`; `/components/action-form` มีตัวอย่าง `<form>` ดิบ
324
+
325
+ ## [0.36.0] — 2026-10-07
326
+
327
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.37.0
328
+
329
+ มาตรฐานใหม่ (ขอจาก Pi Auth): **ปุ่มที่มีงานค้างต้องหมุน และฟอร์มที่ส่งไม่ได้ต้องบอกผู้ใช้เสมอ — ห้ามเงียบ**
330
+
331
+ ### Added
332
+
333
+ - `Button loading`: ไอคอนนำหน้าเปลี่ยนเป็น spinner (ไม่มีไอคอน = เติมหน้าข้อความ ความกว้างปุ่มไม่กระตุก), กดซ้ำไม่ได้, `aria-busy="true"`, ปุ่มยังรับโฟกัส (`focusableWhenDisabled` — โฟกัสไม่หลุดไป body ตอนกด) · ไม่ใส่ `loading` + `type="submit"` ภายใน `<form action={…}>` = หมุนเองจาก `useFormStatus()` (ทุกปุ่ม submit ในฟอร์มนั้น) · `loading={false}` ปิดการตรวจอัตโนมัติ
334
+ - `useFormInvalidNotice()` → `{ formProps, message, notice }`: จับ event `invalid` ของฟอร์ม รวมชื่อช่องที่ไม่ผ่านเป็นข้อความเดียว "กรุณากรอกหรือแก้ไข: ชื่อ, อีเมล" (`role="alert"`) — รวมช่องที่เบราว์เซอร์โฟกัสไม่ได้ (ซ่อนในแท็บ/ขั้นที่ปิด, input ซ่อนของ Select) ซึ่งปกติไม่มีอะไรขึ้นเลยนอกจาก console · เลื่อนไปที่ Field ของช่องแรกและโฟกัส control ที่กดได้ในนั้น · หายเองเมื่อแก้ครบ · ชื่อช่องจาก `<label>` (รวมแบบครอบของ `LabeledField`), `aria-label`, `aria-labelledby`, label ของ `Field`, placeholder, `name`
335
+ - `UiText.invalidForm` (`fields` ใช้ `{fields}`, `generic`) — ไทย/อังกฤษในตัว
336
+
337
+ ### Changed
338
+
339
+ - `FormDialog` / `ActionForm`: ปุ่มส่งใช้ `loading` ระหว่างรอ action (เดิมแค่ disabled จาง ๆ) และใส่ตาข่าย `useFormInvalidNotice` ให้เอง — แอปไม่ต้องแก้อะไร
340
+
341
+ ## [0.35.0] — 2026-10-07
342
+
343
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — publish 0.36.0 ทีเดียวได้ทั้งสองรุ่น
344
+
345
+ ### Added
346
+
347
+ - `SortableProvider renderOverlay={(drag) => …}`: วาดการ์ดลอยตามเมาส์แบบ Trello/Todoist (portal ไป `document.body` + drop animation 200ms) แถวต้นทางเหลือเป็นช่องจาง ๆ บอกตำแหน่งที่จะวาง · `useSortableState().overlay` บอกว่ากำลังใช้ overlay
348
+ - `useSortableDropZone(groupId, { onDwell, dwellMs = 600 })`: ลากค้างเหนือโซนครบเวลาแล้วเรียก `onDwell` — ใช้เปิดโฟลเดอร์ที่ย่ออยู่ให้เอง
349
+
350
+ ### Changed
351
+
352
+ - **`@suphark/ui/sortable` ลากลื่นขึ้น** (ผู้ใช้แจ้งว่าสะดุดตอนเอาเข้าโฟลเดอร์): หาเป้าหมายจากตำแหน่งเมาส์ (`pointerWithin` → `rectIntersection`) แทน `closestCenter` ทั้งตาราง, จำเป้าหมายล่าสุดกันสั่นตอนรายการย้ายข้ามกลุ่ม, ย้ายข้ามกลุ่มแทรก **ก่อนหรือหลัง** แถวเป้าหมายตามครึ่งแถว (เดิมแทรกก่อนเสมอ → ลากเข้าจากด้านล่างแล้วกระโดดข้าม), วางบนหัวโฟลเดอร์ = ขึ้นบนสุดของโฟลเดอร์ (เดิมไปท้ายกลุ่ม/ท้ายตาราง), วัดตำแหน่งใหม่ทุกครั้งที่ layout เปลี่ยน (`MeasuringStrategy.Always`) และ transition 200ms แบบ ease-out · วัดด้วย probe ลาก APP_URL ลง 222px: เดิมกระโดดไปท้ายกลุ่มแล้วไปท้ายตาราง, ตอนนี้ลงตรงใต้เมาส์และไม่เปลี่ยนเลยระหว่างค้างนิ่ง
353
+ - showcase: `/components/sortable-list` ใช้การ์ดลอย + ลากค้างเหนือโฟลเดอร์ที่ย่ออยู่แล้วเปิดให้เอง
354
+
355
+ ### Docs
356
+
357
+ - README (`@suphark/ui/sortable`): วิธีตรวจว่า `id` ทำงาน — `aria-describedby` ของที่จับเป็น id ที่ส่งเข้าไปตรง ๆ ไม่มี prefix `DndDescribedBy-` (prefix มีเฉพาะตอน dnd-kit สร้างเอง) และ element คำอธิบายถูกสร้างหลัง hydrate ต้องรอ mount ก่อน query (Pi Config ยืนยันบนของจริงหลัง bump 0.34.0)
358
+
359
+ ## [0.34.0] — 2026-10-06
360
+
361
+ ### Fixed
362
+
363
+ - **`SortableProvider` ทำ hydration mismatch ทุกหน้าที่ SSR** (รายงานจาก Pi Config ตอนลากบนของจริง): ไม่ได้ส่ง `id` ให้ `DndContext` → dnd-kit สร้าง `aria-describedby="DndDescribedBy-N"` จากตัวนับที่เดินไม่ตรงกันระหว่าง server กับ client, React 19 ขึ้น error และ **ไม่ซ่อมให้** — attribute ค้างชี้ element ที่ไม่มีอยู่ ผู้ใช้ screen reader จึงไม่ได้ยินคำอธิบายวิธีลาก · ตอนนี้ค่าเริ่มต้นเป็น `useId()` (ตรงกันทั้งสองฝั่ง) และรับ prop **`id`** สำหรับ id ที่สื่อความหมาย — กระทบทุกแอปที่ใช้ `@suphark/ui/sortable` ใน Next.js
364
+ - `@suphark/ui/sortable`: เสียงประกาศระหว่างลากในกลุ่มเดียวกันบอกตำแหน่ง **เดิม** ซ้ำ ("ย้ายไปตำแหน่งที่ 1 จาก 2" ทั้งที่เลื่อนลงแล้ว) เพราะลำดับจริงเปลี่ยนตอนปล่อย — ตอนนี้ระหว่างลากประกาศตำแหน่งของเป้าหมาย และตอนปล่อยประกาศตำแหน่งจริงหลังย้าย (เจอระหว่างทดสอบลากด้วยคีย์บอร์ดตามที่ Pi Config ถาม; ลากด้วยคีย์บอร์ดเองทำงานถูกอยู่แล้ว)
365
+ - showcase: `/components/sortable-list` ย้ายแถว "ลากมาที่นี่เพื่อเอาออกจากกลุ่ม" ไปท้ายตาราง — เดิมโผล่เหนือกลุ่มตอนเริ่มลาก ดันแถวที่กำลังลากลง จนลากคีย์ในกลุ่มด้วยคีย์บอร์ดไม่ขยับ (README เพิ่มคำเตือนนี้ + เรื่อง `id`)
366
+
367
+ ## [0.33.0] — 2026-10-06
368
+
369
+ ### Fixed
370
+
371
+ - **คอนทราสต์ไม่ผ่าน WCAG AA 3 จุด** (DueBell วัดจากหน้าจริงทั้งสองธีม — ผมคำนวณจาก token ซ้ำได้ตัวเลขตรงกันทุกจุด):
372
+ - `--info` ธีมสว่าง: `text-info` บนพื้นขาว **4.02:1** → ลดความสว่าง L 0.588 → 0.53 = **5.05:1** (ตัวหนังสือขาวบน `bg-info` ก็ขึ้นเป็น 5.05:1 เท่ากัน) — สีฟ้าเข้มขึ้นเล็กน้อย
373
+ - `--muted-foreground` ธีมสว่าง: บนพื้น `--muted` **4.35:1** (กระทบ `CountBadge tone="muted"`, แท็บที่ไม่ได้เลือกบน `TabsList`, ป้ายนับใน `SegmentedControl` …) → L 0.554 → 0.52 = **5.02:1** (บนขาว 5.4:1) — ตัวหนังสือรองเข้มขึ้นเล็กน้อยทั้งระบบ
374
+ - ป้ายนับ `CountBadge tone="destructive"` (รวมปุ่มแจ้งเตือนใน `AppShell`/`MobileTabBar`) ธีมมืด: ตัวหนังสือขาวบน `--destructive` **2.89:1** → token ใหม่ **`--destructive-foreground`** (สว่าง = ขาว 4.76:1, มืด = สีพื้นเข้ม **6.97:1**) — ไม่ลดความสว่างของ `--destructive` ธีมมืดเพราะ `text-destructive` บนพื้นมืดจะไม่ผ่านแทน
375
+ - ใช้ `text-destructive-foreground` คู่ `bg-destructive` ได้ทุกที่ (ใหม่ใน `@theme`)
376
+
377
+ ## [0.32.0] — 2026-10-06
378
+
379
+ > แก้บั๊กที่แอปรายงานหลัง 0.30.0 — เดิมเตรียมไว้ใน 0.31.0 แต่ 0.31.0 ถูก publish (12:53 น.) ก่อน commit ทั้งสามนี้ (12:56–13:09 น.) ตรวจ tarball แล้วไม่มี จึงออกเป็น 0.32.0
380
+
381
+ ### Fixed
382
+
383
+ - **`@suphark/ui/sortable`: แถวกลุ่มไม่ได้อยู่ใน `SortableContext`** (รายงานจาก Pi Config ตอนย้าย `values-table`) — `SortableGroup` สร้าง context ให้เฉพาะรายการในกลุ่ม ส่วนแถวกลุ่มที่ลงทะเบียนด้วย `useSortableRow({ type: "group" })` ไม่มี context ไหนรับ ทำให้ลากกลุ่มแล้วไม่มีการเลื่อนแถวให้เห็นระหว่างลาก (ปล่อยแล้วลำดับยังถูก) — ตอนนี้ `SortableProvider` สร้าง `SortableContext` ชั้นนอกจาก `order.groups` ให้เอง **ไม่ต้องแก้ call site และลำดับใน context หลุดจากลำดับจริงไม่ได้**
384
+
385
+ - **`ActivityFeed relative={false}` พังทุกแถว** (รายงานจาก Pi Auth): วันที่กลายเป็น `-` และ console เต็มไปด้วย `TypeError: Invalid option` เพราะส่ง `timeStyle` ปนกับ `day`/`month`/`year` ซึ่ง `Intl.DateTimeFormat` ไม่ยอม — ต้นเหตุจริงอยู่ที่ **`ClientDateFormatter` เอา default (`day`/`month`/`year`) ไปผสมกับ options ของผู้เรียกเสมอ** ทำให้ทุกแอปที่ส่ง `dateStyle`/`timeStyle` พังเหมือนกัน; ตอนนี้ถ้าผู้เรียกใช้ `dateStyle`/`timeStyle` จะไม่ผสม default ให้ และ `ActivityFeed` เปลี่ยนไปใช้ `{ dateStyle: "medium", timeStyle: "short" }` (เทสต์ใหม่ครอบทั้งสองชั้น)
386
+
387
+ ### Added
388
+
389
+ - `prepublishOnly` เรียก `scripts/release-check.mjs` ก่อน publish: working tree ต้องสะอาด, HEAD ต้องตรงกับ `origin/main` หลัง fetch และ CHANGELOG ต้องมีหัวข้อของเวอร์ชันนั้น — กันปล่อยรุ่นที่ขาด commit (เกิดมาแล้ว 3 ครั้ง: 0.17.0, 0.30.0, 0.31.0; ข้อเสนอจาก Pi Auth/Pi Config)
390
+
391
+ ### Changed
392
+
393
+ - `ActivityFeed`: รับ `locale` แล้วส่งต่อให้ตัวฟอร์แมตวันที่ (ไม่ส่ง = `th-TH` เหมือนเดิม) — เดิมวันที่เป็นไทยตายตัวสำหรับทุกคน แอปสองภาษาจึงแสดงผิดเมื่อผู้ใช้เลือกอังกฤษ (รายงานจาก Pi Auth ตอนทำหน้า audit log); `emptyLabel` ค่าเริ่มต้นย้ายไปอ่านจาก `UiText.activity.empty` (คีย์ใหม่)
394
+
395
+ ## [0.31.0] — 2026-10-06
396
+
397
+ > 0.30.0 ถูก publish ก่อน commit ของ `NotificationCenter` (ตรวจ tarball แล้วไม่มีไฟล์ `shared/notification-center.tsx`) จึงเลื่อนมาออกเป็น 0.31.0 — ของอื่นใน 0.30.0 อยู่ครบตามรายการข้างล่าง
398
+
399
+ ### Added
400
+
401
+ - **`NotificationCenter`** — ศูนย์การแจ้งเตือน (ยกจาก DueBell `notification-modal` + `urgent-bills-widget`): ชิปนับหลายกลุ่มที่กดกรองได้ + รายการที่ต้องจัดการ พร้อมปุ่มลัดต่อรายการและเมนู "…" · ออกแบบตามข้อกำหนดที่ DueBell ให้มา — ชิปเป็น array (`counts`) ไม่ใช่ prop ตายตัว, คำสั่งเป็น array (`actions` + `menuActions`), ของโดเมนส่งเป็น node ที่แอปเตรียมแล้ว (`status`/`meta`/`footer` — ยอดเงินที่ซ่อนได้, ความคืบหน้ารอบ), **แพ็กเกจไม่ฟอร์แมตวันที่เอง** และ **ไม่ตัดรายการเอง** (จะจำกัดจำนวนให้แอป slice ก่อนแล้วส่ง `viewAll` — ตัวเลขในชิปจึงตรงกับที่ผู้ใช้เห็นเสมอ); `busy` ปิดปุ่มทุกตัวระหว่างทำคำสั่งลัด · คีย์ `UiText.notifications`
402
+
403
+ ## [0.30.0] — 2026-10-05
404
+
405
+ ### Added
406
+
407
+ - **ข้อความที่ผู้ใช้เห็นในแพ็กเกจเปลี่ยนภาษาได้ครบแล้ว** — เก็บข้อความไทยที่ฝังตายใน JSX อีก 34 จุดเข้า `UiText`: `DataTableToolbar` (ค้นหา/ล้าง/ตัวกรอง/คำอธิบาย/ปิด), `DataTableFacetedFilter` + `FormFieldCombobox` ("ไม่พบข้อมูล"), `MultiSelectCombobox` (ล้างทั้งหมด/ลบ/เพิ่ม), `ResourcePropertyFilter` (ทั้งแผง 14 จุด), `AppShell`/`SubNav`/`TopTabs` (เมนูหลัก/เมนูย่อย/เลือกหน้า/ไม่พบ/ล้างคำค้นหา), `ErrorState` (เซสชันหมดอายุ) — คีย์ใหม่: `UiText.combobox`, `UiText.filter`, `UiText.nav`, `UiText.sessionExpired` และ `UiText.table.{search,clearSearch,filters,filtersDescription,closeFilters,clearFilter}`
408
+ - **เทสต์กันข้อความฝังตาย** `no-hardcoded-text` (อยู่ใน `pnpm test`/CI): ไล่ JSX ของ `shared`/`app-shell`/`data-table`/`sortable` หาข้อความไทยที่ไม่มีทาง override — ยกเว้นได้แต่ต้องเขียนเหตุผลใน ALLOW; มีชุด "ปลูกการละเมิด" พิสูจน์ว่าตัวตรวจจับของจริงได้และไม่จับของที่ถูกต้อง (คอมเมนต์/JSDoc/`?? "…"`/พจนานุกรมของ component) — ไอเดียจาก Pi Config ที่เจอว่าเทสต์แบบนี้ล้มเหลวเงียบได้ง่าย
409
+ - `SegmentedControl`: `tone` ต่อตัวเลือก (`success` / `warning` / `destructive` / `info`) — ลงสีเฉพาะตัวที่ถูกเลือก ใช้กับตัวกรองที่ "ประเภท" มีความหมายทางสี เช่นรายรับ/รายจ่าย (คำขอจาก DueBell ที่ต้องใช้ไอคอนแทนไปก่อน)
410
+
411
+ ### Fixed
412
+
413
+ - `MonthStepper`: ปุ่มกลางตอนอยู่เดือนปัจจุบันเคยเป็น `<Button disabled>` ซึ่ง screen reader อ่านว่า "ปุ่ม ปิดใช้งาน" ทั้งที่มันคือป้ายชื่อเดือน — ตอนนี้เรนเดอร์เป็นป้ายธรรมดาเมื่อไม่มีอะไรให้กด (รายงานจาก DueBell) และตอนกดได้ใช้ tooltip ของ `Button` แทน `title` (เลิก tooltip ซ้อนของเบราว์เซอร์)
414
+
415
+ ## [0.29.0] — 2026-10-05
416
+
417
+ ### Added
418
+
419
+ - `InlineEdit`: คุมโหมดแก้ไขจากข้างนอกได้ — `editing` + `onEditingChange` (controlled), `defaultEditing` และ `onCancel` (กด Esc/✕) สำหรับแอปที่เข้าโหมดแก้ไขด้วย F2 หรือเมนูคลิกขวา "เปลี่ยนชื่อ" (คำขอจาก Pi Files — เดิมต้อง `.click()` ปุ่มดินสอผ่าน DOM)
420
+ - **`@suphark/ui/sortable` — ลากจัดลำดับ 2 ระดับ (กลุ่ม + รายการในกลุ่ม)** entry ใหม่บน dnd-kit (แยกจาก barrel เหมือน `data-table`/`kanban` เพราะ dnd-kit สร้าง context ตอน import) — ยกสมองส่วนลากออกจากแอป ส่วนการเรนเดอร์แถวยังเป็นของแอป (ตาราง/การ์ด/รายการ) ตามสเปกของ Pi Config (`values-table.tsx`)
421
+ - `SortableProvider` — sensors (เมาส์ + คีย์บอร์ด), collision ที่กันลากข้ามชนิด (กลุ่มชนได้แค่กลุ่ม, รายการชนได้แค่รายการกับที่วางของกลุ่ม), ย้ายข้ามกลุ่มระหว่างลาก, commit ตอนปล่อย และ **คืนลำดับเดิมอัตโนมัติเมื่อบันทึกล้มเหลว** (throw หรือ `{ ok: false, error }`) — ไม่มี `onCommitError` = ขึ้น toast ให้; ส่งมา = ส่ง error ดิบให้แอปจัดการเอง ไม่กลืนเงียบ
422
+ - `SortableGroup` (ขอบเขตการเรียงของกลุ่ม), `useSortableRow({ type, id })` → `{ ref, style, handleProps, isDragging }`, `useSortableDropZone(groupId)` (แปะที่ไหนก็ได้ — หัวกลุ่ม หรือแถว placeholder ของกลุ่มที่ไม่มีหัว; กลุ่มที่ย่ออยู่ก็ยังรับการวางได้), `useSortableState()` → `{ dragging, disabled }`, `DragHandle` (ใช้ `Button` ของแพ็กเกจ จึงได้ tooltip จาก `aria-label`; ปิดการลากแล้วที่จับจางลงเอง หรือซ่อนไปเลยด้วย `hideWhenDisabled`)
423
+ - `announcements` + `screenReaderInstructions` ส่งเป็น prop ได้ (callback ได้ `{ id, kind, groupId, index, total }` ให้แอปประกอบข้อความจากชื่อจริง) ไม่ส่ง = แม่แบบจาก `UiText.sortable`
424
+ - helper แบบ pure ใน `lib/sortable-order`: `sortableOrderOf`, `reorderGroups`, `reorderItems`, `moveItemToGroup`, `groupOfItem`, `sameOrder`, `moveWithin`, `sortableOrderToPayload` (คืน `{ groups: string[], items: [{ groupId: string | null, itemIds }] }` — ลำดับกลุ่มกับรายการในกลุ่มครบในก้อนเดียว, `UNGROUPED` → `null`)
425
+ - `UiText.sortable` — `handle`, `instructions`, `started`, `moved`, `dropped`, `cancelled` (`{index}` / `{total}`)
426
+ - showcase: หน้า `/components/sortable-list` — ตาราง 2 ระดับ ลากเรียงกลุ่ม/คีย์/ย้ายข้ามกลุ่ม พร้อมแถว "ลากมาที่นี่เพื่อเอาออกจากกลุ่ม" ที่โผล่เฉพาะตอนลากคีย์
427
+
428
+ ## [0.28.1] — 2026-10-05
429
+
430
+ ### Fixed
431
+
432
+ - **`AppShell` ปุ่มย่อ/ขยายเมนูยังมี "Toggle Sidebar" อังกฤษค้างอยู่ใน `sr-only`** (รายงานจาก DueBell): `SidebarTrigger` ของ shadcn ฝังข้อความนั้นเป็น children ซึ่งแทนด้วย props ไม่ได้ (JSX children ชนะ) — 0.27.1 เปลี่ยนได้แค่ `aria-label` ตอนนี้ `AppShell` เรนเดอร์ปุ่มเองจาก `Button` ของแพ็กเกจ + `useSidebar()` จึงใช้ `UiText.toggleSidebar` ทั้งชื่อและ tooltip และไม่มีข้อความอังกฤษเหลือในปุ่ม (ยังคง `data-sidebar="trigger"` ไว้ให้ selector เดิมใช้ได้)
433
+
434
+ ## [0.28.0] — 2026-10-05
435
+
436
+ ### Added
437
+
438
+ - **`ActivityFeed`** — เส้นเวลา "ใครทำอะไรเมื่อไหร่" จาก array เดียว (audit log ของ Pi Auth, ประวัติงานของ Pi Done, timeline ของเอกสาร): avatar ของผู้ทำหรือไอคอน+โทนสำหรับเหตุการณ์ของระบบ, เวลาสัมพัทธ์ที่คำนวณฝั่ง client หลัง mount (ไม่ทำ hydration mismatch) และช่องใส่ปุ่ม/ป้ายท้ายแถว
439
+ - **`InlineEdit`** — แก้ค่าในที่: คลิกดินสอ → ช่องกรอก (เลือกข้อความเดิมไว้ให้พิมพ์ทับได้ทันที) → Enter/✓ บันทึก, Esc/✕ ยกเลิก พร้อม `validate`, สถานะกำลังบันทึก และ toast เมื่อล้มเหลว (ถือ `{ ok: false, error }` เป็นล้มเหลว, `redirect()` เป็นสำเร็จ) — ใช้กับค่าสั้น ๆ ที่ไม่คุ้มเปิด `FormDialog`
440
+ - **`SelectionBar`** — แถบคำสั่งหมู่ "เลือก N รายการ" + ปุ่มคำสั่ง + ปุ่มล้าง (`floating` กลางล่างจอ หรือ `inline` ในหน้า) ใช้คู่ `DataTable` หรือ `SimpleTable`; ข้อความจาก `UiText.selection`
441
+ - **`SimpleTable` เลือกหลายแถวได้** — `selected` + `onSelectedChange` เปิดช่องติ๊กหน้าแถวและหัวตาราง (เลือกบางส่วนแสดงเป็นขีด), แถวที่เลือกมีพื้นจาง; คลิกช่องติ๊กไม่ไปโดน `onRowClick`
442
+ - `SimpleTabs`: แท็บล้นความกว้างแล้วเลื่อนแนวนอนได้ (ซ่อนแถบเลื่อน) + เงาขอบบอกว่ายังมีแท็บต่อ
443
+ - `UiText` คีย์ใหม่: `edit`, `selection.{selected,clear}`, `table.{rowsPerPage,pageOf,rowsSelected,noResults,deleteTitle,deleteDescription,deleting,deleted,delete}`
444
+
445
+ ### Fixed
446
+
447
+ - **`DataTable` เลิกใช้ข้อความอังกฤษตายตัว** — "Rows per page", "Page X of Y", "N of M row(s) selected", "No results.", เมนู Edit/Delete, กล่องยืนยันลบ และ toast ตอนลบ ทั้งหมดมาจาก `UiText.table` (ค่าเริ่มต้นภาษาไทย) — `resourceName` แทรกผ่าน `{resource}`
448
+
449
+ ## [0.27.1] — 2026-10-05
450
+
451
+ ### Added
452
+
453
+ - `Button`: ถ้า `aria-label` ไปอยู่บน element ใน `render=` (เช่น `render={<Link aria-label="…"/>}`) ก็ใช้เป็น tooltip ได้ (เดิมต้องอยู่บน `Button` เท่านั้น — รายงานจาก Pi Auth)
454
+ - เทสต์ชุดใหม่ `button-tooltip-triggers`: ยืนยันว่าปุ่มไอคอนที่ส่งเป็น `render=`/`trigger` ของ `DropdownMenuTrigger` / `PopoverTrigger` / `FormDialog` / `RowActions` ยังได้ tooltip จริง (เช็กจาก popup ที่ขึ้น ไม่ใช่ `data-slot` — `data-slot` ของปุ่มจะกลายเป็นของ trigger ชั้นนอกเสมอ ใช้ชี้วัดไม่ได้)
455
+
456
+ ### Fixed
457
+
458
+ - **ตัวห่อปุ่มของแพ็กเกจเองหล่น 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 ด้วย)
459
+ - `ActionButton` / `LinkButton`: มี `tooltip` ของตัวเองอยู่แล้ว → ส่ง `tooltip={false}` ให้ `Button` ชั้นใน กันขึ้นสองชั้น (props `tooltip`/`tooltipSide` ของ `ActionButton` ยังเป็นแบบเดิมทุกประการ)
460
+ - `DataTableRowActions`: ปุ่ม "…" ใช้ `aria-label` จาก `UiText.actions` แทน `sr-only` อังกฤษ "Open menu"
461
+ - **กฎใหม่กันพลาดซ้ำ**: `pnpm check:button` (อยู่ใน `verify` และ CI) ห้ามไฟล์ในแพ็กเกจ import `Button` จาก `components/ui/button` ยกเว้นไฟล์ shadcn เองกับตัวห่อ
462
+ - **`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`)
463
+
464
+ ## [0.27.0] — 2026-10-05
465
+
466
+ ### Added
467
+
468
+ - **`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 ให้)
469
+ - props ใหม่: `tooltip` (ทับข้อความ, `false` = ปิด), `tooltipSide`, `tooltipDelay` — ไม่ต้องมี `TooltipProvider` ใน tree (ใส่ได้ถ้าอยากคุม delay ร่วมกัน)
470
+ - `SimpleTooltip` ปิด tooltip อัตโนมัติของ `Button` ที่ห่ออยู่ให้แล้ว (ไม่ขึ้นสองชั้น) — แอปที่ห่อ tooltip ด้วยกลไกอื่นใช้ `ButtonTooltipSuppress` (export ใหม่) หรือ `tooltip={false}`
471
+ - barrel export `Button` จาก `components/shared/button/button.tsx` (ครอบ `components/ui/button.tsx` ที่ยังเป็นไฟล์ shadcn ดิบ ไม่ถูกแก้มือ); `buttonVariants` เหมือนเดิม; props อื่นและหน้าตาไม่เปลี่ยน
472
+ - `DataTablePagination`: ปุ่มเลื่อนหน้าทั้ง 4 ใช้ `aria-label` จาก `UiText` (เดิมเป็นข้อความอังกฤษตายตัวใน `sr-only`); `ThemeToggle` ใช้ `aria-label` จาก `UiText.theme.label` แทน `title` (เลิก tooltip ซ้อนของเบราว์เซอร์)
473
+ - README หัวข้อ "ปุ่มไอคอน — tooltip มาให้เอง (มาตรฐานทุกโครงการ)" + แถวใหม่ใน `docs/agent-rules.md`
474
+ - ใช้ข้อความใน `<span className="sr-only">` (รูปแบบของ shadcn) เป็น tooltip ได้เมื่อไม่มี `aria-label`
475
+ - **ปุ่มไอคอนในแพ็กเกจเองได้ tooltip ครบ**: `RowActions`, `ActionMenu`, `SimpleDialog`/`SimpleSheet` (ปุ่มปิด), `SimplePagination`, `DataTablePagination`, `DataTableToolbar`, `CommandPalette`, `ThemeToggle`/`ThemeSwitch`, `Notice`, `UploadList`, `SecretReveal`, `TimePicker`, `MonthStepper`, `RuleBuilder`, `ResourcePropertyFilter`, `AuthParts` (17 ไฟล์เปลี่ยนมา import `Button` ตัวที่มี tooltip)
476
+ - `UiText.pagination` เพิ่ม `first` / `last` (หน้าแรก/หน้าสุดท้าย)
477
+ - showcase: หน้า `/components/button` เพิ่มแถวปุ่มไอคอน (auto / `tooltip="…"` / `tooltip={false}` / trigger ของ dialog / ห่อ `SimpleTooltip`)
478
+
479
+ ## [0.26.1] — 2026-10-03
480
+
481
+ แก้ตามผล audit 2026-10-03 (`docs/reports/audit-2026-10-03.md`) — ไม่เปลี่ยน props
482
+
483
+ ### Fixed
484
+
485
+ - **`RuleBuilder`**: แก้ค่าในเงื่อนไขแล้ว element ถูกสร้างใหม่ทุกครั้ง (key ผูกกับ object) → ช่องที่กำลังพิมพ์หลุดโฟกัสทุกตัวอักษร — object ใหม่ที่มาแทนใช้ key เดิม
486
+ - **`RowActions` / `ConfirmActionButton` / `ActionMenu` / `StepForm` / `ChatThread`**: server action ที่ `redirect()`/`notFound()` เคยขึ้น toast "NEXT_REDIRECT" — ถือว่าสำเร็จ (Next นำทางให้แล้ว); `ActionMenu`/`StepForm`/`ChatThread` ถือ `{ ok: false, error }` เป็นล้มเหลวด้วย (ตัวช่วยใหม่ใน `lib/action-result.ts`: `errorMessage`, `failedResult`, `isNavigationError` — `@suphark/ui/next` ยัง export สองตัวแรกจาก confirm-action-button เหมือนเดิม)
487
+ - **`RowActions` + `confirm`**: เคยปิด dialog ก่อนรัน action — ตอนนี้ปุ่มยืนยันหมุนระหว่างรอ ล้มเหลวคง dialog ไว้ให้ลองใหม่ สำเร็จค่อยปิด (ตามที่ 0.25.1 ตั้งใจ)
488
+ - **`useDialogs().confirm`** รองรับ `requireText` (เดิมข้ามการพิมพ์ยืนยันไปเงียบ ๆ)
489
+ - **`DataTable` / `useDataTable`**: เลือกแถวตาม `row.id` เป็นค่าเริ่มต้น (ไม่มี id = index เหมือนเดิม) — เดิมใช้ลำดับแถว ข้อมูลเปลี่ยน/แทรกแถวแล้วแถวที่เลือกกลายเป็นแถวอื่น (`DeleteManyButton` ลบผิดรายการ); การลากเรียงเทียบ id แบบ string
490
+ - **`useDataTable` `columnFilters` (regression 0.25.1)**: เทียบด้วยค่าแต่ตั้ง state เป็นค่าจริงจาก prop — เดิม `JSON.parse` ทำให้ `Date` กลายเป็น string; ค่าที่ stringify ไม่ได้ (BigInt/วงอ้างอิง) ไม่ throw ตอน render
491
+ - **`DataTable` ช่องค้นหา**: Enter ไม่ส่งซ้ำอีกรอบหลัง 250 ms; ปุ่มล้างตัวกรองล้างข้อความที่พิมพ์ค้างด้วย
492
+ - **`NumberInput` / `TagsInput` ใน `<label>` ที่ครอบ** (`LabeledField` ไม่ใส่ `htmlFor`): คลิกข้อความ label เคยกดปุ่ม "ลด"/ลบป้ายแรก — ช่องพิมพ์อยู่ก่อนปุ่มใน DOM แล้ววางตำแหน่งด้วย CSS `order`
493
+ - `NumberInput`: ทั้งช่องเคยจางเหมือนปิดใช้งานเมื่อค่าเท่ากับ `min`/`max` (`has-disabled` จับปุ่ม −/+ ที่ปิด) — จางเฉพาะเมื่อช่องปิดจริง; `step` ที่เปลี่ยนหลัง mount ไม่ทำให้ทศนิยมถูกปฏิเสธอีก; `size="sm"` 16px บนมือถือ (iOS ไม่ซูม)
494
+ - `TagsInput`: ค่าที่ถูกปฏิเสธค้างในช่อง = ช่อง invalid (ฟอร์มไม่ส่งรายการที่ขาดค่านั้นไปเงียบ ๆ)
495
+ - `TimePicker`: ปุ่ม "ตอนนี้" ปัดตาม `roundToStep`
496
+ - **`StepForm`**: ดับเบิลคลิก "ถัดไป" ไม่ข้ามขั้นกลาง; `onSubmit` ที่จบเร็ว/sync ไม่ถูกส่งซ้ำ; ตัวควบคุม Base UI ที่ไม่ผ่าน (Select/Checkbox/Switch) โฟกัสตัวมันเอง ไม่ใช่ช่องอื่น
497
+ - **`CommandPalette`**: เช็ก Ctrl/⌘ ก่อนอ่าน `e.key` (keydown จาก autofill ไม่มี key ไม่ throw); ไม่เปิดด้วย AltGr (ctrl+alt); แป้นละตินเทียบ `e.key` (Dvorak ฯลฯ) แป้นไทยใช้ `e.code`; 2 ตัวในหน้าเดียวเปิดทีละตัว
498
+ - `ChatThread`: ข้อความที่พิมพ์ระหว่างรอส่งไม่ถูกล้างทิ้ง
499
+ - `SettingsLayout`: มือถือกด "กลับ" ขณะหัวข้อที่เปิดถูกกรองด้วยคำค้น โฟกัสไปช่องค้นหาแทนหลุดไป body
500
+ - **service worker (PWA)**: reload หลังมีเวอร์ชันใหม่เคยเกิดทุก `focusout` (ย้ายช่อง/กดบันทึก = reload กลางฟอร์ม) — รอนิ่ง 2 วินาทีหลังออกจากช่อง ยกเลิกเมื่อมีการคลิก/พิมพ์/โฟกัสใหม่
501
+ - `PullToRefresh`: ฟัง scroll แบบ capture ที่ document (root ที่ mount ทีหลัง/หน้าเลื่อนทั้ง document ใช้ได้)
502
+ - `ProviderButtons`: ลิงก์ที่ปิดอยู่ดูปิดจริงและถอด `href` (กันคลิกกลาง); `aria-busy` ทั้งสองแบบ
503
+ - `useTableSearchParams`: ค่าที่รอส่งไม่ค้างเมื่อ URL ไม่เปลี่ยน; key `__proto__` จาก URL ไม่ทับ prototype
504
+ - `MonthStepper`: JSDoc กลับมาแสดงใน editor; `DesignSystemShowcase` กระโดดข้ามแท็บรอบใหม่ได้เวลารอเต็ม
505
+ - CSS: scrollbar ทั้งหน้า (`*{scrollbar-width}`) ย้ายเข้า `@layer base` — เดิมทับ `no-scrollbar`/className ของแอปทุกที่ (เช่น คอลัมน์ TimePicker)
506
+
507
+ ### Security
508
+
509
+ - **`ActionMenu` href + confirm**: นำทางผ่าน `location.assign`/`window.open` ซึ่งไม่กัน `javascript:` แบบ `<a href>` — รับเฉพาะ http(s)/path ภายใน (`lib/safe-url.ts` `safeNavigableUrl` ใช้ร่วมกับ `ClientRedirect`); ลิงก์แท็บใหม่ที่ไม่มี onClick เปิดทันทีในคลิก (ไม่โดน popup blocker)
510
+ - `check:barrel` จับ `export *` ทุกรูปแบบ (alias `@suphark/ui/...`, แพ็กเกจ, quote เดี่ยว, comment ท้ายบรรทัด) และรันใน `verify` ของแพ็กเกจ + `prepublishOnly` (publish ไม่ผ่าน CI ก็ยังถูกตรวจ); README สอนขั้นตอนที่ถูก (`fix:barrel`)
511
+ - showcase: demo ไม่มีค่าที่หน้าตาเหมือนคีย์จริง (`pk_live_`) แล้ว; หน้า time-picker บอกรูปแบบค่า DateTimePicker ที่มี offset
512
+ - tests 12 กรณี (`fixes-0-26-1.test.tsx`, RuleBuilder, service worker)
513
+
514
+ - showcase (security): `next` ^16.3.5 → ^16.3.8 — 16.3.5 มี advisory **critical** GHSA-vcvr-r3jv-pc5j (RCE ใน `next/og` ImageResponse; showcase ไม่ได้ใช้ `next/og` จึงไม่ถูกโจมตีได้จริง แต่ `pnpm audit` แจ้ง) — `pnpm audit --prod` เหลือ 0; devDependency `next` ของแพ็กเกจ bump ตาม (peer ยัง `>=15`)
515
+ - showcase (security): `@suphark/auth-client` ^0.6.1 → **^0.6.2** (แก้ dot segment ต้นทาง) — ลบตัวกรอง `?next=` ชั่วคราวใน `/auth/[...pi]` ออก; ทดสอบ `/auth/logout` 7 กรณี (`/.//`, `/..//`, `/%2e%2e//`, `/./../..//`, tab, `//`, `/brand`) ด้วย server จริงบนเครื่องแล้ว — ปลายทางไม่ออกนอกโดเมน
516
+ - showcase: `@suphark/config-client` ^0.3.0 → ^0.9.2 (CLI `pi-config pull` ตอน build บน Vercel — ข้าม 0.9.0 ที่ pull ค่าหลายบรรทัด CRLF ไม่ได้; 0.9.x บังคับ `PI_CONFIG_URL` เป็น https, ไม่ตาม redirect, timeout 30s; 0.9.2 แก้ crash exit 127 บน Windows) — ทดสอบ pull ด้วย token จาก env (แบบ Vercel) ได้ 6 คีย์; diff/--check บน Windows exit 0/1 ถูกต้อง 10 รอบไม่ crash
517
+ - showcase (security): `safeNextPath` ปฏิเสธผลที่ normalize แล้วเป็น `//host` — dot segment `/.//evil.com`, `/..//evil.com`, `/%2e%2e//evil.com` เคยผ่าน (origin ตอน parse ยังเดิม) แล้ว redirect ไป evil.com; `/auth/[...pi]` กรอง `?next=` ด้วยตัวนี้ก่อนส่งให้ handler ของ `@suphark/auth-client` 0.6.1 ที่มีช่องเดียวกัน (แก้ต้นทางใน 0.6.2 — ลบตัวกรองได้หลัง bump) — production ก่อน deploy นี้: `curl /auth/logout?next=/.//evil.com` → 303 https://evil.com/ (รายงานโดย pi-auth และ pi-config)
518
+
519
+ ## [0.26.0] — 2026-10-03
520
+
521
+ ปิดงานค้างจากผลรีวิว + prop ที่ DueBell ขอ — เพิ่มอย่างเดียว
522
+
523
+ ### Added
524
+
525
+ - `NumberInput`: `inputRef` (ref ของช่องที่พิมพ์ — focus เองได้) และ `autoFocus` (คำขอจาก DueBell)
526
+ - `TimePicker`: `roundToStep` — ปัดนาทีที่พิมพ์ลงตาม `minuteStep` (เช่น step 60 + พิมพ์ 14:30 → 14:00); ค่าเริ่มต้นเก็บตามที่พิมพ์เหมือนเดิม (คำขอจาก DueBell)
527
+
528
+ ### Changed
529
+
530
+ - **ช่องกรอกใช้ขนาดตัวอักษรตามบทบาท `text-body`** (16px มือถือ / 15px จอกว้าง) — `Input`, `Textarea`, `SelectTrigger`, `NativeSelect` (+ `NumberInput`, `TagsInput`): เดิม `text-base md:text-sm` = 14px บนจอกว้าง เล็กกว่าข้อความรอบข้าง 1px และ `text-body` ที่แอปใส่ให้ช่องได้ผลแค่มือถือ (pi-done ต้องเติม `md:text-body` เอง 19 จุด — ลบได้แล้ว); `SelectTrigger`/`NativeSelect` เดิม 14px แม้บนมือถือ (iOS ซูมเมื่อโฟกัส) — แก้ไฟล์ shadcn 4 ไฟล์เป็นข้อยกเว้น (เพิ่มในรายการคืนไฟล์หลัง `shadcn add --overwrite` ใน CLAUDE.md); `className` ของแอปยังแทนได้ (`cn` รู้จักคลาสตามบทบาท)
531
+ - ปุ่ม `ActionButton`/`LinkButton` ที่มี tooltip ไม่สร้าง `TooltipProvider` ต่อปุ่มแล้ว (ตาราง 50 แถว = 100+ provider) — ใช้ `delay={0}` ที่ trigger แทน หน้าตา/จังหวะเหมือนเดิม
532
+ - `DesignSystemShowcase`: แท็บอื่นนอกจาก "ภาพรวม" โหลดแบบ lazy เมื่อเปิดแท็บ (เดิม import ทุกแท็บ ~1 MB) — ลิงก์ข้ามแท็บไปหัวข้อรอแท็บโหลดเสร็จก่อนเลื่อน
533
+ - ลบ dependency `cn` ที่ไม่ได้ใช้ (แอปไม่ต้องติดตั้งเพิ่ม)
534
+ - docs: agent-rules — barrel ที่ client import ห้าม re-export schema ของ zod (ค่า) ให้ export แค่ type (DueBell −18% ต่อหน้า); `ChartConfig` ใช้เฉพาะค่าที่แอปกำหนดเอง (ค่าถูกเขียนลง `<style>` ตรง ๆ)
535
+ - CI: `permissions: contents: read` (+ `pull-requests: read` ให้ gitleaks) — GITHUB_TOKEN อ่านได้อย่างเดียว; pin action ด้วย commit SHA (checkout v4.4.0, gitleaks-action v2.3.9, pnpm/action-setup v4.3.0, setup-node v4.4.0)
536
+ - showcase: `/components/[slug]` โหลด demo แบบ `next/dynamic` ต่อ slug (`src/docs/demos.tsx` + `DemoView`) — เดิมทุกหน้าโหลด demo ทั้ง 129 ตัว (~409 kB gzip ต่อหน้า เช่น `/components/badge` พก recharts)
537
+ - showcase: demo `code-block`/`secret-reveal` ใช้ค่า `example_…` แทน `sk_live_…` (ไม่ให้ secret scanner แจ้งเตือนผิด)
538
+ - showcase: `@suphark/auth-client` ^0.1.7 → **^0.6.1** (แก้ open redirect ใน `safeNextPath` ของ handler `/auth/login|callback|logout` — ทดสอบ `/auth/logout?next=/%09/evil.com` แล้วไม่ออกนอกโดเมน); breaking ระหว่างทาง (ลบ Prisma adapter, เปลี่ยนชื่อ `ACCESS_DENIED`) ไม่กระทบ showcase
539
+
540
+ ## [0.25.1] — 2026-10-02
541
+
542
+ แก้ตามผลรีวิว codebase (bug / security / performance) — ไม่เปลี่ยน props
543
+
544
+ ### Fixed
545
+
546
+ - **`NumberInput`**: input ซ่อนของ Base UI (ที่ส่งค่าใน FormData) ได้ `step` ไปด้วย → browser ปฏิเสธค่าทศนิยม/ค่าที่ไม่ลง step นับจาก `min` แล้ว **บล็อก submit เงียบ ๆ** (เช่น 1500.5 เมื่อ step=1, 150 เมื่อ min=0 step=100 — DueBell เจอ) → input ซ่อนใช้ `step="any"` (ปุ่ม −/+ ยังเลื่อนตาม `step`; min/max ยังตรวจ)
547
+ - **`TimePicker`**: พิมพ์ผิดรูปแบบแล้วกดส่ง ฟอร์มเคยส่งค่าเดิมไปเงียบ ๆ → ช่องเป็น invalid จริง (`setCustomValidity`, ข้อความ `UiText.time.invalid`) ฟอร์มไม่ส่งจนแก้
548
+ - **`DateTimePicker`**: แยกไฟล์ (`form/date-time-picker.tsx`); ข้อความวัน/เวลาและค่าใน input ซ่อนคำนวณหลัง hydrate (เดิม server UTC กับ browser ต่างกัน 7 ชม. = hydration mismatch เมื่อมีค่าเริ่มต้น); **`name` ส่ง `"YYYY-MM-DDTHH:mm+07:00"` (มี offset)** แทนค่าที่ไม่มีโซนเวลา → `new Date(formData.get(name))` บน server ได้เวลาที่ถูก (เดิมคลาด 7 ชม.)
549
+ - `TimeColumns`: เลื่อนเฉพาะในคอลัมน์ (เดิม `scrollIntoView` เลื่อนทั้งหน้าทุกครั้งที่เลือก)
550
+ - **`MonthStepper` / `useMonthCursor`**: "เดือนนี้" คิดตามเวลาไทย (`Asia/Bangkok`) ให้ server กับ browser ตรงกัน — เดิมช่วง 00:00–06:59 ของวันที่ 1 ทุกเดือน server (UTC) เห็นเดือนก่อน = hydration mismatch; `currentMonthKey(now?, timeZone?)` รับโซนเวลาได้ (ไม่ส่ง = เครื่องเหมือนเดิม)
551
+ - **`StepForm`**: ดับเบิลคลิกปุ่มส่ง/ถัดไปเคยเรียก `onSubmit` 2 ครั้ง หรือข้ามหน้าตรวจทาน (คลิกที่สองตกที่ปุ่ม "ส่ง" ตำแหน่งเดิม) → กันซ้ำแบบ synchronous + ไม่นับคลิกภายใน 500 ms หลังเปลี่ยนขั้น; input ซ่อนของ Base UI ผิด → โฟกัสช่องที่มองเห็นของมันเอง (เดิมโฟกัสช่องแรกของขั้น)
552
+ - **`RowActions` / `ConfirmActionButton`**: action ที่คืน `FormState` `{ ok: false, error }` (ไม่ได้ throw) เคยถูกถือว่าสำเร็จ (ปิด dialog + refresh ไม่มีข้อความ) → toast error + คง dialog ไว้ (`failedResult`)
553
+ - **`ActionMenu`**: รายการที่มี `href` + `confirm` เคยไปเลยโดยไม่ถาม → ถามก่อนแล้วค่อยไป; `onClick` ที่ throw/reject แสดง toast และคง dialog ไว้ (เดิม unhandled rejection เงียบ)
554
+ - `ChatThread`: `onSend` ล้มเหลว = toast + คงข้อความในช่องไว้ให้ลองใหม่
555
+ - `CommandPalette`: คีย์ลัดเทียบ `e.code` — แป้นภาษาไทย Ctrl+K ใช้ได้แล้ว (เดิม `e.key` = "า")
556
+ - `SettingsLayout`: มือถือกด "กลับ" แล้วคืนโฟกัสไปหัวข้อเดิม (เดิมหลุดไป body); จอกว้างไม่ย้ายโฟกัสไปหัวข้อ (เดิมย้ายแค่ครั้งแรก)
557
+ - `TagsInput`: วางหลายค่าแทรกที่ cursor ของข้อความที่พิมพ์ค้าง (เดิมทิ้งข้อความนั้น)
558
+ - `ProviderButtons`: ปุ่มที่ `pendingId` ตรงแสดง spinner และกดซ้ำไม่ได้ (รวมแบบ `href`)
559
+ - `useTicker`: subscribe คงที่ข้าม render — เดิมสมัครใหม่ทุก render ทำให้ "x นาทีที่แล้ว" ไม่อัปเดตถ้า parent render บ่อยกว่า interval
560
+ - `useTableSearchParams`: setter หลายตัวติดกัน (เช่น setFilter แล้ว setSort) ต่อจากค่าที่เพิ่งตั้ง (เดิมตัวหลังทับตัวแรก)
561
+ - `useDataTable`: sync `columnFilters` ตามค่า (เดิมตามตัวอ้างอิง → array literal ทำให้ render ซ้ำ + ทับตัวกรองที่ผู้ใช้เพิ่งเลือก)
562
+
563
+ ### Security
564
+
565
+ - `ClientRedirect`: รับเฉพาะ http(s)/path — ปฏิเสธ `javascript:`/`data:` (`location.replace` ไม่มีตัวกันแบบ `<a href>`/router)
566
+ - showcase: `/login` ใช้ `safeNextPath` (`src/lib/safe-next.ts`) กัน open redirect `next=/%09/evil.com` (WHATWG URL ตัด tab → `//evil.com`); production (`VERCEL_ENV=production`) ที่ไม่มี env ของ Pi Auth หรือตั้ง `PI_AUTH_DISABLED` = build ล้ม แทนเปิดเว็บสาธารณะเงียบ ๆ — ตัว `safeNextPath` ของ `@suphark/auth-client` แก้ใน repo auth แล้ว (0.6.1 รอ publish)
567
+
568
+ ### Performance
569
+
570
+ - **`DataTable`**: ส่วนลากเรียง (dnd-kit ~16 kB gzip) แยกเป็น `data-table-sortable.tsx` โหลดแบบ lazy เฉพาะเมื่อส่ง `onReorder` — เดิมทุกตารางพกไปด้วย
571
+ - **`FormFieldInput`**: variant `autocomplete` (cmdk + Radix Dialog ที่ cmdk พ่วงมา) แยกเป็น `form-field-autocomplete.tsx` โหลดแบบ lazy — เดิมทุกฟอร์มที่ใช้ช่องข้อความธรรมดาพก cmdk
572
+ - `DataTable` ช่องค้นหา: ส่งให้ตารางเมื่อหยุดพิมพ์ 250 ms (Enter = ทันที) — เดิมกรอง/เรียง/render ทุกแถว (และ `router.replace` ถ้าผูก URL) ทุกตัวอักษร
573
+ - `TimePicker` ไม่พา react-day-picker มาด้วยแล้ว (แยก `DateTimePicker`); import locale ตรงไฟล์ (`date-fns/locale/th`, `date-fns/locale/en-US`, `react-day-picker/locale/th`) — dev server ไม่ต้อง compile locale ทั้ง ~700 ไฟล์
574
+ - `PullToRefresh`: ติดตั้ง `touchmove` แบบ non-passive เฉพาะตอนจอมือถือ + หน้าอยู่บนสุด (เดิมติดตลอด → browser ต้องรอ JS ก่อนเลื่อนทุกครั้ง)
575
+ - `TreeView`: `expandedIds` แบบ controlled ไม่ทำให้ memo ของแถวคำนวณใหม่ทุก render
576
+ - showcase: หน้า number-input อธิบาย step ตามพฤติกรรมใหม่
577
+ - tests 12 กรณี (`fixes-0-25-1.test.tsx`, `lazy-parts.test.tsx` + StepForm/NumberInput ใน `inputs-0-25.test.tsx`)
578
+
579
+ ## [0.25.0] — 2026-10-02
580
+
581
+ input ใหม่ตามแผนจาก shadcnstudio.com — เพิ่มอย่างเดียว
582
+
583
+ ### Added
584
+
585
+ - **`NumberInput`** — ช่องตัวเลขพร้อมปุ่ม −/+ (Base UI NumberField): `min`/`max`/`step`/`largeStep` (Shift+ลูกศร), `format` (Intl เช่น ทศนิยม 2 ตำแหน่ง/สกุลเงิน), `locale` (ค่าเริ่มต้น th-TH), `prefix`/`suffix` (฿, %, ชิ้น), `hideButtons`, `size` sm|default, `name` ส่งค่าดิบใน FormData — แทน `<Input type="number">` + InputGroup + state
586
+ - **`TimePicker`** — ช่องเวลา 24 ชม. ค่า `"HH:mm"`: พิมพ์ได้เลย (`"1430"`, `"9.15"`, `"14:30 น."` → แปลงตอน blur/Enter, ผิดรูปแบบ = `aria-invalid`) หรือกดไอคอนนาฬิกาเลือกจากคอลัมน์ชั่วโมง/นาที (`minuteStep`, ปุ่ม "ตอนนี้"); `name` ส่งใน FormData
587
+ - **`DateTimePicker`** — ปฏิทิน (ภาษาไทย) + คอลัมน์เวลาในกล่องเดียว ค่า `Date | null`, `defaultTime` เมื่อเลือกวันครั้งแรก, ปุ่มล้าง; `name` ส่ง `"YYYY-MM-DDTHH:mm"` (เวลาท้องถิ่น แบบ datetime-local)
588
+ - `TimeColumns` (คอลัมน์ listbox ใช้ลูกศร/Home/End), `parseTimeInput`, `toLocalDateTimeString`, `type TimeValue`
589
+ - **`TagsInput`** — พิมพ์หลายค่าเป็นป้าย: Enter/จุลภาค (+ เว้นวรรคถ้า `splitOnSpace`) เพิ่ม, วางหลายค่าพร้อมกัน, Backspace ลบตัวสุดท้าย, ไม่รับค่าซ้ำ (`allowDuplicates`), `max`, `transform`, `validate` คืนข้อความ error (ค่าที่ไม่ผ่านคงอยู่ในช่องให้แก้), ปลอดภัยกับ IME ไทย; `name` ส่งทุกค่า → `formData.getAll(name)` — redirect URI/โดเมน/อีเมล
590
+ - **`StepForm`** — ฟอร์มหลายขั้น (wizard) จาก array `steps` (`title`, `description`, `content`, `validate(formData)`): แถบขั้นตอน (ขั้นที่ผ่านแล้วกดย้อนได้, จอแคบแสดง "ขั้นที่ x จาก y"), ปุ่มย้อนกลับ/ถัดไป/ส่ง, "ถัดไป" ตรวจ required/pattern + `validate` ของขั้นนั้น, ส่ง = ตรวจทุกขั้นอีกครั้งแล้วพากลับไปขั้นที่ผิด, input ตัวแทนที่ซ่อนของ Base UI ผิด = แสดงข้อความใต้ฟอร์ม; ทุกขั้นอยู่ใน form เดียว → `onSubmit(formData)` ได้ค่าครบ (ส่ง server action ต่อได้), throw = แสดงข้อความ, ปุ่มหมุนระหว่างรอ, โฟกัสย้ายไปหัวข้อขั้นใหม่
591
+ - `UiText` key ใหม่: `number`, `time`, `tags`, `stepForm`
592
+ - showcase: หน้า `/components/{number-input,time-picker,tags-input,step-form}` (129 หน้า); tests 11 กรณี
593
+
594
+ ### Performance
595
+
596
+ - **barrel ไม่ใช้ `export *` แล้ว** (`index.ts` 66 บรรทัด, `next.ts`, `data-table.ts`, `components/data-table/index.ts` → รายชื่อ export ชัด ๆ แยก `type`; รายชื่อ export เท่าเดิมทุกตัว ตรวจด้วย TypeScript แล้ว) — วัดบน showcase (Next 16.3 Turbopack): server component ที่ import จาก `@suphark/ui` แบบ `export *` ทำให้ client component แทบทั้งแพ็กเกจ (ปฏิทิน, date-fns, cmdk, input-otp, accordion, questionnaire …) ถูกส่งไปทุกหน้า — **root layout ที่ใช้แค่ `ThemeProvider`/`Toaster` ลดจาก 988 kB (gzip 311 kB) เหลือ 61 kB (gzip 18 kB)**, หน้า `/login` 991 → 114 kB; แอปไม่ต้องแก้โค้ด แค่ bump (`optimizePackageImports` / `turbopackRemoveUnused*` ไม่ช่วย — ลองแล้ว)
597
+ - `scripts/barrel-exports.mjs`: `pnpm check:barrel` (ใน verify + CI — เจอ `export *` ใน `packages/ui/src` = ล้ม) และ `pnpm fix:barrel` (แปลงให้หลัง `shadcn add`)
598
+ - หมายเหตุ: ตัวเลข date-fns 824 kB บน bundlephobia คือทั้งไลบรารี — ใน build จริงมีเฉพาะฟังก์ชันที่ใช้ + locale ไทย/อังกฤษ
599
+
600
+ ## [0.24.0] — 2026-10-02
601
+
602
+ เติมของเดิมตามที่สำรวจ shadcnstudio.com (หยิบเฉพาะ "หน้าที่" ที่ยังไม่มี ทำเป็นตัวเดียว + props) — เพิ่มอย่างเดียว ค่าเริ่มต้นหน้าตาเดิม
603
+
604
+ ### Added
605
+
606
+ - **`TextareaField`** — ช่องข้อความหลายบรรทัดพร้อม `label`/`hint`/`error` + **ตัวนับตัวอักษร** เมื่อมี `maxLength` (แดงเมื่อเต็ม, screen reader อ่าน "พิมพ์แล้ว x จาก y ตัวอักษร" ผ่าน `aria-describedby`) + `minRows`/`maxRows` (Textarea ขยายตามเนื้อหาอยู่แล้ว — `maxRows` จำกัดแล้วเลื่อนใน); controlled/uncontrolled; `textareaClassName`
607
+ - **`UserAvatars`** — รูปผู้ใช้ซ้อนกัน + "+N" จาก array (`users`, `max`, `size` sm|default|lg, `label` นำหน้ารายชื่อให้ screen reader) ชี้ที่ "+N" เห็นชื่อที่เหลือ; ใช้จาก server component ได้ — แทน AvatarGroup + Avatar×N + AvatarGroupCount
608
+ - **จุดสถานะ** `status?: "online" | "away" | "busy" | "offline"` ที่ `Initials`, `UserIdentity` และรายการของ `UserAvatars` (`PresenceDot` + `type Presence` export ด้วย) ใช้ token success/warning/destructive/muted
609
+ - **`TableSkeleton`** (`rows`, `columns`, `header`), **`ListSkeleton`** (`rows`, `avatar`, `lines`), **`CardSkeleton`** (`count`, `variant` stat|content) — โครงระหว่างโหลดสำเร็จรูปสำหรับ `loading.tsx` มี `role="status"` + "กำลังโหลด…" ความกว้างสลับแบบคงที่ (ไม่ random → ไม่ hydration mismatch)
610
+ - **`Notice`**: `icon` (true = ไอคอนตามโทน หรือส่งเอง) และ `dismissible` + `onDismiss` (ปุ่ม X อ่าน `UiText.close`) — ไม่ส่ง = หน้าตาเดิม; ไฟล์เป็น `"use client"` แล้ว (ใช้จาก server component ได้เหมือนเดิม)
611
+ - **`AuthLayout`**: `size?: "sm" | "md" | "lg"` ความกว้างการ์ด (ค่าเริ่มต้น sm = เดิม) — หน้า consent/เลือกองค์กรของ Pi Auth ใช้ md (ขอจาก Pi Auth)
612
+ - `UiText` key ใหม่: `loading`, `presence`, `characterCount`
613
+ - showcase: หน้า `/components/{textarea-field,user-avatars,loading-skeletons}` (125 หน้า), demo `notice` มี icon + dismissible; tests 10 กรณี
614
+
615
+ ## [0.23.0] — 2026-10-02
616
+
617
+ ### Added
618
+
619
+ - `ProviderButtons`: prop `badgePlacement` — ค่าเริ่มต้น `"corner"` = badge (เช่น "ใช้ล่าสุด") เป็นป้ายเล็กทับขอบบนขวาของปุ่มแบบ Clerk "Last used" ไม่กินที่ข้อความ; `"inline"` = ท้ายข้อความแบบ 0.21.0; ปุ่มเต็มกว้าง `py-2` กันป้ายชนบรรทัดแรก — ขอจากเจ้าของผ่าน Pi Auth (0.22.0 ที่ publish ไปไม่มีส่วนนี้)
620
+
621
+ ## [0.22.0] — 2026-10-02
622
+
623
+ ### Added
624
+
625
+ - `ProviderButtons`: prop `text` ต่อ provider (ข้อความเต็มของปุ่มแทน "ดำเนินการต่อด้วย {label}" เช่น ชื่อ SSO ยาว "Estar (Microsoft 365)"); ปุ่มเต็มกว้าง (1 ตัว / `layout="list"`) **ขึ้นบรรทัดใหม่แทนตัดเป็น …** ; ตาราง 2 คอลัมน์ยังตัด แต่ `aria-label`/`title` เป็นข้อความเต็ม — ขอจาก Pi Auth
626
+ - showcase: demo `/components/auth-layout` ปุ่ม SSO ใช้ `text` ยาว
627
+
628
+ ## [0.21.0] — 2026-10-02
629
+
630
+ ตัวสำเร็จรูปของ primitive ใหม่จาก shadcn (0.20.0) — เพิ่มอย่างเดียว
631
+
632
+ ### Added
633
+
634
+ - **`ChatThread`** — หน้าแชตจาก array ข้อความ (แทน MessageScrollerProvider/MessageScroller/Viewport/Content/Item/Button + Message/MessageAvatar/MessageContent/MessageHeader/MessageFooter + Bubble/BubbleContent + Marker + InputGroup รวม 15+ ชิ้นตามตัวอย่างทางการของ shadcn): `messages: { id, from: "me" | "other", content, author?, avatar?, meta? }[]`, เลื่อนตามข้อความใหม่ (ข้อความของเรายึดเป็นจุดเลื่อน), ปุ่ม "ไปข้อความล่าสุด", `typing` (แถว "กำลังพิมพ์…" ด้วย Marker), `onSend` = มีช่องพิมพ์ (Enter ส่ง · Shift+Enter ขึ้นบรรทัด · **ไม่ส่งระหว่างประกอบคำด้วย IME** แบบที่ PDF ทำ `submitOnEnter`), คืน Promise = ปุ่มหมุนแล้วล้างช่องเมื่อสำเร็จ, `empty`, `disabled`
635
+ - **`SimpleQuestionnaire`** — แบบสอบถามทีละข้อจาก array คำถาม (แทน Questionnaire 15 ชิ้น): `{ name, title, description?, required?, multiple?, choices?: { value, label, description?, disabled? }[], input?: boolean | placeholder }`, คีย์ลัดตัวเลข/ตัวอักษร, ข้อบังคับไม่มีปุ่มข้าม, `onSubmit(values)` ได้ object (ข้อเดียว = string, หลายข้อ = string[], ข้อที่ข้ามไม่มี key) ไม่ต้องอ่าน FormData เอง
636
+ - **`AuthLayout`** — หน้าเข้าสู่ระบบ/สมัคร/ยืนยันตัวตนสำเร็จรูปแบบ Clerk (server component ได้): `logo`, `title` (h1), `description`, เนื้อ, `cardFooter` (แถบล่างการ์ดพื้นต่างสี เช่น "ยังไม่มีบัญชี? สมัคร"), `footer` (ใต้การ์ด เช่น "ยืนยันตัวตนโดย Pi Auth · ข้อกำหนด"), `actions` (มุมขวาบน ธีม/ภาษา), `variant` centered (พื้นลายจุดจาง) | split (ฝั่งซ้าย `aside` สีแบรนด์ แบบ shadcn login blocks — มือถือซ่อน), `brandColor`/`brandForeground` (สีแบรนด์ของ tenant), `fullScreen`
637
+ - **`ProviderButtons`** (Google/Microsoft/SSO: 1 ตัว = "ดำเนินการต่อด้วย …" เต็มกว้าง, 2+ ตัว = ตาราง 2 คอลัมน์, `badge` เช่น "ใช้ล่าสุด", `href` หรือ `onClick`, `pendingId` กันกดซ้ำ), **`AuthDivider`** ("หรือ"), **`PasswordInput`** (ปุ่มแสดง/ซ่อน + `aria-pressed`, `autoComplete` ค่าเริ่มต้น `current-password` ตามแนวทาง web.dev)
638
+ - showcase: หน้า `/login` เปลี่ยนเป็น `AuthLayout` (โลโก้, ปุ่ม "ดำเนินการต่อด้วย Pi Auth", แถบล่าง, "ยืนยันตัวตนโดย Pi Auth", ปุ่มธีม) + หน้า `/components/auth-layout` (ฟอร์มเต็มทั้งสองแบบ)
639
+ - `UiText` key ใหม่: `questionnaire` (ปุ่ม/ความคืบหน้า "ข้อ {current} จาก {total}"), `chat`, `auth`
640
+ - showcase: หน้า `/components/{chat-thread,simple-questionnaire,marker}` (122 หน้ารวม auth-layout); tests 7 กรณี (รวม 192)
641
+
642
+ ## [0.20.1] — 2026-10-01
643
+
644
+ ### Fixed
645
+
646
+ - **`confirm.requireText` ถูกทิ้งเงียบ ๆ** (รายงานจาก Pi Config): `RowActions`, `ConfirmActionButton` และ `ActionMenu` ส่งให้ `ConfirmDialog` แค่ title/description/label/destructive — ใส่ `confirm: { requireText: slug }` แล้วลบได้ด้วยคลิกเดียวทั้งที่ `ConfirmSpec` ประกาศว่ารองรับ → ส่ง `requireText` + `requireTextLabel` ต่อครบทั้งสามที่; tests 2 กรณี (รวม 185)
647
+
648
+ ## [0.20.0] — 2026-10-01
649
+
650
+ ขั้น "layout" ของแผนรวม UI ทุกแอป (ดู `todo/todo.md`) — เพิ่มอย่างเดียว ไม่มี prop เดิมเปลี่ยนความหมาย
651
+
652
+ ### Added
653
+
654
+ - **`MobileTabBar`** (`/next`) + **`AppShell mobileTabs`** — แถบเมนูล่างของมือถือ 3–5 แท็บ (ยกจาก DueBell `MobileNav`): ซ่อนตั้งแต่ `md`, แท็บสูง ≥ 56px, `badge` เป็น `CountBadge` บนไอคอน (รวมใน aria-label), `onClick` แทนการไปหน้า, ไฮไลต์ตาม path (`/` ตรงตัว, อื่น ๆ prefix หรือ `exact`); `icon` เป็น **element** (`<Home />`) จึงส่งจาก server layout ได้เหมือน `AppNav`; `AppShell` ทั้ง layout sidebar/top เว้นที่ใต้เนื้อหาให้เอง
655
+ - utility **`pb-safe`** (safe-area ล่างของ iPhone) และ **`pb-tab-bar`** (เว้นที่ให้พ้นแถบล่าง) ใน `styles.css`
656
+ - **`SettingsLayout`** — หน้าตั้งค่าหลายหัวข้อ (ยกจาก DueBell master-detail + PDF ค้นหาได้): จอกว้างสารบัญซ้าย sticky + เนื้อหา, มือถือรายการหัวข้อ → หน้าหัวข้อ + ปุ่ม "กลับ" (ย้ายโฟกัสไปหัวข้อให้ screen reader), `searchable` ค้น label/description/`keywords` (ไทย+อังกฤษ), `badge` ท้ายรายการ, คุมเองด้วย `value`/`onValueChange` หรือ `initialSection`
657
+ - **`FilterBar`** — แถบค้นหา + select ตัวกรองเหนือรายการ client-side (Pi Auth เขียนซ้ำ 14 หน้า): ปุ่ม "ล้างตัวกรอง" โผล่เมื่อมีค่าค้าง (เทียบกับ `allValue` ของแต่ละตัว) และตั้งกลับให้เองถ้าไม่ส่ง `onClear`; ตัวกรองแบบอื่นใส่เป็น `children` + `active`
658
+ - **`SetupChecklist`** — รายการ "ต้องตั้งอะไรให้ครบ" (ยกจาก Pi Auth `SetupSteps`): ✓ เมื่อทำแล้ว, ขั้นบังคับถัดไปถูกเน้น (`aria-current="step"`), ขั้น `optional` ไม่นับ, ครบแล้วซ่อนทั้งกล่อง (`hideWhenComplete`); `action` รับ `{ current, done }` ให้แอปส่ง `LinkButton` เอง (barrel ไม่พึ่ง next)
659
+ - **`MonthStepper`** + **`useMonthCursor`** — เลือกเดือน ‹ เดือน › กดชื่อเดือนกลับเดือนนี้, `min`/`max`, `locale` (`th-TH` = พ.ศ., `th-TH-u-ca-gregory` = ค.ศ.), `format` long|short, `formatLabel` (ยกจาก DueBell `MonthSelector`)
660
+ - **`lib/month.ts`** (barrel): `MonthKey` (`"YYYY-MM"` แบบเดียวกับ `CalendarMonthGrid`), `parseMonthKey`, `isMonthKey`, `toMonthKey`, `currentMonthKey`, `shiftMonth` (ข้ามปีให้), `compareMonth`, `formatMonthKey` — แทน `CycleMonth` ของ DueBell
661
+ - `UiText` key ใหม่ (optional ทั้งหมด มีค่าไทย/อังกฤษใน `UI_TEXT_TH`/`UI_TEXT_EN`): `month`, `setup`, `filters`, `settings`, `mobileTabs`
662
+ - **ชุด component สำเร็จรูป (Simple\*)** — ทิศทางของแพ็กเกจ: แอปเขียน component เดียว + props แทนการประกอบ primitive หลายชิ้นซ้อนกันแบบ shadcn (ดู `docs/agent-rules.md` "ใช้ตัวสำเร็จรูปก่อน"):
663
+ - `SimpleDialog` (แทน Dialog 7 ชิ้น) / `SimpleSheet` (+ `side`) — trigger, title, description, children, `footer` (ReactNode หรือ `(close) => …`), `size`, controlled ได้; **ปุ่ม X อ่าน `UiText.close`** แก้ปัญหา "Close" แปลภาษาไม่ได้โดยไม่แตะไฟล์ shadcn
664
+ - `SimpleTabs` (items: value/label/content/icon/badge/disabled, `variant` default|line, `fullWidth`) / `SimpleAccordion` (`multiple`, `initialOpen`)
665
+ - `SimpleTooltip` (ห่อ element เดียว, content ว่าง = ไม่มี tooltip) / `SimplePopover` (trigger + title/description + เนื้อหา)
666
+ - `ActionMenu` — เมนูคำสั่งจาก array: icon, shortcut, separator, label, href/external, destructive, **`confirm` เปิด `ConfirmDialog` ก่อนเรียก onClick**; trigger ไม่ส่ง = ปุ่ม …
667
+ - `SimplePagination` + `paginationRange` — page/pageCount/onPageChange, จำนวนปุ่มคงที่, มือถือเหลือ "x / y"
668
+ - `SimpleTable<T>` — columns (key/header/cell/align/className) + rows + getRowKey + empty + onRowClick; ค่าจาก key ตรง ๆ ถ้าไม่ส่ง cell
669
+ - `SegmentedControl<T>` — เลือกทีละหนึ่งและต้องมีค่าเสมอ (กดตัวเดิมไม่หลุด), `count`, `icon`, `size`, `fullWidth`
670
+ - `UiText` key ใหม่: `close`, `pagination`
671
+ - **shadcn/ui base-nova ล่าสุดทั้งชุด** (`shadcn add --all --overwrite`, CLI 4.21.0): primitive ใหม่ `bubble`, `marker`, `message`, `message-scroller`, `questionnaire`, `direction` (export ใน barrel) และ `toast` (Base UI — **ไม่อยู่ใน barrel** เพราะชื่อ `toast`/`Toaster` ชนกับ sonner ที่แพ็กเกจใช้; import จาก `@suphark/ui/components/ui/toast`); dep ใหม่ `@shadcn/react`
672
+ - ไฟล์เดิมได้ class/ลำดับจาก upstream (ส่วนใหญ่เรียง class ใหม่) + sidebar deps ของ useEffect/useMemo ครบขึ้น
673
+ - **คงของที่แก้ไว้เอง** (ติดมาจาก supplychain ตั้งแต่ 0.1.0 — upstream ยังไม่มี): `scroll-area.tsx` (`data-[orientation=…]` ของ Base UI — ไม่งั้นแถบเลื่อนกว้าง 0 มองไม่เห็นทั้งแอป), `slider.tsx`, `tabs.tsx` (orientation แบบเดียวกัน), `table.tsx` (prop `containerClassName`)
674
+ - CLI รุ่นนี้เขียน `import { cn } from "cn"` — เปลี่ยนกลับเป็น `@suphark/ui/lib/utils` ทุกไฟล์ เพราะ `cn` ของแพ็กเกจรู้จักคลาสตามบทบาท (`text-body` ฯลฯ, 0.13.1) ส่วน `cn` ทั่วไปจะลบทิ้งเงียบ ๆ
675
+ - `hooks/use-mobile.ts` (CLI เขียนทับ) เติม `"use client"` คืน; biome ปิด `noDoubleEquals` เฉพาะ `components/ui/**` (`field.tsx` ของ upstream ใช้ `==`)
676
+ - showcase: หน้า `/components/{month-stepper,filter-bar,setup-checklist,settings-layout,mobile-tab-bar,simple-dialog,simple-sheet,simple-tabs,simple-accordion,simple-tooltip,simple-popover,action-menu,simple-pagination,simple-table,segmented-control}` (118 หน้า) + `/shell` มี `mobileTabs` ให้ลองบนจอมือถือ; tests 20 กรณีเพิ่ม (รวม 183)
677
+
678
+ ## [0.19.0] — 2026-09-30
679
+
680
+ ปิดรายการ "component เล็กที่ใช้ร่วม" ของแผนรวม UI ทุกแอป (ดู `todo/todo.md`) — เพิ่มอย่างเดียว ไม่มี prop เดิมเปลี่ยนความหมาย
681
+
682
+ ### Added
683
+
684
+ - **`Stat` เพิ่ม `icon` / `badge` / `delta` / `tone`** + **`StatGrid`** (Pi Auth มี 17 การ์ดแบบนี้, DueBell มี DeltaBadge เอง): `delta` = `{ value, direction: "up"|"down"|"flat", tone?, hint? }` เลือกไอคอนเทรนด์และสีให้ (ขึ้น = success, ลง = destructive — ทับด้วย `tone` ได้เพราะบางตัวชี้วัด "ลง" คือดี), `icon` มีกรอบพื้นตามโทน, `tone` เปลี่ยนสีตัวเลข (`default|success|warning|destructive|info`); `StatGrid` คอลัมน์ 2|3|4 (มือถือ 1 → sm 2)
685
+ - **`SettingsRow`** / **`SwitchRow`** / **`DangerZone`** (barrel) — แถวตั้งค่า: ชื่อ+คำอธิบายซ้าย control ขวา (จอแคบตกลงมา), `htmlFor` ผูกชื่อกับ control, `tone="destructive"`; `SwitchRow` = แถว + `Switch` ที่ผูก id ให้แล้ว (คลิกชื่อแล้วสลับ) รับ props ของ `Switch` ทั้งหมด; `DangerZone` = กรอบ `border-destructive/40` + พื้น subtle + เส้นคั่นแถว (Pi Auth/Pi PDF เขียน layout นี้ซ้ำ)
686
+ - **`ConfirmDialog` / `useConfirm` `requireText`** — ต้องพิมพ์ข้อความให้ตรงก่อนปุ่มยืนยันจะกดได้ (ลบของที่กู้ไม่ได้ แบบ GitHub); `requireTextLabel` ทับข้อความเหนือช่อง, ค่าเริ่มต้นจาก `UiText.confirmTypeToConfirm`/`…Suffix` (key ใหม่); ปิดกล่องแล้วล้างสิ่งที่พิมพ์เสมอ (ยืนยันรายการถัดไปทันทีไม่ได้) — ใช้ผ่าน `confirm({ requireText: app.name, destructive: true })` ได้เลย
687
+ - **`UserIdentity`** (barrel) — avatar (`Initials` สีพื้นคงที่ต่อชื่อ) + ชื่อ + อีเมล + `badge`/`hint`, `size` sm|default, `hideAvatar`; **ไม่มีชื่อ = ใช้อีเมลเป็นบรรทัดหลัก** (ผู้ใช้ที่ยังไม่ตั้งชื่อ) — Pi Auth เขียนซ้ำ ~15 จุด
688
+ - **`CountBadge`** (barrel) — ป้ายจำนวนท้ายเมนู/แท็บ: เกิน `max` เป็น `99+`, `0` ซ่อนอัตโนมัติ (`showZero` บังคับแสดง), `tone` muted|primary|destructive
689
+ - **`SaveStatus`** (barrel, ยกจาก Pi PDF) — สถานะบันทึกอัตโนมัติบรรทัดเดียว: `saving` (spinner) / `saved` (✓ เขียว) / `error` (`role="alert"`, ข้อความของแอปหรือ `UiText.actionFailed`) / `idle` + `savedAt` = เวลาสัมพัทธ์ผ่าน `ClientDateFormatter` (อัปเดตเองหลัง mount ไม่ทำ hydration mismatch); `language`/`text`
690
+ - **`SliderField`** (barrel, ยกจาก Pi PDF) — ช่องเลื่อน + ป้ายชื่อ + ค่าปัจจุบันมุมขวา: `value`/`onValueChange` เป็น number เดี่ยว, `unit` หรือ `formatValue`, `description`, `size` sm|default, `disabled`
691
+ - **`SkipLink`** (barrel) — ลิงก์ "ข้ามไปเนื้อหา" สำหรับหน้าที่ไม่ได้ใช้ `AppShell` (Pi PDF มีของตัวเอง); `AppShell` เปลี่ยนมาใช้ตัวเดียวกันนี้ภายใน
692
+ - **`FormDialog` `revealSuccess`** — แบบ **ไม่ต้องส่งฟังก์ชัน** จึงใช้จาก **server component** ได้ (รายงานจาก Pi Auth: `renderSuccess` เป็นฟังก์ชัน ข้าม server/client boundary ไม่ได้ → `Functions cannot be passed directly to Client Components`): server action คืน `{ ok: true, data: { values: { "Signing secret": "whsec_…" } } }` แล้ว dialog แสดง `SecretReveal` ให้เอง + ปุ่ม "เสร็จแล้ว" (ไม่ปิดเอง); `successTitle` (ค่าเริ่มต้น `UiText.showOnce`), `successDescription` แทน description ของฟอร์ม — เคส "สร้างแล้วโชว์ secret ครั้งเดียว" (webhook secret / SCIM token / service account key / client secret) ซ้ำกันทุกแอป; `renderSuccess` เดิมยังใช้ได้จาก client component
693
+ - showcase: หน้า `/components/settings-row`, `/components/user-identity` + หน้า `stat` ใหม่ (ไอคอน/delta/StatGrid) — 104 หน้า; tests 9 กรณีเพิ่ม (รวม 163)
694
+
695
+ ## [0.18.0] — 2026-09-30
696
+
697
+ > 0.17.0 ถูก publish ตอนที่ `LanguageSwitch` ยังไม่เข้า (commit ถัดไป) — ของชิ้นนี้จึงออกเป็น 0.18.0
698
+
699
+ ### Added
700
+
701
+ - **`LanguageSwitch`** + **`LanguageMenuItems`** (barrel) — สลับภาษาที่ Pi PDF (`language-selector.tsx`) / Pi Config (`locale-toggle.tsx` + `locale-menu-items.tsx`) / Pi Auth เขียนซ้ำ; แพ็กเกจไม่รู้จัก i18n ของแอป → แอปส่ง `locales[{ value, label, hint?, flag? }]` + `value` + `onValueChange` เอง (เขียน cookie/เรียก server action/`router.refresh()` ฝั่งแอป):
702
+ - `LanguageSwitch` (นอกเมนู เช่น header/หน้าก่อนล็อกอิน — คู่กับ `ThemeToggle`): `mode` `auto` (ค่าเริ่มต้น: ≤2 ภาษา = ปุ่มเดียววนรอบแสดงชื่อภาษาถัดไป, >2 = เมนูพร้อมธง/บรรทัดรอง) / `toggle` / `menu`, `showLabel` (menu = รหัสภาษาตัวใหญ่, false = ปุ่มไอคอน), `label` (ค่าเริ่มต้น **`UiText.language.label`** — key ใหม่), `align`, props ของ `Button` ส่งผ่านหมด; `locales` ว่าง = ไม่ render
703
+ - `LanguageMenuItems` (ในเมนูผู้ใช้ — คู่กับ `ThemeMenuItems`): `DropdownMenuRadioGroup` + หัวกลุ่มจาก `UiText.language`, `disabled` ปิดระหว่าง pending
704
+ - `useCopyToClipboard`: `copyToClipboard(text, key?, labels?)` — ทับข้อความ toast เฉพาะครั้งนั้นได้ (ข้อสังเกตจาก Pi Config: `RevealedValues` มีข้อความต่างกันทุกปุ่ม "คัดลอกคีย์ X / ค่าของ X / ทั้งชุด N ค่า" แต่เดิม hook รับข้อความเดียวตอนสร้าง จึงต้องปิด toast แล้วเรียก `toast.success()` เอง)
705
+ - `SimpleSelect`: **`emptyValue`** — บอกว่า option ไหนหมายถึง "ไม่เลือก/ไม่มี" แล้วคอมโพเนนต์ส่ง **ค่าว่าง** ใน FormData แทน sentinel (Base UI ไม่รับ `value=""`) — เดิม `name` ใช้ได้เฉพาะเคสที่ค่าที่เลือก = ค่าที่ส่ง 1:1 แอปที่มี "ไม่มีกลุ่ม" ต้องทำ hidden input แปลงค่าเอง (รายงานจาก Pi Config `SectionSelect`: `__none__` หลุดเข้า server action); ใช้คู่ `required` ไม่ได้ (hidden input ไม่เข้า constraint validation) ให้ตรวจที่ schema
706
+ - `SearchInput`: ส่ง `aria-label` ตรง ๆ ได้แล้ว (เดิมคอมโพเนนต์เขียนทับเงียบ ๆ หลัง `{...props}` ต้องใช้ `label` เท่านั้น — รายงานจาก Pi PDF)
707
+ - showcase: หน้า `/components/language-switch` — 102 หน้า; tests 8 กรณีเพิ่ม (รวม 154)
708
+
709
+ ## [0.17.0] — 2026-09-30
710
+
711
+ ### Added
712
+
713
+ - **`CopyButton`** (barrel) — ปุ่มคัดลอกข้อความ: ไอคอน Copy → ✓ (`text-success`) ชั่วครู่แล้วกลับ, ใช้ `useCopyToClipboard` (มี fallback `execCommand` สำหรับ webview ที่บล็อก clipboard API) แอปไม่ต้องเขียน state เอง (Pi Auth เขียนซ้ำ 12 ไฟล์, Pi Config ใน `RevealedValues`): `value`, `children` (ไม่ส่ง = ปุ่มไอคอนล้วน), `label`/`copiedLabel` (ค่าเริ่มต้น `UiText.copy`/`copied` — เป็น aria-label เมื่อไม่มี children และเป็น `title` เสมอ), `toast` (ค่าเริ่มต้น **false** เพราะปุ่มแสดง ✓ อยู่แล้ว — ส่ง `true` หรือข้อความของแอปได้), `timeout`, `onCopied`, props อื่นของ `Button` (variant/size/className) ส่งผ่านหมด
714
+ - **`CodeBlock`** (barrel, สเปกจาก Pi Config — ของเดิมอยู่ `features/docs/code-block.tsx` ใช้ 2 ไฟล์; Pi Auth มีแบบเดียวกัน ~9 จุด) — บล็อกโค้ด/คำสั่ง: หัวแถบ `title ?? lang ?? "shell"` (mono, `title={false}` = ไม่มีหัวแถบ) + `CopyButton`, เนื้อเป็น `<pre><code>` mono เลื่อนแนวนอน; `copyText` เมื่อคัดลอกไม่เท่ากับที่แสดง (เช่นคัดลอกเฉพาะค่า token ไม่เอา `KEY=`), `copyLabel`, `showCopy`, `wrap` (ตัดบรรทัดแทนเลื่อน), `maxHeightClassName`; ไม่มี syntax highlight โดยตั้งใจ (ไม่เพิ่ม dependency)
715
+ - `SearchInput`: ใช้แบบ **uncontrolled** ได้แล้ว — ไม่ส่ง `value` = คุมค่าในตัวเองจาก `defaultValue` (ปุ่มล้าง/Escape ยังทำงาน) จึงวางใน `<form>` ที่ submit ไป server action/URL param พร้อม `name` ได้ (คำถามจาก Pi Auth: `/console/logs`, `/platform/users`); `onValueChange` ยังเรียกทุกตัวอักษรทั้งสองโหมด — **ไม่มี debounce ในตัว** (ให้แอป debounce เอง) จดไว้ใน docstring แล้ว
716
+ - showcase: หน้า `/components/copy-button`, `/components/code-block` — 101 หน้า; tests 5 กรณีเพิ่ม (รวม 146)
717
+ - test: `date-picker.test.tsx` ตั้ง timeout 20s (เปิด popup = render ปฏิทินทั้งเดือนใน jsdom ช้าเป็นครั้งคราวเกิน 5s ตอนรันทั้งชุด — ทำให้ `pnpm verify`/CI แดงสุ่ม ๆ)
718
+
719
+ ## [0.16.0] — 2026-09-30
720
+
721
+ ### Added
722
+
723
+ - **`SearchInput`** (barrel) — ช่องค้นหามาตรฐานที่ Pi Auth (~15 จุด) / Pi PDF (5) / DueBell (8) เขียนซ้ำ: ไอคอนแว่นขยายซ้าย, ปุ่มล้าง (X) ขวาเมื่อมีข้อความ (กดแล้ว focus กลับที่ช่อง), **Escape ล้างค่า** (`clearOnEscape`), `type="search"` แต่ซ่อนปุ่ม X ของ WebKit; controlled ล้วน `value` + `onValueChange(next)` (แอปคุม state/debounce เอง), `onClear`, `size` xs|sm|default (h-7/h-8/h-9), `label`/`clearLabel`/`placeholder` (ค่าเริ่มต้นจาก **`UiText.search`** `{ placeholder, label, clear }` — key ใหม่), `groupClassName` (กรอบ) แยกจาก `className` (ตัว `<input>`), ส่ง `ref` ถึง input ได้; props อื่นของ `<input>` ส่งผ่านหมด
724
+ - showcase: หน้า `/components/search-input`; **เมนูซ้ายของ `/components` มีช่องค้นหาแล้ว** — กรองตามชื่อ/slug/ชื่อ export/คำอธิบาย, บอกจำนวนที่พบ (`aria-live`), กด `/` ที่ไหนก็ได้เพื่อโฟกัสช่องค้นหา (ไม่ดักเมื่อกำลังพิมพ์ในช่องอื่น)
725
+
726
+ ## [0.15.0] — 2026-09-30
727
+
728
+ ### Added
729
+
730
+ - **`InfoHint`** (ขอจาก Pi Auth): ไอคอน (i) ท้ายหัวข้อ ที่เก็บคำอธิบายยาว ๆ ไว้ใน tooltip แทนการวาง
731
+ เป็น `description` ใต้หัวข้อถาวร — ใช้กับคำอธิบายที่คนซึ่งรู้อยู่แล้วไม่ต้องอ่านซ้ำทุกครั้ง
732
+ props: `children` (เนื้อหา), `label` (ชื่อที่ screen reader อ่าน — บังคับ), `side`, `className`
733
+ trigger เป็น **ปุ่มจริง** ไม่ใช่ `<span>` จึงเปิดได้ด้วยคีย์บอร์ด (Tab) และจอสัมผัส ไม่ใช่ hover อย่างเดียว
734
+ - showcase: หน้า component `/components/info-hint`
735
+
736
+ ## [0.14.0] — 2026-09-29
737
+
738
+ > **0.13.0 และ 0.13.1 ถูก publish ขึ้น npm เมื่อ 2026-09-25 จากโค้ดที่ไม่เคยเข้า git** (สเกลตัวอักษรตามบทบาท ขอจาก Pi Done) — รุ่นนี้กู้โค้ดจาก tarball 0.13.1 กลับเข้า `main` แล้ว (3-way merge กับ 0.12.0) บันทึกของสองรุ่นนั้นอยู่ข้างล่างตามที่ publish จริง
739
+
740
+ รุ่นแรกของแผนรวม UI จากทุกแอป (สำรวจ Pi Auth / DueBell / Pi PDF / Pi Config — ดู `todo/todo.md`) — เพิ่มอย่างเดียว ไม่มี prop ถูกเปลี่ยนความหมาย
741
+
742
+ ### Added
743
+
744
+ - **token สีโทนใน `styles.css`**: `--info` / `--info-foreground` (`bg-info`, `text-info`) และพื้นอ่อนครบ 4 โทน `--{info|success|warning|destructive}-subtle` / `-subtle-foreground` / `-border` (`bg-info-subtle text-info-subtle-foreground border-info-border` …) สลับ dark ให้เอง — แทน `sky-*` ที่ PDF ใช้ ~20 จุด และ `--info` ที่ DueBell ตั้งเอง (ถ้าแอปตั้ง `--info` ไว้เองให้ลบออก ค่าของแพ็กเกจ = sky-600 / dark sky-400)
745
+ - `EmptyState`: `variant` (`dashed` ค่าเริ่มต้น · `card` · `plain`), `size="compact"`, `tone="destructive"`, `secondaryAction` (ขอจาก PDF ที่มีสำเนาของตัวเอง)
746
+ - `CheckboxField`: `hint` — คำอธิบายใต้ label (Config เขียน `<p>` ต่อท้ายเอง ~8 จุด)
747
+ - `SimpleSelect`: `name` + `required` — ส่งค่าใน FormData ของ server action ได้ (Config เคยใช้ hidden input เอง)
748
+ - `FormDialog`: `renderSuccess(state)` — สำเร็จแล้วแสดงผลแทนฟอร์ม (secret/token ที่ออกครั้งเดียว) พร้อมปุ่ม "เสร็จแล้ว"; Pi Auth `SecretDialog` ใช้แทนได้
749
+ - `ConfirmActionButton`: `redirectTo` — สำเร็จแล้ว `router.push` แทน refresh (Pi Auth `uninstall-button.tsx` ใช้แทนได้)
750
+ - `useCopyToClipboard({ toast, timeout })`: คืน `copiedKey` (`copyToClipboard(text, key)` — หลายปุ่มในหน้าเดียวรู้ว่าปุ่มไหนเพิ่งคัดลอก), `toast: false` หรือข้อความของแอป (แปลภาษาได้), ใช้ `copyText` ที่มี fallback; `copyToClipboard` คืน `Promise<boolean>` — ของเดิม (`isCopied`, `copyToClipboard(text)`) ใช้ได้เหมือนเดิม (Pi Auth เขียน copy เอง 12 ไฟล์)
751
+ - `formatBytes(bytes, locale, { minUnit })` — บังคับหน่วยเล็กสุด เช่น `"MB"` (PDF มีสำเนาเพราะต้องการแสดงเป็น MB)
752
+
753
+ ### Changed
754
+
755
+ - `Notice` / `StatusBadge` ใช้ token โทนแทน palette ดิบ (ถอด `ui-audit-ok-file`) — สีเท่าเดิม ยกเว้นตัวอักษรของ `Notice` จาก 900→800 (dark 100→200) ให้ตรงกับ `StatusBadge`
756
+ - `FormDialog`: state ของ action อยู่ในเนื้อ dialog — ปิดแล้วเปิดใหม่ได้ฟอร์มรอบใหม่เสมอ (เดิม `keepOpen` เปิดใหม่ยังเห็นผลรอบก่อน และ state ที่มี secret จะถูกส่งเป็น `prev` ไปกับ submit ถัดไป)
757
+ - `useCopyToClipboard`: ไม่มี `navigator.clipboard` แล้วลอง fallback แทนการเตือน "Clipboard not supported"
758
+
759
+ ### ตรวจแล้วไม่แก้
760
+
761
+ - hydration mismatch ของ trigger ที่ render ผ่าน Button (PDF รายงาน, ห่อ wrapper `slot-props.ts`): showcase ใช้ `render={<Button/>}` กับ Dialog/Sheet/Popover/DropdownMenu/Tooltip ไม่มี hydration error ใน dev — น่าจะมาจาก wrapper `asChild` ของ PDF เอง ให้ PDF ลองเปลี่ยนเป็น `render=` ตรง ๆ ก่อน
762
+ - ข้อความ sr-only "Close" ใน `DialogContent`/`SheetContent` ยังแปลไม่ได้ — อยู่ในไฟล์ shadcn (ห้ามแก้มือ) รอตัดสินใจ
763
+
764
+ ### Fixed
765
+
766
+ - showcase: เมนู "ออกจากระบบ" ที่หัวเว็บดูเหมือนกดไม่ได้ — ออกแล้วกลับหน้าเดิม middleware ส่งไปล็อกอิน และ Pi Auth ยังมี session จึงล็อกอินกลับทันที → `UserButton afterSignOutUrl="/login"` จอดหน้าเข้าสู่ระบบ
767
+
768
+ ## [0.13.1] - 2026-09-25
769
+
770
+ ### Fixed
771
+
772
+ - **`cn()` ลบคลาสตามบทบาททิ้งเงียบ ๆ** (พบตอน Pi Done ย้ายไปใช้ 0.13.0): tailwind-merge รู้จักเฉพาะ
773
+ `text-xs … text-9xl` ว่าเป็นขนาดตัวอักษร `text-body`/`text-caption` จึงถูกเดาว่าเป็น "สี" แล้วโดน
774
+ `text-foreground/80` หรือ `text-muted-foreground` ที่อยู่ในสตริงเดียวกันลบทิ้ง — build ผ่าน ไม่มี
775
+ error ใด ๆ แต่ตัวอักษรกลับไปใช้ขนาดที่สืบทอดมา (ชื่องานใน Pi Done ควรเป็น 16px บนมือถือ กลายเป็น
776
+ ค่า root) ตอนนี้ `cn()` ใช้ `extendTailwindMerge` ประกาศคลาสทั้ง 6 ไว้ในกลุ่ม `font-size` จาก
777
+ `TYPOGRAPHY_ROLES` โดยตรง เพิ่มบทบาทใหม่ในอนาคตจึงไม่ต้องแก้ที่นี่อีก
778
+
779
+ ## [0.13.0] — 2026-09-25
780
+
781
+ ### Added
782
+
783
+ - **สเกลตัวอักษรตามบทบาท + ตอบสนองจอ** (ขอจาก Pi Done — บนมือถืออ่านเล็กเกินไป): คลาสใหม่ `text-title-lg` `text-title` `text-subtitle` `text-body` `text-caption` `text-label` ค่าเป็น **mobile-first แล้วแน่นขึ้นบนจอ ≥ 48rem** (body 16→15px, caption 14→13px, subtitle 17→16px) — เหตุผลเดียวกับที่ iOS ใช้ body 17px ส่วน macOS ใช้ 13px: มือถือถือห่างและใช้นิ้ว เดสก์ท็อปอ่านใกล้และชี้ด้วยเมาส์
784
+ - ทำได้โดย `@theme` ชี้ `--text-*` ไปที่ `--fs-*` แล้วเปลี่ยน `--fs-*` ใน media query — utility ยังเป็นคลาสเดียวและยังใช้ variant ได้ (`md:text-body`); **ไม่แตะ root font-size** ซึ่งจะทับค่าที่ผู้ใช้ตั้งไว้ในเบราว์เซอร์
785
+ - `text-xs … text-2xl` ยังใช้ได้เหมือนเดิม ไม่ใช่ breaking — แต่ `text-xs` (12px) ไม่ควรเป็นขนาดเนื้อหาอีกต่อไป
786
+ - `TYPOGRAPHY_ROLES` export จาก `@suphark/ui` เป็นแหล่งความจริงเดียวของตาราง (showcase `/design-system#tokens-typography` render จากค่านี้ ไม่เขียนซ้ำ)
787
+ - **ความสูงขั้นต่ำของรายการเมนูบนอุปกรณ์สัมผัส**: `@media (pointer: coarse)` ตั้ง `min-height: 44px` ให้ `dropdown-menu-item` / `checkbox-item` / `radio-item` / `sub-trigger` / `select-item` / `command-item` — เดิมไม่มีความสูงเลย (`py-1` ≈ 26px) ต่ำกว่าเกณฑ์ Apple 44pt / Material 48dp; ใช้ `pointer: coarse` ไม่ใช่ความกว้างจอ เพราะหน้าต่างแคบบนเดสก์ท็อปยังใช้เมาส์
788
+ - audit: กฎ `raw-text-scale` (warning, opt-in ด้วย `--semantic-text`) จับ `text-xs/text-sm/…` ที่ยังไม่ย้ายไป role class — ให้แอปทยอยย้ายทีละส่วนแล้วเปิดกฎกันถอยหลัง
789
+
790
+ ### Fixed
791
+
792
+ - **`arbitrary-text` ของ audit ไม่เคยจับ `text-[0.8rem]`**: regex เดิม `\d+` ตามด้วยหน่วยทันที จึงข้ามค่าทศนิยมทั้งหมด (ทั้งใน `audit.mjs` และ `audit-tailwind-typography.mjs`) — แก้เป็น `\d*\.?\d+` แล้วพบของค้าง 15 จุดที่ผ่าน CI เขียวมาตลอด
793
+ - แทนขนาด arbitrary 12 จุดด้วย role class: `text-[0.7rem]`/`text-[0.625rem]`/`text-[0.6rem]` → `text-label` (HeaderCrumb badge, SubNav, TopTabs, IconPicker, Initials, Stepper, SecretReveal, PrimarySecondaryText, showcase site-header) และข้อความ error ของ `FormFieldInput` → `text-caption` (บนมือถือขึ้นเป็น 14px อ่านออกและพ้นเกณฑ์ iOS zoom)
794
+ - ที่ยังเหลือคือใน `components/ui/**` (`button.tsx` `toggle.tsx` `calendar.tsx` = `text-[0.8rem]`) ซึ่งเป็นไฟล์ที่ shadcn generate — ห้ามแก้มือ รอรอบ regenerate
795
+
796
+ ## [0.12.0] — 2026-09-23
797
+
798
+ ### Added
799
+
800
+ - **`OrganizationSwitcher` prop `itemAction(org)`** (ขอจาก Pi Auth) — ปุ่มท้ายแถวของแต่ละองค์กรในเมนู เช่น ⚙ "ตั้งค่าองค์กร" แบบ Vercel/Clerk: แอปส่ง element ที่กดได้มาเอง (พร้อม aria-label/title) component ห่อให้และ **หยุด click/keydown ไม่ให้สลับองค์กร**; คืน `null` = แถวนั้นไม่มีปุ่ม; มี `itemAction` แล้วองค์กรเดียวก็ยังมีเมนู (เดิมยุบเป็นป้ายเฉย ๆ เมื่อไม่มี `footer`) — เดิมแอปต้องวาง "ตั้งค่าองค์กร" เป็นรายการแยกใน `footer`
801
+ - **`OrganizationSwitcher` prop `selectableActive`** (ขอจาก Pi Auth) — แถวที่ active กดได้ด้วย เรียก `onSwitch` เหมือนแถวอื่น (ค่าเริ่มต้น false = คงพฤติกรรมเดิม กดแล้วไม่ทำอะไร); สำหรับเมนูที่ "เลือก" = ไปหน้าของสิ่งนั้นไม่ใช่แค่สลับ เช่น crumb บัญชีของ Pi Auth ที่กดชื่อบัญชีแล้วเข้าคอนโซลบัญชีนั้น — ตัดรายการ "คอนโซลบัญชี" ท้ายเมนูที่ซ้ำช่องทางออกได้
802
+ - showcase: หน้า `organization-switcher` ตัวแรกใส่ ⚙ ท้ายแถว; tests 2 กรณี (รวม 130)
803
+
804
+ ## [0.11.2] — 2026-09-22
805
+
806
+ ### Fixed
807
+
808
+ - **`HeaderCrumbs` บนจอแคบใช้ย้อนกลับ/สลับไม่ได้** (ผู้ใช้ชี้ใน `/shell-top` มือถือ): เดิมซ่อน crumb ก่อนสุดท้ายทั้งก้อนแต่ยังเห็นตัวคั่น `/ /` และไม่มีทางเข้าถึงตัวสลับองค์กร/แอป → บน `< sm` ยุบเป็นปุ่ม `…` (aria-label `collapsedLabel` ค่าเริ่มต้น "แสดงเส้นทางทั้งหมด") เปิด `Popover` แสดง crumb ที่ซ่อนเรียงลงมา — แต่ละตัวยังเป็น switcher ของมันเอง (DropdownMenu ซ้อนใน Popover ทำงานได้ ทดสอบสลับองค์กรใน popover แล้ว); ตัวคั่นซ่อนตามตัวที่ซ่อน
809
+ - `HeaderCrumb`: `overflow-hidden` + label `min-w-0` และ wrapper ตัวสุดท้ายบังคับ `max-w-full` — ตัวสุดท้ายหดตามช่องแทนบานทับ `header.actions`; `badge` ซ่อนบนจอแคบ (`hidden sm:inline`) ให้ที่กับชื่อ
810
+ - `SubNav`: key ของกลุ่มใช้ชื่อกลุ่มแทน index (biome `noArrayIndexKey` — suppression เดิมหลุดตอน 0.11.1 จัดรูปแบบใหม่ ทำให้ `biome ci` ใน CI ไม่ผ่าน)
811
+ - showcase: `/shell-top` header actions ซ่อน LayoutSwitch + กระดิ่งบนจอแคบ (ก่อนหน้ากินที่จน crumb เหลือ 25px); tests 2 กรณี (รวม 128)
812
+
813
+ ## [0.11.1] — 2026-09-22
814
+
815
+ ### Changed
816
+
817
+ - showcase: เพิ่มหน้า stub ให้ลิงก์เมนู mock ที่ไม่มีหน้าจริง (`/shell-top/roles`, `/shell-top/security`, `/shell/bidding`, `/shell/orgs`) — Next prefetch เคย 404 ใน console ทุกครั้งที่เข้า `/shell`, `/shell-top`
818
+
819
+ - `SubNav` (AppShell layout top): หัวกลุ่มกับรายการเคยเป็นสี muted เท่ากันจนแยกไม่ออก (ผู้ใช้ Pi Auth ชี้ในหน้า Settings) → หัวกลุ่มเป็น `text-[0.7rem] font-semibold uppercase tracking-wider text-muted-foreground/70`, รายการที่ไม่ active เป็น `text-foreground/80` (hover `text-foreground`), กลุ่มที่ 2 ขึ้นไปมีเส้นคั่น `border-t border-border/60` + `pt-3`; active และ `<select>` บนจอแคบคงเดิม — ไม่มี prop ใหม่ (ขอจาก Pi Auth)
820
+
821
+ ## [0.11.0] — 2026-09-21
822
+
823
+ ### Added
824
+
825
+ - `AppShell` (layout sidebar): slot **`sidebarContentBefore`** / **`sidebarContentAfter`** วางใน `SidebarContent` ก่อน/หลังเมนู — เลื่อนไปกับเมนู (ต่างจาก `sidebarHeader` ที่ตรึงบน) ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน; สำหรับต้นไม้โฟลเดอร์ + ถังขยะ + มิเตอร์พื้นที่แบบ Google Drive (ขอจาก Pi Files — เดิมต้องยัดใน `sidebarHeader` แล้วจำกัด max-h เอง)
826
+ - showcase: `/shell` ใส่ `TreeView` + `ProgressBar` ใน `sidebarContentAfter` เป็นตัวอย่าง (`app/shell/sidebar-drive.tsx`)
827
+
828
+ ## [0.10.0] — 2026-09-21
829
+
830
+ ### Added
831
+
832
+ - **`SectionNav`** (`@suphark/ui/next`, สเปกจาก Pi Auth — ชิ้นสุดท้ายที่แอปยังประกอบเอง) — หัวหมวด + แถบแท็บย่อยใต้หัวข้อสำหรับ layout ของหมวดที่มีหลายหน้า (directory/access/security/integrations): `title`/`description`/`actions` ส่งต่อ `PageHeader`, `tabs[{ href? | slug?, label, exact?, badge? }]` (href หรือ `${base}/${slug}`), แต่ละแท็บเป็น `NavLink` (`-mb-px border-b-2` ไฮไลต์ `border-primary` ตาม pathname, `aria-current=page`), badge ต่อท้าย label, `navLabel` (ค่าเริ่มต้น `UiText.sectionNav.tabs` — "แท็บย่อย"/"Section tabs"), ไม่มี tabs = ไม่มี nav; ไม่มี state ใช้จาก server component ได้ (เฉพาะแถบ `<nav>` เป็น client component เล็ก ๆ เพื่ออ่าน UiText); types `SectionNavProps`, `SectionNavTab`; `UiText.sectionNav` (optional)
833
+ - showcase: หน้า `section-nav` (แท็บชี้หน้าจริงในหมวดนำทาง — Link prefetch จะไม่ 404) — 97 หน้า; tests 3 กรณี (รวม 126)
834
+
835
+ ## [0.9.0] — 2026-09-21
836
+
837
+ ### Added
838
+
839
+ - **`RuleBuilder<F extends string>`** (barrel, สเปกจาก Pi Files — แทน rule-builder ชั่วคราว 390 บรรทัดของกฎ storage/lifecycle) — ตัวสร้างเงื่อนไขแบบต้นไม้ AND/OR/NOT ซ้อนได้ generic ไม่ผูก schema: แอปส่ง `fields[{ key, label, operators[{ value, label }], defaultLeaf(), renderValue({ leaf, onChange, disabled }), describe?, group?, disabled?: boolean | string }]` (ตัวแก้ค่าเป็นของแอป — ToggleGroup/Input/number+หน่วย), component จัดโครง: กลุ่ม = กรอบ (root border ปกติ, ลูก border-dashed bg-muted/30) + ToggleGroup and/or + ปุ่ม "เงื่อนไข"/"กลุ่มย่อย" (ซ่อนเมื่อถึง `maxDepth` ค่าเริ่มต้น 3 รวม root; กลุ่มใหม่สลับ op จากแม่), leaf = grid `[ฟิลด์ 11rem | operator (เมื่อ >1) | ค่า | NOT]` มือถือเรียงลง (เปลี่ยนฟิลด์ → `defaultLeaf()` ของฟิลด์ใหม่), NOT = กรอบ border-destructive/40 + ป้าย "ไม่ใช่" + ปุ่ม "เอา NOT ออก" (`allowNot`), ปุ่มลบทุก item; ฟิลด์ใช้ `NativeSelect` (`label` เป็น string, `group` = optgroup, `disabled` string = title เหตุผล, ฟิลด์ที่เลือกอยู่แล้วไม่ถูกปิด), key ของ item คงที่ต่อ object (WeakMap) แก้ item หนึ่งไม่ re-mount พี่น้อง, `onChange` ต้นไม้ใหม่ทั้งก้อน immutable, `role=group` aria-label "กลุ่ม AND ระดับ n", `disabled` ทั้ง builder, `showSummary` ประโยคสรุปใต้ builder, `defaultField`, `language`/`text` (and/or/not/wrapNot/addCondition/addGroup/removeCondition/removeNot/emptyAnd/emptyOr/summary/field/operator/group()/andJoin/orJoin/notPrefix/unknownField()); data-slot `rule-builder`/`rule-group[data-op][data-depth]`/`rule-item`/`rule-leaf`/`rule-not`/`rule-builder-summary`
840
+ - types `RuleLeaf<F>`/`RuleGroup<F>`/`RuleNot<F>`/`RuleCondition<F>`/`RuleFieldDef<F>`/`RuleBuilderProps<F>`/`RuleBuilderText`; helpers **`describeCondition(cond, fields, text?, language?)`** (AND " และ ", OR " หรือ ", NOT "ไม่ใช่ …", กลุ่มย่อย >1 ข้อครอบวงเล็บ, leaf ใช้ `describe` ของฟิลด์หรือ "label operator value", ฟิลด์ไม่รู้จัก → "ฟิลด์ไม่รู้จัก: key"), `isRuleGroup`/`isRuleNot` type guards, `emptyGroup(op = "and")`
841
+ - showcase: หน้า `rule-builder` (ชุดกฎ storage ตัวอย่าง + JSON สด) — 96 หน้า; tests 4 กรณี (รวม 123)
842
+
843
+ ### Fixed
844
+
845
+ - **`CommandItem` ที่ active มองไม่ออกว่าตัวไหน** (รายงานจาก Pi Done ใน dark mode): shadcn ใช้ `data-selected:bg-muted` แต่ Tailwind `data-selected:` = `[data-selected]` (มีแอตทริบิวต์) ขณะที่ cmdk ใส่ `data-selected="false"` ให้รายการอื่นด้วย → ทุกแถวได้ bg-muted เท่ากัน; `styles.css` override `[data-slot=command-item][data-selected="true"]` เป็น `color-mix(muted, foreground 8%)` (dark 14%) — ไม่แตะ `command.tsx` ของ shadcn (CommandPalette, Combobox, SearchableSelect ได้ผลด้วย)
846
+
847
+ ## [0.8.0] — 2026-09-21
848
+
849
+ รอบ feedback หลังทุกแอปย้ายมา 0.7.0 ครบ 6/6 — สเปกจาก Pi Done (ColorPicker, CommandPalette client-side), Pi Mail (ActionForm), Pi Files (file explorer 4 ตัว) ทำรวดเดียว; minor ทั้งหมด ไม่เปลี่ยน API เดิม
850
+
851
+ ### Added
852
+
853
+ - **`ColorPicker<T>`** + **`ColorSwatch`** (barrel) — กริดเลือกสีจากชุดที่แอปส่ง `colors[{ value, label, swatchClassName? | swatch? (CSS) }]` (แพ็กเกจไม่ bundle palette): ปุ่ม `aria-pressed` + `aria-label`/`title`, ที่เลือก = `ring-2 ring-primary ring-offset-1` + ✓ ขาว, hover scale-105, roving tabindex + ลูกศร ↑↓←→/Home/End (จำนวนคอลัมน์อ่านจาก grid จริง), `columnsClassName` (grid-cols-5), `size` sm|md (size-7/size-8), `language`/`text`; `ColorSwatch` จุดสี size-3.5 สำหรับ trigger/badge (`color` CSS หรือ class) — ไม่มี popover ในตัว แอปครอบเองแบบ IconPicker (สเปกจาก Pi Done แทน metadata-color-picker 9 จุด)
854
+ - **`CommandPalette` โหมด client-side / controlled** (ขอจาก Pi Done): `filter(query) => CommandPaletteSection[]` คำนวณฝั่งแอปทุกครั้งที่ query เปลี่ยน (เรียกแม้ query ว่าง — ใช้แสดง action/ล่าสุด, ไม่ debounce) แสดงก่อนผลจาก `search`; item ใน section รับ `onSelect()` (action ล้วน `href` ไม่บังคับแล้ว; มีทั้งคู่เรียก onSelect ก่อนแล้วไป href) + `shortcut` (ป้ายท้ายรายการ); `open`/`onOpenChange` คุมจากภายนอก, `showTrigger={false}` ซ่อนปุ่มของแพ็กเกจ, `shortcut={null}` ไม่ดักคีย์ลัด (แอปดักเอง); type `CommandPaletteSectionItem`
855
+ - **`ActionForm`** (ข้อสังเกตจาก Pi Mail): `hideSuccessMessage` ซ่อนข้อความสำเร็จเมื่อแอปแจ้งผลเอง (ใน compact ข้อความดันปุ่มตกบรรทัด — ยังแสดง error), `submitDisabled` + `submitProps` (variant/size/className ของปุ่มส่ง); **`FormState<TData>`** — `ok: true` มี `data?: TData` และ `ActionForm<TData>`/`FormDialog<TData>`/`FormAction<TData>` generic → `onSuccess(s)` ได้ `s.data` ไม่ต้อง cast (`TData` ค่าเริ่มต้น `undefined` — โค้ดเดิมไม่ต้องแก้)
856
+ - **`TreeView`** (barrel, สเปกจาก Pi Files) — ต้นไม้ทั่วไป `role=tree` ตาม WAI-ARIA (treeitem มี aria-level/expanded/selected, roving tabindex): `nodes[{ id, label, icon?, hasChildren?, children?, disabled?, badge? }]`, `selectedId`/`onSelect(id, node)`, `expandedIds`/`defaultExpandedIds`/`onExpandedChange` (controlled ได้), `loadChildren(id, node)` โหลดลูก lazy (Spinner ระหว่างโหลด, cache ในตัว, error = ลูกว่าง), `onDrop(targetId, e, node)`/`canDrop` HTML5 dnd ไฮไลต์ node (`data-drop-target`), `renderLabel(node, { expanded, selected, level })`, คีย์บอร์ด ↑↓ เลื่อน → ขยาย/เข้าไปลูกแรก ← ยุบ/กลับแม่ Home/End Enter/Space เลือก, `indent` (12px/ระดับ), `emptyState`, `label`; สีตาม token ของ Sidebar (เลือก = `bg-sidebar-accent`) — ลูกศรยุบ/ขยายไม่ใช่ button (ไม่ซ้อน focusable ใน treeitem)
857
+ - **`Dropzone`** (barrel, สเปกจาก Pi Files) — พื้นที่ลากวางไฟล์: การ์ดเดี่ยว (เส้นประ + ไอคอน + ข้อความ + ปุ่มเลือกไฟล์; คลิกการ์ด/ปุ่ม/Enter เปิด picker) หรือ `overlay` ห่อ `children` แสดงชั้น "วางไฟล์ที่นี่" เฉพาะตอนลากไฟล์เข้าหน้า (ฟัง window + กัน browser เปิดไฟล์เมื่อวางนอกโซน); `onFiles(File[])`, `multiple` (true), `accept` (นามสกุล/กลุ่ม/mime — export `matchesAccept`), `maxBytes`, `onRejected(files, "size" | "type" | "multiple")`, `disabled`, `directory` (`webkitdirectory` + วางโฟลเดอร์ไล่อ่านไฟล์ข้างในผ่าน `webkitGetAsEntry`), `hint` (ค่าเริ่มต้นแสดง maxBytes), `icon`, `language`/`text`; data-attribute `data-active`/`data-disabled`
858
+ - **`UploadList`** (barrel, สเปกจาก Pi Files) — รายการอัปโหลด `items[{ id, name, sizeBytes, progress 0–1, status queued|uploading|done|error|aborted, error?, mime? }]` พร้อม `ProgressBar` ต่อแถว (queued จาง), `FileTypeIcon`, สรุป "x/y เสร็จ"/"เสร็จทั้งหมด" (aria-live), `onCancel`/`onRetry`/`onClearDone`/`onClose`, ยุบ/ขยาย (`defaultCollapsed`), `floating` มุมล่างขวาแบบ Drive (w-80 ไม่เกินจอ), `title`, `language`/`text`; `items` ว่าง = ไม่ render
859
+ - **`FileTypeIcon`** + **`getFileKind(mime, filename)`** (barrel) — ไอคอน lucide ตามชนิดไฟล์ (folder/image/video/audio/pdf/doc/sheet/slides/zip/code/text/default) จาก mime ก่อน นามสกุลสำรอง สีตาม token (pdf destructive, sheet success, folder/slides warning, สื่อ primary), `size` px, `data-kind`; **`formatBytes(bytes, locale)`** (lib/formatters) ฐาน 1024 "1.5 KB"/"12 MB"
860
+ - showcase: หน้า `color-picker` (กริด + ครอบ Popover), `tree-view` (lazy + drop ไฟล์จริง), `dropzone` (การ์ด + overlay), `upload-list` (mock progress + ลอย), `file-type-icon`; `command-palette` เพิ่มแบบ client-side คุม open เอง; `action-form` เพิ่ม hideSuccessMessage/submitDisabled/data (SecretReveal) — 95 หน้า; tests 17 กรณีเพิ่ม (รวม 119)
861
+
862
+ ## [0.7.0] — 2026-09-20
863
+
864
+ component ระดับแอปที่ Pi Auth และ Pi Done เขียนซ้ำกัน — สเปกจาก Pi Auth ออกแบบให้ทั้งสองแอปใช้ร่วม (ไม่พึ่ง i18n/auth ของแอป ทุกข้อความรับเป็น prop หรือ `UiText`)
865
+
866
+ ### Added
867
+
868
+ - **`CommandPalette`** (`@suphark/ui/next`) — ค้นหาแบบ ⌘K/Ctrl+K ทั้งแอป: `pages` (หน้าในเมนูที่แอปกรองสิทธิ์แล้ว — กรองฝั่ง client ตาม label/group/href/keywords) + `search?(query) => Promise<CommandPaletteSection[]>` (server action ของแอป เรียกเมื่อ ≥ `minQuery` ตัว, debounce `debounceMs` 150, เก็บ query คู่ผล ผลล่าสุดเท่านั้นที่แสดง), `compact` (ปุ่มไอคอนใน header) หรือปุ่มเต็มกว้าง + `Kbd` (⌘K/CtrlK ตาม platform หลัง mount) สำหรับ `sidebarHeader`, `shortcut` (ค่าเริ่มต้น k), `text` (fallback `UiText.palette`), `onNavigate` (ค่าเริ่มต้น router.push + ปิด); `Command shouldFilter={false}`, `CommandEmpty` แสดง "กำลังค้นหา…" ขณะ pending; types `CommandPaletteItem`, `CommandPaletteSection`, `CommandPaletteText`
869
+ - **`OrganizationSwitcher`** (`@suphark/ui/next`) — ตัวสลับองค์กร/tenant แบบ Clerk บน `HeaderCrumb` + `DropdownMenu`: `organizations[{ id, name, slug?, logoUrl? }]`, `activeId`, `onSwitch(id)` (server action — component ทำ useTransition + `router.refresh()` ปิดได้ด้วย `refresh={false}`), `placement="header"` (HeaderCrumb ใน layout top) / `"sidebar"` (ปุ่มเต็มกว้าง + SwitcherChevron ใน `sidebarHeader`), `fallbackLabel`/`heading` (ค่าเริ่มต้น `UiText.switcher`), `hint(org)` (ค่าเริ่มต้น `/slug`), `footer` (slot ท้ายเมนู เช่น สร้างองค์กร); องค์กรเดียว = ไม่มีเมนู, active มี ✓ และกดไม่ทำอะไร, pending ปิดปุ่ม, โลโก้ `<img size-5>` หรือ Building2; types `OrganizationOption`, `OrganizationSwitcherProps`
870
+ - `UiText.palette` / `UiText.switcher` (optional, มีค่าเริ่มต้นไทย/อังกฤษ)
871
+ - **`IconPicker`** (barrel, generic `T extends string`) — เลือกไอคอนจากชุดที่แอปส่ง `icons[{ name, icon: LucideIcon, labelTh, labelEn, keywords? }]` (แพ็กเกจไม่ bundle ชุดไอคอน): ค้นหา name/labelTh/labelEn/keywords ผ่าน `useDeferredValue`, preview ไอคอน+ชื่อด้านบน, กริด `SelectionButton` (aria-pressed, title/aria-label ตาม `language` th/en), `accentColor` (inline style สี dynamic ของผู้ใช้), `heightClassName`/`columnsClassName`, `text` ทับข้อความ — สเปกจาก DueBell ให้ Pi Done (metadata icon) ใช้ร่วม
872
+ - **`SelectionButton`** (barrel) — ปุ่ม toggle `aria-pressed` + สีเน้นเมื่อเลือก (Button outline) — chip ประเภท/ตัวกรอง, ช่องใน IconPicker (ยกจาก DueBell)
873
+ - **`CalendarMonthGrid<T>`** (barrel) — ตารางเดือนเลือกวันที่แอปคุมเนื้อหาต่อวัน: `month "YYYY-MM"`, `selectedDay`, `onSelectDay(day, date)`, `itemsByDay`, `renderDay(ctx)` (คืน null = ✓ ของแพ็กเกจเมื่อ complete), `dayState(items, ctx) → { complete?, hasItems? }` → data-attribute `data-selected/today/has-items/complete/weekend` ให้ CSS ของแอปแต่ง, (data-attribute เป็น boolean มี/ไม่มี — CSS ใช้ `[data-selected]`), `weekStartsOn 0|1`, `weekdayLabels` (export `WEEKDAYS_TH`/`WEEKDAYS_EN`), `today` (ไม่ส่ง = browser หลัง mount ผ่าน `useMounted` — SSR ได้ไม่ mismatch), `dayLabel` (aria-label "วันที่ N มี M รายการ"), `legend`, `dayClassName`; ช่องว่างก่อนวันที่ 1 aspect-square; ปุ่มวัน `aria-pressed` — สเปกจาก DueBell + Pi Done
874
+ - **`ProgressBar`** (barrel) — แถบความคืบหน้าตั้ง `tone` (default/success/warning/destructive), `size` sm/md/lg, `label`, `showValue`/`format`, `trackClassName`/`indicatorClassName`/`indicatorStyle` (สี dynamic) จาก props — `Progress` ของ shadcn ต่อท้าย Track+Indicator ให้เสมอ ส่ง children เข้าไปจะได้ 2 แถบ (รายงานจาก DueBell) จึงประกอบจาก `ProgressPrimitive.Root` โดยตรง
875
+ - showcase: หน้า `icon-picker`, `selection-button`, `calendar-month-grid`, `progress-bar`; tests 6 กรณีเพิ่ม (รวม 100)
876
+ - **error รายช่องในฟอร์ม server action** (ขอจาก Pi Mail): `FormState` แบบ `ok: false` รับ `errors?: Record<string, string>`; `ActionForm` และ `FormDialog` รับ `children` เป็นฟังก์ชัน `({ state, pending, errors, fieldError }) => ReactNode` (type `FormRenderContext`) → `<LabeledField error={fieldError("email")}>` ไม่ต้องมี hook ของแอป; `FormMessage` ยังแสดง `error` รวมเหมือนเดิม
877
+ - `UserMenu`: prop `menuLabel` + `UiText.userMenu`/`signOut` (ขอจาก Pi PDF — แอปอังกฤษได้ label ไทย); `ThemeMenuItems`: prop `onThemeChange(theme)` สำหรับบันทึกธีมเป็น preference ของบัญชี + ข้อความจาก `UiText.theme`
878
+ - `LabeledField`: prop `htmlFor` — ส่งมาแล้ว label ไม่ครอบ control (ผูกด้วย id) ให้ tooling ที่ไม่คำนวณ nested label เห็น accessible name (ข้อสังเกตจาก Pi Files)
879
+ - `DataTable`: `showToolbar` / `showPagination` (ค่าเริ่มต้น true) ปิดแถบค้นหา/ตัวกรอง/View และ pagination สำหรับตารางตั้งค่าเล็ก ๆ (ขอจาก Pi Files)
880
+
881
+ ### Fixed
882
+
883
+ - **`AppShell` (layout sidebar) มี `<main>` ซ้อน 2 ชั้น** (รายงานจาก DueBell): `SidebarInset` ของ shadcn เป็น `<main>` อยู่แล้ว → ใส่ `id={APP_MAIN_ID}`/`tabIndex` ที่ SidebarInset และเลิกสร้าง `<main>` ชั้นใน (เป็น `<div>`); layout top ยังมี `<main>` ตัวเดียวเหมือนเดิม
884
+ - showcase: หน้า `command-palette`, `organization-switcher`; `/shell` (sidebarHeader + ยุบเป็นไอคอน) และ `/shell-top` (crumb องค์กร) ใช้สองตัวนี้จริงแทน mock เดิม; tests 5 กรณี (คีย์ลัด toggle + กรอง keywords, search debounce/ผลล่าสุด/push+ปิด, org เดียวไม่มีเมนู, active ไม่เรียก onSwitch + refresh, fallback label)
885
+
886
+ ## [0.6.1] — 2026-09-20
887
+
888
+ ### Added
889
+
890
+ - **`npx suphark-ui-audit`** (`bin` ของแพ็กเกจ, `scripts/audit.mjs`) — ตรวจโค้ดแอปปลายทางตามกติกา design system ในคำสั่งเดียว: error = `window-dialog` (window.confirm/alert/prompt), `radix-state` (`data-[state=…]` ที่ Base UI ไม่มี), `arbitrary-text` (`text-[Npx]`), `base-ui-props` (สคริปต์เดิม); warning = `raw-palette` (bg-red-500 …), `important` (utility ลงท้าย `!` — เฉพาะ `--important` สำหรับโค้ดแพ็กเกจ); `--strict` ให้ warning เป็น error; ยกเว้น `// ui-audit-ok: เหตุผล` / `/* ui-audit-ok-file: เหตุผล */`; ข้าม `components/ui/**`, tests, generated; README + `docs/agent-rules.md` มีวิธีใช้ — repo นี้รัน `pnpm audit:ui` ใน `verify` และ CI (0 error 0 warning)
891
+ - `FormFieldRadioCards`: prop `aria-label` ส่งต่อไป RadioGroup เมื่อไม่แสดง `label` (ข้อสังเกตจาก Pi Auth หลังย้ายมาใช้ — ของเดิมมี aria-label)
892
+
893
+ ### Changed
894
+
895
+ - ล้าง raw palette ในแพ็กเกจเป็น semantic token: labs badge ใน `AppNav` → `text-warning-muted-foreground`, `FloatingSaveButton` สำเร็จ → `bg-success`, `FormMessage` สำเร็จ → `text-success`, ดอกจัน/ข้อความ error ใน `FormField*` → `text-destructive`, ไอคอน `ThemeToggle` → `text-warning`/`text-primary`/`text-muted-foreground`, ตัวอย่างใน design-system; ที่ยังใช้ palette โดยตั้งใจและจดไว้ (`ui-audit-ok-file`): `Notice`/`StatusBadge` (โทน 50/900 คู่ dark — รอตัดสินใจ token ระดับ tone), `lib/status-colors.ts` (ชุดสีให้ผู้ใช้เลือก)
896
+
897
+ ## [0.6.0] — 2026-09-20
898
+
899
+ component สำเร็จรูปสำหรับฟอร์ม server action ที่ Pi Auth ประกอบเองอยู่ (DueBell/Mail/Files ใช้ได้เหมือนกัน) — ไม่เปลี่ยน API เดิม; **รวม 0.5.2 ที่ยังไม่ได้ publish**
900
+
901
+ ### Added
902
+
903
+ - **`LabeledField`** + **`CheckboxField`** (barrel) — ช่องฟอร์มสำหรับ server action ธรรมดา (ไม่ใช่ react-hook-form): label ครอบ control + `hint` หรือ `error` (ตั้ง `data-invalid`) ประกอบจาก shadcn `Field/FieldLabel/FieldDescription/FieldError`; `CheckboxField` = Checkbox + label บรรทัดเดียว ใส่ `name` แล้วส่ง "on" ใน FormData — ยกจาก Pi Auth (42 ไฟล์) คง API เดิมเป๊ะ; ใช้จาก server component ได้ (ชื่อ `LabeledField` เพราะ `Field` ชนกับ shadcn และ `FormField` ชนกับ react-hook-form)
904
+ - **`ActionForm`** (barrel — ไม่พึ่ง next) — ฟอร์ม inline คู่กับ `FormDialog`: `useActionState` + `FormMessage` + ปุ่มส่ง (ป้ายค่าเริ่มต้น `UiText.save`), ไม่ใช้ `action=` บน `<form>` จึงคงค่าที่พิมพ์เมื่อ action คืน error; props `action`, `title?`, `submitLabel?`, `compact?` (ไม่มี Card, ปุ่ม sm, จัดแนวนอน wrap สำหรับแถวตาราง), `className?`, `onSuccess?(state)`; export type **`FormAction`** จาก barrel (FormDialog ใช้ชนิดเดียวกัน)
905
+ - **`Stepper`** (`@suphark/ui/next` — ใช้ next/link) — แถบขั้นตอน wizard ทุกขั้นกดเข้าได้: `steps[{ key, href, label, status: done|todo|problem, statusLabel? }]`, `current`, `ariaLabel?`; แนวนอนเลื่อนได้บนจอแคบ แนวตั้งบน lg; วงกลม เลข/✓/⚠ + `StatusBadge` ใต้ชื่อ; `aria-current="step"`; server component ได้; types `StepperStep`, `StepStatus`
906
+ - **`FormFieldRadioCards`** (backward-compatible): ใช้ **โดยไม่มี `control`** ในฟอร์ม server action ได้ — `name` ส่งต่อไป `RadioGroup` (มี hidden input ใน FormData) + `defaultValue` (uncontrolled) หรือ `value`+`onValueChange` (controlled); **`layout="row"`** = การ์ดแนวนอน ปุ่มวิทยุจริงซ้าย ไอคอน+ชื่อบรรทัดบน คำอธิบายบรรทัดล่าง (`grid sm:grid-cols-2`, checked = `border-primary bg-primary/5`); layout `grid` เดิม จุดเลือกมุมขวาบนตอนนี้ตามสถานะจริงของปุ่มวิทยุ (CSS `peer-data-checked`) จึงถูกทั้ง controlled/uncontrolled
907
+ - showcase: หน้า `labeled-field`, `action-form`, `stepper`, `form-field-radio-cards`; tests 8 กรณี (labeled-field, action-form ส่ง FormData/คงค่า/onSuccess/compact, stepper, radio-cards FormData + row)
908
+
909
+ ## [0.5.2] — 2026-09-20 (ไม่ได้ publish แยก — รวมใน 0.6.0)
910
+
911
+ รอบรีวิวประสิทธิภาพ + บั๊กที่เหลือ — ไม่เปลี่ยน props
912
+
913
+ ### Fixed
914
+
915
+ - **`PullToRefresh`** (`@suphark/ui/pwa`): effect ผูก `pullDistance`/`isRefreshing` เป็น deps → ถอด/ใส่ listener 4 ตัวบน `window` ใหม่ทุก `touchmove` และ `touchend` หลุดได้ระหว่างนั้น (ดึงแล้วไม่รีเฟรช/ป้ายค้าง) — อ่านค่าผ่าน ref ลงทะเบียนครั้งเดียว
916
+ - **`ThemeToggle`** เมื่อส่ง `persistedTheme`: effect ผูก `theme` ด้วย → ผู้ใช้กดเปลี่ยนแล้วถูก set กลับเป็นค่าที่บันทึกไว้ทันที (server action ยังไม่กลับ) — sync เฉพาะเมื่อ `persistedTheme` เปลี่ยน (รวมครั้งแรก)
917
+ - **`DataTableRowActions`**: `EditDialog` ถูก mount ทุกแถวตลอดเวลา (50 แถว = 50 Dialog+ฟอร์ม และ `initialData` stale) — mount เฉพาะตอนเปิด
918
+ - **`useDataTable`**: `onGlobalFilterChange` อ่านค่าเก่าจาก closure (พิมพ์เร็วใน tick เดียวหายตัวอักษร) → อ่านจาก `table.getState()`; `getPaginationRowModel` ไม่ใส่เมื่อ `manualPagination` (server แบ่งหน้าแล้ว ไม่ตัดซ้ำ); ตัด `useMemo` ที่ไม่มีผล
919
+ - **`FormFieldCombobox`**: cache ตัวเลือก key ด้วย function identity ใน `Map` module-level → inline fetcher ทำให้โตไม่จำกัด (memory leak) และไม่เคย hit — key ที่เป็นฟังก์ชันเก็บใน `WeakMap` (ส่ง `fetcherKey` ยังดีที่สุด)
920
+
921
+ ### Changed (ประสิทธิภาพ)
922
+
923
+ - **`ClientDateFormatter`**: ทุก instance เคยมี `setInterval` ของตัวเอง + render เพิ่ม 1 รอบตอน mount → ใช้ **`useTicker()`** นาฬิกากลางแชร์ timer เดียวต่อ interval + `useMounted()` (ไม่มี render เพิ่ม) + cache `Intl.DateTimeFormat`
924
+ - **`FormattedNumber`**: จาก state + 2 effects (3 renders/เซลล์) และ `new Intl.NumberFormat` ทุกครั้ง → `useMounted()` + `useMemo` + cache formatter ต่อ locale+options (ตารางหลายร้อยเซลล์เห็นผล)
925
+ - **`DataTableFacetedFilter`**: `selectedValues` Set ใหม่ทุก render ทำให้ `useMemo` เรียง options ไม่เคย hit → memo จาก `filterValue` จริง
926
+ - **`AppNav`**: กลุ่มเมนูที่กางได้เปลี่ยนจาก remount ทั้ง subtree ด้วย `key` เมื่อลูก active/ค้นหา → Collapsible แบบ controlled (`NavCollapsible`) เปิดอัตโนมัติเมื่อสถานะเปลี่ยน ผู้ใช้กาง/หุบเองได้ ไม่ mount ใหม่ มี animation
927
+ - **`RelativeDate`** ใช้ `useMounted()` กลาง
928
+
929
+ ### Added
930
+
931
+ - **`useMounted()`** (barrel) — true เฉพาะฝั่ง client หลัง hydrate ผ่าน `useSyncExternalStore` (ไม่มี render เพิ่ม) — ใช้กับค่าที่ server/browser ต่างกัน (เวลา, โซนเวลา, localStorage)
932
+ - **`useTicker(intervalMs)`** (barrel) — ตัวนับที่เพิ่มทุก interval, ทุก instance แชร์ `setInterval` เดียวต่อ interval (เริ่ม/หยุดตามผู้ใช้ตัวแรก/สุดท้าย); 0 = ไม่ tick
933
+ - **`getNumberFormat` / `getDateTimeFormat`** (`@suphark/ui/lib/intl-cache`, barrel) — Intl formatter ที่ cache ต่อ locale+options
934
+ - tests: `use-ticker` (แชร์ timer/หยุดเมื่อถอดหมด), `intl-cache`
935
+
936
+ ### ไม่ได้แก้ตามที่ประเมินไว้ (0.5.2)
937
+
938
+ - `DataTableToolbar` render ชุดตัวกรอง 2 ครั้ง (desktop + Sheet มือถือ): ตรวจแล้ว `SheetContent` ของ Base UI ไม่ mount ลูกเมื่อปิด ชุดใน Sheet จึงมีเฉพาะตอนเปิด — ต้นทุนจริงมีแค่ชุด desktop ที่ `hidden` บนมือถือ (ปุ่ม + trigger ไม่มี popup) ไม่คุ้มเสี่ยง hydration flash จากการ gate ด้วย `useIsMobile()`
939
+
940
+ ### Added (showcase/เครื่องมือ)
941
+
942
+
943
+ - showcase: หน้า **`/changelog`** render `packages/ui/CHANGELOG.md` ตอน build (parser เล็ก ๆ ไม่เพิ่ม dep: รุ่น/หมวด Added-Changed-Fixed/รายการ/inline code-bold-link) + ลิงก์ในเมนูบน
944
+ - **`pnpm check:changelog`** (`scripts/check-changelog.mjs`) — ทุกการเปลี่ยนแปลงใน `packages/ui/src`, `package.json`, `README.md`, `apps/showcase/src` ต้องมีบรรทัดใน CHANGELOG; ตรวจ working tree ก่อน commit (อยู่ใน `pnpm verify`) และช่วง commit ใน CI (`CHANGELOG_RANGE`)
945
+
946
+ ## [0.5.1] — 2026-09-20
947
+
948
+ ### Fixed
949
+
950
+ - **`RelativeDate` hydration mismatch (React #418) บน production** — เดิม render "วันนี้/เมื่อวาน" บน server ด้วย "ตอนนี้" ของ server (UTC และ module ที่โหลดไว้ก่อน) ซึ่งต่างจาก browser ได้ทั้งโซนเวลาและข้ามวัน → บน Vercel หน้า `/shell` (ตารางมี RelativeDate) hydrate พังทั้งหน้าและ React render ใหม่ทั้ง tree (เห็น theme script ซ้ำ 2 ตัว) — ตอนนี้คำนวณเฉพาะฝั่ง client หลัง mount (`useSyncExternalStore` guard แบบเดียวกับ `ClientDateFormatter`) ก่อน mount แสดงไอคอน + ช่องว่างขนาดเดิม
951
+ - `RelativeDate`: เอาข้อความเฉพาะ supplychain ("วันที่อ้างอิงราคาจริง") ออกจาก tooltip → prop ใหม่ `titlePrefix` (ค่าเริ่มต้น "วันที่") และ `emptyText`; สีเป็น semantic token (`text-muted-foreground`) แทน `text-slate-*` ตามกฎ design system (className/iconClassName ทับได้เหมือนเดิม)
952
+ - showcase: launch config `showcase-prod` (:3202, `next start` หลัง build) สำหรับตรวจ production build ในเครื่อง
953
+
954
+ ## [0.5.0] — 2026-09-19
955
+
956
+ ยกของที่พิสูจน์แล้วจาก Pi Done (todo/kanban) เข้าแพ็กเกจ — ไม่เปลี่ยน props เดิม
957
+
958
+ ### Added
959
+
960
+ - **`DialogsProvider` + `useDialogs()`** (`@suphark/ui`): กล่องโต้ตอบแบบ promise ทั้งแอปบน AlertDialog — `alert / confirm / prompt / choose / checklist` ครอบครั้งเดียวที่ root แล้วเรียกได้ทุกที่โดยไม่ต้อง render อะไรในผู้เรียก (ต่างจาก `useConfirm()` ที่ยังอยู่เหมือนเดิม); เปิดคำขอใหม่ขณะกล่องเปิด → คำขอเดิม resolve เป็นค่ายกเลิก; `UiText` เพิ่ม optional `ok`, `alertTitle`, `promptTitle`, `chooseTitle`, `checklistTitle` (มีค่าใน `UI_TEXT_TH`/`UI_TEXT_EN`)
961
+ - **`createKanbanCollisionDetection()`** (`@suphark/ui/kanban` — entry แยกเพราะ @dnd-kit/core สร้าง context ตอน import ใช้จาก server component ไม่ได้): collision detection ของ dnd-kit สำหรับบอร์ดหลายคอลัมน์ — pointer ในช่องว่างระหว่างการ์ด/ใต้การ์ดสุดท้ายแปลงเป็น "การ์ดที่ใกล้ที่สุดในคอลัมน์" (ไม่นับการ์ดที่ลาก) แทนการรายงานคอลัมน์ซึ่งทำให้ dnd-kit ล้าง transform (การ์ดเด้งกลับ, drop ไปท้ายคอลัมน์); คอลัมน์ว่างรายงาน droppable ของรายการ; รองรับ droppable อื่นของคอลัมน์ผ่าน `resolveColumnId` (เช่น handle ลากเรียงคอลัมน์)
962
+ - **`useTrackedState()`** (`@suphark/ui`): `useState` + `getLatest()` ที่อัปเดต synchronous — mutation ติดกันใน tick เดียว (drag-drop, automation, bulk) อ่านค่าล่าสุดแทน closure ตอน render
963
+ - **`@suphark/ui/pwa`** (entry ใหม่): `PwaServiceWorkerRegistration` (ลงทะเบียน SW + เก็บ `beforeinstallprompt` + `watchServiceWorkerUpdates`: เช็คอัปเดตตอนโหลด/กลับมาที่แท็บ/ทุก 15 นาที แล้ว reload ครั้งเดียวเมื่อ worker ใหม่คุมหน้า เลื่อนขณะพิมพ์ ไม่ reload ตอนติดตั้งครั้งแรก), `PwaInstallCard` (`appName`, `text` ค่าเริ่มต้นไทย + `PWA_INSTALL_CARD_TEXT_EN`), `PullToRefresh` (มือถือ, `router.refresh()`, `PULL_TO_REFRESH_TEXT_EN`), install-prompt store; `PullToRefresh` ใช้ next/navigation จึงอยู่ entry นี้ไม่ใช่ barrel หลัก
964
+ - showcase: หน้า `dialogs`, `kanban-board` (บอร์ด 3 คอลัมน์ลากได้จริง), `pwa-install-card`
965
+
966
+ ## [0.4.4] — 2026-09-19
967
+
968
+ รอบแก้บั๊กจาก code review ทั้ง codebase + audit Web Interface Guidelines — ไม่เปลี่ยน props เดิม
969
+
970
+ ### Fixed
971
+
972
+ - **`DeleteManyButton`**: `AlertDialogAction` ของ Base UI เป็นปุ่มธรรมดา (ไม่ใช่ Close) → ลบสำเร็จแล้วกล่องค้างเปิด กด Delete ซ้ำได้ด้วย ids ที่ลบไปแล้ว — คุม `open` เอง ปิดหลังสำเร็จ + `table.resetRowSelection()`; ปิดไม่ได้ระหว่าง pending
973
+ - **`useDataTable`**: effect sync `initialPageSize` ทับ `pagination.pageSize` ที่ควบคุมจาก prop/URL ตอน mount (URL=50 แต่ตารางกลายเป็น 25 และตัดแถวที่ server ส่งมา) — ข้ามเมื่อ `pagination` ถูกส่งมา (controlled)
974
+ - **`FormFieldSelect`**: `onValueChange` ไม่ถูกเรียกเลยเมื่อไม่ส่ง `variant` (ค่าเริ่มต้น select)
975
+ - **`FormFieldInput isNumeric`**: แปลงเป็น number ทันทีทำให้ศูนย์ท้ายทศนิยมหายขณะพิมพ์ ("1.0" → "1" พิมพ์ 1.05 ไม่ได้) — คงเป็น string จนทศนิยมจบ
976
+ - **`ErrorState`**: hardcode `/api/auth/get-session` + `/sign-in` ของ supplychain → แอปอื่นได้ 404 แล้วถูกตีความว่า "เซสชันหมดอายุ" พร้อมลิงก์ที่ไม่มี — ตรวจเซสชันเฉพาะเมื่อส่ง prop ใหม่ `sessionCheckUrl` (และ `signInHref` ค่าเริ่มต้น `/sign-in`); response ที่ไม่ใช่ 401/ok ไม่ถือว่าหมดอายุ (supplychain ส่ง `sessionCheckUrl="/api/auth/get-session"` เมื่อย้ายมาใช้)
977
+ - **`formatRelativeThaiDate`**: `Math.abs` ทำให้วันพรุ่งนี้เป็น "เมื่อวาน" — ใช้ `differenceInCalendarDays` คงเครื่องหมาย เพิ่ม "พรุ่งนี้"; ไกลกว่านั้น date-fns บอกทิศทางเอง
978
+ - **data attribute ของ Radix ที่หลงมาบน Base UI** (ไม่เคยทำงาน): `AppNav` ลูกศรกลุ่มเมนูไม่หมุน (`data-[state=open]` → `data-open`), `FormFieldRadioCards` การ์ดที่เลือกไม่ไฮไลต์ (`peer-data-[state=checked]` → `peer-data-checked`), trigger เปิดเมนูไม่มีพื้นหลัง (`UserMenu`, `DataTableColumnHeader`, `DataTableRowActions`, `HeaderCrumb` → `data-popup-open`), แท็บ `DesignSystemShowcase` ไม่ไฮไลต์ (`data-[state=active]` → `data-active`)
979
+ - **`ConfirmActionButton` / `RowActions`**: action โยน error → เด้ง error boundary ทั้งหน้าและ dialog ค้าง — จับแล้ว `toast.error(message)` (ข้อความจาก `UiText.actionFailed` optional) คง dialog ไว้ให้ลองใหม่
980
+ - **`HeaderBreadcrumbs hideIds`**: regex `[0-9a-z]{16,}` ซ่อน segment คำธรรมดายาว ≥16 (เช่น `organizationmembers`) — ต้องมีตัวเลขปนจึงถือเป็น id
981
+ - **`RowActions` รายการ `href`** render เป็น `<Link>` จริง (Ctrl/⌘+click, คลิกกลาง, เปิดแท็บใหม่ได้) ยกเว้นมี `confirm`; **`TransitionLink`** ไม่ดัก modifier keys/คลิกกลางแล้ว
982
+ - **`RefreshButton`**: `{...props}` ทับ `isLoading` ที่รวม pending ของ `router.refresh()` เมื่อผู้เรียกส่ง `isLoading={false}`
983
+ - **cmdk value ชนกันเมื่อ label ซ้ำ** (`Combobox`, `FormFieldCombobox`, `FormFieldSelect variant="combobox"`, `FormFieldInput variant="autocomplete"`): ต่อ `value` ท้าย label ให้ระบุรายการได้ (ค้นด้วยรหัสได้ด้วย)
984
+
985
+ ### Added (Web Interface Guidelines)
986
+
987
+ - **`AppShell`**: กล่องเนื้อหาเป็น `<main id="app-main" tabIndex={-1}>` (export `APP_MAIN_ID`) + skip link "ข้ามไปเนื้อหา" (มองเห็นเมื่อกด Tab ตัวแรก) ทั้ง layout sidebar และ top
988
+ - `styles.css`: `@media (prefers-reduced-motion: reduce)` ตัด animation/transition ทุก component (tw-animate, animate-spin, dialog zoom)
989
+ - `SubNav`/`TopTabs`: `focus-visible:ring-2` ให้เท่ากับ HeaderCrumb; `<select>` ของ SubNav ใส่ `text-foreground` (Windows dark mode); `ConfirmDialog` ปุ่มยืนยัน `aria-busy` ตอน pending; ข้อความ "ไม่พบ" ใช้ curly quotes
990
+ - showcase: `/login` กัน `next=//host` (protocol-relative) ; `LayoutSwitch` ป้ายเป็น `sr-only` บนจอแคบ; demo `form-fields` เพิ่มช่อง isNumeric + `onValueChange`
991
+
992
+ ## [0.4.3] — 2026-09-19
993
+
994
+ ### Added
995
+
996
+ - **`useConfirm()`** (barrel) — `window.confirm` แบบ promise บน ConfirmDialog สำหรับโค้ดฝั่ง client ที่ไม่ใช่ server action (ยกมาจาก DueBell ที่ย้าย window.confirm 8 จุดด้วย hook นี้): `const { confirm, confirmDialog } = useConfirm(); if (!(await confirm({ description, destructive: true }))) return;` แล้ว render `{confirmDialog}`; เรียกซ้ำขณะเปิดอยู่ → คำขอเดิม resolve(false)
997
+ - `ConfirmSpec` object รับ `destructive?` (RowActions/ConfirmActionButton ใช้เป็นตัวทับค่าเดิมของรายการ/variant)
998
+ - README: ตาราง "ยืนยันก่อนทำ — ห้ามใช้ window.confirm" (เลือก RowActions.confirm / ConfirmActionButton / useConfirm / ConfirmDialog) และหมายเหตุ Turbopack ค้าง `Package path ./styles.css is exported … but no valid target file` หลังอัปเดตแพ็กเกจ = cache ของ dev server ให้ลบ `.next`
999
+
1000
+ ## [0.4.2] — 2026-09-19
1001
+
1002
+ ### Added
1003
+
1004
+ - **`ConfirmDialog`** (barrel) — กล่องยืนยันมาตรฐานบน `AlertDialog` ของแพ็กเกจ ใช้แทน `window.confirm` (webview/automation บางตัวปิด window.confirm ทิ้ง กดแล้วไม่เกิดอะไร และไม่เข้า design system): controlled `open`/`onOpenChange`, `title?` (ค่าเริ่มต้น `UiText.confirmTitle`), `description`, `confirmLabel?` (ค่าเริ่มต้น `UiText.confirm`), `cancelLabel?`, `destructive` (ปุ่มยืนยันสีแดง), `pending` (ปุ่มหมุน + ปิดไม่ได้ชั่วคราว), `onConfirm`; พร้อม type `ConfirmSpec` (= `string | { title?, description, confirmLabel?, cancelLabel? }`) และ `normalizeConfirm()`
1005
+ - **`ConfirmActionButton`** (`@suphark/ui/next`) — ปุ่มเดี่ยว + ConfirmDialog + server action ในตัว (`confirm`, `action`, `data`, `onDone?` + props ของ ActionButton เช่น `variant`/`icon`/`permission`) รอ action เสร็จแล้ว `router.refresh()` — ใช้แทน `<form action>` + window.confirm ที่ Pi Auth มี (ชื่อไม่ใช้ `ConfirmButton` เพราะมีปุ่มธรรมดาชื่อนั้นอยู่แล้วใน standard buttons)
1006
+ - `UiText.confirm` / `UiText.confirmTitle` (optional มีค่าเริ่มต้นไทย/อังกฤษ) — แอปที่ส่ง UiText เองไม่ต้องเพิ่ม
1007
+ - export type `RowActionItem` (รายการที่ไม่ใช่ separator)
1008
+
1009
+ ### Changed
1010
+
1011
+ - **`RowActions` / `RowMenu` prop `confirm`** เปิด `ConfirmDialog` แทน `window.confirm` (ขอจาก Pi Auth ผ่าน Pi Mail) — รับ `ConfirmSpec`: string เดิมยังใช้ได้ (= description, title/ปุ่มค่าเริ่มต้น) หรือ object กำหนด `title`/`confirmLabel`/`cancelLabel`; ปุ่มยืนยันสีแดงเมื่อ `destructive`; dropdown ปิดก่อน dialog เปิด และ dialog render นอก DropdownMenu (กัน focus trap ของ Base UI ชนกัน); กดยืนยันแล้วค่อยรัน `href`/`onSelect`/`action` ใน transition เหมือนเดิม — โค้ดเรียกเดิมไม่ต้องแก้
1012
+ - ไม่มี `window.confirm` เหลือในแพ็กเกจ
1013
+ - showcase: หน้า `/components/confirm-dialog` (ConfirmDialog + ConfirmActionButton) และ `/components/row-actions` ใช้ confirm แบบ object
1014
+
1015
+ ## [0.4.1] — 2026-09-19
1016
+
1017
+ ### Added
1018
+
1019
+ - **`SwitcherChevron`** (barrel) — ลูกศร ⇅ มาตรฐานท้าย trigger แบบ switcher/select: `ChevronsUpDown` ขนาด `size-3` (12px) `shrink-0 text-muted-foreground opacity-70` aria-hidden (เล็กบางแบบ Clerk) รับ `className` ทับได้ — ใช้ตัวเดียวทั้งแพ็กเกจ/แอป (`OrganizationSwitcher` ใน @suphark/auth-ui จะย้ายมาใช้ด้วย)
1020
+
1021
+ ### Fixed
1022
+
1023
+ - `HeaderCrumb`: ลูกศรใหญ่ 24px ในแอปปลายทาง (Pi Auth) — เดิมใช้ `size-3.5!` ซึ่ง Tailwind ของแอปที่ scan ผ่าน `@source` ไม่ generate class แบบ `!` → svg ไม่มีขนาดจึงตกไป 24px ของ lucide; เปลี่ยนเป็น `SwitcherChevron` (ไม่ใช้ `!`)
1024
+
1025
+ ### Changed
1026
+
1027
+ - ลูกศรของ `UserMenu`, `Combobox`, `MultiSelectCombobox`, `FormFieldCombobox`, `FormFieldSelect` เปลี่ยนมาใช้ `SwitcherChevron` (เดิม h-4 w-4 opacity-50 ปนกันหลายขนาด) — ทุกจุดได้ 12px จางเท่ากัน; ไอคอน sort ใน `DataTableColumnHeader` ไม่ใช่ switcher คงเดิม; `components/ui/searchable-select.tsx` เป็นไฟล์ shadcn ไม่แตะ
1028
+ - นโยบาย: หลีกเลี่ยง `!` บน utility ในแพ็กเกจถ้าไม่จำเป็น (เหลือเฉพาะ `group-data-[collapsible=icon]:p-0!` ใน `AppBrand` ซึ่ง `sidebar.tsx` ของ shadcn generate class เดียวกันอยู่แล้ว)
1029
+
1030
+ ## [0.4.0] — 2026-09-19
1031
+
1032
+ ### Added
1033
+
1034
+ - **`AppShell layout="top"`** — โครงแบบ Clerk dashboard (ขอจาก Pi Auth): header 56px มีโลโก้ + **crumb switchers** คั่น `/` (`crumbs` = array หรือ component ที่ render `<HeaderCrumbs>`) + `header.actions`/`banner`/`center`; **แถบแท็บหลัก** ใต้ header จาก `nav` ชั้นแรก (active underline ตาม prefix, badge, เลื่อนแนวนอนบนมือถือ); แท็บที่มี `items` → **sub-nav ซ้าย** 224px (รองรับหัวกลุ่ม 2 ชั้น, `searchable`, มือถือเป็น select); `contentMaxWidth`, `brandColor` ใช้ได้เหมือนเดิม; breadcrumb ของหน้าปิดเป็นค่าเริ่มต้นในโหมดนี้ — โหมด `sidebar` เดิมไม่เปลี่ยน
1035
+ - component ประกอบ (export จาก `@suphark/ui/next`): `HeaderCrumb` (ปุ่ม switcher: icon/label/badge/hint + ⌄ ใช้เป็น trigger ของ DropdownMenu ใดก็ได้), `HeaderCrumbs`, `TopTabs`, `SubNav`, helpers `topLevelTabs`/`isTabActive`
1036
+ - showcase: `/shell-top` ตัวอย่างเต็ม (องค์กร → แอป → Production [Invite-only], แท็บ 6 ตัว, "ตั้งค่า" มี sub-nav 2 กลุ่ม)
1037
+
1038
+ ## [0.3.3] — 2026-09-19
1039
+
1040
+ ### Fixed
1041
+
1042
+ - `FormDialog`: จำกัดความสูงไว้ในจอ (`max-h-[calc(100dvh-2rem)]`) header/footer ตรึง เฉพาะเนื้อฟอร์มเลื่อน — ฟอร์มยาวไม่ล้นในหน้าต่างเตี้ย (รายงานจาก Pi Auth หน้าลงทะเบียนแอป); `contentClassName` ยังใช้กำหนดความกว้างได้เหมือนเดิม
1043
+
1044
+ ### Added
1045
+
1046
+ - export type `DateRange` (react-day-picker) จาก barrel — แอปใช้กับ `DateRangePicker` ได้โดยไม่ต้องติดตั้ง react-day-picker
1047
+
1048
+ ### Changed (showcase เท่านั้น — ไม่กระทบ API)
1049
+
1050
+ - หน้า **/components** แบบ ui.shadcn.com/docs/components: 74 หน้า (ทะเบียน `apps/showcase/src/docs/registry.ts` + demo ต่อไฟล์ใน `src/docs/demos/`) แต่ละหน้ามีตัวอย่างจริง/โค้ด (อ่าน source ของ demo ตอน build)/import line/หมายเหตุ/ลิงก์ shadcn+source, เมนูซ้ายตามหมวด, prev/next
1051
+
1052
+ - `DesignSystemShowcase` รับ prop `links?: { shell }` และตัดหัวใหญ่/badge ออก (แอปเป็นคนมีหัวเดียว); แท็บป้ายสั้น (จอแคบเลื่อนแนวนอน 1 แถว — แก้แท็บทับหัวเรื่องเพราะ `group-data-horizontal/tabs:h-8`); hero ไม่มีกรอบ + `HeroSpecimen` (component จริง + สวิตช์ --brand); CardTitle/CardDescription/หัว section ใหญ่ขึ้น 1 ขั้น; h2 ภาษาไทยอย่างเดียว; ปุ่ม "ตัวอย่างหน้าจอ" ชี้ไป `/shell`
1053
+ - ช่องค้นหาในหน้า Overview ค้นได้จริง: `search-index.ts` (26 หัวข้อ) → สลับแท็บ + เลื่อนไป anchor (`id` บนทุก section หลัก) + empty state
1054
+
1055
+ ## [0.3.2] — 2026-09-18
1056
+
1057
+ ### Fixed
1058
+
1059
+ - `copyText`/`selectContents` export จาก barrel หลักด้วย (แอปใช้ทำปุ่มคัดลอกเองได้)
1060
+ - `EnvSnippet`: ค่ายาว (client secret) ล้นออกนอก dialog — ตัดบรรทัดในกรอบ (`whitespace-pre-wrap break-all` + `min-w-0`) แทน scroll แนวนอน (รายงานจากหน้า Register app ของ Pi Auth)
1061
+ - `EnvSnippet` / `SecretReveal`: ปุ่มคัดลอกเงียบเมื่อ `navigator.clipboard` ถูกบล็อก (webview/embedded browser เช่น browser ใน Claude desktop) — เพิ่ม `copyText()` (`@suphark/ui/lib/copy-text`) ที่ fallback ไป `execCommand("copy")`; ถ้ายังไม่ได้ EnvSnippet จะเลือกข้อความให้และบอกให้กด Ctrl+C (`UiText.copyFailed` optional)
1062
+
1063
+ ## [0.3.1] — 2026-09-18
1064
+
1065
+ ### Changed
1066
+
1067
+ - `AppShell`: slot `sidebarHeader` ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน (เดิมล้นทับเนื้อหา — รายงานจาก Pi Auth) + prop ใหม่ `sidebarHeaderCollapsed` สำหรับไอคอนแทนตอนยุบ
1068
+ - `HeaderBreadcrumbs`: `labels` รับฟังก์ชัน `(segment, href) => ReactNode` ได้ (ใส่ `t()` ของ next-intl) และ map รับ ReactNode — ฟังก์ชันต้องมาจาก client component (ส่งจาก server layout จะได้ 500 "Functions cannot be passed directly to Client Components"; showcase สาธิตด้วย `breadcrumbs: <ShellBreadcrumbs />`)
1069
+ - README: อธิบายว่า `ThemeMenuItems` มี group + หัวในตัว ไม่ต้องห่อเอง
1070
+
1071
+ ## [0.3.0] — 2026-09-18
1072
+
1073
+ ### Added
1074
+
1075
+ - **AppShell** (`@suphark/ui/next`) — โครงแอปกลางแทน ConsoleShell ของ Pi Auth และ main layout ของ supplychain: shadcn Sidebar (ยุบเป็นไอคอน/offcanvas/none, Sheet บนมือถือ, ⌘B, cookie `sidebar_state`), โลโก้ `AppBrand` (name/logo/subtitle/version), ค้นหาเมนู, เมนู 2 ชั้น `AppNav` (ไฮไลต์ตาม pathname, กางอัตโนมัติเมื่อลูก active, dropdown เมื่อยุบ, badge, labs, pin ผ่าน `pin` prop), slot `sidebarHeader`/`sidebarFooter`, header (SidebarTrigger + `HeaderBreadcrumbs` อัตโนมัติ + `center`/`actions`/`banner`), `brandColor` ต่อ tenant, `contentMaxWidth`
1076
+ - `UserMenu` (variant `sidebar` | `header`): หัวผู้ใช้ + children (ใส่ DropdownMenuItem/ThemeMenuItems เอง) + `onSignOut`
1077
+ - `HeaderBreadcrumbs` (labels/skip/hideIds/homeHref), `NavLink` (จาก Pi Auth)
1078
+ - โมเดลเมนู `AppNavGroup`/`AppNavItem` (icon เป็น ReactNode — ประกาศใน server component ได้) + helpers `filterNav` (ตัดตามสิทธิ์ทุกชั้น), `searchNav`, `flattenNav`, `isNavActive` (+tests รวม 40)
1079
+ - barrel หลัก export `toast` จาก sonner (แอปไม่ต้องติดตั้ง sonner เอง)
1080
+ - showcase: `/shell` ตัวอย่าง layout ที่ใช้ AppShell + UserMenu + ThemeMenuItems + filterNav + cookie defaultOpen
1081
+
1082
+ ## [0.2.0] — 2026-09-18
1083
+
1084
+ ### Added
1085
+
1086
+ - entry **`@suphark/ui/data-table`** (ยกจาก supplychain `components/data-table`): `DataTable`, `DataTableColumnHeader`, `DataTableToolbar`, `DataTablePagination`, `DataTableFacetedFilter`, `DataTableDateRangeFilter`, `DataTableViewOptions`, `DataTableRowActions`, `useDataTable`, types (`DataTableProps`, `FacetedFilterConfig`) + `DeleteManyButton`, `ResourcePropertyFilter` (+helpers) — ต้องมี peer `@tanstack/react-table` (optional; แอปที่ไม่ใช้ตารางไม่ต้องติดตั้ง); ลากเรียงแถวใช้ `@dnd-kit/*` เป็น dependency ของแพ็กเกจ
1087
+ - `useTableSearchParams` / `TABLE_PARAM` (state ตารางใน URL) ที่ `@suphark/ui/next`
1088
+ - tests: data-table, data-table-faceted-filter, resource-property-filter (รวม 34)
1089
+ - exports `./package.json`, `./components/data-table`
1090
+ - showcase: การ์ด DataTable จริงในแท็บ Data Display; code recipe ใช้ import path ของแพ็กเกจ
1091
+ - `apps/showcase/vercel.json` + `docs/ops/deploy-vercel.md`
1092
+
1093
+ ## [0.1.1] — 2026-09-18
1094
+
1095
+ ### Added
1096
+
1097
+ - `NativeSelect` / `NativeSelectOption` (shadcn `native-select` — `<select>` ธรรมดาสำหรับฟอร์ม server action/FormData) ตามที่ Pi Auth ใช้ 17 ไฟล์
1098
+ - dependency `cn` (^0.3.0) — shadcn CLI รุ่นใหม่สร้างไฟล์ที่ `import { cn } from "cn"` แทน `lib/utils`; ทั้งสองทางใช้ได้ ไฟล์เดิม 59 ตัวยังใช้ `@suphark/ui/lib/utils`
1099
+
1100
+ ### Changed
1101
+
1102
+ - ตัด `__tests__`/`*.test.*` ออกจาก tarball (`files` ใน package.json) — test ยังอยู่ใน repo
1103
+
1104
+ ### Notes
1105
+
1106
+ - `ThemeToggle` props `persistedTheme` / `onThemeChange` เป็น optional อยู่แล้ว — ไม่ส่ง = ใช้ next-themes อย่างเดียว (ใช้แทน ThemeToggle ของ @pi/ui ได้โดยไม่ต้องเปลี่ยน call site)
1107
+
1108
+ ## [0.1.0] — 2026-09-18
1109
+
1110
+ รุ่นแรก — ยกจาก `supplychainnextjs-1` (source of truth) + ชุด generic จาก Pi Auth `@pi/ui`
1111
+
1112
+ ### Added
1113
+
1114
+ - **shadcn/ui base-nova 59 primitives** (`components/ui/*`) + tests (combobox, date-picker, slider, tabs)
1115
+ - **token/ฟอนต์** `styles.css`: slate + success/warning/sidebar/chart, radius, scrollbar, Noto Sans Thai Looped, dark variant
1116
+ - **shared/button**: AddNew, Save, Submit, Cancel, Confirm, Delete, FloatingSave, Print, ActionButton (base)
1117
+ - **shared/form** (react-hook-form): FormFieldInput/Select/Combobox/Date/Boolean/RadioGroup/RadioCards/ColorSelect/FormattedNumber, SimpleSelect
1118
+ - **shared**: Combobox, MultiSelectCombobox, DateRangePicker, FormattedNumber, PrimarySecondaryText, ClientDateFormatter, ErrorState, PageSkeleton, ClientRedirect
1119
+ - **จาก Pi Auth**: FormDialog, FormMessage/FormState, NameIdFields/toIdentifier (+test), StatusBadge/statusTone, RowMenu, RowActions, SecretReveal, EnvSnippet/randomSecret, Notice, EmptyState, EmptyRow, DataList/DataRow, Stat, DescriptionList, PageHeader, SectionCard, Initials/initialsOf, UiText (TH/EN provider)
1120
+ - **theme** บน next-themes: ThemeProvider, ThemeToggle (เมนู 3 ตัวเลือก), ThemeSwitch (ปุ่มเดี่ยว), ThemeMenuItems (ใส่ใน DropdownMenu), useTheme
1121
+ - **lib**: `cn`, `formatThaiFullDate`/`formatRelativeThaiDate`, status-colors, `UiPermissionProvider`/`usePermission` (แอป inject ตัวตรวจสิทธิ์เอง)
1122
+ - **hooks**: `useIsMobile`, `useCopyToClipboard`
1123
+ - **สีแบรนด์ต่อ tenant**: rule `[style*="--brand"], [data-brand]` ใน `styles.css` ให้ตั้ง `--brand`/`--brand-foreground` บน wrapper แล้ว primary/ring/sidebar-primary ตาม (แก้ตามบั๊ก @pi/ui `4cd39d8` ที่ `--primary: var(--brand)` บน :root ไม่สืบทอดไปยัง wrapper)
1124
+ - entry `@suphark/ui/next` สำหรับ component ที่ใช้ next/navigation (LinkButton, RefreshButton, TransitionLink, PageBreadcrumb, PageShell, FormDialog, RowActions, RowMenu)
1125
+ - showcase `components/design-system/*` (8 sections) + `docs/design-system.md`
1126
+ - audit scripts `scripts/audit-base-ui-props.mjs`, `scripts/audit-tailwind-typography.mjs` (รับ path เป็น argument ให้แอปปลายทางใช้ได้)
1127
+
1128
+ ### Changed (เทียบต้นทาง)
1129
+
1130
+ - `NameIdFields` เขียนใหม่บน `ui/field.tsx` (Field/FieldLabel/FieldDescription) แทน `pi/Field`
1131
+ - `ActionButton`/`LinkButton` ใช้ `usePermission` จาก `lib/permission` (context) แทน `@/lib/auth`
1132
+ - `FormattedNumber` รับ `DecimalLike` (`{ toString(): string }`) แทน `Prisma.Decimal`
1133
+ - `FormFieldInput` แยก `CopyableInput` เพื่อไม่เรียก hook ในเงื่อนไข
1134
+ - `ui/form.tsx`, `hooks/use-mobile.ts` เพิ่ม `"use client"` (จำเป็นเมื่อ import ผ่าน barrel จาก server component)
1135
+ - `ui/date-picker.tsx` แก้ `parts[0] ?? ""` ให้ผ่าน `noUncheckedIndexedAccess` ของแอปปลายทาง
1136
+
1137
+ ### Not included (รุ่นถัดไป)
1138
+
1139
+ - data-table (tanstack) + DeleteManyButton, sidebar/app shell, rich-text-editor (lexical), form-field-image-upload, pdf-export/print, rate, view-tracker, icon-picker, resource-property-filter