@suphark/ui 0.45.2 → 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 +11 -1
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/components/sortable/sortable-list.tsx +7 -20
- package/src/index.ts +1 -0
- package/src/lib/types.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
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
|
+
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
8
18
|
|
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
|
});
|
|
@@ -399,14 +396,16 @@ export function SortableProvider({
|
|
|
399
396
|
} else map.delete(rowId);
|
|
400
397
|
}, []);
|
|
401
398
|
|
|
402
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* กรอบของทั้งก้อน: หัวกลุ่ม + แถวรายการที่แสดงอยู่ (ไม่สนใจ transform ระหว่างลาก)
|
|
401
|
+
* หัวกลุ่มกับแถวรายการต้องเป็นพี่น้องกัน (`<tr>` ใน `<tbody>` เดียวกัน / การ์ดในลิสต์เดียวกัน)
|
|
402
|
+
* ห้ามให้ node ของกลุ่มครอบแถวรายการไว้ ไม่งั้นกรอบนี้นับซ้ำและแถวลูกจะเลื่อนสองชั้น
|
|
403
|
+
*/
|
|
403
404
|
const blockRect = useCallback((groupId: string, header: HTMLElement) => {
|
|
404
405
|
let { top, bottom, left, right } = getClientRect(header, { ignoreTransform: true });
|
|
405
406
|
for (const itemId of latest.current.items[groupId] ?? []) {
|
|
406
407
|
const node = nodes.current.items.get(itemId);
|
|
407
|
-
|
|
408
|
-
// และกรอบของแถวลูกติด transform ของพ่อ (ignoreTransform ไม่ช่วย) → นับซ้ำแล้วก้อนใหญ่เกินจริง
|
|
409
|
-
if (!node?.isConnected || header.contains(node)) continue;
|
|
408
|
+
if (!node?.isConnected) continue;
|
|
410
409
|
const r = getClientRect(node, { ignoreTransform: true });
|
|
411
410
|
top = Math.min(top, r.top);
|
|
412
411
|
bottom = Math.max(bottom, r.bottom);
|
|
@@ -937,11 +936,6 @@ export function SortableProvider({
|
|
|
937
936
|
const groupIds = useMemo(() => order.groups.map((id) => dndId("group", id)), [order.groups]);
|
|
938
937
|
|
|
939
938
|
const groupOf = useCallback((itemId: string) => groupOfItem(order, itemId), [order]);
|
|
940
|
-
const insideGroup = useCallback((itemId: string, groupId: string) => {
|
|
941
|
-
const group = nodes.current.groups.get(groupId);
|
|
942
|
-
const item = nodes.current.items.get(itemId);
|
|
943
|
-
return Boolean(group && item && group !== item && group.contains(item));
|
|
944
|
-
}, []);
|
|
945
939
|
const rootDrag =
|
|
946
940
|
activeDrag !== null &&
|
|
947
941
|
(activeDrag.kind === "group" ||
|
|
@@ -968,7 +962,6 @@ export function SortableProvider({
|
|
|
968
962
|
selected,
|
|
969
963
|
select,
|
|
970
964
|
groupOf,
|
|
971
|
-
insideGroup,
|
|
972
965
|
register,
|
|
973
966
|
shifts,
|
|
974
967
|
}),
|
|
@@ -986,7 +979,6 @@ export function SortableProvider({
|
|
|
986
979
|
selected,
|
|
987
980
|
select,
|
|
988
981
|
groupOf,
|
|
989
|
-
insideGroup,
|
|
990
982
|
register,
|
|
991
983
|
shifts,
|
|
992
984
|
],
|
|
@@ -1136,12 +1128,7 @@ export function useSortableRow({
|
|
|
1136
1128
|
? // มี overlay ลอยตามเมาส์แล้ว แถวต้นทางเหลือเป็นช่องจาง ๆ บอกตำแหน่งที่จะวาง
|
|
1137
1129
|
{ ...style, opacity: 0.4 }
|
|
1138
1130
|
: { ...style, position: "relative", zIndex: 1, opacity: 0.85 };
|
|
1139
|
-
} else if (
|
|
1140
|
-
type === "item" &&
|
|
1141
|
-
state.rootDrag &&
|
|
1142
|
-
shift &&
|
|
1143
|
-
!(groupId && state.insideGroup(id, groupId))
|
|
1144
|
-
) {
|
|
1131
|
+
} else if (type === "item" && state.rootDrag && shift) {
|
|
1145
1132
|
const own = state.activeGroup !== null && state.activeGroup === groupId;
|
|
1146
1133
|
style = {
|
|
1147
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;
|