@noya-app/noya-designsystem 0.1.54 → 0.1.56

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.
@@ -0,0 +1,404 @@
1
+ import { UniqueIdentifier, useDndContext, useDroppable } from "@dnd-kit/core";
2
+ import {
3
+ horizontalListSortingStrategy,
4
+ SortableContext,
5
+ useSortable,
6
+ verticalListSortingStrategy,
7
+ } from "@dnd-kit/sortable";
8
+ import * as React from "react";
9
+ import {
10
+ ActiveDragContext,
11
+ ActiveDragContextType,
12
+ DragRegistrationContext,
13
+ RegisteredList,
14
+ SharedDragProviderContexts,
15
+ useActiveDrag,
16
+ useDragRegistration,
17
+ } from "./DragRegistration";
18
+ import { withDragProvider } from "./SharedDragProvider";
19
+ import {
20
+ AcceptsFromList,
21
+ defaultAcceptsDrop,
22
+ DropValidator,
23
+ MoveDragItemHandler,
24
+ RelativeDropPosition,
25
+ validateDropIndicator,
26
+ } from "./sorting";
27
+
28
+ type UseSortableReturnType = ReturnType<typeof useSortable>;
29
+
30
+ type ItemChildrenProps<T> = {
31
+ ref: React.Ref<T>;
32
+ relativeDropPosition?: RelativeDropPosition;
33
+ style?: React.CSSProperties;
34
+ } & UseSortableReturnType["attributes"];
35
+
36
+ // Context for drop indicators within a sortable list
37
+ interface SortableItemContextProps {
38
+ keys: UniqueIdentifier[];
39
+ acceptsDrop: DropValidator;
40
+ axis: "x" | "y";
41
+ listId: string;
42
+ getDropTargetParentIndex?: (index: number) => number | undefined;
43
+ activeDragContext: React.Context<ActiveDragContextType | null>;
44
+ }
45
+
46
+ const SortableItemContext = React.createContext<SortableItemContextProps>({
47
+ keys: [],
48
+ acceptsDrop: defaultAcceptsDrop,
49
+ axis: "y",
50
+ listId: "",
51
+ getDropTargetParentIndex: undefined,
52
+ activeDragContext: ActiveDragContext,
53
+ });
54
+
55
+ // Custom hook for drop indicators in sortable lists
56
+ function useSortableDropIndicator({
57
+ id,
58
+ index,
59
+ isDragging,
60
+ overIndex,
61
+ activeIndex,
62
+ keys,
63
+ acceptsDrop,
64
+ axis,
65
+ listId,
66
+ getDropTargetParentIndex,
67
+ }: {
68
+ id: UniqueIdentifier;
69
+ index: number;
70
+ isDragging: boolean;
71
+ overIndex: number;
72
+ activeIndex: number;
73
+ keys: UniqueIdentifier[];
74
+ acceptsDrop: DropValidator;
75
+ axis: "x" | "y";
76
+ listId: string;
77
+ getDropTargetParentIndex?: (index: number) => number | undefined;
78
+ }): RelativeDropPosition | undefined {
79
+ const { active, over, activatorEvent } = useDndContext();
80
+ const { activeDragContext } = React.useContext(SortableItemContext);
81
+ const { activeItem, delta: position } = useActiveDrag(activeDragContext);
82
+
83
+ if (
84
+ index < 0 ||
85
+ isDragging ||
86
+ !active ||
87
+ !over ||
88
+ !activeItem ||
89
+ !(
90
+ // Same-list drop: check normal overIndex matching
91
+ (
92
+ (activeItem.listId === listId && index === overIndex) ||
93
+ // Cross-list drop: check if we're hovering over this specific item
94
+ (activeItem.listId !== listId && over.id === id)
95
+ )
96
+ )
97
+ ) {
98
+ return undefined;
99
+ }
100
+
101
+ const isCrossContainer = activeItem.listId !== listId;
102
+
103
+ // For cross-list drops, use combined keys like MultiSortable does
104
+ const validationKeys = isCrossContainer ? [active.id, ...keys] : keys;
105
+
106
+ const eventX = (activatorEvent as PointerEvent | null)?.clientX ?? 0;
107
+ const eventY = (activatorEvent as PointerEvent | null)?.clientY ?? 0;
108
+ const offsetLeft = eventX + position.x;
109
+ const offsetTop = eventY + position.y;
110
+
111
+ const parentIndex =
112
+ overIndex === -1 ? undefined : getDropTargetParentIndex?.(overIndex);
113
+
114
+ const isValidParent =
115
+ parentIndex === undefined || parentIndex === -1
116
+ ? false
117
+ : acceptsDrop(activeIndex, parentIndex, "inside", listId, listId);
118
+
119
+ const overIdAcceptsDrop =
120
+ overIndex === -1
121
+ ? undefined
122
+ : acceptsDrop(activeIndex, overIndex, "inside", listId, listId);
123
+
124
+ const relativeDropPosition =
125
+ index >= 0 && index === overIndex && !isDragging && active && over
126
+ ? validateDropIndicator({
127
+ acceptsDrop,
128
+ keys: validationKeys,
129
+ activeId: active.id,
130
+ overId: over.id,
131
+ offsetStart: axis === "x" ? offsetLeft : offsetTop,
132
+ elementStart: axis === "x" ? over.rect.left : over.rect.top,
133
+ elementSize: axis === "x" ? over.rect.width : over.rect.height,
134
+ sourceListId: listId,
135
+ targetListId: listId,
136
+ })
137
+ : undefined;
138
+
139
+ const showDragInsideIndicatorOnParent =
140
+ !overIdAcceptsDrop && isValidParent && parentIndex === index;
141
+
142
+ return (
143
+ relativeDropPosition ??
144
+ (showDragInsideIndicatorOnParent ? "inside" : undefined)
145
+ );
146
+ }
147
+
148
+ interface RootProps {
149
+ id?: string;
150
+ keys: UniqueIdentifier[];
151
+ axis?: "x" | "y";
152
+ children?: React.ReactNode;
153
+ renderOverlay?: (index: number) => React.ReactNode;
154
+ onMoveItem?: MoveDragItemHandler;
155
+ acceptsFromList?: AcceptsFromList;
156
+ acceptsDrop?: DropValidator;
157
+ getDropTargetParentIndex?: (index: number) => number | undefined;
158
+ sharedDragProviderContexts?: SharedDragProviderContexts;
159
+ containerRef?: React.RefObject<HTMLElement>;
160
+ }
161
+
162
+ function SortableItem<TElement extends HTMLElement>({
163
+ id,
164
+ disabled,
165
+ children,
166
+ }: {
167
+ id: UniqueIdentifier;
168
+ disabled?: boolean;
169
+ children: (props: ItemChildrenProps<TElement>) => React.ReactNode;
170
+ }) {
171
+ const { keys, acceptsDrop, axis, listId, getDropTargetParentIndex } =
172
+ React.useContext(SortableItemContext);
173
+
174
+ const { attributes, listeners, setNodeRef, isDragging, index, overIndex } =
175
+ useSortable({ id, disabled });
176
+
177
+ const ref = React.useCallback(
178
+ (node: TElement | null) => setNodeRef(node),
179
+ [setNodeRef]
180
+ );
181
+
182
+ const relativeDropPosition = useSortableDropIndicator({
183
+ id,
184
+ index,
185
+ isDragging,
186
+ overIndex,
187
+ keys,
188
+ acceptsDrop,
189
+ axis,
190
+ listId,
191
+ activeIndex: index,
192
+ getDropTargetParentIndex,
193
+ });
194
+
195
+ return children({
196
+ ref,
197
+ ...attributes,
198
+ ...listeners,
199
+ relativeDropPosition,
200
+ style: {
201
+ opacity: isDragging ? 0.5 : 1,
202
+ },
203
+ });
204
+ }
205
+
206
+ function SortableRoot_({
207
+ id: idProp,
208
+ keys,
209
+ onMoveItem,
210
+ renderOverlay,
211
+ acceptsFromList,
212
+ acceptsDrop = defaultAcceptsDrop,
213
+ axis = "y",
214
+ children,
215
+ getDropTargetParentIndex,
216
+ sharedDragProviderContexts,
217
+ containerRef,
218
+ }: RootProps) {
219
+ const defaultId = React.useId();
220
+ const id = idProp ?? defaultId;
221
+
222
+ const { registerList, unregisterList } = useDragRegistration(
223
+ sharedDragProviderContexts?.dragRegistrationContext ??
224
+ DragRegistrationContext
225
+ );
226
+
227
+ const { setNodeRef: setDroppableRef } = useDroppable({
228
+ id,
229
+ // Make the container droppable only when the list is empty
230
+ // This avoids conflicts with individual sortable items.
231
+ // This isn't strictly necessary since we also use a custom collision detection
232
+ // that prioritizes items over containers, but it helps with dropping into
233
+ // the best position (either above or below, rather than always below).
234
+ disabled: keys.length > 0,
235
+ });
236
+
237
+ // Register this list with the global drag system
238
+ React.useEffect(() => {
239
+ const listConfig: RegisteredList = {
240
+ id,
241
+ keys,
242
+ onMoveItem: onMoveItem ?? (() => {}),
243
+ renderOverlay: (id) => {
244
+ const index = keys.findIndex((key) => key === id);
245
+ return renderOverlay?.(index) ?? null;
246
+ },
247
+ acceptsFromList:
248
+ acceptsFromList ??
249
+ (({ sourceListId, targetListId }) => sourceListId === targetListId),
250
+ acceptsDrop,
251
+ getDropIndicator: (parameters) => {
252
+ const { absolutePosition, active, over, sourceListId, targetListId } =
253
+ parameters;
254
+
255
+ const offsetStart =
256
+ axis === "x" ? absolutePosition.x : absolutePosition.y;
257
+ const elementStart = axis === "x" ? over.rect.left : over.rect.top;
258
+ const elementSize = axis === "x" ? over.rect.width : over.rect.height;
259
+
260
+ const relativeDropPosition = validateDropIndicator({
261
+ acceptsDrop,
262
+ keys,
263
+ activeId: active.id,
264
+ overId: over.id,
265
+ offsetStart,
266
+ elementStart,
267
+ elementSize,
268
+ sourceListId,
269
+ targetListId,
270
+ });
271
+
272
+ return relativeDropPosition;
273
+ },
274
+ };
275
+
276
+ registerList(listConfig);
277
+
278
+ return () => {
279
+ unregisterList(id);
280
+ };
281
+ }, [
282
+ id,
283
+ onMoveItem,
284
+ renderOverlay,
285
+ acceptsFromList,
286
+ acceptsDrop,
287
+ axis,
288
+ registerList,
289
+ unregisterList,
290
+ keys,
291
+ ]);
292
+
293
+ const contextValue: SortableItemContextProps = React.useMemo(
294
+ () => ({
295
+ keys,
296
+ acceptsDrop,
297
+ axis,
298
+ listId: id,
299
+ getDropTargetParentIndex,
300
+ activeDragContext:
301
+ sharedDragProviderContexts?.activeDragContext ?? ActiveDragContext,
302
+ }),
303
+ [
304
+ keys,
305
+ acceptsDrop,
306
+ axis,
307
+ id,
308
+ getDropTargetParentIndex,
309
+ sharedDragProviderContexts,
310
+ ]
311
+ );
312
+
313
+ // Set the droppable ref after every render.
314
+ // Since we don't render the dom node internally, we aggressively set it
315
+ // after every render to make sure we are using the latest.
316
+ React.useEffect(() => {
317
+ if (containerRef) {
318
+ setDroppableRef(containerRef.current);
319
+ }
320
+ });
321
+
322
+ return (
323
+ <SortableItemContext.Provider value={contextValue}>
324
+ <SortableContext
325
+ items={keys}
326
+ strategy={
327
+ axis === "x"
328
+ ? horizontalListSortingStrategy
329
+ : verticalListSortingStrategy
330
+ }
331
+ >
332
+ {children}
333
+ </SortableContext>
334
+ </SortableItemContext.Provider>
335
+ );
336
+ }
337
+
338
+ const SortableRoot = withDragProvider(SortableRoot_) as typeof SortableRoot_;
339
+
340
+ export interface SortableProps<TItem, TElement extends HTMLElement>
341
+ extends Omit<RootProps, "children" | "keys" | "renderOverlay"> {
342
+ items: TItem[];
343
+ keyExtractor: (item: TItem) => UniqueIdentifier;
344
+ shouldRenderOverlay?: boolean;
345
+ renderItem: (
346
+ item: TItem,
347
+ props: ItemChildrenProps<TElement>,
348
+ { isOverlay }: { isOverlay: boolean }
349
+ ) => React.ReactNode;
350
+ }
351
+
352
+ const Sortable_ = function Sortable_<TItem, TElement extends HTMLElement>({
353
+ items,
354
+ keyExtractor,
355
+ shouldRenderOverlay = true,
356
+ renderItem,
357
+ ...rest
358
+ }: SortableProps<TItem, TElement>) {
359
+ const keys = React.useMemo(
360
+ () => items.map(keyExtractor),
361
+ [items, keyExtractor]
362
+ );
363
+
364
+ const renderOverlay = React.useCallback(
365
+ (index: number) => {
366
+ return renderItem(
367
+ items[index],
368
+ {
369
+ ref: () => {},
370
+ style: {},
371
+ tabIndex: -1,
372
+ "aria-disabled": true,
373
+ "aria-pressed": false,
374
+ "aria-roledescription": "Item being dragged",
375
+ "aria-describedby": "Item being dragged",
376
+ role: "button",
377
+ },
378
+ { isOverlay: true }
379
+ );
380
+ },
381
+ [renderItem, items]
382
+ );
383
+
384
+ return (
385
+ <SortableRoot
386
+ {...rest}
387
+ keys={keys}
388
+ renderOverlay={shouldRenderOverlay ? renderOverlay : undefined}
389
+ >
390
+ {keys.map((key, index) => (
391
+ <SortableItem<TElement> key={key} id={key}>
392
+ {(props) => {
393
+ return renderItem(items[index], props, { isOverlay: false });
394
+ }}
395
+ </SortableItem>
396
+ ))}
397
+ </SortableRoot>
398
+ );
399
+ };
400
+
401
+ export const Sortable = Object.assign(Sortable_, {
402
+ Root: SortableRoot,
403
+ Item: SortableItem,
404
+ });
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActiveDragContextType,
4
+ DragRegistrationContextType,
5
+ SharedDragProviderContexts,
6
+ } from "./DragRegistration";
7
+ import { SharedDragProvider } from "./SharedDragProvider";
8
+ import { Sortable, SortableProps } from "./Sortable";
9
+
10
+ export function createSharedDrag(): {
11
+ Provider: typeof SharedDragProvider;
12
+ props: SharedDragProviderContexts;
13
+ Sortable: <TItem, TElement extends HTMLElement>(
14
+ props: SortableProps<TItem, TElement>
15
+ ) => React.ReactNode;
16
+ } {
17
+ const dragRegistrationContext =
18
+ React.createContext<DragRegistrationContextType | null>(null);
19
+ const activeDragContext = React.createContext<ActiveDragContextType | null>(
20
+ null
21
+ );
22
+
23
+ const Provider = ({ children }: { children: React.ReactNode }) => (
24
+ <SharedDragProvider
25
+ contexts={{
26
+ dragRegistrationContext,
27
+ activeDragContext,
28
+ }}
29
+ >
30
+ {children}
31
+ </SharedDragProvider>
32
+ );
33
+
34
+ const dragProps = {
35
+ dragRegistrationContext,
36
+ activeDragContext,
37
+ };
38
+
39
+ return {
40
+ Provider,
41
+ Sortable: <TItem, TElement extends HTMLElement>(
42
+ props: SortableProps<TItem, TElement>
43
+ ) => <Sortable sharedDragProviderContexts={dragProps} {...props} />,
44
+ props: dragProps,
45
+ };
46
+ }
@@ -0,0 +1,180 @@
1
+ import { UniqueIdentifier } from "@dnd-kit/core";
2
+ import * as React from "react";
3
+
4
+ export type RelativeDropPosition = "above" | "below" | "inside";
5
+
6
+ export type DropValidator = (
7
+ sourceIndex: number,
8
+ targetIndex: number,
9
+ position: RelativeDropPosition,
10
+ sourceListId: string,
11
+ targetListId: string
12
+ ) => boolean;
13
+
14
+ export type AcceptsDrop = (
15
+ sourceIndex: number,
16
+ targetIndex: number,
17
+ position: RelativeDropPosition,
18
+ sourceListId: string,
19
+ targetListId: string
20
+ ) => boolean;
21
+
22
+ export type MoveDragItemHandler = (
23
+ sourceIndex: number,
24
+ targetIndex: number,
25
+ position: RelativeDropPosition,
26
+ sourceListId: string,
27
+ targetListId: string
28
+ ) => void;
29
+
30
+ export type AcceptsFromList = ({
31
+ sourceListId,
32
+ targetListId,
33
+ }: {
34
+ sourceListId: string;
35
+ targetListId: string;
36
+ }) => boolean;
37
+
38
+ export interface DragItem {
39
+ id: UniqueIdentifier;
40
+ listId: string;
41
+ index: number;
42
+ }
43
+
44
+ export interface DropTarget {
45
+ id: UniqueIdentifier;
46
+ listId: string;
47
+ index: number;
48
+ }
49
+
50
+ export const normalizeListTargetIndex = (
51
+ index: number,
52
+ position: "above" | "below"
53
+ ): number => {
54
+ return position === "above" ? index : index + 1;
55
+ };
56
+
57
+ export const defaultAcceptsDrop: DropValidator = (
58
+ sourceIndex,
59
+ targetIndex,
60
+ position
61
+ ) => {
62
+ if (position === "inside") return false;
63
+
64
+ const normalized = normalizeListTargetIndex(targetIndex, position);
65
+
66
+ if (sourceIndex === normalized || sourceIndex + 1 === normalized) {
67
+ return false;
68
+ }
69
+
70
+ return true;
71
+ };
72
+
73
+ export type SortableItemContextProps = {
74
+ keys: UniqueIdentifier[];
75
+ acceptsDrop: DropValidator;
76
+ axis: "x" | "y";
77
+ position: { x: number; y: number };
78
+ setActivatorEvent: (event: PointerEvent) => void;
79
+ getDropTargetParentIndex?: (index: number) => number | undefined;
80
+ listId: string;
81
+ };
82
+
83
+ export const SortableItemContext =
84
+ React.createContext<SortableItemContextProps>({
85
+ keys: [],
86
+ acceptsDrop: defaultAcceptsDrop,
87
+ axis: "y",
88
+ position: { x: 0, y: 0 },
89
+ setActivatorEvent: () => {},
90
+ getDropTargetParentIndex: undefined,
91
+ listId: "",
92
+ });
93
+
94
+ export type ValidateDropIndicatorParams = {
95
+ acceptsDrop: DropValidator; // Function that validates if a drop is allowed
96
+ keys: UniqueIdentifier[]; // Array of all sortable item IDs
97
+ activeId: UniqueIdentifier; // ID of item being dragged
98
+ overId: UniqueIdentifier; // ID of item being dragged over
99
+ offsetStart: number; // Current drag position (x or y coordinate)
100
+ elementStart: number; // Start position of target element (left or top)
101
+ elementSize: number; // Size of target element (width or height)
102
+ sourceListId: string; // ID of the list the item is being dragged from
103
+ targetListId: string; // ID of the list the item is being dragged to
104
+ };
105
+
106
+ export function validateDropIndicator({
107
+ acceptsDrop,
108
+ keys,
109
+ activeId,
110
+ overId,
111
+ offsetStart,
112
+ elementStart,
113
+ elementSize,
114
+ sourceListId,
115
+ targetListId,
116
+ }: ValidateDropIndicatorParams): RelativeDropPosition | undefined {
117
+ const activeIndex = keys.findIndex((id) => id === activeId); // index of item being dragged
118
+ const overIndex = keys.findIndex((id) => id === overId); // index of item being dragged over
119
+
120
+ if (overIndex === -1) return undefined;
121
+ if (activeIndex === -1 && sourceListId === targetListId) return undefined;
122
+
123
+ const acceptsDropInside = acceptsDrop(
124
+ activeIndex,
125
+ overIndex,
126
+ "inside",
127
+ sourceListId,
128
+ targetListId
129
+ );
130
+
131
+ // Drop into the middle third if possible
132
+ if (
133
+ isContainedInMiddleThird(elementStart, elementSize, offsetStart) &&
134
+ acceptsDropInside
135
+ ) {
136
+ return "inside";
137
+ }
138
+
139
+ const containingHalf = getContainingHalf(
140
+ elementStart,
141
+ elementSize,
142
+ offsetStart
143
+ );
144
+
145
+ // Drop above or below if possible
146
+ const acceptedHalf = acceptsDrop(
147
+ activeIndex,
148
+ overIndex,
149
+ containingHalf,
150
+ sourceListId,
151
+ targetListId
152
+ );
153
+
154
+ return acceptedHalf
155
+ ? containingHalf
156
+ : // Lastly, check if inside but not middle third
157
+ acceptsDropInside
158
+ ? "inside"
159
+ : undefined;
160
+ }
161
+
162
+ function isContainedInMiddleThird(
163
+ elementStart: number,
164
+ elementSize: number,
165
+ offsetStart: number
166
+ ): boolean {
167
+ return (
168
+ offsetStart >= elementStart + elementSize / 3 &&
169
+ offsetStart <= elementStart + (elementSize * 2) / 3
170
+ );
171
+ }
172
+
173
+ function getContainingHalf(
174
+ elementStart: number,
175
+ elementSize: number,
176
+ offsetStart: number
177
+ ): "above" | "below" {
178
+ if (offsetStart < elementStart + elementSize / 2) return "above";
179
+ return "below";
180
+ }
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import { ToastProvider } from "../components/Toast";
4
4
  import { DialogProvider } from "./DialogContext";
5
5
  import { FloatingWindowProvider } from "./FloatingWindowContext";
6
+ import { OpenPortalsProvider } from "./OpenPortalsContext";
6
7
 
7
8
  export type DesignSystemConfigurationContextValue = {
8
9
  platform: PlatformName;
@@ -39,11 +40,13 @@ export const DesignSystemConfigurationProvider = React.memo(
39
40
 
40
41
  return (
41
42
  <DesignSystemConfigurationContext.Provider value={contextValue}>
42
- <DialogProvider>
43
- <ToastProvider>
44
- <FloatingWindowProvider>{children}</FloatingWindowProvider>
45
- </ToastProvider>
46
- </DialogProvider>
43
+ <OpenPortalsProvider>
44
+ <DialogProvider>
45
+ <ToastProvider>
46
+ <FloatingWindowProvider>{children}</FloatingWindowProvider>
47
+ </ToastProvider>
48
+ </DialogProvider>
49
+ </OpenPortalsProvider>
47
50
  </DesignSystemConfigurationContext.Provider>
48
51
  );
49
52
  }