@musecat/uikit 0.1.1

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.
Files changed (180) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +51 -0
  3. package/index.ts +78 -0
  4. package/package.json +103 -0
  5. package/packages/Components/Box/Box.tsx +173 -0
  6. package/packages/Components/Box/Box.types.ts +48 -0
  7. package/packages/Components/Button/Button.tsx +275 -0
  8. package/packages/Components/Button/Button.types.ts +44 -0
  9. package/packages/Components/Card/Card.default.tsx +180 -0
  10. package/packages/Components/Card/Card.foldable.tsx +383 -0
  11. package/packages/Components/Card/Card.module.scss +23 -0
  12. package/packages/Components/Card/Card.tsx +8 -0
  13. package/packages/Components/Card/Card.types.ts +91 -0
  14. package/packages/Components/Card/Card.utils.ts +66 -0
  15. package/packages/Components/Checkbox/Checkbox.module.scss +11 -0
  16. package/packages/Components/Checkbox/Checkbox.tsx +104 -0
  17. package/packages/Components/Checkbox/Checkbox.types.ts +24 -0
  18. package/packages/Components/CodeBox/CodeBox.copy.tsx +43 -0
  19. package/packages/Components/CodeBox/CodeBox.module.scss +72 -0
  20. package/packages/Components/CodeBox/CodeBox.tsx +91 -0
  21. package/packages/Components/CodeBox/CodeBox.types.ts +17 -0
  22. package/packages/Components/ContextMenu/ContextMenu.module.scss +8 -0
  23. package/packages/Components/ContextMenu/ContextMenu.options.tsx +126 -0
  24. package/packages/Components/ContextMenu/ContextMenu.tsx +381 -0
  25. package/packages/Components/ContextMenu/ContextMenu.types.ts +46 -0
  26. package/packages/Components/ContributionGraph/ContributionGraph.module.scss +42 -0
  27. package/packages/Components/ContributionGraph/ContributionGraph.tsx +454 -0
  28. package/packages/Components/ContributionGraph/ContributionGraph.types.ts +27 -0
  29. package/packages/Components/DatePicker/DatePicker.calendar.tsx +161 -0
  30. package/packages/Components/DatePicker/DatePicker.core.tsx +582 -0
  31. package/packages/Components/DatePicker/DatePicker.module.scss +26 -0
  32. package/packages/Components/DatePicker/DatePicker.tsx +786 -0
  33. package/packages/Components/DatePicker/DatePicker.types.ts +36 -0
  34. package/packages/Components/DatePicker/DatePicker.utils.ts +80 -0
  35. package/packages/Components/DatePicker/hooks/useCalendar.ts +61 -0
  36. package/packages/Components/DatePicker/hooks/useSelection.ts +208 -0
  37. package/packages/Components/Divider/Divider.tsx +46 -0
  38. package/packages/Components/Divider/Divider.types.ts +12 -0
  39. package/packages/Components/Header/Header.module.scss +74 -0
  40. package/packages/Components/Header/Header.tsx +47 -0
  41. package/packages/Components/Header/Header.types.ts +8 -0
  42. package/packages/Components/Icon/Icon.group.tsx +122 -0
  43. package/packages/Components/Icon/Icon.group.types.ts +20 -0
  44. package/packages/Components/Icon/Icon.tsx +341 -0
  45. package/packages/Components/Icon/Icon.types.ts +59 -0
  46. package/packages/Components/Icon/SVG/data.tsx +232 -0
  47. package/packages/Components/Input/Input.module.scss +5 -0
  48. package/packages/Components/Input/Input.tsx +87 -0
  49. package/packages/Components/Input/Input.types.ts +14 -0
  50. package/packages/Components/Label/Label.module.scss +19 -0
  51. package/packages/Components/Label/Label.tsx +115 -0
  52. package/packages/Components/Label/Label.types.ts +28 -0
  53. package/packages/Components/Layout/Layout.docs.module.scss +42 -0
  54. package/packages/Components/Layout/Layout.docs.tsx +150 -0
  55. package/packages/Components/Layout/Layout.docs.types.ts +18 -0
  56. package/packages/Components/Layout/Layout.module.scss +91 -0
  57. package/packages/Components/Layout/Layout.tsx +306 -0
  58. package/packages/Components/Layout/Layout.types.ts +71 -0
  59. package/packages/Components/Maps/OSM/MapOSM.module.scss +11 -0
  60. package/packages/Components/Maps/OSM/MapOSM.shared.ts +121 -0
  61. package/packages/Components/Maps/OSM/MapOSM.tsx +279 -0
  62. package/packages/Components/Maps/OSM/MapOSM.types.ts +18 -0
  63. package/packages/Components/Nav/Nav.module.scss +137 -0
  64. package/packages/Components/Nav/Nav.tsx +304 -0
  65. package/packages/Components/Nav/Nav.types.ts +32 -0
  66. package/packages/Components/Nav/hooks/useNavDrag.ts +374 -0
  67. package/packages/Components/Nav/hooks/useNavIndicator.ts +137 -0
  68. package/packages/Components/Pagination/Pagination.tsx +254 -0
  69. package/packages/Components/Pagination/Pagination.types.ts +18 -0
  70. package/packages/Components/Pill/Pill.tsx +127 -0
  71. package/packages/Components/Pill/Pill.types.ts +33 -0
  72. package/packages/Components/Profile/Profile.shared.ts +74 -0
  73. package/packages/Components/Profile/Profile.tsx +382 -0
  74. package/packages/Components/Profile/Profile.types.ts +55 -0
  75. package/packages/Components/Progress/Progress.module.scss +15 -0
  76. package/packages/Components/Progress/Progress.tsx +88 -0
  77. package/packages/Components/Progress/Progress.types.ts +22 -0
  78. package/packages/Components/Radio/Radio.module.scss +11 -0
  79. package/packages/Components/Radio/Radio.tsx +121 -0
  80. package/packages/Components/Radio/Radio.types.ts +20 -0
  81. package/packages/Components/Select/Select.keyboard.tsx +76 -0
  82. package/packages/Components/Select/Select.trigger.tsx +88 -0
  83. package/packages/Components/Select/Select.tsx +11 -0
  84. package/packages/Components/Select/Select.types.ts +38 -0
  85. package/packages/Components/Select/Select.utils.ts +84 -0
  86. package/packages/Components/Select/hooks/useSelectNavigation.ts +461 -0
  87. package/packages/Components/Select/hooks/useSelectSelection.ts +142 -0
  88. package/packages/Components/Select/hooks/useSelectState.ts +314 -0
  89. package/packages/Components/Select/select.control.tsx +228 -0
  90. package/packages/Components/Select/select.inner.tsx +312 -0
  91. package/packages/Components/Skeleton/Skeleton.tsx +30 -0
  92. package/packages/Components/Skeleton/Skeleton.types.ts +12 -0
  93. package/packages/Components/Spinner/Spinner.module.scss +78 -0
  94. package/packages/Components/Spinner/Spinner.tsx +125 -0
  95. package/packages/Components/Spinner/Spinner.types.ts +19 -0
  96. package/packages/Components/Text/Text.module.scss +7 -0
  97. package/packages/Components/Text/Text.tsx +49 -0
  98. package/packages/Components/Text/Text.types.ts +33 -0
  99. package/packages/Components/TimePicker/TimePicker.core.tsx +448 -0
  100. package/packages/Components/TimePicker/TimePicker.module.scss +30 -0
  101. package/packages/Components/TimePicker/TimePicker.tsx +511 -0
  102. package/packages/Components/TimePicker/TimePicker.types.ts +22 -0
  103. package/packages/Components/Timeline/Timeline.tsx +111 -0
  104. package/packages/Components/Timeline/Timeline.types.ts +23 -0
  105. package/packages/Components/Title/Title.tsx +305 -0
  106. package/packages/Components/Title/Title.types.ts +64 -0
  107. package/packages/Components/Toggle/Toggle.module.scss +32 -0
  108. package/packages/Components/Toggle/Toggle.tsx +272 -0
  109. package/packages/Components/Toggle/Toggle.types.ts +17 -0
  110. package/packages/Components/Tooltip/Tooltip.module.scss +44 -0
  111. package/packages/Components/Tooltip/Tooltip.tsx +340 -0
  112. package/packages/Components/Tooltip/Tooltip.types.ts +38 -0
  113. package/packages/Frameworks/Dialog/Dialog.background.tsx +88 -0
  114. package/packages/Frameworks/Dialog/Dialog.boot.tsx +32 -0
  115. package/packages/Frameworks/Dialog/Dialog.store.ts +199 -0
  116. package/packages/Frameworks/Dialog/Dialog.tsx +185 -0
  117. package/packages/Frameworks/Dialog/Dialog.types.ts +264 -0
  118. package/packages/Frameworks/Dialog/Dialog.utils.ts +22 -0
  119. package/packages/Frameworks/Dialog/contents/Dialog.footer.tsx +79 -0
  120. package/packages/Frameworks/Dialog/contents/Dialog.funnel.tsx +260 -0
  121. package/packages/Frameworks/Dialog/contents/Dialog.header.tsx +137 -0
  122. package/packages/Frameworks/Dialog/hooks/useDialogPosition.ts +298 -0
  123. package/packages/Frameworks/Dialog/hooks/useSheetDrag.ts +325 -0
  124. package/packages/Frameworks/Dialog/hooks/useSheetGeometry.ts +201 -0
  125. package/packages/Frameworks/Dialog/hooks/useSheetProgressive.ts +98 -0
  126. package/packages/Frameworks/Dialog/renderers/renderModal.tsx +228 -0
  127. package/packages/Frameworks/Dialog/renderers/renderPopover.tsx +220 -0
  128. package/packages/Frameworks/Dialog/renderers/renderSheet.tsx +571 -0
  129. package/packages/Frameworks/EdgeEffect/EdgeEffect.tsx +52 -0
  130. package/packages/Frameworks/EdgeEffect/EdgeEffect.types.ts +5 -0
  131. package/packages/Frameworks/Motion/Motion.presets.ts +49 -0
  132. package/packages/Frameworks/Pressable/Pressable.module.scss +19 -0
  133. package/packages/Frameworks/Pressable/Pressable.tsx +651 -0
  134. package/packages/Frameworks/Pressable/Pressable.types.ts +73 -0
  135. package/packages/Frameworks/Squircle/Squircle.tsx +263 -0
  136. package/packages/Frameworks/Squircle/Squircle.types.ts +15 -0
  137. package/packages/Frameworks/Theme/Radius.types.ts +55 -0
  138. package/packages/Frameworks/Theme/Theme.types.ts +325 -0
  139. package/packages/Frameworks/Toaster/Toaster.boot.tsx +47 -0
  140. package/packages/Frameworks/Toaster/Toaster.module.scss +168 -0
  141. package/packages/Frameworks/Toaster/Toaster.types.ts +13 -0
  142. package/packages/Frameworks/View/DNDView/DNDView.module.scss +8 -0
  143. package/packages/Frameworks/View/DNDView/DNDView.tsx +470 -0
  144. package/packages/Frameworks/View/DNDView/DNDView.types.ts +61 -0
  145. package/packages/Frameworks/View/HScrollView/HScrollView.module.scss +59 -0
  146. package/packages/Frameworks/View/HScrollView/HScrollView.tsx +386 -0
  147. package/packages/Frameworks/View/HScrollView/HScrollView.types.ts +46 -0
  148. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.counter.tsx +27 -0
  149. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.footer.tsx +76 -0
  150. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.header.tsx +49 -0
  151. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.list.tsx +74 -0
  152. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.slide.tsx +71 -0
  153. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.tsx +175 -0
  154. package/packages/Frameworks/View/ImageView/Image.controls.tsx +39 -0
  155. package/packages/Frameworks/View/ImageView/Image.hooks.ts +82 -0
  156. package/packages/Frameworks/View/ImageView/Image.module.scss +7 -0
  157. package/packages/Frameworks/View/ImageView/Image.tsx +375 -0
  158. package/packages/Frameworks/View/ImageView/Image.types.ts +71 -0
  159. package/packages/Frameworks/View/ImageView/Image.utils.ts +79 -0
  160. package/packages/Frameworks/View/View.module.scss +34 -0
  161. package/packages/Frameworks/View/View.tsx +200 -0
  162. package/packages/Frameworks/View/View.types.ts +36 -0
  163. package/packages/Frameworks/_shared/Padding.types.ts +52 -0
  164. package/packages/Frameworks/_shared/StopParentInteraction.tsx +45 -0
  165. package/packages/Frameworks/_shared/Wind.types.ts +17 -0
  166. package/packages/Frameworks/_shared/bodyScrollLock.ts +127 -0
  167. package/packages/Frameworks/_shared/layer.constants.ts +8 -0
  168. package/packages/Frameworks/_shared/normalize.ts +39 -0
  169. package/packages/Frameworks/_shared/sizing.ts +55 -0
  170. package/packages/Frameworks/_shared/useControllableState.ts +29 -0
  171. package/packages/Styles/_animation.scss +42 -0
  172. package/packages/Styles/_color.scss +131 -0
  173. package/packages/Styles/_font.scss +30 -0
  174. package/packages/Styles/_icon.scss +782 -0
  175. package/packages/Styles/_importer.scss +9 -0
  176. package/packages/Styles/_system.scss +69 -0
  177. package/packages/Styles/_textstyle.scss +111 -0
  178. package/packages/Styles/_theme.scss +452 -0
  179. package/packages/Styles/_viewport-global.scss +99 -0
  180. package/packages/Styles/_viewport.scss +108 -0
@@ -0,0 +1,470 @@
1
+ "use client";
2
+
3
+ import {
4
+ closestCenter,
5
+ DndContext,
6
+ type DragEndEvent,
7
+ DragOverlay,
8
+ type DragStartEvent,
9
+ MouseSensor,
10
+ PointerSensor,
11
+ TouchSensor,
12
+ type UniqueIdentifier,
13
+ useSensor,
14
+ useSensors,
15
+ } from "@dnd-kit/core";
16
+ import {
17
+ arrayMove,
18
+ horizontalListSortingStrategy,
19
+ rectSortingStrategy,
20
+ SortableContext,
21
+ useSortable,
22
+ verticalListSortingStrategy,
23
+ } from "@dnd-kit/sortable";
24
+ import { CSS } from "@dnd-kit/utilities";
25
+ import clsx from "clsx";
26
+ import {
27
+ type CSSProperties,
28
+ type HTMLAttributes,
29
+ type KeyboardEvent,
30
+ useCallback,
31
+ useId,
32
+ useMemo,
33
+ useRef,
34
+ useState,
35
+ } from "react";
36
+ import Icon from "@/packages/Components/Icon/Icon";
37
+ import type { PressableProps } from "@/packages/Frameworks/Pressable/Pressable.types";
38
+ import styles from "@/packages/Frameworks/View/DNDView/DNDView.module.scss";
39
+ import type {
40
+ DNDViewItemState,
41
+ DNDViewProps,
42
+ DNDViewStrategy,
43
+ } from "@/packages/Frameworks/View/DNDView/DNDView.types";
44
+ import View from "@/packages/Frameworks/View/View";
45
+
46
+ const SORTING_STRATEGY: Record<DNDViewStrategy, typeof rectSortingStrategy> = {
47
+ horizontal: horizontalListSortingStrategy,
48
+ rect: rectSortingStrategy,
49
+ vertical: verticalListSortingStrategy,
50
+ };
51
+ const DROP_ANIMATION = {
52
+ duration: 180,
53
+ easing: "cubic-bezier(.2, 0, 0, 1)",
54
+ };
55
+ const DIRECTION_KEY = {
56
+ down: "ArrowDown",
57
+ left: "ArrowLeft",
58
+ right: "ArrowRight",
59
+ up: "ArrowUp",
60
+ } as const;
61
+ const PICKUP_KEY = {
62
+ enter: "Enter",
63
+ space: "Space",
64
+ } as const;
65
+
66
+ function translateOnly(
67
+ transform: Parameters<typeof CSS.Transform.toString>[0],
68
+ ) {
69
+ if (!transform) return undefined;
70
+
71
+ return CSS.Transform.toString({
72
+ ...transform,
73
+ scaleX: 1,
74
+ scaleY: 1,
75
+ });
76
+ }
77
+
78
+ function getClosestVerticalId(
79
+ ids: UniqueIdentifier[],
80
+ nodes: Map<UniqueIdentifier, HTMLDivElement>,
81
+ id: UniqueIdentifier,
82
+ direction: "up" | "down",
83
+ ) {
84
+ const currentNode = nodes.get(id);
85
+ if (!currentNode) return undefined;
86
+
87
+ const currentRect = currentNode.getBoundingClientRect();
88
+ const currentCenterX = currentRect.left + currentRect.width / 2;
89
+
90
+ return ids
91
+ .reduce<
92
+ {
93
+ distanceX: number;
94
+ distanceY: number;
95
+ id: UniqueIdentifier;
96
+ }[]
97
+ >((candidates, candidateId) => {
98
+ if (candidateId === id) return candidates;
99
+
100
+ const rect = nodes.get(candidateId)?.getBoundingClientRect();
101
+ if (!rect) return candidates;
102
+
103
+ const isCandidate =
104
+ direction === "down"
105
+ ? rect.top > currentRect.top
106
+ : rect.top < currentRect.top;
107
+ if (!isCandidate) return candidates;
108
+
109
+ candidates.push({
110
+ id: candidateId,
111
+ distanceY: Math.abs(rect.top - currentRect.top),
112
+ distanceX: Math.abs(rect.left + rect.width / 2 - currentCenterX),
113
+ });
114
+ return candidates;
115
+ }, [])
116
+ .sort((a, b) => a.distanceY - b.distanceY || a.distanceX - b.distanceX)[0]
117
+ ?.id;
118
+ }
119
+
120
+ type SortableItemProps<T> = {
121
+ dragHandle?: boolean;
122
+ item: T;
123
+ id: UniqueIdentifier;
124
+ index: number;
125
+ itemStyle?: CSSProperties;
126
+ keyboardDragging?: boolean;
127
+ onItemKeyDown?: (
128
+ event: KeyboardEvent<HTMLDivElement>,
129
+ id: UniqueIdentifier,
130
+ ) => void;
131
+ registerNode?: (id: UniqueIdentifier, node: HTMLDivElement | null) => void;
132
+ renderItem: DNDViewProps<T>["renderItem"];
133
+ renderHandle?: DNDViewProps<T>["renderHandle"];
134
+ };
135
+
136
+ function DefaultHandle(props: HTMLAttributes<HTMLElement>) {
137
+ const { color: _color, ...handleProps } = props;
138
+ return (
139
+ <Icon
140
+ icon="iMenu"
141
+ box
142
+ className={clsx(styles.Handle, props.className)}
143
+ pressable={
144
+ {
145
+ ...handleProps,
146
+ type: "button",
147
+ "aria-label": handleProps["aria-label"] ?? "Drag item",
148
+ } as PressableProps
149
+ }
150
+ />
151
+ );
152
+ }
153
+
154
+ function SortableItem<T>({
155
+ dragHandle,
156
+ item,
157
+ id,
158
+ index,
159
+ itemStyle,
160
+ keyboardDragging,
161
+ onItemKeyDown,
162
+ registerNode,
163
+ renderItem,
164
+ renderHandle,
165
+ }: SortableItemProps<T>) {
166
+ const {
167
+ attributes,
168
+ listeners,
169
+ setNodeRef,
170
+ setActivatorNodeRef,
171
+ transform,
172
+ transition,
173
+ isDragging,
174
+ isSorting,
175
+ } = useSortable({ id });
176
+ const style = useMemo(
177
+ () => ({
178
+ ...itemStyle,
179
+ transform: translateOnly(transform),
180
+ transition,
181
+ opacity: isDragging ? 0 : itemStyle?.opacity,
182
+ }),
183
+ [isDragging, itemStyle, transform, transition],
184
+ );
185
+ const sortableProps = {
186
+ ...attributes,
187
+ ...listeners,
188
+ };
189
+ const handleProps = {
190
+ ...sortableProps,
191
+ ref: setActivatorNodeRef,
192
+ } as HTMLAttributes<HTMLElement>;
193
+ const setItemNodeRef = useCallback(
194
+ (node: HTMLDivElement | null) => {
195
+ setNodeRef(node);
196
+ registerNode?.(id, node);
197
+ },
198
+ [id, registerNode, setNodeRef],
199
+ );
200
+ const handleKeyDown = useCallback(
201
+ (event: KeyboardEvent<HTMLDivElement>) => {
202
+ onItemKeyDown?.(event, id);
203
+ },
204
+ [id, onItemKeyDown],
205
+ );
206
+ const state = {
207
+ item,
208
+ id,
209
+ index,
210
+ dragging: isDragging || Boolean(keyboardDragging),
211
+ sorting: isSorting,
212
+ handle: null,
213
+ handleProps,
214
+ } satisfies DNDViewItemState<T>;
215
+ const handle = dragHandle ? (
216
+ renderHandle ? (
217
+ renderHandle(handleProps, state)
218
+ ) : (
219
+ <DefaultHandle {...handleProps} />
220
+ )
221
+ ) : null;
222
+ const resolvedState = {
223
+ ...state,
224
+ handle,
225
+ };
226
+
227
+ return (
228
+ <View
229
+ ref={setItemNodeRef}
230
+ style={{
231
+ transformOrigin: "center",
232
+ touchAction: !dragHandle ? "none" : "pan-y",
233
+ zIndex: isDragging || keyboardDragging ? 1 : undefined,
234
+ ...style,
235
+ }}
236
+ onKeyDown={handleKeyDown}
237
+ {...(!dragHandle ? sortableProps : undefined)}
238
+ >
239
+ {renderItem(item, resolvedState)}
240
+ </View>
241
+ );
242
+ }
243
+
244
+ export default function DNDView<T>({
245
+ items,
246
+ onReorder,
247
+ getKey,
248
+ renderItem,
249
+ renderHandle,
250
+ disabled,
251
+ dragHandle = true,
252
+ keyboard = true,
253
+ strategy = "vertical",
254
+ activationDistance = 6,
255
+ touchActivationDelay = 120,
256
+ touchActivationTolerance = 8,
257
+ className,
258
+ style,
259
+ alignItems,
260
+ justifyContent,
261
+ gap = 8,
262
+ wrap,
263
+ fullWidth,
264
+ gridTemplateColumns,
265
+ gridTemplateRows,
266
+ gridAutoFlow,
267
+ width,
268
+ height,
269
+ padding,
270
+ margin,
271
+ itemStyle,
272
+ ...viewProps
273
+ }: DNDViewProps<T>) {
274
+ const dndContextId = useId();
275
+ const itemNodesRef = useRef(new Map<UniqueIdentifier, HTMLDivElement>());
276
+ const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
277
+ const [keyboardActiveId, setKeyboardActiveId] =
278
+ useState<UniqueIdentifier | null>(null);
279
+ const ids = useMemo(() => items.map(getKey), [getKey, items]);
280
+ const activeIndex = activeId ? ids.indexOf(activeId) : -1;
281
+ const activeItem = activeIndex >= 0 ? items[activeIndex] : undefined;
282
+ const pointerSensor = useSensor(PointerSensor, {
283
+ activationConstraint: {
284
+ distance: activationDistance,
285
+ },
286
+ });
287
+ const mouseSensor = useSensor(MouseSensor, {
288
+ activationConstraint: {
289
+ distance: activationDistance,
290
+ },
291
+ });
292
+ const touchSensor = useSensor(TouchSensor, {
293
+ activationConstraint: {
294
+ delay: touchActivationDelay,
295
+ tolerance: touchActivationTolerance,
296
+ },
297
+ });
298
+ const sensors = useSensors(pointerSensor, mouseSensor, touchSensor);
299
+ const isGrid = Boolean(
300
+ gridTemplateColumns || gridTemplateRows || gridAutoFlow,
301
+ );
302
+ const isHorizontal = strategy === "horizontal";
303
+ const isRect = strategy === "rect";
304
+ const sortingStrategy = SORTING_STRATEGY[strategy];
305
+ const isKeyboardEnabled = keyboard && !disabled;
306
+ const registerItemNode = useCallback(
307
+ (id: UniqueIdentifier, node: HTMLDivElement | null) => {
308
+ if (node) {
309
+ itemNodesRef.current.set(id, node);
310
+ return;
311
+ }
312
+
313
+ itemNodesRef.current.delete(id);
314
+ },
315
+ [],
316
+ );
317
+
318
+ const handleKeyboardReorder = useCallback(
319
+ (event: KeyboardEvent<HTMLDivElement>, id: UniqueIdentifier) => {
320
+ if (!isKeyboardEnabled) return;
321
+
322
+ if (event.code === PICKUP_KEY.space || event.code === PICKUP_KEY.enter) {
323
+ event.preventDefault();
324
+ setKeyboardActiveId((currentId) => (currentId === id ? null : id));
325
+ return;
326
+ }
327
+
328
+ if (keyboardActiveId !== id) return;
329
+
330
+ const currentIndex = ids.indexOf(id);
331
+ const targetId = isRect
332
+ ? event.code === DIRECTION_KEY.right
333
+ ? ids[currentIndex + 1]
334
+ : event.code === DIRECTION_KEY.left
335
+ ? ids[currentIndex - 1]
336
+ : event.code === DIRECTION_KEY.down
337
+ ? getClosestVerticalId(ids, itemNodesRef.current, id, "down")
338
+ : event.code === DIRECTION_KEY.up
339
+ ? getClosestVerticalId(ids, itemNodesRef.current, id, "up")
340
+ : undefined
341
+ : event.code === DIRECTION_KEY.down ||
342
+ event.code === DIRECTION_KEY.right
343
+ ? ids[currentIndex + 1]
344
+ : event.code === DIRECTION_KEY.up || event.code === DIRECTION_KEY.left
345
+ ? ids[currentIndex - 1]
346
+ : undefined;
347
+ if (targetId === undefined) return;
348
+
349
+ event.preventDefault();
350
+
351
+ const targetIndex = ids.indexOf(targetId);
352
+ if (targetIndex < 0 || currentIndex < 0 || targetIndex === currentIndex) {
353
+ return;
354
+ }
355
+
356
+ onReorder(arrayMove(items, currentIndex, targetIndex));
357
+ },
358
+ [ids, isKeyboardEnabled, isRect, items, keyboardActiveId, onReorder],
359
+ );
360
+
361
+ const handleDragStart = useCallback(({ active }: DragStartEvent) => {
362
+ setKeyboardActiveId(null);
363
+ setActiveId(active.id);
364
+ }, []);
365
+
366
+ const handleDragEnd = useCallback(
367
+ ({ active, over }: DragEndEvent) => {
368
+ setActiveId(null);
369
+ if (!over || active.id === over.id) return;
370
+
371
+ const oldIndex = ids.indexOf(active.id);
372
+ const newIndex = ids.indexOf(over.id);
373
+ if (oldIndex < 0 || newIndex < 0) return;
374
+ onReorder(arrayMove(items, oldIndex, newIndex));
375
+ },
376
+ [ids, items, onReorder],
377
+ );
378
+
379
+ const handleDragCancel = useCallback(() => {
380
+ setActiveId(null);
381
+ }, []);
382
+
383
+ const renderOverlay = () => {
384
+ if (!activeItem || activeId == null) return null;
385
+
386
+ const handleProps = {
387
+ "aria-label": "Drag item",
388
+ } as HTMLAttributes<HTMLElement>;
389
+ const state = {
390
+ item: activeItem,
391
+ id: activeId,
392
+ index: activeIndex,
393
+ dragging: true,
394
+ sorting: false,
395
+ handle: null,
396
+ handleProps,
397
+ } satisfies DNDViewItemState<T>;
398
+ const handle = dragHandle ? (
399
+ renderHandle ? (
400
+ renderHandle(handleProps, state)
401
+ ) : (
402
+ <DefaultHandle {...handleProps} />
403
+ )
404
+ ) : null;
405
+
406
+ return renderItem(activeItem, {
407
+ ...state,
408
+ handle,
409
+ });
410
+ };
411
+
412
+ return (
413
+ <DndContext
414
+ id={dndContextId}
415
+ sensors={sensors}
416
+ collisionDetection={closestCenter}
417
+ onDragStart={handleDragStart}
418
+ onDragEnd={handleDragEnd}
419
+ onDragCancel={handleDragCancel}
420
+ >
421
+ <SortableContext
422
+ items={ids}
423
+ strategy={sortingStrategy}
424
+ disabled={disabled}
425
+ >
426
+ <View
427
+ {...viewProps}
428
+ className={className}
429
+ style={{ touchAction: "pan-y", userSelect: "none", ...style }}
430
+ alignItems={alignItems}
431
+ justifyContent={justifyContent}
432
+ column={!isGrid && !isHorizontal && !isRect}
433
+ row={!isGrid && (isHorizontal || isRect)}
434
+ gap={gap}
435
+ wrap={isRect ? (wrap ?? "wrap") : wrap}
436
+ fullWidth={fullWidth}
437
+ gridTemplateColumns={gridTemplateColumns}
438
+ gridTemplateRows={gridTemplateRows}
439
+ gridAutoFlow={gridAutoFlow}
440
+ width={width}
441
+ height={height}
442
+ padding={padding}
443
+ margin={margin}
444
+ >
445
+ {items.map((item, index) => {
446
+ const id = getKey(item);
447
+ return (
448
+ <SortableItem
449
+ key={id}
450
+ item={item}
451
+ dragHandle={dragHandle}
452
+ id={id}
453
+ index={index}
454
+ itemStyle={itemStyle}
455
+ keyboardDragging={keyboardActiveId === id}
456
+ onItemKeyDown={handleKeyboardReorder}
457
+ registerNode={registerItemNode}
458
+ renderItem={renderItem}
459
+ renderHandle={renderHandle}
460
+ />
461
+ );
462
+ })}
463
+ </View>
464
+ </SortableContext>
465
+ <DragOverlay dropAnimation={DROP_ANIMATION}>
466
+ {renderOverlay()}
467
+ </DragOverlay>
468
+ </DndContext>
469
+ );
470
+ }
@@ -0,0 +1,61 @@
1
+ import type { UniqueIdentifier } from "@dnd-kit/core";
2
+ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
3
+ import type { UIKitSizeValue } from "@/packages/Frameworks/_shared/sizing";
4
+ import type { ViewProps } from "@/packages/Frameworks/View/View.types";
5
+
6
+ export type DNDViewItemState<T> = {
7
+ item: T;
8
+ id: UniqueIdentifier;
9
+ index: number;
10
+ dragging: boolean;
11
+ sorting: boolean;
12
+ handle: ReactNode;
13
+ handleProps: HTMLAttributes<HTMLElement>;
14
+ };
15
+
16
+ export type DNDViewStrategy = "vertical" | "horizontal" | "rect";
17
+
18
+ export interface DNDViewProps<T>
19
+ extends Pick<
20
+ ViewProps,
21
+ | "className"
22
+ | "style"
23
+ | "alignItems"
24
+ | "justifyContent"
25
+ | "gap"
26
+ | "wrap"
27
+ | "fullWidth"
28
+ | "gridTemplateColumns"
29
+ | "gridTemplateRows"
30
+ | "gridAutoFlow"
31
+ | "background"
32
+ | "color"
33
+ | "themePreset"
34
+ | "themeInteractive"
35
+ | "selected"
36
+ | "border"
37
+ | "radius"
38
+ | "backgroundBlur"
39
+ | "data-color-mode"
40
+ > {
41
+ items: T[];
42
+ onReorder: (items: T[]) => void;
43
+ getKey: (item: T) => UniqueIdentifier;
44
+ renderItem: (item: T, state: DNDViewItemState<T>) => ReactNode;
45
+ renderHandle?: (
46
+ props: HTMLAttributes<HTMLElement>,
47
+ state: DNDViewItemState<T>,
48
+ ) => ReactNode;
49
+ disabled?: boolean;
50
+ dragHandle?: boolean;
51
+ keyboard?: boolean;
52
+ strategy?: DNDViewStrategy;
53
+ activationDistance?: number;
54
+ touchActivationDelay?: number;
55
+ touchActivationTolerance?: number;
56
+ width?: UIKitSizeValue;
57
+ height?: UIKitSizeValue;
58
+ padding?: UIKitSizeValue;
59
+ margin?: UIKitSizeValue;
60
+ itemStyle?: CSSProperties;
61
+ }
@@ -0,0 +1,59 @@
1
+ .Viewport {
2
+ width: 100%;
3
+ min-width: 0;
4
+ overflow: visible;
5
+ }
6
+
7
+ [data-h-scroll-active="true"] .Viewport {
8
+ width: 100%;
9
+ overflow: auto hidden;
10
+ overscroll-behavior-x: contain;
11
+ -webkit-overflow-scrolling: touch;
12
+ }
13
+
14
+ [data-h-scroll-active="true"].SingleChild .Viewport {
15
+ width: 100%;
16
+ margin-inline: 0;
17
+ padding-inline: 0;
18
+ cursor: grab;
19
+ }
20
+
21
+ .Viewport[data-embla-active="true"] {
22
+ overflow: hidden;
23
+ }
24
+
25
+ .Container {
26
+ display: flex;
27
+ min-width: 0;
28
+ gap: var(--view-default-gap, 1.2rem);
29
+ }
30
+
31
+ [data-h-scroll-active="true"] .Container {
32
+ touch-action: pan-y pinch-zoom;
33
+ }
34
+
35
+ .Container > * {
36
+ min-width: 0;
37
+ }
38
+
39
+ [data-h-scroll-active="true"] .Container > * {
40
+ flex: 0 0 auto;
41
+ width: var(--h-scroll-item-width, auto);
42
+ height: var(--h-scroll-item-height, auto);
43
+ }
44
+
45
+ .FullWidth .Container > * {
46
+ width: 100%;
47
+ }
48
+
49
+ /* Hide scrollbars */
50
+ .Viewport::-webkit-scrollbar {
51
+ width: 0;
52
+ height: 0;
53
+ display: none;
54
+ }
55
+
56
+ .Viewport {
57
+ scrollbar-width: none;
58
+ -ms-overflow-style: none;
59
+ }