torch-glare 2.1.7 → 2.2.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.
@@ -0,0 +1,524 @@
1
+ "use client";
2
+
3
+ import { cva, VariantProps } from "class-variance-authority";
4
+ import * as React from "react";
5
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
6
+ import { cn } from "../utils/cn";
7
+ import { Themes } from "../utils/types";
8
+
9
+ /**
10
+ * ContextMenu — a right-click (or long-press) menu.
11
+ *
12
+ * Same surface as DropdownMenu (items, groups, the boxed look, auto-grouping),
13
+ * built on @radix-ui/react-context-menu so it opens at the pointer on
14
+ * right-click instead of anchoring to a clicked trigger button. The styling and
15
+ * auto-group logic are kept self-contained here (mirrored in DropdownMenu).
16
+ */
17
+
18
+ interface ContextMenuContentProps {
19
+ variant?: "PresentationStyle";
20
+ className?: string;
21
+ theme?: Themes;
22
+ }
23
+
24
+ // A second right-click while the menu is open should simply close it. Radix
25
+ // would otherwise keep it open (re-anchoring is unreliable), so we make the Root
26
+ // controlled, track `open` in context, and let the Trigger close + swallow the
27
+ // event on re-click.
28
+ const ContextMenuOpenContext = React.createContext<{
29
+ open: boolean;
30
+ setOpen: (open: boolean) => void;
31
+ } | null>(null);
32
+
33
+ const ContextMenu = ({
34
+ open: openProp,
35
+ onOpenChange,
36
+ children,
37
+ ...props
38
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Root>) => {
39
+ const [open, setOpenState] = React.useState(false);
40
+ const isControlled = openProp !== undefined;
41
+ const actualOpen = isControlled ? openProp : open;
42
+
43
+ const setOpen = React.useCallback(
44
+ (next: boolean) => {
45
+ if (!isControlled) setOpenState(next);
46
+ onOpenChange?.(next);
47
+ },
48
+ [isControlled, onOpenChange]
49
+ );
50
+
51
+ return (
52
+ <ContextMenuOpenContext.Provider value={{ open: actualOpen, setOpen }}>
53
+ <ContextMenuPrimitive.Root
54
+ open={actualOpen}
55
+ onOpenChange={setOpen}
56
+ {...props}
57
+ >
58
+ {children}
59
+ </ContextMenuPrimitive.Root>
60
+ </ContextMenuOpenContext.Provider>
61
+ );
62
+ };
63
+ ContextMenu.displayName = "ContextMenu";
64
+
65
+ // The right-click zone. Wrap it around the target the menu should open from.
66
+ const ContextMenuTrigger = React.forwardRef<
67
+ React.ElementRef<typeof ContextMenuPrimitive.Trigger>,
68
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Trigger>
69
+ >(({ onContextMenuCapture, ...props }, ref) => {
70
+ const ctx = React.useContext(ContextMenuOpenContext);
71
+
72
+ return (
73
+ <ContextMenuPrimitive.Trigger
74
+ ref={ref}
75
+ // Capture phase runs before Radix's bubble-phase open handler. If the menu
76
+ // is already open, close it and swallow the event so Radix doesn't reopen
77
+ // it — a second right-click just dismisses the menu.
78
+ onContextMenuCapture={(event) => {
79
+ onContextMenuCapture?.(event);
80
+ if (ctx?.open) {
81
+ event.preventDefault();
82
+ event.stopPropagation();
83
+ ctx.setOpen(false);
84
+ }
85
+ }}
86
+ {...props}
87
+ />
88
+ );
89
+ });
90
+ ContextMenuTrigger.displayName = ContextMenuPrimitive.Trigger.displayName;
91
+
92
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
93
+
94
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
95
+
96
+ const ContextMenuGroup = React.forwardRef<
97
+ React.ElementRef<typeof ContextMenuPrimitive.Group>,
98
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Group> &
99
+ VariantProps<typeof menuGroupStyles>
100
+ >(({ className, variant = "Boxed", ...props }, ref) => (
101
+ <ContextMenuPrimitive.Group
102
+ ref={ref}
103
+ className={cn(menuGroupStyles({ variant }), className)}
104
+ {...props}
105
+ />
106
+ ));
107
+ ContextMenuGroup.displayName = ContextMenuPrimitive.Group.displayName;
108
+
109
+ const ContextMenuRadioGroup = React.forwardRef<
110
+ React.ElementRef<typeof ContextMenuPrimitive.RadioGroup>,
111
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioGroup> &
112
+ VariantProps<typeof menuGroupStyles>
113
+ >(({ className, variant = "Boxed", ...props }, ref) => (
114
+ <ContextMenuPrimitive.RadioGroup
115
+ ref={ref}
116
+ className={cn(menuGroupStyles({ variant }), className)}
117
+ {...props}
118
+ />
119
+ ));
120
+ ContextMenuRadioGroup.displayName = ContextMenuPrimitive.RadioGroup.displayName;
121
+
122
+ const ContextMenuContent = React.forwardRef<
123
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
124
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> &
125
+ ContextMenuContentProps & { autoGroup?: boolean }
126
+ >(
127
+ (
128
+ {
129
+ theme,
130
+ className,
131
+ variant = "PresentationStyle",
132
+ autoGroup = true,
133
+ collisionPadding = 8,
134
+ children,
135
+ ...props
136
+ },
137
+ ref
138
+ ) => (
139
+ <ContextMenuPrimitive.Portal>
140
+ <ContextMenuPrimitive.Content
141
+ data-theme={theme}
142
+ ref={ref}
143
+ collisionPadding={collisionPadding}
144
+ className={cn(
145
+ menuContentStyles({ variant }),
146
+ // Cap to the space Radix has after collision handling so a tall menu
147
+ // scrolls instead of overflowing off-screen.
148
+ "max-h-[var(--radix-context-menu-content-available-height)]",
149
+ className
150
+ )}
151
+ {...props}
152
+ >
153
+ {autoGroup ? autoGroupChildren(children) : children}
154
+ </ContextMenuPrimitive.Content>
155
+ </ContextMenuPrimitive.Portal>
156
+ )
157
+ );
158
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
159
+
160
+ const ContextMenuSubTrigger = React.forwardRef<
161
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
162
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
163
+ inset?: boolean;
164
+ } & VariantProps<typeof MenuItemStyles>
165
+ >(
166
+ ({ className, inset, children, variant = "Default", size = "M", ...props }, ref) => (
167
+ <ContextMenuPrimitive.SubTrigger
168
+ ref={ref}
169
+ className={cn(MenuItemStyles({ variant, size }), "justify-between", className)}
170
+ {...props}
171
+ >
172
+ <div className="justify-between">
173
+ <div className="flex gap-2">{children}</div>
174
+ <i className="ri-arrow-right-s-line text-[16px] rtl:rotate-180"></i>
175
+ </div>
176
+ </ContextMenuPrimitive.SubTrigger>
177
+ )
178
+ );
179
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
180
+
181
+ const ContextMenuSubContent = React.forwardRef<
182
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
183
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> & {
184
+ variant?: "PresentationStyle";
185
+ autoGroup?: boolean;
186
+ }
187
+ >(
188
+ ({ className, variant = "PresentationStyle", autoGroup = true, children, ...props }, ref) => (
189
+ <ContextMenuPrimitive.Portal>
190
+ <ContextMenuPrimitive.SubContent
191
+ ref={ref}
192
+ className={cn(menuContentStyles({ variant }), className)}
193
+ {...props}
194
+ >
195
+ {autoGroup ? autoGroupChildren(children) : children}
196
+ </ContextMenuPrimitive.SubContent>
197
+ </ContextMenuPrimitive.Portal>
198
+ )
199
+ );
200
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
201
+
202
+ const ContextMenuItem = React.forwardRef<
203
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
204
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
205
+ inset?: boolean;
206
+ } & VariantProps<typeof MenuItemStyles>
207
+ >(
208
+ ({ className, inset, children, variant = "Default", size = "M", active, ...props }, ref) => (
209
+ <ContextMenuPrimitive.Item
210
+ {...props}
211
+ ref={ref}
212
+ className={cn(MenuItemStyles({ variant, size, active }), className)}
213
+ >
214
+ <div>{children}</div>
215
+ </ContextMenuPrimitive.Item>
216
+ )
217
+ );
218
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
219
+
220
+ const ContextMenuCheckboxItem = React.forwardRef<
221
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
222
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> &
223
+ VariantProps<typeof MenuItemStyles>
224
+ >(
225
+ ({ className, children, checked, variant = "Default", size = "M", onSelect, ...props }, ref) => (
226
+ <ContextMenuPrimitive.CheckboxItem
227
+ ref={ref}
228
+ className={cn(MenuItemStyles({ variant, size }), "relative", className)}
229
+ checked={checked}
230
+ // Keep the menu open when toggling; preventDefault stops Radix's auto-close.
231
+ onSelect={(event) => {
232
+ event.preventDefault();
233
+ onSelect?.(event);
234
+ }}
235
+ {...props}
236
+ >
237
+ <div className="relative flex items-center">
238
+ <span className="h-full flex items-center justify-center">
239
+ {/* Unchecked box; hidden once the item is checked. */}
240
+ <div className="flex justify-center items-center h-full [[data-state=checked]_&]:hidden">
241
+ <div className="w-[16px] h-[16px] rounded-[3px] border border-white-alpha-40 bg-black-alpha-15 group-hover:border-white-700 group-hover:bg-black-alpha-075"></div>
242
+ </div>
243
+
244
+ {/* Checked indicator only renders when checked. */}
245
+ <ContextMenuPrimitive.ItemIndicator>
246
+ <div className="bg-blue-sparkle-600 flex justify-center items-center w-[16px] h-[16px] rounded-[3px]">
247
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
248
+ <path d="M5.8339 8.84977L11.1961 3.48755L12.0211 4.3125L5.8339 10.4997L2.12158 6.7874L2.94654 5.96245L5.8339 8.84977Z" fill="#F9F9F9" />
249
+ </svg>
250
+ </div>
251
+ </ContextMenuPrimitive.ItemIndicator>
252
+ </span>{" "}
253
+ {children}
254
+ </div>
255
+ </ContextMenuPrimitive.CheckboxItem>
256
+ )
257
+ );
258
+ ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
259
+
260
+ const ContextMenuRadioItem = React.forwardRef<
261
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
262
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> &
263
+ VariantProps<typeof MenuItemStyles>
264
+ >(({ className, children, variant = "Default", size = "M", onSelect, ...props }, ref) => (
265
+ <ContextMenuPrimitive.RadioItem
266
+ ref={ref}
267
+ className={cn(MenuItemStyles({ variant, size }), "relative", className)}
268
+ // Keep the menu open when selecting; preventDefault stops Radix's auto-close.
269
+ onSelect={(event) => {
270
+ event.preventDefault();
271
+ onSelect?.(event);
272
+ }}
273
+ {...props}
274
+ >
275
+ <div className="relative flex items-center">
276
+ <span className="h-full left-2 flex h-3.5 w-3.5 items-center justify-center">
277
+ {/* Unselected dot; hidden once the item is selected. */}
278
+ <div className="flex justify-center items-center h-full [[data-state=checked]_&]:hidden">
279
+ <div className="w-[14px] h-[14px] rounded-[100px] border border-white-alpha-40 bg-black-alpha-15 group-hover:border-white-700 group-hover:bg-black-alpha-075"></div>
280
+ </div>
281
+ <ContextMenuPrimitive.ItemIndicator>
282
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 14" fill="none">
283
+ <rect width="14" height="14" rx="7" fill="#005ECC" />
284
+ <rect x="5" y="5" width="4" height="4" rx="2" fill="white" />
285
+ </svg>
286
+ </ContextMenuPrimitive.ItemIndicator>
287
+ </span>
288
+ {children}
289
+ </div>
290
+ </ContextMenuPrimitive.RadioItem>
291
+ ));
292
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
293
+
294
+ // Item types that should be boxed together when sitting loose in the menu.
295
+ // ContextMenuSub is included because its trigger renders as an inline row.
296
+ const GROUPABLE_TYPES = [
297
+ ContextMenuItem,
298
+ ContextMenuCheckboxItem,
299
+ ContextMenuRadioItem,
300
+ ContextMenuSub,
301
+ ] as const;
302
+
303
+ const isGroupable = (child: React.ReactNode): child is React.ReactElement =>
304
+ React.isValidElement(child) &&
305
+ (GROUPABLE_TYPES as readonly React.ElementType[]).includes(
306
+ child.type as React.ElementType
307
+ );
308
+
309
+ // Wraps consecutive runs of loose items in a Boxed ContextMenuGroup so items
310
+ // render inside a container even when the consumer doesn't write one. Labels,
311
+ // separators and explicit groups act as boundaries and pass through unchanged.
312
+ function autoGroupChildren(children: React.ReactNode): React.ReactNode {
313
+ const out: React.ReactNode[] = [];
314
+ let run: React.ReactElement[] = [];
315
+
316
+ const flush = (key: string) => {
317
+ if (run.length === 0) return;
318
+ out.push(
319
+ <ContextMenuGroup key={key} variant="Boxed">
320
+ {run}
321
+ </ContextMenuGroup>
322
+ );
323
+ run = [];
324
+ };
325
+
326
+ React.Children.toArray(children).forEach((child, index) => {
327
+ if (isGroupable(child)) {
328
+ run.push(child);
329
+ } else {
330
+ flush(`auto-group-${index}`);
331
+ out.push(child);
332
+ }
333
+ });
334
+ flush("auto-group-last");
335
+
336
+ return out;
337
+ }
338
+
339
+ const ContextMenuLabel = React.forwardRef<
340
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
341
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
342
+ inset?: boolean;
343
+ }
344
+ >(({ className, inset, ...props }, ref) => (
345
+ <ContextMenuPrimitive.Label
346
+ ref={ref}
347
+ className={cn(
348
+ "text-content-presentation-global-primary-light typography-body-small-medium px-[12px] pt-1 flex justify-start items-center",
349
+ className
350
+ )}
351
+ {...props}
352
+ />
353
+ ));
354
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
355
+
356
+ const ContextMenuShortcut = ({
357
+ className,
358
+ ...props
359
+ }: React.HTMLAttributes<HTMLSpanElement>) => (
360
+ <span
361
+ className={cn("ml-auto ltr:ml-0 rtl:mr-auto text-xs tracking-widest opacity-60", className)}
362
+ {...props}
363
+ />
364
+ );
365
+ ContextMenuShortcut.displayName = "ContextMenuShortcut";
366
+
367
+ export {
368
+ ContextMenu,
369
+ ContextMenuTrigger,
370
+ ContextMenuContent,
371
+ ContextMenuItem,
372
+ ContextMenuGroup,
373
+ ContextMenuPortal,
374
+ ContextMenuSub,
375
+ ContextMenuSubTrigger,
376
+ ContextMenuSubContent,
377
+ ContextMenuRadioGroup,
378
+ ContextMenuCheckboxItem,
379
+ ContextMenuRadioItem,
380
+ ContextMenuLabel,
381
+ ContextMenuShortcut,
382
+ };
383
+
384
+ const MenuItemStyles = cva(
385
+ [
386
+ "text-content-presentation-global-primary-light typography-body-medium-regular",
387
+ "outline-none",
388
+ "border",
389
+ "border-transparent",
390
+ "flex",
391
+ "items-center",
392
+ "justify-start",
393
+ "text-overflow",
394
+ "overflow-hidden",
395
+ "p-[2px]",
396
+ "transition-all",
397
+ "bg-[rgba(184,192,204,0.36)]",
398
+ "ease-in-out",
399
+ "duration-300",
400
+ "[&>div]:flex",
401
+ "[&>div]:px-[12px]",
402
+ "[&>div]:py-[4px]",
403
+ "[&>div]:gap-2",
404
+ "[&>div]:w-full",
405
+ "[&>div]:rounded-[8px]",
406
+ "[&>div]:items-center ",
407
+ "group",
408
+ ],
409
+ {
410
+ variants: {
411
+ variant: {
412
+ Default: [
413
+ "text-content-presentation-global-primary-light",
414
+ "[&>div]:hover:bg-white-50 [&>div]:hover:shadow-[0_0_16px_0_rgba(0,0,0,0.36)]",
415
+ "[&>div]:hover:text-black-1000",
416
+ "[&[data-highlighted]>div]:bg-white-alpha-75",
417
+ "[&[data-highlighted]>div]:text-black-1000",
418
+ "[&[data-disabled]>div]:text-content-presentation-global-primary-light",
419
+ "[&[data-disabled]>div]:opacity-50",
420
+ // Disabled items are pointer-events:none by default (Radix), so
421
+ // re-enable them to allow hover styling without making them selectable.
422
+ "[&[data-disabled]]:pointer-events-auto",
423
+ "[&[data-disabled]>div]:hover:text-content-presentation-global-primary-light",
424
+ "[&[data-disabled]>div]:hover:bg-transparent",
425
+ "[&[data-disabled]>div]:hover:shadow-none",
426
+ ],
427
+ info: [
428
+ "text-blue-sparkle-200",
429
+ "[&>div]:hover:bg-white-50 [&>div]:hover:shadow-[0_0_16px_0_rgba(0,0,0,0.36)]",
430
+ "[&>div]:hover:text-blue-sparkle-700",
431
+ "[&[data-highlighted]>div]:bg-white-alpha-75",
432
+ "[&[data-highlighted]>div]:text-blue-sparkle-700",
433
+ "[&[data-disabled]>div]:text-content-presentation-global-primary-light",
434
+ "[&[data-disabled]>div]:opacity-50",
435
+ "[&[data-disabled]]:pointer-events-auto",
436
+ "[&[data-disabled]>div]:hover:text-content-presentation-global-primary-light",
437
+ "[&[data-disabled]>div]:hover:bg-transparent",
438
+ "[&[data-disabled]>div]:hover:shadow-none",
439
+ ],
440
+ Negative: [
441
+ "text-medium-red-200",
442
+ "[&>div]:hover:bg-white-50 [&>div]:hover:shadow-[0_0_16px_0_rgba(0,0,0,0.36)]",
443
+ "[&>div]:hover:text-medium-red-600",
444
+ "[&[data-highlighted]>div]:bg-white-alpha-75",
445
+ "[&[data-highlighted]>div]:text-medium-red-600",
446
+ "[&[data-disabled]>div]:text-content-presentation-global-primary-light",
447
+ "[&[data-disabled]>div]:opacity-50",
448
+ "[&[data-disabled]]:pointer-events-auto",
449
+ "[&[data-disabled]>div]:hover:text-content-presentation-global-primary-light",
450
+ "[&[data-disabled]>div]:hover:bg-transparent",
451
+ "[&[data-disabled]>div]:hover:shadow-none",
452
+ ],
453
+ },
454
+ size: {
455
+ S: ["typography-body-small-regular", "h-[24px]"],
456
+ M: ["typography-body-medium-regular", "h-[32px]"],
457
+ },
458
+ active: {
459
+ true: [
460
+ "bg-background-presentation-action-selected",
461
+ "text-content-presentation-action-light-primary",
462
+ ],
463
+ },
464
+ defaultVariants: {
465
+ variant: "Default",
466
+ size: "M",
467
+ active: false,
468
+ },
469
+ },
470
+ compoundVariants: [
471
+ {
472
+ active: true,
473
+ variant: "info",
474
+ className: ["text-content-presentation-state-negative"],
475
+ },
476
+ ],
477
+ }
478
+ );
479
+
480
+ const menuContentStyles = cva(
481
+ [
482
+ "p-1",
483
+ "rounded-[14px]",
484
+ "min-w-[240px]",
485
+ "outline-none",
486
+ "overflow-scroll",
487
+ // Only animate the OPEN (enter) state. An exit animation on [data-state=closed]
488
+ // holds the old DOM node during close, which breaks close/reposition on a
489
+ // second right-click (Radix issue #2572).
490
+ "data-[state=open]:animate-in",
491
+ "data-[state=open]:fade-in-0",
492
+ "overflow-x-hidden",
493
+ "scrollbar-hide",
494
+ "backdrop-blur-[21px]",
495
+ "flex gap-1 flex-col",
496
+ ],
497
+ {
498
+ variants: {
499
+ variant: {
500
+ PresentationStyle: [
501
+ "bg-[rgba(61,64,69,0.72)]",
502
+ "shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
503
+ ],
504
+ },
505
+ defaultVariants: {
506
+ variant: "PresentationStyle",
507
+ },
508
+ },
509
+ }
510
+ );
511
+
512
+ const menuGroupStyles = cva(["flex", "flex-col"], {
513
+ variants: {
514
+ variant: {
515
+ // Visually contains its items in a bordered card.
516
+ Boxed: ["gap-[1px]", "rounded-[10px]", "bg-bldue-500", "overflow-hidden"],
517
+ // No container — semantic grouping only (Radix default behavior).
518
+ Plain: [],
519
+ },
520
+ },
521
+ defaultVariants: {
522
+ variant: "Boxed",
523
+ },
524
+ });