@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 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.45.2] — 2026-10-08
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.45.2",
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
- /** กรอบของทั้งก้อน: หัวกลุ่ม + แถวรายการที่แสดงอยู่ (ไม่สนใจ transform ระหว่างลาก) */
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
- // แถวที่อยู่ "ใน" node ของกลุ่มเอง (แอปแปะ ref กลุ่มที่ <tbody>) — กรอบของกลุ่มครอบอยู่แล้ว
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
@@ -730,6 +730,7 @@ export {
730
730
  statusColorThemes,
731
731
  TAILWIND_COLORS,
732
732
  } from "./lib/status-colors";
733
+ export type { DistributiveOmit, DistributivePick } from "./lib/types";
733
734
  export {
734
735
  TYPOGRAPHY_ROLES,
735
736
  type TypographyRole,
@@ -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;