@suphark/ui 0.37.0 → 0.39.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
@@ -2,7 +2,41 @@
2
2
 
3
3
  รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
4
 
5
- ## [0.37.0] — 2026-10-07
5
+ ## [0.39.0] — 2026-10-07
6
+
7
+ ### Added
8
+
9
+ - **`SortableProvider freeLayout` — ชั้นบนสุดจัดได้อิสระ** (เจ้าของแจ้งว่าเดิม "ล็อก" ให้รายการไม่มีกลุ่มอยู่บนเสมอ กลุ่มอยู่ล่างเสมอ): กลุ่มกับรายการไม่มีกลุ่มสลับที่กันได้ทุกแบบ — กลุ่มขึ้นไปเหนือรายการ, รายการไปอยู่ระหว่างกลุ่ม, เอารายการออกจากกลุ่มไปวางก่อนกลุ่มไหนก็ได้ · หัวกลุ่ม: **ครึ่งบน = วางก่อนกลุ่ม, ครึ่งล่าง = เข้ากลุ่มเป็นตัวแรก** · ระหว่างลากทุกอย่างในชั้นบนสุดเลื่อนหลบเป็นก้อน (กลุ่ม = หัว + รายการ) · โซนท้ายตาราง = วางท้ายสุด · ไม่เปิด = แบบเดิม (แอปที่ยังเรนเดอร์รายการไม่มีกลุ่มก่อนไม่เปลี่ยนพฤติกรรม)
10
+ - `SortableOrder.root` (optional) + `rootEntries(order)` (ลำดับชั้นบนสุดสำหรับวนเรนเดอร์), `moveRootEntry`, `moveItemToRoot`, type `SortableRootEntry` · `sortableOrderToPayload(order).root` + `sortableOrderOf(..., { root })` สำหรับบันทึก/โหลด — `groups` / `items[UNGROUPED]` ยังตรงกับ root เสมอ (payload เดิมใช้ต่อได้)
11
+ - showcase: `/components/sortable-list` เปิด `freeLayout` และเรนเดอร์ตาม `rootEntries`
12
+
13
+ ### Fixed
14
+
15
+ - (จาก 0.38.1 ที่ไม่ได้ขึ้น npm แยก) ลากรายการเข้ากลุ่มแล้ววางเป็นรายการแรกไม่ได้ — ดูด้านล่าง
16
+ - โซน "ท้ายตาราง" (`useSortableDropZone(UNGROUPED)`) ถูก collision เปลี่ยนเป้าหมายเป็นรายการไม่มีกลุ่มตัวแรก → วางแล้วไปโผล่บนสุดแทนท้ายสุด
17
+
18
+ ## [0.38.1] — 2026-10-07
19
+
20
+ > ประกาศพร้อม publish แล้ว แต่ยังไม่ขึ้น npm — รวมอยู่ใน 0.39.0
21
+
22
+ ### Fixed
23
+
24
+ - **`@suphark/ui/sortable` ลากรายการเข้ากลุ่มแล้ววางเป็นรายการแรกไม่ได้** (เจ้าของแจ้ง): ระหว่างลากขึ้นเป็นตัวแรกถูก แต่พอปล่อยกลับไปเป็นตัวที่สอง — ตอนชี้หัวกลุ่ม collision เลือก "รายการที่ใกล้ที่สุดที่ไม่ใช่ตัวที่ลาก" (= ตัวแรกเดิม) แล้ว `onDragEnd` ย้ายไปแทนที่ตัวนั้น · ตอนนี้ชี้หัวกลุ่ม = รายการแรกที่แสดงอยู่ **รวมตัวที่ลากเอง** · ยืนยันบน demo: ลากจากนอกกลุ่ม / จากกลุ่มอื่น / จากท้ายกลุ่มเดียวกันขึ้นหัวกลุ่ม → เป็นตัวแรกทุกกรณี, วางครึ่งล่างของแถวยังไปอยู่ "หลัง" แถวนั้นตามเดิม (ทำซ้ำบน production v0.38.0 ได้ก่อนแก้)
25
+
26
+
27
+ ### Fixed
28
+
29
+ - **`@suphark/ui/sortable` ลากกลุ่มแล้วไม่เห็นอะไรขยับ** (เจ้าของแจ้ง): หัวกลุ่มมักอยู่ใน `SortableGroup` ซึ่งเป็น context ของรายการ dnd-kit จึงหากลุ่มในลำดับไม่เจอ ไม่มีแถวไหนได้ transform เลยจนปล่อย · ตอนนี้ provider คำนวณระยะเลื่อนของกลุ่มเอง และเลื่อน **ทั้งก้อน** (หัวกลุ่ม + แถวรายการ) — กลุ่มอื่นหลบเท่าความสูงทั้งก้อน, ช่องจาง ๆ ของกลุ่มที่ลากเลื่อนไปจุดวาง (ไม่มี overlay = ทั้งก้อนลอยตามเมาส์) · ที่วางของกลุ่มวัดรวมแถวรายการ ใช้ได้ทุกโครงสร้างโดยไม่ต้องแก้ call site · วัดบนหน้า demo: ลาก "ฐานข้อมูล" ลงเลย "อีเมล" → ฐานข้อมูลทั้งก้อน +134px, อีเมลทั้งก้อน −135px, ปล่อยแล้วไม่มี style ค้าง
30
+ - แถวต้นทางจางค้างหลังปล่อย (เจอกับหัวกลุ่ม): side effect ของ drop animation ใน dnd-kit จำ opacity ที่เราตั้งไว้เป็นค่าเดิมแล้วเขียนคืน — ปิด side effect นั้น (opacity คุมผ่าน style ของ React อยู่แล้ว)
31
+
32
+ ### Added
33
+
34
+ - `SortableProvider multiSelect` (+ `selected`/`onSelectedChange` ถ้าจะคุมเอง): เลือกหลายรายการแล้วลากไปพร้อมกัน — ⌘/Ctrl+คลิก ทีละตัว, Shift+คลิก เป็นช่วง (เฉพาะแถวที่แสดง), คลิกเฉย ๆ ล้าง (คลิกปุ่ม/ช่องกรอกในแถวไม่นับ) · ที่จับ: `x` เลือก/ไม่เลือก, ⌘/Ctrl+A ทั้งหมด, Esc ล้าง · ลากตัวที่เลือก = ทุกตัวที่เลือกจางลงตรงที่เดิมแล้วไปรวมที่จุดวาง เรียงตามลำดับเดิม (ข้ามกลุ่มได้) · ลากตัวที่ไม่ได้เลือก = ลากตัวเดียวและล้างการเลือก
35
+ - `useSortableRow` คืน `selected` + `selectProps` (กระจายลงแถว มี `data-selected` ไว้ทำสี) · `useSortableState` คืน `selected`/`clearSelection` (ใช้คู่ `SelectionBar`)
36
+ - `renderOverlay` ได้ `SortableDrag` = `{ kind, id, ids }` (`ids` = ทุกตัวที่ลากไปด้วยกัน — ทำการ์ดซ้อน + จำนวน) · `SortableAnnouncementContext.count` + `UiText.sortable.startedMany` ("เริ่มลาก {count} รายการที่เลือกไว้")
37
+ - helper pure: `flatItems(order)`, `gatherItems(order, activeId, ids)`
38
+ - showcase: `/components/sortable-list` เปิด `multiSelect` + `SelectionBar` + การ์ดซ้อนตอนลากหลายคีย์
39
+
6
40
 
7
41
  ### Added
8
42
 
package/README.md CHANGED
@@ -234,6 +234,8 @@ const [order, setOrder] = useState(() =>
234
234
  - ตั้ง `id` ของ `SortableProvider` ได้ (ไม่ตั้ง = `useId()`) — ต้องคงที่ระหว่าง SSR กับ client ไม่งั้น `aria-describedby` ของ dnd-kit จะ hydration mismatch · ตรวจได้จากที่จับ: `aria-describedby` จะเป็น **id ที่ส่งเข้าไปตรง ๆ** (ไม่มี prefix) และต้องชี้ element ที่มีข้อความวิธีลาก — element นี้สร้างหลัง hydrate จึงต้องรอหน้า mount ก่อนค่อย query
235
235
  - `onCommit` ล้มเหลว (throw หรือ `{ ok: false, error }`) = คืนลำดับเดิม + toast; ถ้าอยากจัดการ error เองใส่ `onCommitError`
236
236
  - การ์ดลอยตามเมาส์: `renderOverlay={(drag) => drag.kind === "item" ? <ItemCard id={drag.id} /> : <GroupCard id={drag.id} />}` (วาดใน portal จึงเป็น `div` ได้ ไม่ต้องเป็น `<tr>`) — ไม่ใส่ = แถวขยับเองแบบเดิม · โฟลเดอร์ที่ย่ออยู่: `useSortableDropZone(id, { onDwell: collapsed ? open : undefined })` ลากค้าง 600ms แล้วเปิดให้ · วางบนหัวกลุ่ม = ขึ้นบนสุดของกลุ่ม, วางบนโซนของ `UNGROUPED` = ต่อท้าย
237
+ - เลือกหลายรายการแล้วลากพร้อมกัน: `SortableProvider multiSelect` แล้วกระจาย `row.selectProps` ลงแถว (ทำสีด้วย `data-[selected]:bg-primary/10`) — ⌘/Ctrl+คลิก, Shift+คลิก, ที่จับกด `x` / ⌘/Ctrl+A, Esc ล้าง · แถบนับใช้ `SelectionBar` + `useSortableState().selected/clearSelection` · `renderOverlay` ได้ `drag.ids` ไว้ทำการ์ดซ้อน
238
+ - จัดชั้นบนสุดอิสระ (กลุ่มกับรายการไม่มีกลุ่มสลับกันได้): `SortableProvider freeLayout` + เรนเดอร์ตาม `rootEntries(order)` (ครอบทั้งหมดด้วย `<SortableGroup itemIds={order.items[UNGROUPED]}>` แล้วแต่ละกลุ่มมี `SortableGroup` ของตัวเองข้างใน) + บันทึก `payload.root` และส่งกลับ `sortableOrderOf(..., { root })` ตอนโหลด · หัวกลุ่มครึ่งบน = ก่อนกลุ่ม, ครึ่งล่าง = เข้ากลุ่ม
237
239
  - `sortableOrderToPayload(order)` คืน `{ groups, items }` — `groups` คือลำดับของกลุ่ม, `items` คือรายการในแต่ละกลุ่มเรียงตามนั้น (ของที่ยังไม่มีกลุ่มมาก่อนด้วย `groupId: null`)
238
240
  - `SortableProvider disabled` (เช่นระหว่างกรอง ซึ่งลำดับที่เห็นไม่ใช่ลำดับจริง) ทำให้ `DragHandle` จางลงและกดไม่ได้ — ไม่อยากให้เห็นเลยใส่ `hideWhenDisabled`
239
241
  - ข้อความ screen reader: ใส่ `announcements`/`screenReaderInstructions` เป็น prop (callback ได้ `{ id, kind, groupId, index, total }`) หรือปล่อยให้ใช้แม่แบบจาก `UiText.sortable`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/ui",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
@@ -28,6 +28,8 @@ export interface UiText {
28
28
  instructions: string;
29
29
  /** ใช้ `{index}` / `{total}` */
30
30
  started: string;
31
+ /** เริ่มลากหลายรายการที่เลือกไว้ — ใช้ `{count}` */
32
+ startedMany: string;
31
33
  moved: string;
32
34
  dropped: string;
33
35
  cancelled: string;
@@ -255,6 +257,7 @@ export const UI_TEXT_TH: UiText = {
255
257
  handle: "ลากเพื่อจัดลำดับ",
256
258
  instructions: "กด Space เพื่อเริ่มลาก ใช้ปุ่มลูกศรเลื่อน แล้วกด Space อีกครั้งเพื่อวาง (Esc = ยกเลิก)",
257
259
  started: "เริ่มลากรายการที่ {index} จาก {total}",
260
+ startedMany: "เริ่มลาก {count} รายการที่เลือกไว้",
258
261
  moved: "ย้ายไปตำแหน่งที่ {index} จาก {total}",
259
262
  dropped: "วางที่ตำแหน่งที่ {index} จาก {total}",
260
263
  cancelled: "ยกเลิกการลาก",
@@ -425,6 +428,7 @@ export const UI_TEXT_EN: UiText = {
425
428
  instructions:
426
429
  "Press Space to start dragging, use arrow keys to move, Space again to drop (Esc to cancel)",
427
430
  started: "Picked up item {index} of {total}",
431
+ startedMany: "Picked up {count} selected items",
428
432
  moved: "Moved to position {index} of {total}",
429
433
  dropped: "Dropped at position {index} of {total}",
430
434
  cancelled: "Dragging cancelled",