@djangocfg/widget-visual 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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/package.json +106 -0
  4. package/src/design/ColorPalette/ColorPalette.tsx +129 -0
  5. package/src/design/ColorPalette/README.md +34 -0
  6. package/src/design/ColorPalette/color-palette.stories.tsx +95 -0
  7. package/src/design/ColorPalette/components/Swatch.tsx +102 -0
  8. package/src/design/ColorPalette/hooks/useCopyToClipboard.ts +41 -0
  9. package/src/design/ColorPalette/index.ts +12 -0
  10. package/src/design/ColorPalette/lazy.tsx +21 -0
  11. package/src/design/ColorPalette/types.ts +63 -0
  12. package/src/design/ColorPalette/utils.ts +114 -0
  13. package/src/design/ColorPicker/ColorPicker.tsx +257 -0
  14. package/src/design/ColorPicker/color-picker.stories.tsx +113 -0
  15. package/src/design/ColorPicker/context/ColorPickerContext.tsx +28 -0
  16. package/src/design/ColorPicker/context/ColorPickerStore.tsx +166 -0
  17. package/src/design/ColorPicker/context/index.ts +2 -0
  18. package/src/design/ColorPicker/index.ts +15 -0
  19. package/src/design/ColorPicker/lazy.tsx +24 -0
  20. package/src/design/ColorPicker/lib/color-utils.ts +323 -0
  21. package/src/design/ColorPicker/parts/ColorPickerAlphaSlider.tsx +72 -0
  22. package/src/design/ColorPicker/parts/ColorPickerArea.tsx +155 -0
  23. package/src/design/ColorPicker/parts/ColorPickerEyeDropper.tsx +73 -0
  24. package/src/design/ColorPicker/parts/ColorPickerFormatSelect.tsx +64 -0
  25. package/src/design/ColorPicker/parts/ColorPickerHueSlider.tsx +64 -0
  26. package/src/design/ColorPicker/parts/ColorPickerInput.tsx +512 -0
  27. package/src/design/ColorPicker/parts/ColorPickerSwatch.tsx +63 -0
  28. package/src/design/ColorPicker/parts/index.ts +7 -0
  29. package/src/design/ColorPicker/types.ts +77 -0
  30. package/src/gallery/components/Gallery.tsx +182 -0
  31. package/src/gallery/components/compact/GalleryCompact.tsx +396 -0
  32. package/src/gallery/components/compact/index.ts +1 -0
  33. package/src/gallery/components/index.ts +21 -0
  34. package/src/gallery/components/lightbox/GalleryLightbox.tsx +426 -0
  35. package/src/gallery/components/lightbox/index.ts +1 -0
  36. package/src/gallery/components/media/GalleryImage.tsx +105 -0
  37. package/src/gallery/components/media/GalleryMedia.tsx +70 -0
  38. package/src/gallery/components/media/GalleryVideo.tsx +241 -0
  39. package/src/gallery/components/media/index.ts +3 -0
  40. package/src/gallery/components/preview/GalleryCarousel.tsx +374 -0
  41. package/src/gallery/components/preview/GalleryGrid.tsx +519 -0
  42. package/src/gallery/components/preview/index.ts +2 -0
  43. package/src/gallery/components/shared/ImageSpinner.tsx +37 -0
  44. package/src/gallery/components/shared/index.ts +1 -0
  45. package/src/gallery/components/thumbnails/GalleryThumbnails.tsx +198 -0
  46. package/src/gallery/components/thumbnails/GalleryThumbnailsVirtual.tsx +164 -0
  47. package/src/gallery/components/thumbnails/index.ts +2 -0
  48. package/src/gallery/gallery.stories.tsx +182 -0
  49. package/src/gallery/hooks/index.ts +23 -0
  50. package/src/gallery/hooks/useGallery.ts +138 -0
  51. package/src/gallery/hooks/useImageDimensions.ts +224 -0
  52. package/src/gallery/hooks/usePinchZoom.ts +239 -0
  53. package/src/gallery/hooks/usePreloadImages.ts +119 -0
  54. package/src/gallery/hooks/useSwipe.ts +87 -0
  55. package/src/gallery/hooks/useVirtualList.ts +129 -0
  56. package/src/gallery/hooks/useZoom.ts +321 -0
  57. package/src/gallery/index.ts +66 -0
  58. package/src/gallery/thumbnails.stories.tsx +127 -0
  59. package/src/gallery/types.ts +185 -0
  60. package/src/gallery/utils/imageAnalysis.ts +52 -0
  61. package/src/gallery/utils/index.ts +16 -0
  62. package/src/gallery/utils/normalizeUrl.ts +31 -0
  63. package/src/index.ts +18 -0
  64. package/src/lottie/LottiePlayer.client.tsx +319 -0
  65. package/src/lottie/index.tsx +66 -0
  66. package/src/lottie/lazy.tsx +65 -0
  67. package/src/lottie/lottie-player.stories.tsx +173 -0
  68. package/src/lottie/types.ts +138 -0
  69. package/src/lottie/useLottie.ts +187 -0
  70. package/src/lottie/usePrefersReducedMotion.ts +46 -0
  71. package/src/marquee/Marquee.tsx +637 -0
  72. package/src/marquee/index.ts +7 -0
  73. package/src/marquee/lazy.tsx +14 -0
  74. package/src/marquee/marquee.stories.tsx +118 -0
  75. package/src/marquee/types.ts +40 -0
  76. package/src/qrcode/QRCode.tsx +468 -0
  77. package/src/qrcode/index.ts +10 -0
  78. package/src/qrcode/lazy.tsx +19 -0
  79. package/src/qrcode/qr-code.stories.tsx +102 -0
  80. package/src/qrcode/types.ts +57 -0
@@ -0,0 +1,637 @@
1
+ 'use client';
2
+
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { Slot as SlotPrimitive } from '@radix-ui/react-slot';
5
+ import { useDirection as useDirectionPrimitive } from '@radix-ui/react-direction';
6
+ import * as React from 'react';
7
+ import { useComposedRefs } from '@djangocfg/ui-core/lib';
8
+ import { cn } from '@djangocfg/ui-core/lib';
9
+ import type { MarqueeProps, MarqueeContextValue, MarqueeEdgeProps, Side, Orientation } from './types';
10
+
11
+ const ROOT_NAME = 'Marquee';
12
+ const CONTENT_NAME = 'MarqueeContent';
13
+
14
+ type RootElement = HTMLDivElement;
15
+ type ContentElement = HTMLDivElement;
16
+
17
+ interface Dimensions {
18
+ width: number;
19
+ height: number;
20
+ }
21
+
22
+ interface ElementDimensions {
23
+ rootSize: number;
24
+ contentSize: number;
25
+ }
26
+
27
+ function createResizeObserverStore() {
28
+ const listeners = new Set<() => void>();
29
+ let observer: ResizeObserver | null = null;
30
+ const elements = new Map<Element, Dimensions>();
31
+ const refCounts = new Map<Element, number>();
32
+ const isSupported = typeof ResizeObserver !== 'undefined';
33
+ let notificationScheduled = false;
34
+
35
+ const snapshotCache = new WeakMap<
36
+ Element,
37
+ WeakMap<
38
+ Element,
39
+ { horizontal: ElementDimensions; vertical: ElementDimensions }
40
+ >
41
+ >();
42
+
43
+ function notify() {
44
+ if (notificationScheduled) return;
45
+ notificationScheduled = true;
46
+ queueMicrotask(() => {
47
+ notificationScheduled = false;
48
+ for (const callback of listeners) {
49
+ callback();
50
+ }
51
+ });
52
+ }
53
+
54
+ function cleanup() {
55
+ if (observer) {
56
+ observer.disconnect();
57
+ observer = null;
58
+ }
59
+ elements.clear();
60
+ refCounts.clear();
61
+ }
62
+
63
+ function subscribe(callback: () => void) {
64
+ listeners.add(callback);
65
+ return () => {
66
+ listeners.delete(callback);
67
+ if (listeners.size === 0) {
68
+ cleanup();
69
+ }
70
+ };
71
+ }
72
+
73
+ function getSnapshot(
74
+ rootElement: RootElement | null,
75
+ contentElement: ContentElement | null,
76
+ orientation: Orientation,
77
+ ): ElementDimensions | null {
78
+ if (!rootElement || !contentElement) return null;
79
+
80
+ const rootDims = elements.get(rootElement);
81
+ const contentDims = elements.get(contentElement);
82
+
83
+ if (!rootDims || !contentDims) return null;
84
+
85
+ const rootSize = orientation === 'vertical' ? rootDims.height : rootDims.width;
86
+ const contentSize = orientation === 'vertical' ? contentDims.height : contentDims.width;
87
+
88
+ let rootCache = snapshotCache.get(rootElement);
89
+ if (!rootCache) {
90
+ rootCache = new WeakMap();
91
+ snapshotCache.set(rootElement, rootCache);
92
+ }
93
+
94
+ let contentCache = rootCache.get(contentElement);
95
+ if (!contentCache) {
96
+ contentCache = {
97
+ horizontal: { rootSize: -1, contentSize: -1 },
98
+ vertical: { rootSize: -1, contentSize: -1 },
99
+ };
100
+ rootCache.set(contentElement, contentCache);
101
+ }
102
+
103
+ const cached = contentCache[orientation];
104
+ if (cached.rootSize === rootSize && cached.contentSize === contentSize) {
105
+ return cached;
106
+ }
107
+
108
+ const snapshot = { rootSize, contentSize };
109
+ contentCache[orientation] = snapshot;
110
+ return snapshot;
111
+ }
112
+
113
+ function observe(
114
+ rootElement: RootElement | null,
115
+ contentElement: Element | null,
116
+ ) {
117
+ if (!isSupported || !rootElement || !contentElement) return;
118
+
119
+ if (!observer) {
120
+ observer = new ResizeObserver((entries) => {
121
+ let hasChanged = false;
122
+
123
+ for (const entry of entries) {
124
+ const element = entry.target;
125
+ const { width, height } = entry.contentRect;
126
+
127
+ const currentData = elements.get(element);
128
+
129
+ if (
130
+ !currentData ||
131
+ currentData.width !== width ||
132
+ currentData.height !== height
133
+ ) {
134
+ elements.set(element, { width, height });
135
+ hasChanged = true;
136
+ }
137
+ }
138
+
139
+ if (hasChanged) {
140
+ notify();
141
+ }
142
+ });
143
+ }
144
+
145
+ refCounts.set(rootElement, (refCounts.get(rootElement) ?? 0) + 1);
146
+ refCounts.set(contentElement, (refCounts.get(contentElement) ?? 0) + 1);
147
+
148
+ observer.observe(rootElement);
149
+ observer.observe(contentElement);
150
+
151
+ const rootRect = rootElement.getBoundingClientRect();
152
+ const contentRect = contentElement.getBoundingClientRect();
153
+
154
+ const rootData = { width: rootRect.width, height: rootRect.height };
155
+ const contentData = {
156
+ width: contentRect.width,
157
+ height: contentRect.height,
158
+ };
159
+
160
+ elements.set(rootElement, rootData);
161
+ elements.set(contentElement, contentData);
162
+
163
+ if (
164
+ rootData.width > 0 &&
165
+ rootData.height > 0 &&
166
+ contentData.width > 0 &&
167
+ contentData.height > 0
168
+ ) {
169
+ notify();
170
+ }
171
+ }
172
+
173
+ function unobserve(
174
+ rootElement: RootElement | null,
175
+ contentElement: Element | null,
176
+ ) {
177
+ if (!observer || !rootElement || !contentElement) return;
178
+
179
+ const rootCount = (refCounts.get(rootElement) ?? 1) - 1;
180
+ const contentCount = (refCounts.get(contentElement) ?? 1) - 1;
181
+
182
+ if (rootCount <= 0) {
183
+ observer.unobserve(rootElement);
184
+ elements.delete(rootElement);
185
+ refCounts.delete(rootElement);
186
+ } else {
187
+ refCounts.set(rootElement, rootCount);
188
+ }
189
+
190
+ if (contentCount <= 0) {
191
+ observer.unobserve(contentElement);
192
+ elements.delete(contentElement);
193
+ refCounts.delete(contentElement);
194
+ } else {
195
+ refCounts.set(contentElement, contentCount);
196
+ }
197
+ }
198
+
199
+ return {
200
+ subscribe,
201
+ getSnapshot,
202
+ observe,
203
+ unobserve,
204
+ };
205
+ }
206
+
207
+ const resizeObserverStore = createResizeObserverStore();
208
+
209
+ function useResizeObserverStore(
210
+ rootRef: React.RefObject<RootElement | null>,
211
+ contentRef: React.RefObject<ContentElement | null>,
212
+ orientation: Orientation,
213
+ ) {
214
+ const onSubscribe = React.useCallback(
215
+ (callback: () => void) => resizeObserverStore.subscribe(callback),
216
+ [],
217
+ );
218
+
219
+ const getSnapshot = React.useCallback(
220
+ () =>
221
+ resizeObserverStore.getSnapshot(
222
+ rootRef.current,
223
+ contentRef.current,
224
+ orientation,
225
+ ),
226
+ [rootRef, contentRef, orientation],
227
+ );
228
+
229
+ return React.useSyncExternalStore(onSubscribe, getSnapshot, getSnapshot);
230
+ }
231
+
232
+ const MarqueeContext = React.createContext<MarqueeContextValue | null>(null);
233
+
234
+ function useMarqueeContext(consumerName: string) {
235
+ const context = React.useContext(MarqueeContext);
236
+ if (!context) {
237
+ throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
238
+ }
239
+ return context;
240
+ }
241
+
242
+ export function Marquee(props: MarqueeProps) {
243
+ const {
244
+ side = 'left',
245
+ dir: dirProp,
246
+ speed = 50,
247
+ delay = 0,
248
+ loopCount = 0,
249
+ gap = '1rem',
250
+ asChild,
251
+ autoFill = false,
252
+ pauseOnHover = false,
253
+ pauseOnKeyboard = false,
254
+ reverse = false,
255
+ className,
256
+ style: styleProp,
257
+ ref,
258
+ ...marqueeProps
259
+ } = props;
260
+
261
+ const orientation: Orientation =
262
+ side === 'top' || side === 'bottom' ? 'vertical' : 'horizontal';
263
+
264
+ const dir = useDirectionPrimitive(dirProp);
265
+
266
+ const rootRef = React.useRef<RootElement>(null);
267
+ const contentRef = React.useRef<ContentElement>(null);
268
+ const composedRef = useComposedRefs(ref, rootRef);
269
+
270
+ const [paused, setPaused] = React.useState(false);
271
+
272
+ const onKeyDown = React.useCallback(
273
+ (event: React.KeyboardEvent) => {
274
+ if (pauseOnKeyboard && event.key === ' ') {
275
+ event.preventDefault();
276
+ setPaused((prev) => !prev);
277
+ }
278
+ },
279
+ [pauseOnKeyboard],
280
+ );
281
+
282
+ const dimensions = useResizeObserverStore(rootRef, contentRef, orientation);
283
+
284
+ const duration = React.useMemo(() => {
285
+ const safeSpeed = Math.max(0.001, speed);
286
+
287
+ if (!dimensions) {
288
+ const defaultDistance = autoFill ? 1000 : 2000;
289
+ return defaultDistance / safeSpeed;
290
+ }
291
+
292
+ const { rootSize, contentSize } = dimensions;
293
+
294
+ if (autoFill) {
295
+ const multiplier =
296
+ contentSize < rootSize ? Math.ceil(rootSize / contentSize) : 1;
297
+ return (contentSize * multiplier) / safeSpeed;
298
+ } else {
299
+ return contentSize < rootSize
300
+ ? rootSize / safeSpeed
301
+ : contentSize / safeSpeed;
302
+ }
303
+ }, [dimensions, speed, autoFill]);
304
+
305
+ const style = React.useMemo<React.CSSProperties>(
306
+ () => ({
307
+ '--marquee-duration': `${duration}s`,
308
+ '--marquee-gap': gap,
309
+ '--marquee-delay': `${delay}s`,
310
+ '--marquee-loop-count':
311
+ loopCount === 0 || loopCount === Infinity
312
+ ? 'infinite'
313
+ : loopCount.toString(),
314
+ ...styleProp,
315
+ }),
316
+ [duration, gap, delay, loopCount, styleProp],
317
+ );
318
+
319
+ const contextValue = React.useMemo<MarqueeContextValue>(
320
+ () => ({
321
+ side,
322
+ orientation,
323
+ dir,
324
+ speed,
325
+ loopCount,
326
+ contentRef,
327
+ rootRef,
328
+ autoFill,
329
+ paused,
330
+ pauseOnHover,
331
+ pauseOnKeyboard,
332
+ reverse,
333
+ }),
334
+ [
335
+ side,
336
+ orientation,
337
+ dir,
338
+ speed,
339
+ loopCount,
340
+ autoFill,
341
+ paused,
342
+ pauseOnHover,
343
+ pauseOnKeyboard,
344
+ reverse,
345
+ ],
346
+ );
347
+
348
+ const MarqueePrimitive = asChild ? SlotPrimitive : 'div';
349
+
350
+ return (
351
+ <MarqueeContext.Provider value={contextValue}>
352
+ <div data-slot="marquee-wrapper" className="grid">
353
+ <MarqueePrimitive
354
+ role="marquee"
355
+ aria-live="off"
356
+ data-orientation={orientation}
357
+ data-slot="marquee"
358
+ dir={dir}
359
+ tabIndex={pauseOnKeyboard ? 0 : undefined}
360
+ {...marqueeProps}
361
+ ref={composedRef}
362
+ className={cn(
363
+ 'relative flex overflow-hidden motion-reduce:animate-none',
364
+ orientation === 'vertical' && 'h-full flex-col',
365
+ orientation === 'horizontal' && 'w-full',
366
+ paused && '[&_*]:[animation-play-state:paused]',
367
+ pauseOnHover && 'group',
368
+ pauseOnKeyboard &&
369
+ 'rounded-md focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
370
+ className,
371
+ )}
372
+ style={style}
373
+ onKeyDown={pauseOnKeyboard ? onKeyDown : undefined}
374
+ />
375
+ </div>
376
+ </MarqueeContext.Provider>
377
+ );
378
+ }
379
+
380
+ Marquee.displayName = ROOT_NAME;
381
+
382
+ const marqueeContentVariants = cva(
383
+ 'flex min-w-full shrink-0 gap-(--marquee-gap)',
384
+ {
385
+ variants: {
386
+ side: {
387
+ left: 'animate-marquee-left',
388
+ right: 'animate-marquee-right',
389
+ top: 'min-h-full min-w-auto animate-marquee-up flex-col',
390
+ bottom: 'min-h-full min-w-auto animate-marquee-down flex-col',
391
+ },
392
+ dir: {
393
+ ltr: '',
394
+ rtl: '',
395
+ },
396
+ pauseOnHover: {
397
+ true: 'group-hover:[animation-play-state:paused]',
398
+ false: '',
399
+ },
400
+ reverse: {
401
+ true: '[animation-direction:reverse]',
402
+ false: '',
403
+ },
404
+ },
405
+ compoundVariants: [
406
+ {
407
+ side: 'left',
408
+ dir: 'rtl',
409
+ className: 'animate-marquee-left-rtl',
410
+ },
411
+ {
412
+ side: 'right',
413
+ dir: 'rtl',
414
+ className: 'animate-marquee-right-rtl',
415
+ },
416
+ ],
417
+ defaultVariants: {
418
+ side: 'left',
419
+ dir: 'ltr',
420
+ pauseOnHover: false,
421
+ reverse: false,
422
+ },
423
+ },
424
+ );
425
+
426
+ export function MarqueeContent(props: React.ComponentProps<'div'> & { asChild?: boolean }) {
427
+ const {
428
+ className,
429
+ asChild,
430
+ ref,
431
+ children,
432
+ style: styleProp,
433
+ ...contentProps
434
+ } = props;
435
+
436
+ const context = useMarqueeContext(CONTENT_NAME);
437
+ const composedRef = useComposedRefs(ref, context.contentRef);
438
+
439
+ const isVertical = context.orientation === 'vertical';
440
+ const isRtl = context.dir === 'rtl';
441
+
442
+ const dimensions = useResizeObserverStore(
443
+ context.rootRef,
444
+ context.contentRef,
445
+ context.orientation,
446
+ );
447
+
448
+ React.useEffect(() => {
449
+ if (context.rootRef.current && context.contentRef.current) {
450
+ resizeObserverStore.observe(
451
+ context.rootRef.current,
452
+ context.contentRef.current,
453
+ );
454
+
455
+ return () => {
456
+ resizeObserverStore.unobserve(
457
+ context.rootRef.current,
458
+ context.contentRef.current,
459
+ );
460
+ };
461
+ }
462
+ }, [context.rootRef, context.contentRef]);
463
+
464
+ const multiplier = React.useMemo(() => {
465
+ if (!context.autoFill || !dimensions) return 1;
466
+
467
+ const { rootSize, contentSize } = dimensions;
468
+ if (contentSize === 0) return 1;
469
+
470
+ return contentSize < rootSize ? Math.ceil(rootSize / contentSize) : 1;
471
+ }, [context.autoFill, dimensions]);
472
+
473
+ const onMultipliedChildrenRender = React.useCallback(
474
+ (count: number) => {
475
+ return Array.from({ length: Math.max(0, count) }).map((_, i) => (
476
+ <React.Fragment key={i}>{children}</React.Fragment>
477
+ ));
478
+ },
479
+ [children],
480
+ );
481
+
482
+ const style = React.useMemo(
483
+ () => ({
484
+ ...styleProp,
485
+ animationDuration: 'var(--marquee-duration)',
486
+ animationDelay: 'var(--marquee-delay)',
487
+ animationIterationCount: 'var(--marquee-loop-count)',
488
+ animationDirection: context.reverse ? 'reverse' : 'normal',
489
+ }),
490
+ [styleProp, context.reverse],
491
+ );
492
+
493
+ const ContentPrimitive = asChild ? SlotPrimitive : 'div';
494
+
495
+ return (
496
+ <>
497
+ <ContentPrimitive
498
+ data-orientation={context.orientation}
499
+ data-slot="marquee-content"
500
+ {...contentProps}
501
+ style={style}
502
+ className={cn(
503
+ marqueeContentVariants({
504
+ side: context.side,
505
+ dir: context.dir,
506
+ pauseOnHover: context.pauseOnHover,
507
+ reverse: context.reverse,
508
+ className,
509
+ }),
510
+ isVertical && 'flex-col',
511
+ isVertical
512
+ ? 'mb-(--marquee-gap)'
513
+ : isRtl
514
+ ? 'ml-(--marquee-gap)'
515
+ : 'mr-(--marquee-gap)',
516
+ )}
517
+ >
518
+ <div
519
+ ref={composedRef}
520
+ className={cn(
521
+ 'flex shrink-0 gap-(--marquee-gap)',
522
+ isVertical && 'flex-col',
523
+ )}
524
+ >
525
+ {children}
526
+ </div>
527
+ {onMultipliedChildrenRender(multiplier - 1)}
528
+ </ContentPrimitive>
529
+ <ContentPrimitive
530
+ role="presentation"
531
+ aria-hidden="true"
532
+ {...contentProps}
533
+ style={style}
534
+ className={cn(
535
+ marqueeContentVariants({
536
+ side: context.side,
537
+ dir: context.dir,
538
+ pauseOnHover: context.pauseOnHover,
539
+ reverse: context.reverse,
540
+ className,
541
+ }),
542
+ isVertical && 'flex-col',
543
+ )}
544
+ >
545
+ {onMultipliedChildrenRender(multiplier)}
546
+ </ContentPrimitive>
547
+ </>
548
+ );
549
+ }
550
+
551
+ MarqueeContent.displayName = CONTENT_NAME;
552
+
553
+ export function MarqueeItem(props: React.ComponentProps<'div'> & { asChild?: boolean }) {
554
+ const { className, asChild, ...itemProps } = props;
555
+
556
+ const ItemPrimitive = asChild ? SlotPrimitive : 'div';
557
+
558
+ return (
559
+ <ItemPrimitive
560
+ data-slot="marquee-item"
561
+ {...itemProps}
562
+ className={cn('shrink-0', className)}
563
+ />
564
+ );
565
+ }
566
+
567
+ MarqueeItem.displayName = 'MarqueeItem';
568
+
569
+ const marqueeEdgeVariants = cva('pointer-events-none absolute z-10', {
570
+ variants: {
571
+ side: {
572
+ left: 'top-0 left-0 h-full bg-gradient-to-r from-background to-transparent',
573
+ right:
574
+ 'top-0 right-0 h-full bg-gradient-to-l from-background to-transparent',
575
+ top: 'top-0 left-0 w-full bg-gradient-to-b from-background to-transparent',
576
+ bottom:
577
+ 'bottom-0 left-0 w-full bg-gradient-to-t from-background to-transparent',
578
+ },
579
+ size: {
580
+ default: '',
581
+ sm: '',
582
+ lg: '',
583
+ },
584
+ },
585
+ compoundVariants: [
586
+ {
587
+ side: ['left', 'right'],
588
+ size: 'default',
589
+ className: 'w-1/4',
590
+ },
591
+ {
592
+ side: ['left', 'right'],
593
+ size: 'sm',
594
+ className: 'w-1/6',
595
+ },
596
+ {
597
+ side: ['left', 'right'],
598
+ size: 'lg',
599
+ className: 'w-1/3',
600
+ },
601
+ {
602
+ side: ['top', 'bottom'],
603
+ size: 'default',
604
+ className: 'h-1/4',
605
+ },
606
+ {
607
+ side: ['top', 'bottom'],
608
+ size: 'sm',
609
+ className: 'h-1/6',
610
+ },
611
+ {
612
+ side: ['top', 'bottom'],
613
+ size: 'lg',
614
+ className: 'h-1/3',
615
+ },
616
+ ],
617
+ defaultVariants: {
618
+ size: 'default',
619
+ },
620
+ });
621
+
622
+ export function MarqueeEdge(props: MarqueeEdgeProps) {
623
+ const { side, size = 'default', className, asChild, ...edgeProps } = props;
624
+
625
+ const EdgePrimitive = asChild ? SlotPrimitive : 'div';
626
+
627
+ return (
628
+ <EdgePrimitive
629
+ data-size={size}
630
+ data-slot="marquee-edge"
631
+ {...edgeProps}
632
+ className={cn(marqueeEdgeVariants({ side, size, className }))}
633
+ />
634
+ );
635
+ }
636
+
637
+ MarqueeEdge.displayName = 'MarqueeEdge';
@@ -0,0 +1,7 @@
1
+ export {
2
+ Marquee,
3
+ MarqueeContent,
4
+ MarqueeEdge,
5
+ MarqueeItem,
6
+ } from './Marquee';
7
+ export type { MarqueeProps, MarqueeEdgeProps } from './types';
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+
3
+ import { createLazyComponent } from '@djangocfg/widget-kit/lazy';
4
+ import type { MarqueeProps } from './types';
5
+
6
+ export const LazyMarquee = createLazyComponent<MarqueeProps>(
7
+ () => import('./Marquee').then((mod) => ({ default: mod.Marquee })),
8
+ {
9
+ displayName: 'LazyMarquee',
10
+ fallback: <div data-slot="marquee-skeleton" className="h-8 w-full animate-pulse rounded-sm bg-muted" />,
11
+ }
12
+ );
13
+
14
+ export type { MarqueeProps } from './types';