@suphark/ui 0.45.1 → 0.47.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 +17 -1
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/components/sortable/sortable-list.tsx +20 -21
- package/src/index.ts +1 -0
- package/src/lib/types.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.47.0] — 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **`@suphark/ui/sortable` รองรับโครงเดียว: หัวกลุ่มกับแถวรายการเป็นพี่น้องกัน** (`<tr>` ใน `<tbody>` เดียวกัน หรือการ์ดในลิสต์เดียวกัน) — เจ้าของเลือกให้เหลือรูปแบบเดียวเพื่อดูแลง่าย หลัง Pi Config ย้ายจากโครง "`<tbody>` ต่อกลุ่ม" มาแบบนี้แล้ว (71735f0) · **ห้ามให้ node ของกลุ่มครอบแถวรายการไว้** (เช่นแปะ `ref` ของกลุ่มที่ `<tbody>` ที่ห่อแถวคีย์) ไม่งั้นแถวลูกจะเลื่อนสองชั้นและกรอบของก้อนถูกนับซ้ำ · ถอดโค้ดที่เคยรองรับโครงนั้นออก (`insideGroup` ใน context ภายใน, การกันนับซ้ำใน `blockRect`) — ไม่มี props ใดเปลี่ยน แอปที่ทำตามตัวอย่างใน `/components/sortable-list` ไม่ต้องแก้อะไร
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- 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 ที่พิสูจน์แล้วว่าแดงเมื่อถอดตัวแก้)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `@suphark/ui/sortable`: ลากรายการเข้ากลุ่มอื่นไปวาง "ใต้" แถวหนึ่ง ระหว่างลากอยู่ถูกที่ แต่ปล่อยแล้วกระโดดไปอยู่ "เหนือ" แถวนั้น (เจ้าของเจอในตัวอย่างโครง `<tbody>`) — ตอนข้ามกลุ่มจัดตำแหน่งก่อน/หลังไว้แล้ว แต่ตอนปล่อย `reorderItems` ย้ายไปแทนที่แถวที่ชี้ซ้ำอีกรอบ · ตอนนี้ถ้าปล่อยตอนยังชี้แถวเดิมที่ข้ามกลุ่มเข้ามา ใช้ตำแหน่งที่จัดไว้เลย
|
|
20
|
+
- showcase: `/components/sortable-list` **เอาตัวอย่างโครง `<tbody>` ต่อกลุ่มออก** — เจ้าของเลือกให้มีรูปแบบเดียว (หัวกลุ่ม + แถวรายการเป็น `<tr>` ใน `<tbody>` เดียวกัน) เพื่อดูแลง่าย
|
|
21
|
+
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
8
24
|
|
package/README.md
CHANGED
|
@@ -236,6 +236,7 @@ const [order, setOrder] = useState(() =>
|
|
|
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
237
|
- เลือกหลายรายการแล้วลากพร้อมกัน: `SortableProvider multiSelect` แล้วกระจาย `row.selectProps` ลงแถว (ทำสีด้วย `data-[selected]:bg-primary/10`) — ⌘/Ctrl+คลิก, Shift+คลิก, ที่จับกด `x` / ⌘/Ctrl+A, Esc ล้าง · แถบนับใช้ `SelectionBar` + `useSortableState().selected/clearSelection` · `renderOverlay` ได้ `drag.ids` ไว้ทำการ์ดซ้อน
|
|
238
238
|
- จัดชั้นบนสุดอิสระ (กลุ่มกับรายการไม่มีกลุ่มสลับกันได้): `SortableProvider freeLayout` + เรนเดอร์ตาม `rootEntries(order)` (ครอบทั้งหมดด้วย `<SortableGroup itemIds={order.items[UNGROUPED]}>` แล้วแต่ละกลุ่มมี `SortableGroup` ของตัวเองข้างใน) + บันทึก `payload.root` และส่งกลับ `sortableOrderOf(..., { root })` ตอนโหลด · หัวกลุ่มครึ่งบน = ก่อนกลุ่ม, ครึ่งล่าง = เข้ากลุ่ม
|
|
239
|
+
- ห่อ component ที่ props เป็น union (เช่น `DatePicker` ที่มี `mode="range"`): ใช้ `DistributiveOmit`/`DistributivePick` แทน `Omit`/`Pick` ไม่งั้น union ถูกยุบจนแยกตาม `mode` ไม่ได้
|
|
239
240
|
- `sortableOrderToPayload(order)` คืน `{ groups, items }` — `groups` คือลำดับของกลุ่ม, `items` คือรายการในแต่ละกลุ่มเรียงตามนั้น (ของที่ยังไม่มีกลุ่มมาก่อนด้วย `groupId: null`)
|
|
240
241
|
- `SortableProvider disabled` (เช่นระหว่างกรอง ซึ่งลำดับที่เห็นไม่ใช่ลำดับจริง) ทำให้ `DragHandle` จางลงและกดไม่ได้ — ไม่อยากให้เห็นเลยใส่ `hideWhenDisabled`
|
|
241
242
|
- ข้อความ 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.
|
|
3
|
+
"version": "0.47.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 เอง",
|
|
@@ -153,8 +153,6 @@ interface SortableState {
|
|
|
153
153
|
selected: ReadonlySet<string>;
|
|
154
154
|
select: (id: string, mode: SelectMode) => void;
|
|
155
155
|
groupOf: (itemId: string) => string | undefined;
|
|
156
|
-
/** แถวรายการอยู่ใน node ของกลุ่มเองไหม (กลุ่มเป็น <tbody> ทั้งก้อน) — ถ้าใช่ เลื่อนตามพ่ออยู่แล้ว */
|
|
157
|
-
insideGroup: (itemId: string, groupId: string) => boolean;
|
|
158
156
|
register: (kind: SortableKind, id: string, node: HTMLElement | null) => void;
|
|
159
157
|
shifts: ShiftStore;
|
|
160
158
|
}
|
|
@@ -173,7 +171,6 @@ const SortableStateContext = createContext<SortableState>({
|
|
|
173
171
|
selected: EMPTY,
|
|
174
172
|
select: () => {},
|
|
175
173
|
groupOf: () => undefined,
|
|
176
|
-
insideGroup: () => false,
|
|
177
174
|
register: () => {},
|
|
178
175
|
shifts: createShiftStore(),
|
|
179
176
|
});
|
|
@@ -319,6 +316,9 @@ export function SortableProvider({
|
|
|
319
316
|
// กันกระพริบตอนย้ายข้ามกลุ่ม: จำเป้าหมายล่าสุดไว้ใช้ตอนที่ layout กำลังขยับแล้วหาเป้าหมายไม่เจอ
|
|
320
317
|
const lastOverId = useRef<UniqueIdentifier | null>(null);
|
|
321
318
|
const movedAcross = useRef(false);
|
|
319
|
+
// เป้าหมายตอนย้ายข้ามกลุ่มครั้งล่าสุด — ตำแหน่งถูกจัดไว้แล้ว (ก่อน/หลังตามครึ่งแถว) ถ้าปล่อยตอนยังชี้ตัวเดิม
|
|
320
|
+
// ห้าม reorderItems ซ้ำ (arrayMove ไปแทนที่ตัวนั้น = วาง "หลัง" กลายเป็น "ก่อน")
|
|
321
|
+
const crossedAt = useRef<UniqueIdentifier | null>(null);
|
|
322
322
|
const beforeDrag = useRef<SortableOrder | null>(null);
|
|
323
323
|
// อ่านลำดับล่าสุดตอนปล่อย (onDragOver เปลี่ยนไปหลายรอบแล้ว prop ยังไม่ทัน)
|
|
324
324
|
const latest = useRef(order);
|
|
@@ -396,14 +396,16 @@ export function SortableProvider({
|
|
|
396
396
|
} else map.delete(rowId);
|
|
397
397
|
}, []);
|
|
398
398
|
|
|
399
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* กรอบของทั้งก้อน: หัวกลุ่ม + แถวรายการที่แสดงอยู่ (ไม่สนใจ transform ระหว่างลาก)
|
|
401
|
+
* หัวกลุ่มกับแถวรายการต้องเป็นพี่น้องกัน (`<tr>` ใน `<tbody>` เดียวกัน / การ์ดในลิสต์เดียวกัน)
|
|
402
|
+
* ห้ามให้ node ของกลุ่มครอบแถวรายการไว้ ไม่งั้นกรอบนี้นับซ้ำและแถวลูกจะเลื่อนสองชั้น
|
|
403
|
+
*/
|
|
400
404
|
const blockRect = useCallback((groupId: string, header: HTMLElement) => {
|
|
401
405
|
let { top, bottom, left, right } = getClientRect(header, { ignoreTransform: true });
|
|
402
406
|
for (const itemId of latest.current.items[groupId] ?? []) {
|
|
403
407
|
const node = nodes.current.items.get(itemId);
|
|
404
|
-
|
|
405
|
-
// และกรอบของแถวลูกติด transform ของพ่อ (ignoreTransform ไม่ช่วย) → นับซ้ำแล้วก้อนใหญ่เกินจริง
|
|
406
|
-
if (!node?.isConnected || header.contains(node)) continue;
|
|
408
|
+
if (!node?.isConnected) continue;
|
|
407
409
|
const r = getClientRect(node, { ignoreTransform: true });
|
|
408
410
|
top = Math.min(top, r.top);
|
|
409
411
|
bottom = Math.max(bottom, r.bottom);
|
|
@@ -626,6 +628,7 @@ export function SortableProvider({
|
|
|
626
628
|
|
|
627
629
|
function onDragStart({ active }: DragStartEvent) {
|
|
628
630
|
beforeDrag.current = latest.current;
|
|
631
|
+
crossedAt.current = null;
|
|
629
632
|
lastOverId.current = null;
|
|
630
633
|
const kind: SortableKind = kindOf(active.id) === "g" ? "group" : "item";
|
|
631
634
|
const raw = rawOf(active.id);
|
|
@@ -679,6 +682,7 @@ export function SortableProvider({
|
|
|
679
682
|
const raw = rawOf(active.id);
|
|
680
683
|
const apply = (next: SortableOrder) => {
|
|
681
684
|
if (next === current) return;
|
|
685
|
+
crossedAt.current = over.id;
|
|
682
686
|
movedAcross.current = true;
|
|
683
687
|
latest.current = next;
|
|
684
688
|
onOrderChange(next);
|
|
@@ -694,6 +698,8 @@ export function SortableProvider({
|
|
|
694
698
|
const from = containerOf(active.id, current);
|
|
695
699
|
const to = containerOf(over.id, current);
|
|
696
700
|
if (!from || !to || from === to) {
|
|
701
|
+
// ชี้ตัวอื่นในกลุ่มเดียวกันแล้ว = เรียงต่อแบบปกติ (ไม่ใช่ตำแหน่งที่จัดไว้ตอนข้ามกลุ่ม)
|
|
702
|
+
if (over.id !== crossedAt.current) crossedAt.current = null;
|
|
697
703
|
updateTarget(active, over, keyboard, pointerY);
|
|
698
704
|
return;
|
|
699
705
|
}
|
|
@@ -778,7 +784,12 @@ export function SortableProvider({
|
|
|
778
784
|
index,
|
|
779
785
|
);
|
|
780
786
|
}
|
|
781
|
-
} else if (
|
|
787
|
+
} else if (
|
|
788
|
+
over &&
|
|
789
|
+
kindOf(active.id) === "i" &&
|
|
790
|
+
kindOf(over.id) === "i" &&
|
|
791
|
+
over.id !== crossedAt.current
|
|
792
|
+
) {
|
|
782
793
|
next = reorderItems(current, rawOf(active.id), rawOf(over.id));
|
|
783
794
|
}
|
|
784
795
|
// bulk: ตัวที่เลือกไว้ตัวอื่นตามมารวมที่จุดวางของตัวที่จับ (เรียงตามลำดับเดิม)
|
|
@@ -925,11 +936,6 @@ export function SortableProvider({
|
|
|
925
936
|
const groupIds = useMemo(() => order.groups.map((id) => dndId("group", id)), [order.groups]);
|
|
926
937
|
|
|
927
938
|
const groupOf = useCallback((itemId: string) => groupOfItem(order, itemId), [order]);
|
|
928
|
-
const insideGroup = useCallback((itemId: string, groupId: string) => {
|
|
929
|
-
const group = nodes.current.groups.get(groupId);
|
|
930
|
-
const item = nodes.current.items.get(itemId);
|
|
931
|
-
return Boolean(group && item && group !== item && group.contains(item));
|
|
932
|
-
}, []);
|
|
933
939
|
const rootDrag =
|
|
934
940
|
activeDrag !== null &&
|
|
935
941
|
(activeDrag.kind === "group" ||
|
|
@@ -956,7 +962,6 @@ export function SortableProvider({
|
|
|
956
962
|
selected,
|
|
957
963
|
select,
|
|
958
964
|
groupOf,
|
|
959
|
-
insideGroup,
|
|
960
965
|
register,
|
|
961
966
|
shifts,
|
|
962
967
|
}),
|
|
@@ -974,7 +979,6 @@ export function SortableProvider({
|
|
|
974
979
|
selected,
|
|
975
980
|
select,
|
|
976
981
|
groupOf,
|
|
977
|
-
insideGroup,
|
|
978
982
|
register,
|
|
979
983
|
shifts,
|
|
980
984
|
],
|
|
@@ -1124,12 +1128,7 @@ export function useSortableRow({
|
|
|
1124
1128
|
? // มี overlay ลอยตามเมาส์แล้ว แถวต้นทางเหลือเป็นช่องจาง ๆ บอกตำแหน่งที่จะวาง
|
|
1125
1129
|
{ ...style, opacity: 0.4 }
|
|
1126
1130
|
: { ...style, position: "relative", zIndex: 1, opacity: 0.85 };
|
|
1127
|
-
} else if (
|
|
1128
|
-
type === "item" &&
|
|
1129
|
-
state.rootDrag &&
|
|
1130
|
-
shift &&
|
|
1131
|
-
!(groupId && state.insideGroup(id, groupId))
|
|
1132
|
-
) {
|
|
1131
|
+
} else if (type === "item" && state.rootDrag && shift) {
|
|
1133
1132
|
const own = state.activeGroup !== null && state.activeGroup === groupId;
|
|
1134
1133
|
style = {
|
|
1135
1134
|
transform: shift.transform,
|
package/src/index.ts
CHANGED
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type helper ที่แอปใช้ตอนห่อ component ของแพ็กเกจ — ไม่มีโค้ดตอนรัน
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `Omit` ที่ทำทีละสมาชิกของ union — props ของ component ที่มี `mode`/variant
|
|
7
|
+
* (เช่น `DatePickerProps` ที่เป็น `DatePickerSingleProps | DatePickerRangeProps`)
|
|
8
|
+
* ถ้าใช้ `Omit` ปกติจะถูกยุบเป็นก้อนเดียว แยกตาม discriminant ไม่ได้อีก (Pi Auth เจอตอนห่อ `DateField`)
|
|
9
|
+
*
|
|
10
|
+
* type DateFieldProps = DistributiveOmit<DatePickerProps, "locale"> & { label: string };
|
|
11
|
+
* // แยกตาม props.mode ได้ตามปกติ ไม่ต้อง cast
|
|
12
|
+
*/
|
|
13
|
+
export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
14
|
+
|
|
15
|
+
/** `Pick` ที่ทำทีละสมาชิกของ union (คู่กับ `DistributiveOmit`) */
|
|
16
|
+
export type DistributivePick<T, K extends PropertyKey> = T extends unknown
|
|
17
|
+
? Pick<T, Extract<keyof T, K>>
|
|
18
|
+
: never;
|