@zentauri-ui/zentauri-components 1.7.5 → 1.7.6
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/README.md +8 -5
- package/cli/registry.json +2 -0
- package/dist/chunk-DEZRB6DS.mjs +83 -0
- package/dist/chunk-DEZRB6DS.mjs.map +1 -0
- package/dist/chunk-V5JTDRV5.mjs +278 -0
- package/dist/chunk-V5JTDRV5.mjs.map +1 -0
- package/dist/chunk-Z4KHAD6Y.js +295 -0
- package/dist/chunk-Z4KHAD6Y.js.map +1 -0
- package/dist/chunk-ZX2IBIZT.js +92 -0
- package/dist/chunk-ZX2IBIZT.js.map +1 -0
- package/dist/design-system/context-menu.d.ts +41 -0
- package/dist/design-system/context-menu.d.ts.map +1 -0
- package/dist/design-system/index.d.ts +2 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/design-system/timeline.d.ts +56 -0
- package/dist/design-system/timeline.d.ts.map +1 -0
- package/dist/ui/context-menu/context-menu.d.ts +11 -0
- package/dist/ui/context-menu/context-menu.d.ts.map +1 -0
- package/dist/ui/context-menu/index.d.ts +4 -0
- package/dist/ui/context-menu/index.d.ts.map +1 -0
- package/dist/ui/context-menu/types.d.ts +81 -0
- package/dist/ui/context-menu/types.d.ts.map +1 -0
- package/dist/ui/context-menu/variants.d.ts +7 -0
- package/dist/ui/context-menu/variants.d.ts.map +1 -0
- package/dist/ui/context-menu.js +500 -0
- package/dist/ui/context-menu.js.map +1 -0
- package/dist/ui/context-menu.mjs +488 -0
- package/dist/ui/context-menu.mjs.map +1 -0
- package/dist/ui/dropdown.js +9 -89
- package/dist/ui/dropdown.js.map +1 -1
- package/dist/ui/dropdown.mjs +1 -81
- package/dist/ui/dropdown.mjs.map +1 -1
- package/dist/ui/scroll-area/scroll-area.d.ts.map +1 -1
- package/dist/ui/scroll-area.js.map +1 -1
- package/dist/ui/scroll-area.mjs.map +1 -1
- package/dist/ui/timeline/animated/animations.d.ts +8 -0
- package/dist/ui/timeline/animated/animations.d.ts.map +1 -0
- package/dist/ui/timeline/animated/index.d.ts +6 -0
- package/dist/ui/timeline/animated/index.d.ts.map +1 -0
- package/dist/ui/timeline/animated/timeline-item-animated.d.ts +8 -0
- package/dist/ui/timeline/animated/timeline-item-animated.d.ts.map +1 -0
- package/dist/ui/timeline/animated/types.d.ts +12 -0
- package/dist/ui/timeline/animated/types.d.ts.map +1 -0
- package/dist/ui/timeline/animated.js +94 -0
- package/dist/ui/timeline/animated.js.map +1 -0
- package/dist/ui/timeline/animated.mjs +71 -0
- package/dist/ui/timeline/animated.mjs.map +1 -0
- package/dist/ui/timeline/index.d.ts +4 -0
- package/dist/ui/timeline/index.d.ts.map +1 -0
- package/dist/ui/timeline/timeline-base.d.ts +37 -0
- package/dist/ui/timeline/timeline-base.d.ts.map +1 -0
- package/dist/ui/timeline/timeline.d.ts +8 -0
- package/dist/ui/timeline/timeline.d.ts.map +1 -0
- package/dist/ui/timeline/types.d.ts +38 -0
- package/dist/ui/timeline/types.d.ts.map +1 -0
- package/dist/ui/timeline/variants.d.ts +19 -0
- package/dist/ui/timeline/variants.d.ts.map +1 -0
- package/dist/ui/timeline.js +63 -0
- package/dist/ui/timeline.js.map +1 -0
- package/dist/ui/timeline.mjs +14 -0
- package/dist/ui/timeline.mjs.map +1 -0
- package/package.json +1 -1
- package/src/design-system/context-menu.ts +44 -0
- package/src/design-system/index.ts +2 -0
- package/src/design-system/timeline.ts +87 -0
- package/src/ui/context-menu/context-menu.test.tsx +176 -0
- package/src/ui/context-menu/context-menu.tsx +536 -0
- package/src/ui/context-menu/index.ts +29 -0
- package/src/ui/context-menu/types.ts +110 -0
- package/src/ui/context-menu/variants.ts +26 -0
- package/src/ui/scroll-area/scroll-area.tsx +0 -2
- package/src/ui/timeline/animated/animations.ts +16 -0
- package/src/ui/timeline/animated/index.ts +22 -0
- package/src/ui/timeline/animated/timeline-item-animated.tsx +76 -0
- package/src/ui/timeline/animated/types.ts +21 -0
- package/src/ui/timeline/index.ts +30 -0
- package/src/ui/timeline/timeline-base.tsx +232 -0
- package/src/ui/timeline/timeline.test.tsx +262 -0
- package/src/ui/timeline/timeline.tsx +24 -0
- package/src/ui/timeline/types.ts +61 -0
- package/src/ui/timeline/variants.ts +60 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
createContext,
|
|
7
|
+
isValidElement,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useId,
|
|
12
|
+
useLayoutEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type KeyboardEvent,
|
|
17
|
+
type MouseEvent,
|
|
18
|
+
type ReactElement,
|
|
19
|
+
type Ref,
|
|
20
|
+
type RefObject,
|
|
21
|
+
} from "react";
|
|
22
|
+
import { FiChevronRight } from "react-icons/fi";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
zuiContextMenuLabelBase,
|
|
26
|
+
zuiContextMenuSeparatorBase,
|
|
27
|
+
} from "../../design-system/context-menu";
|
|
28
|
+
import { cn } from "../../lib/utils";
|
|
29
|
+
import type {
|
|
30
|
+
ContextMenuContentProps,
|
|
31
|
+
ContextMenuContextType,
|
|
32
|
+
ContextMenuItemProps,
|
|
33
|
+
ContextMenuLabelProps,
|
|
34
|
+
ContextMenuPosition,
|
|
35
|
+
ContextMenuProps,
|
|
36
|
+
ContextMenuSeparatorProps,
|
|
37
|
+
ContextMenuSubContentProps,
|
|
38
|
+
ContextMenuSubContextType,
|
|
39
|
+
ContextMenuSubProps,
|
|
40
|
+
ContextMenuSubTriggerProps,
|
|
41
|
+
ContextMenuTriggerProps,
|
|
42
|
+
GetSafePositionProps,
|
|
43
|
+
ReactChildSoleCandidate,
|
|
44
|
+
} from "./types";
|
|
45
|
+
import {
|
|
46
|
+
contextMenuContentVariants,
|
|
47
|
+
contextMenuItemVariants,
|
|
48
|
+
} from "./variants";
|
|
49
|
+
|
|
50
|
+
const ContextMenuContext = createContext<ContextMenuContextType | null>(null);
|
|
51
|
+
const ContextMenuSubContext = createContext<ContextMenuSubContextType | null>(
|
|
52
|
+
null,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const useContextMenu = () => {
|
|
56
|
+
const context = useContext(ContextMenuContext);
|
|
57
|
+
if (!context) {
|
|
58
|
+
throw new Error("ContextMenu components must be used within ContextMenu");
|
|
59
|
+
}
|
|
60
|
+
return context;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const useContextMenuSub = () => {
|
|
64
|
+
const context = useContext(ContextMenuSubContext);
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"ContextMenuSub components must be used within ContextMenuSub",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return context;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {
|
|
74
|
+
return (node: T) => {
|
|
75
|
+
for (const ref of refs) {
|
|
76
|
+
if (typeof ref === "function") {
|
|
77
|
+
ref(node);
|
|
78
|
+
} else if (ref) {
|
|
79
|
+
(ref as RefObject<T | null>).current = node;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const getSafePosition = ({
|
|
86
|
+
position,
|
|
87
|
+
width,
|
|
88
|
+
height,
|
|
89
|
+
collisionPadding,
|
|
90
|
+
}: GetSafePositionProps) => {
|
|
91
|
+
const fallback = position ?? { x: collisionPadding, y: collisionPadding };
|
|
92
|
+
|
|
93
|
+
if (typeof window === "undefined") {
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
x: Math.max(
|
|
99
|
+
collisionPadding,
|
|
100
|
+
Math.min(fallback.x, window.innerWidth - width - collisionPadding),
|
|
101
|
+
),
|
|
102
|
+
y: Math.max(
|
|
103
|
+
collisionPadding,
|
|
104
|
+
Math.min(fallback.y, window.innerHeight - height - collisionPadding),
|
|
105
|
+
),
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const ContextMenu = ({
|
|
110
|
+
children,
|
|
111
|
+
defaultOpen = false,
|
|
112
|
+
open: controlledOpen,
|
|
113
|
+
onOpenChange,
|
|
114
|
+
closeOnEscape = true,
|
|
115
|
+
closeOnOutsideClick = true,
|
|
116
|
+
}: ContextMenuProps) => {
|
|
117
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
118
|
+
const [position, setPosition] = useState<ContextMenuPosition | null>(null);
|
|
119
|
+
const contentId = `${useId()}-context-menu`;
|
|
120
|
+
const triggerRef = useRef<HTMLElement | null>(null);
|
|
121
|
+
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
122
|
+
|
|
123
|
+
const isControlled = controlledOpen !== undefined;
|
|
124
|
+
const open = isControlled ? controlledOpen : uncontrolledOpen;
|
|
125
|
+
|
|
126
|
+
const setOpen = useCallback(
|
|
127
|
+
(nextOpen: boolean) => {
|
|
128
|
+
if (!isControlled) {
|
|
129
|
+
setUncontrolledOpen(nextOpen);
|
|
130
|
+
}
|
|
131
|
+
onOpenChange?.(nextOpen);
|
|
132
|
+
},
|
|
133
|
+
[isControlled, onOpenChange],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const openAt = useCallback(
|
|
137
|
+
(nextPosition: ContextMenuPosition) => {
|
|
138
|
+
setPosition(nextPosition);
|
|
139
|
+
setOpen(true);
|
|
140
|
+
},
|
|
141
|
+
[setOpen],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!open) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
150
|
+
if (!closeOnOutsideClick) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const target = event.target as Node;
|
|
154
|
+
if (contentRef.current?.contains(target)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (triggerRef.current?.contains(target) && event.button !== 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
setOpen(false);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const onKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
164
|
+
if (event.key === "Escape" && closeOnEscape) {
|
|
165
|
+
setOpen(false);
|
|
166
|
+
triggerRef.current?.focus();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const onScroll = () => {
|
|
171
|
+
setOpen(false);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
175
|
+
document.addEventListener("keydown", onKeyDown);
|
|
176
|
+
document.addEventListener("scroll", onScroll, { capture: true });
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
180
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
181
|
+
document.removeEventListener("scroll", onScroll, { capture: true });
|
|
182
|
+
};
|
|
183
|
+
}, [closeOnEscape, closeOnOutsideClick, open, setOpen]);
|
|
184
|
+
|
|
185
|
+
const contextValue = useMemo(
|
|
186
|
+
() => ({
|
|
187
|
+
open,
|
|
188
|
+
setOpen,
|
|
189
|
+
openAt,
|
|
190
|
+
contentId,
|
|
191
|
+
triggerRef,
|
|
192
|
+
contentRef,
|
|
193
|
+
position,
|
|
194
|
+
}),
|
|
195
|
+
[contentId, open, openAt, setOpen, position],
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<ContextMenuContext.Provider value={contextValue}>
|
|
200
|
+
<div className="contents">{children}</div>
|
|
201
|
+
</ContextMenuContext.Provider>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const ContextMenuTrigger = ({
|
|
206
|
+
children,
|
|
207
|
+
className,
|
|
208
|
+
disabled = false,
|
|
209
|
+
}: ContextMenuTriggerProps) => {
|
|
210
|
+
const { open, openAt, contentId, triggerRef } = useContextMenu();
|
|
211
|
+
|
|
212
|
+
const handleContextMenu = (event: MouseEvent<HTMLElement>) => {
|
|
213
|
+
if (disabled) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
const isKeyboardTrigger = event.clientX === 0 && event.clientY === 0;
|
|
218
|
+
if (isKeyboardTrigger) {
|
|
219
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
220
|
+
openAt({ x: rect.left, y: rect.bottom });
|
|
221
|
+
} else {
|
|
222
|
+
openAt({ x: event.clientX, y: event.clientY });
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const childList = Children.toArray(children).filter(
|
|
226
|
+
(node) => node !== null && node !== undefined && typeof node !== "boolean",
|
|
227
|
+
);
|
|
228
|
+
const soleCandidate =
|
|
229
|
+
childList.length === 1 && isValidElement(childList[0])
|
|
230
|
+
? (childList[0] as ReactChildSoleCandidate)
|
|
231
|
+
: undefined;
|
|
232
|
+
|
|
233
|
+
if (soleCandidate) {
|
|
234
|
+
return cloneElement(soleCandidate, {
|
|
235
|
+
ref: mergeRefs(triggerRef, soleCandidate.props.ref),
|
|
236
|
+
onContextMenu: (event) => {
|
|
237
|
+
soleCandidate.props.onContextMenu?.(event);
|
|
238
|
+
if (!event.defaultPrevented) {
|
|
239
|
+
handleContextMenu(event);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
className: cn(className, soleCandidate.props.className),
|
|
243
|
+
tabIndex: soleCandidate.props.tabIndex ?? 0,
|
|
244
|
+
"aria-controls": open ? contentId : undefined,
|
|
245
|
+
"aria-expanded": open,
|
|
246
|
+
"aria-haspopup": "menu",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<span
|
|
252
|
+
ref={triggerRef as Ref<HTMLSpanElement>}
|
|
253
|
+
className={className}
|
|
254
|
+
tabIndex={0}
|
|
255
|
+
onContextMenu={handleContextMenu}
|
|
256
|
+
aria-controls={open ? contentId : undefined}
|
|
257
|
+
aria-expanded={open}
|
|
258
|
+
aria-haspopup="menu"
|
|
259
|
+
>
|
|
260
|
+
{children}
|
|
261
|
+
</span>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const ContextMenuContent = ({
|
|
266
|
+
children,
|
|
267
|
+
className,
|
|
268
|
+
collisionPadding = 8,
|
|
269
|
+
spacing,
|
|
270
|
+
style,
|
|
271
|
+
width = 220,
|
|
272
|
+
...props
|
|
273
|
+
}: ContextMenuContentProps) => {
|
|
274
|
+
const { open, contentId, contentRef, position } = useContextMenu();
|
|
275
|
+
const [menuSize, setMenuSize] = useState({ width, height: 0 });
|
|
276
|
+
|
|
277
|
+
useLayoutEffect(() => {
|
|
278
|
+
if (!open || !contentRef.current) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
283
|
+
const nextSize = {
|
|
284
|
+
width: Math.max(width, rect.width),
|
|
285
|
+
height: rect.height,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
setMenuSize((currentSize) =>
|
|
289
|
+
currentSize.width === nextSize.width &&
|
|
290
|
+
currentSize.height === nextSize.height
|
|
291
|
+
? currentSize
|
|
292
|
+
: nextSize,
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!open) {
|
|
298
|
+
setMenuSize({ width, height: 0 });
|
|
299
|
+
}
|
|
300
|
+
}, [open, width]);
|
|
301
|
+
|
|
302
|
+
if (!open) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const safePosition = getSafePosition({
|
|
307
|
+
position,
|
|
308
|
+
width: menuSize.width,
|
|
309
|
+
height: menuSize.height,
|
|
310
|
+
collisionPadding,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div
|
|
315
|
+
ref={contentRef}
|
|
316
|
+
id={contentId}
|
|
317
|
+
role="menu"
|
|
318
|
+
tabIndex={-1}
|
|
319
|
+
className={cn(
|
|
320
|
+
contextMenuContentVariants({ spacing }),
|
|
321
|
+
"fixed z-50",
|
|
322
|
+
className,
|
|
323
|
+
)}
|
|
324
|
+
style={{
|
|
325
|
+
left: safePosition.x,
|
|
326
|
+
top: safePosition.y,
|
|
327
|
+
minWidth: width,
|
|
328
|
+
...style,
|
|
329
|
+
}}
|
|
330
|
+
{...props}
|
|
331
|
+
>
|
|
332
|
+
{children}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export const ContextMenuItem = ({
|
|
338
|
+
children,
|
|
339
|
+
className,
|
|
340
|
+
closeOnSelect = true,
|
|
341
|
+
disabled = false,
|
|
342
|
+
inset = false,
|
|
343
|
+
leftIcon,
|
|
344
|
+
onClick,
|
|
345
|
+
onKeyDown,
|
|
346
|
+
onSelect,
|
|
347
|
+
rightIcon,
|
|
348
|
+
variant,
|
|
349
|
+
...props
|
|
350
|
+
}: ContextMenuItemProps) => {
|
|
351
|
+
const { setOpen } = useContextMenu();
|
|
352
|
+
|
|
353
|
+
const handleSelect = () => {
|
|
354
|
+
if (disabled) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
onSelect?.();
|
|
358
|
+
if (closeOnSelect) {
|
|
359
|
+
setOpen(false);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
364
|
+
onKeyDown?.(event);
|
|
365
|
+
if (event.defaultPrevented || disabled) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
handleSelect();
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<div
|
|
376
|
+
role="menuitem"
|
|
377
|
+
tabIndex={disabled ? undefined : 0}
|
|
378
|
+
aria-disabled={disabled || undefined}
|
|
379
|
+
className={cn(
|
|
380
|
+
contextMenuItemVariants({ variant }),
|
|
381
|
+
inset && "pl-8",
|
|
382
|
+
disabled && "pointer-events-none cursor-not-allowed opacity-50",
|
|
383
|
+
className,
|
|
384
|
+
)}
|
|
385
|
+
onClick={(event) => {
|
|
386
|
+
onClick?.(event);
|
|
387
|
+
if (!event.defaultPrevented) {
|
|
388
|
+
handleSelect();
|
|
389
|
+
}
|
|
390
|
+
}}
|
|
391
|
+
onKeyDown={handleKeyDown}
|
|
392
|
+
{...props}
|
|
393
|
+
>
|
|
394
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
395
|
+
{leftIcon}
|
|
396
|
+
<span className="truncate">{children}</span>
|
|
397
|
+
</div>
|
|
398
|
+
{rightIcon ? (
|
|
399
|
+
<div className="ml-4 flex items-center">{rightIcon}</div>
|
|
400
|
+
) : null}
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
export const ContextMenuLabel = ({
|
|
406
|
+
children,
|
|
407
|
+
className,
|
|
408
|
+
inset = false,
|
|
409
|
+
...props
|
|
410
|
+
}: ContextMenuLabelProps) => {
|
|
411
|
+
return (
|
|
412
|
+
<p
|
|
413
|
+
className={cn(zuiContextMenuLabelBase, inset && "pl-8", className)}
|
|
414
|
+
{...props}
|
|
415
|
+
>
|
|
416
|
+
{children}
|
|
417
|
+
</p>
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export const ContextMenuSeparator = ({
|
|
422
|
+
className,
|
|
423
|
+
...props
|
|
424
|
+
}: ContextMenuSeparatorProps) => {
|
|
425
|
+
return (
|
|
426
|
+
<div
|
|
427
|
+
role="separator"
|
|
428
|
+
className={cn(zuiContextMenuSeparatorBase, className)}
|
|
429
|
+
{...props}
|
|
430
|
+
/>
|
|
431
|
+
);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export const ContextMenuSub = ({
|
|
435
|
+
children,
|
|
436
|
+
defaultOpen = false,
|
|
437
|
+
}: ContextMenuSubProps) => {
|
|
438
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
439
|
+
const value = useMemo(() => ({ open, setOpen }), [open]);
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<ContextMenuSubContext.Provider value={value}>
|
|
443
|
+
<div className="relative" onPointerLeave={() => setOpen(false)}>
|
|
444
|
+
{children}
|
|
445
|
+
</div>
|
|
446
|
+
</ContextMenuSubContext.Provider>
|
|
447
|
+
);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export const ContextMenuSubTrigger = ({
|
|
451
|
+
children,
|
|
452
|
+
className,
|
|
453
|
+
disabled = false,
|
|
454
|
+
inset = false,
|
|
455
|
+
onFocus,
|
|
456
|
+
onKeyDown,
|
|
457
|
+
onPointerEnter,
|
|
458
|
+
rightIcon = <FiChevronRight aria-hidden="true" />,
|
|
459
|
+
variant,
|
|
460
|
+
...props
|
|
461
|
+
}: ContextMenuSubTriggerProps) => {
|
|
462
|
+
const { open, setOpen } = useContextMenuSub();
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<div
|
|
466
|
+
role="menuitem"
|
|
467
|
+
tabIndex={disabled ? undefined : 0}
|
|
468
|
+
aria-disabled={disabled || undefined}
|
|
469
|
+
aria-expanded={open}
|
|
470
|
+
aria-haspopup="menu"
|
|
471
|
+
className={cn(
|
|
472
|
+
contextMenuItemVariants({ variant }),
|
|
473
|
+
inset && "pl-8",
|
|
474
|
+
disabled && "pointer-events-none cursor-not-allowed opacity-50",
|
|
475
|
+
className,
|
|
476
|
+
)}
|
|
477
|
+
onFocus={(event) => {
|
|
478
|
+
onFocus?.(event);
|
|
479
|
+
}}
|
|
480
|
+
onPointerEnter={(event) => {
|
|
481
|
+
onPointerEnter?.(event);
|
|
482
|
+
if (!disabled) {
|
|
483
|
+
setOpen(true);
|
|
484
|
+
}
|
|
485
|
+
}}
|
|
486
|
+
onKeyDown={(event) => {
|
|
487
|
+
onKeyDown?.(event);
|
|
488
|
+
if (event.defaultPrevented || disabled) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (event.key === "ArrowRight" || event.key === "Enter") {
|
|
492
|
+
event.preventDefault();
|
|
493
|
+
setOpen(true);
|
|
494
|
+
}
|
|
495
|
+
if (event.key === "ArrowLeft") {
|
|
496
|
+
event.preventDefault();
|
|
497
|
+
setOpen(false);
|
|
498
|
+
}
|
|
499
|
+
}}
|
|
500
|
+
{...props}
|
|
501
|
+
>
|
|
502
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
503
|
+
<span className="truncate">{children}</span>
|
|
504
|
+
</div>
|
|
505
|
+
<div className="ml-4 flex items-center">{rightIcon}</div>
|
|
506
|
+
</div>
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export const ContextMenuSubContent = ({
|
|
511
|
+
children,
|
|
512
|
+
className,
|
|
513
|
+
spacing,
|
|
514
|
+
...props
|
|
515
|
+
}: ContextMenuSubContentProps) => {
|
|
516
|
+
const { open } = useContextMenuSub();
|
|
517
|
+
|
|
518
|
+
if (!open) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<div
|
|
524
|
+
role="menu"
|
|
525
|
+
tabIndex={-1}
|
|
526
|
+
className={cn(
|
|
527
|
+
contextMenuContentVariants({ spacing }),
|
|
528
|
+
"absolute left-full top-0 z-50 ml-2",
|
|
529
|
+
className,
|
|
530
|
+
)}
|
|
531
|
+
{...props}
|
|
532
|
+
>
|
|
533
|
+
{children}
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
ContextMenu,
|
|
5
|
+
ContextMenuContent,
|
|
6
|
+
ContextMenuItem,
|
|
7
|
+
ContextMenuLabel,
|
|
8
|
+
ContextMenuSeparator,
|
|
9
|
+
ContextMenuSub,
|
|
10
|
+
ContextMenuSubContent,
|
|
11
|
+
ContextMenuSubTrigger,
|
|
12
|
+
ContextMenuTrigger,
|
|
13
|
+
} from "./context-menu";
|
|
14
|
+
export type {
|
|
15
|
+
ContextMenuContentProps,
|
|
16
|
+
ContextMenuItemProps,
|
|
17
|
+
ContextMenuLabelProps,
|
|
18
|
+
ContextMenuPosition,
|
|
19
|
+
ContextMenuProps,
|
|
20
|
+
ContextMenuSeparatorProps,
|
|
21
|
+
ContextMenuSubContentProps,
|
|
22
|
+
ContextMenuSubProps,
|
|
23
|
+
ContextMenuSubTriggerProps,
|
|
24
|
+
ContextMenuTriggerProps,
|
|
25
|
+
} from "./types";
|
|
26
|
+
export {
|
|
27
|
+
contextMenuContentVariants,
|
|
28
|
+
contextMenuItemVariants,
|
|
29
|
+
} from "./variants";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
|
+
import type {
|
|
3
|
+
ComponentPropsWithRef,
|
|
4
|
+
HTMLAttributes,
|
|
5
|
+
MouseEvent,
|
|
6
|
+
ReactElement,
|
|
7
|
+
ReactNode,
|
|
8
|
+
Ref,
|
|
9
|
+
RefObject,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
contextMenuContentVariants,
|
|
14
|
+
contextMenuItemVariants,
|
|
15
|
+
} from "./variants";
|
|
16
|
+
|
|
17
|
+
export type ContextMenuPosition = {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ContextMenuContextType = {
|
|
23
|
+
open: boolean;
|
|
24
|
+
setOpen: (open: boolean) => void;
|
|
25
|
+
openAt: (position: ContextMenuPosition) => void;
|
|
26
|
+
position: ContextMenuPosition | null;
|
|
27
|
+
contentId: string;
|
|
28
|
+
triggerRef: RefObject<HTMLElement | null>;
|
|
29
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ContextMenuProps = {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
defaultOpen?: boolean;
|
|
35
|
+
open?: boolean;
|
|
36
|
+
onOpenChange?: (open: boolean) => void;
|
|
37
|
+
closeOnEscape?: boolean;
|
|
38
|
+
closeOnOutsideClick?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ContextMenuTriggerProps = {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ContextMenuContentProps = ComponentPropsWithRef<"div"> &
|
|
48
|
+
VariantProps<typeof contextMenuContentVariants> & {
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
collisionPadding?: number;
|
|
51
|
+
width?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ContextMenuItemProps = HTMLAttributes<HTMLDivElement> &
|
|
55
|
+
VariantProps<typeof contextMenuItemVariants> & {
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
closeOnSelect?: boolean;
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
inset?: boolean;
|
|
60
|
+
leftIcon?: ReactNode;
|
|
61
|
+
onSelect?: () => void;
|
|
62
|
+
rightIcon?: ReactNode;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type ContextMenuLabelProps = HTMLAttributes<HTMLParagraphElement> & {
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
inset?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type ContextMenuSeparatorProps = HTMLAttributes<HTMLDivElement>;
|
|
71
|
+
|
|
72
|
+
export type ContextMenuSubContextType = {
|
|
73
|
+
open: boolean;
|
|
74
|
+
setOpen: (open: boolean) => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type ContextMenuSubProps = {
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
defaultOpen?: boolean;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type ContextMenuSubTriggerProps = HTMLAttributes<HTMLDivElement> &
|
|
83
|
+
VariantProps<typeof contextMenuItemVariants> & {
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
disabled?: boolean;
|
|
86
|
+
inset?: boolean;
|
|
87
|
+
rightIcon?: ReactNode;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type ContextMenuSubContentProps = ComponentPropsWithRef<"div"> &
|
|
91
|
+
VariantProps<typeof contextMenuContentVariants> & {
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type GetSafePositionProps = {
|
|
96
|
+
position: ContextMenuPosition | null;
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
collisionPadding: number;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type ReactChildSoleCandidate = ReactElement<{
|
|
103
|
+
className?: string;
|
|
104
|
+
ref?: Ref<HTMLElement>;
|
|
105
|
+
onContextMenu?: (event: MouseEvent<HTMLElement>) => void;
|
|
106
|
+
tabIndex?: number;
|
|
107
|
+
"aria-controls"?: string;
|
|
108
|
+
"aria-expanded"?: boolean;
|
|
109
|
+
"aria-haspopup"?: string;
|
|
110
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiContextMenuContentBase,
|
|
5
|
+
zuiContextMenuItemBase,
|
|
6
|
+
zuiContextMenuItemVariants,
|
|
7
|
+
zuiContextMenuSpacing,
|
|
8
|
+
} from "../../design-system/context-menu";
|
|
9
|
+
|
|
10
|
+
export const contextMenuContentVariants = cva(zuiContextMenuContentBase, {
|
|
11
|
+
variants: {
|
|
12
|
+
spacing: zuiContextMenuSpacing,
|
|
13
|
+
},
|
|
14
|
+
defaultVariants: {
|
|
15
|
+
spacing: "default",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const contextMenuItemVariants = cva(zuiContextMenuItemBase, {
|
|
20
|
+
variants: {
|
|
21
|
+
variant: zuiContextMenuItemVariants,
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
variant: "default",
|
|
25
|
+
},
|
|
26
|
+
});
|