@dryui/ui 1.3.0 → 1.4.0

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 (93) hide show
  1. package/dist/accordion/accordion-content.svelte +1 -1
  2. package/dist/alert/alert.svelte +1 -1
  3. package/dist/app-frame/app-frame.svelte +125 -0
  4. package/dist/app-frame/app-frame.svelte.d.ts +10 -0
  5. package/dist/app-frame/index.d.ts +8 -0
  6. package/dist/app-frame/index.js +1 -0
  7. package/dist/aurora/aurora.svelte +22 -59
  8. package/dist/beam/beam.svelte +28 -9
  9. package/dist/carousel/carousel-button-dots.svelte +25 -8
  10. package/dist/carousel/carousel-button-thumbnails.svelte +25 -8
  11. package/dist/carousel/carousel-root.svelte +115 -4
  12. package/dist/carousel/carousel-slide.svelte +5 -1
  13. package/dist/carousel/carousel-viewport.svelte +2 -0
  14. package/dist/carousel/context.svelte.d.ts +5 -0
  15. package/dist/chart/chart-bars.svelte +25 -16
  16. package/dist/chart/chart-donut.svelte +25 -16
  17. package/dist/chart/chart-root.svelte +134 -30
  18. package/dist/chart/chart-root.svelte.d.ts +1 -0
  19. package/dist/chart/context.svelte.d.ts +3 -1
  20. package/dist/chart/context.svelte.js +1 -0
  21. package/dist/chart/index.d.ts +1 -0
  22. package/dist/chromatic-shift/chromatic-shift.svelte +36 -9
  23. package/dist/collapsible/collapsible-content.svelte +2 -1
  24. package/dist/combobox/combobox-content.svelte +26 -44
  25. package/dist/combobox/combobox-content.svelte.d.ts +1 -1
  26. package/dist/combobox/combobox-input-root.svelte +7 -1
  27. package/dist/combobox/combobox-input.svelte +21 -8
  28. package/dist/country-select/country-select-button-input.svelte +124 -260
  29. package/dist/date-picker/datepicker-content.svelte +18 -26
  30. package/dist/date-picker/datepicker-content.svelte.d.ts +2 -1
  31. package/dist/date-picker/datepicker-input-root.svelte +7 -1
  32. package/dist/date-range-picker/date-range-picker-content.svelte +18 -14
  33. package/dist/date-range-picker/date-range-picker-content.svelte.d.ts +2 -1
  34. package/dist/date-range-picker/date-range-picker-root.svelte +7 -1
  35. package/dist/displacement/displacement.svelte +16 -22
  36. package/dist/drag-and-drop/context.svelte.d.ts +2 -0
  37. package/dist/drag-and-drop/drag-and-drop-handle.svelte +34 -5
  38. package/dist/drag-and-drop/drag-and-drop-item.svelte +23 -14
  39. package/dist/drag-and-drop/drag-and-drop-root.svelte +60 -16
  40. package/dist/god-rays/god-rays.svelte +11 -0
  41. package/dist/gradient-mesh/gradient-mesh.svelte +27 -5
  42. package/dist/hover-card/context.svelte.d.ts +1 -10
  43. package/dist/hover-card/context.svelte.js +1 -2
  44. package/dist/hover-card/hover-card-content.svelte +41 -3
  45. package/dist/hover-card/hover-card-root.svelte +7 -55
  46. package/dist/hover-card/hover-card-trigger.svelte +79 -40
  47. package/dist/hover-card/hover-card-trigger.svelte.d.ts +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +1 -0
  50. package/dist/internal/motion.d.ts +1 -1
  51. package/dist/internal/motion.js +1 -1
  52. package/dist/marquee/marquee.svelte +56 -8
  53. package/dist/mega-menu/context.svelte.d.ts +2 -1
  54. package/dist/mega-menu/mega-menu-button-trigger.svelte +2 -14
  55. package/dist/mega-menu/mega-menu-item.svelte +3 -1
  56. package/dist/mega-menu/mega-menu-panel.svelte +35 -3
  57. package/dist/mega-menu/mega-menu-root.svelte +28 -13
  58. package/dist/menubar/context.svelte.d.ts +2 -2
  59. package/dist/menubar/menubar-button-trigger.svelte +5 -3
  60. package/dist/menubar/menubar-content.svelte +20 -12
  61. package/dist/menubar/menubar-root.svelte +4 -4
  62. package/dist/multi-select-combobox/multi-select-combobox-content.svelte +18 -55
  63. package/dist/multi-select-combobox/multi-select-combobox-content.svelte.d.ts +1 -1
  64. package/dist/noise/noise.svelte +38 -6
  65. package/dist/notification-center/context.svelte.d.ts +0 -1
  66. package/dist/notification-center/notification-center-panel.svelte +54 -35
  67. package/dist/notification-center/notification-center-root.svelte +0 -1
  68. package/dist/notification-center/notification-center-trigger-button.svelte +1 -8
  69. package/dist/option-picker/option-picker-description.svelte +2 -2
  70. package/dist/option-picker/option-picker-item.svelte +10 -3
  71. package/dist/option-picker/option-picker-label.svelte +2 -2
  72. package/dist/option-picker/option-picker-preview.svelte +18 -13
  73. package/dist/phone-input/phone-input-select.svelte +2 -152
  74. package/dist/phone-input/phone-input-select.svelte.d.ts +1 -7
  75. package/dist/rich-text-editor/rich-text-editor-toolbar-button-input.svelte +84 -29
  76. package/dist/scroll-area/scroll-area.svelte +16 -4
  77. package/dist/select/select-content.svelte +21 -31
  78. package/dist/select/select-content.svelte.d.ts +1 -1
  79. package/dist/select/select-root-input.svelte +7 -1
  80. package/dist/shimmer/shimmer.svelte +22 -12
  81. package/dist/tabs/tabs-list.svelte +12 -0
  82. package/dist/transfer/transfer-item.svelte +0 -3
  83. package/dist/transfer/transfer-list-input.svelte +1 -6
  84. package/dist/tree/context.svelte.d.ts +7 -1
  85. package/dist/tree/tree-item-children.svelte +12 -10
  86. package/dist/tree/tree-item-label.svelte +6 -17
  87. package/dist/tree/tree-item-label.svelte.d.ts +2 -2
  88. package/dist/tree/tree-item.svelte +28 -1
  89. package/dist/tree/tree-root.svelte +135 -59
  90. package/dist/typography/heading.svelte +1 -0
  91. package/package.json +8 -2
  92. package/skills/dryui/SKILL.md +1 -0
  93. package/dist/hover-card/hover-card-root.svelte.d.ts +0 -9
@@ -2,6 +2,11 @@
2
2
  import { onMount } from 'svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import type { HTMLAttributes } from 'svelte/elements';
5
+ import {
6
+ observeInViewport,
7
+ observePageVisibility,
8
+ observeReducedMotionPreference
9
+ } from '@dryui/primitives/internal/motion';
5
10
 
6
11
  interface Props extends HTMLAttributes<HTMLDivElement> {
7
12
  scale?: number;
@@ -48,17 +53,12 @@
48
53
  }
49
54
 
50
55
  onMount(() => {
51
- const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
52
- prefersReducedMotion = mql.matches;
53
- const handler = (e: MediaQueryListEvent) => {
54
- prefersReducedMotion = e.matches;
55
- };
56
- mql.addEventListener('change', handler);
57
- documentVisible = !document.hidden;
58
- const handleVisibilityChange = () => {
59
- documentVisible = !document.hidden;
60
- };
61
- document.addEventListener('visibilitychange', handleVisibilityChange);
56
+ const stopMotion = observeReducedMotionPreference((matches) => {
57
+ prefersReducedMotion = matches;
58
+ });
59
+ const stopVisibility = observePageVisibility((visible) => {
60
+ documentVisible = visible;
61
+ });
62
62
 
63
63
  let frameId: number | undefined;
64
64
  let lastTime = 0;
@@ -86,21 +86,15 @@
86
86
  });
87
87
 
88
88
  $effect(() => {
89
- if (!element || typeof IntersectionObserver === 'undefined') return;
90
-
91
- const observer = new IntersectionObserver((entries) => {
92
- const entry = entries[0];
93
- inViewport = entry?.isIntersecting ?? true;
89
+ if (!element) return;
90
+ return observeInViewport(element, (inView) => {
91
+ inViewport = inView;
94
92
  });
95
-
96
- observer.observe(element);
97
-
98
- return () => observer.disconnect();
99
93
  });
100
94
 
101
95
  return () => {
102
- document.removeEventListener('visibilitychange', handleVisibilityChange);
103
- mql.removeEventListener('change', handler);
96
+ stopMotion();
97
+ stopVisibility();
104
98
  };
105
99
  });
106
100
 
@@ -5,6 +5,8 @@ export interface DragAndDropContext {
5
5
  readonly orientation: 'vertical' | 'horizontal';
6
6
  readonly hasHandle: boolean;
7
7
  readonly foreignOverIndex: number | null;
8
+ readonly instructionsId: string;
9
+ readonly itemCount: number;
8
10
  registerHandle: () => void;
9
11
  startDrag: (index: number, event: PointerEvent) => void;
10
12
  handleDragOver: (index: number) => void;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
+ import { mergeIds } from '@dryui/primitives';
4
5
  import { getDragAndDropCtx } from './context.svelte.js';
5
6
 
6
7
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
@@ -8,11 +9,17 @@
8
9
  children?: Snippet | undefined;
9
10
  }
10
11
 
11
- let { index, children, class: className, ...rest }: Props = $props();
12
+ let {
13
+ index,
14
+ children,
15
+ 'aria-describedby': ariaDescribedBy,
16
+ 'aria-label': ariaLabel,
17
+ class: className,
18
+ ...rest
19
+ }: Props = $props();
12
20
 
13
21
  const ctx = getDragAndDropCtx();
14
22
 
15
- // Register that a handle exists so items don't initiate drag themselves
16
23
  ctx.registerHandle();
17
24
 
18
25
  function handlePointerDown(e: PointerEvent) {
@@ -28,18 +35,40 @@
28
35
  ctx.endDrag();
29
36
  }
30
37
  }
38
+
39
+ function handleKeydown(e: KeyboardEvent) {
40
+ const upKey = ctx.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
41
+ const downKey = ctx.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
42
+
43
+ if (e.key === upKey) {
44
+ e.preventDefault();
45
+ ctx.moveItem(index, 'up');
46
+ }
47
+
48
+ if (e.key === downKey) {
49
+ e.preventDefault();
50
+ ctx.moveItem(index, 'down');
51
+ }
52
+
53
+ if (e.key === 'Escape' && ctx.isDragging) {
54
+ e.preventDefault();
55
+ ctx.cancelDrag();
56
+ }
57
+ }
31
58
  </script>
32
59
 
33
60
  <div
34
61
  role="button"
35
62
  tabindex="0"
36
- aria-roledescription="drag handle"
37
- aria-label="Drag to reorder"
38
- aria-pressed={ctx.isDragging && ctx.draggedIndex === index ? 'true' : undefined}
63
+ aria-roledescription="reorder handle"
64
+ aria-label={ariaLabel ?? `Reorder item ${index + 1}`}
65
+ aria-describedby={mergeIds(ariaDescribedBy, ctx.instructionsId)}
66
+ aria-keyshortcuts={ctx.orientation === 'vertical' ? 'ArrowUp ArrowDown' : 'ArrowLeft ArrowRight'}
39
67
  data-dnd-handle
40
68
  data-dragging={ctx.isDragging && ctx.draggedIndex === index ? '' : undefined}
41
69
  onpointerdown={handlePointerDown}
42
70
  onpointerup={handlePointerUp}
71
+ onkeydown={handleKeydown}
43
72
  {...rest}
44
73
  class={className}
45
74
  >
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
+ import { mergeIds } from '@dryui/primitives';
4
5
  import { getDragAndDropCtx } from './context.svelte.js';
5
6
 
6
7
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
@@ -8,7 +9,13 @@
8
9
  children: Snippet<[{ isDragging: boolean; isOver: boolean }]>;
9
10
  }
10
11
 
11
- let { index, children, class: className, ...rest }: Props = $props();
12
+ let {
13
+ index,
14
+ children,
15
+ 'aria-describedby': ariaDescribedBy,
16
+ class: className,
17
+ ...rest
18
+ }: Props = $props();
12
19
 
13
20
  const ctx = getDragAndDropCtx();
14
21
 
@@ -18,22 +25,17 @@
18
25
  ctx.foreignOverIndex === index
19
26
  );
20
27
 
21
- let grabbing = $state(false);
22
-
23
28
  function handlePointerDown(e: PointerEvent) {
24
29
  // Only start drag if there's no handle registered (drag from whole item)
25
30
  if (!ctx.hasHandle) {
26
31
  e.preventDefault();
27
32
  ctx.startDrag(index, e);
28
- grabbing = true;
29
33
  }
30
34
  }
31
35
 
32
- function handlePointerUp() {
33
- grabbing = false;
34
- }
35
-
36
36
  function handleKeydown(e: KeyboardEvent) {
37
+ if (ctx.hasHandle) return;
38
+
37
39
  if (e.key === ' ' || e.key === 'Enter') {
38
40
  e.preventDefault();
39
41
  if (ctx.isDragging && ctx.draggedIndex === index) {
@@ -54,25 +56,32 @@
54
56
  ctx.moveItem(index, 'down');
55
57
  }
56
58
 
57
- if (e.key === 'Escape') {
59
+ if (e.key === 'Escape' && ctx.isDragging) {
58
60
  e.preventDefault();
59
61
  ctx.cancelDrag();
60
62
  }
61
63
  }
62
64
  </script>
63
65
 
66
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex (keyboard reordering keeps the item itself focusable when there is no separate handle) -->
64
67
  <div
65
- role="option"
66
- tabindex="0"
67
- aria-roledescription="sortable"
68
- aria-label="Item {index + 1}, press Space to grab, arrow keys to move"
68
+ role="listitem"
69
+ tabindex={ctx.hasHandle ? undefined : 0}
70
+ aria-roledescription="sortable item"
71
+ aria-posinset={index + 1}
72
+ aria-setsize={ctx.itemCount}
73
+ aria-describedby={mergeIds(ariaDescribedBy, !ctx.hasHandle ? ctx.instructionsId : undefined)}
74
+ aria-keyshortcuts={!ctx.hasHandle
75
+ ? ctx.orientation === 'vertical'
76
+ ? 'ArrowUp ArrowDown'
77
+ : 'ArrowLeft ArrowRight'
78
+ : undefined}
69
79
  data-dnd-item
70
80
  data-index={index}
71
81
  data-dragging={itemIsDragging ? '' : undefined}
72
82
  data-drag-active={ctx.isDragging ? '' : undefined}
73
83
  data-over={isOver ? '' : undefined}
74
84
  onpointerdown={handlePointerDown}
75
- onpointerup={handlePointerUp}
76
85
  onkeydown={handleKeydown}
77
86
  {...rest}
78
87
  class={className}
@@ -2,6 +2,7 @@
2
2
  import { flushSync } from 'svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import type { HTMLAttributes } from 'svelte/elements';
5
+ import { createId, mergeIds } from '@dryui/primitives';
5
6
  import { setDragAndDropCtx } from './context.svelte.js';
6
7
  import { getGroupCtx } from './group-context.svelte.js';
7
8
 
@@ -19,6 +20,7 @@
19
20
  orientation = 'vertical',
20
21
  listId,
21
22
  children,
23
+ 'aria-describedby': ariaDescribedBy,
22
24
  class: className,
23
25
  ...rest
24
26
  }: Props = $props();
@@ -46,6 +48,16 @@
46
48
  let crossListIndex: number | null = null;
47
49
 
48
50
  const groupCtx = getGroupCtx();
51
+ const instructionsId = createId('dry-dnd-instructions');
52
+
53
+ let keyboardInstructions = $derived.by(() => {
54
+ const arrowKeys =
55
+ orientation === 'vertical' ? 'Arrow Up and Arrow Down' : 'Arrow Left and Arrow Right';
56
+ if (hasHandle) {
57
+ return `Use the reorder handle and ${arrowKeys} to move items. Drag with a pointer to reorder, and press Escape to cancel an active drag.`;
58
+ }
59
+ return `Focus an item and use ${arrowKeys} to move it. Drag with a pointer to reorder, and press Escape to cancel an active drag.`;
60
+ });
49
61
 
50
62
  // Register with group when inside one
51
63
  $effect(() => {
@@ -71,6 +83,30 @@
71
83
  const FLIP_DURATION = 200;
72
84
  const FLIP_EASING = 'cubic-bezier(0.2, 0, 0, 1)';
73
85
 
86
+ function getInsertAt(from: number, to: number) {
87
+ return to > from ? to - 1 : to;
88
+ }
89
+
90
+ function announceReorder(from: number, to: number) {
91
+ if (to === from || to === from + 1) return;
92
+ const insertAt = getInsertAt(from, to);
93
+ announce(`Item moved to position ${insertAt + 1} of ${items.length}`);
94
+ }
95
+
96
+ function focusReorderedControl(index: number) {
97
+ const targetItem = rootElement?.querySelector<HTMLElement>(
98
+ `[data-dnd-item][data-index="${index}"]`
99
+ );
100
+ if (!targetItem) return;
101
+
102
+ if (hasHandle) {
103
+ targetItem.querySelector<HTMLElement>('[data-dnd-handle]')?.focus();
104
+ return;
105
+ }
106
+
107
+ targetItem.focus();
108
+ }
109
+
74
110
  function captureRects(): Map<number, DOMRect> {
75
111
  const rects = new Map<number, DOMRect>();
76
112
  if (!rootElement) return rects;
@@ -85,7 +121,7 @@
85
121
  if (to === from || to === from + 1) return;
86
122
 
87
123
  const firstRects = captureRects();
88
- const insertAt = to > from ? to - 1 : to;
124
+ const insertAt = getInsertAt(from, to);
89
125
 
90
126
  const newItems = [...items];
91
127
  const removed = newItems.splice(from, 1);
@@ -337,7 +373,9 @@
337
373
  isPending = false;
338
374
  isDragging = true;
339
375
  createPreview();
340
- announce(`Grabbed item at position ${(draggedIndex ?? 0) + 1}. Use arrow keys to move.`);
376
+ announce(
377
+ `Grabbed item at position ${(draggedIndex ?? 0) + 1} of ${items.length}. Drag to move and release to drop.`
378
+ );
341
379
  }
342
380
 
343
381
  if (isDragging) {
@@ -360,7 +398,7 @@
360
398
  ) {
361
399
  resetState();
362
400
  groupCtx.move(listId, from, toListId, toIdx);
363
- announce('Item moved to another list');
401
+ announce(`Item moved to position ${toIdx + 1} in another list`);
364
402
  return;
365
403
  }
366
404
  animateCrossListDrop(from, toListId, toIdx);
@@ -379,6 +417,7 @@
379
417
  ) {
380
418
  resetState();
381
419
  applyReorder(from, to);
420
+ announceReorder(from, to);
382
421
  return;
383
422
  }
384
423
  animateDrop(from, to);
@@ -490,6 +529,7 @@
490
529
  isAnimating = false;
491
530
  resetState();
492
531
  applyReorder(from, to);
532
+ announceReorder(from, to);
493
533
  })
494
534
  .catch(() => {
495
535
  if (placeholderEl) placeholderEl.style.removeProperty('display');
@@ -607,7 +647,7 @@
607
647
  isAnimating = false;
608
648
  resetState();
609
649
  flushSync(() => groupCtx!.move(listId!, from, toListId, toIdx));
610
- announce('Item moved to another list');
650
+ announce(`Item moved to position ${toIdx + 1} in another list`);
611
651
  })
612
652
  .catch(() => {
613
653
  if (placeholderEl) placeholderEl.style.removeProperty('display');
@@ -645,14 +685,6 @@
645
685
  isPending = false;
646
686
  }
647
687
 
648
- function handleKeydown(e: KeyboardEvent) {
649
- if (e.key === 'Escape' && isDragging && !isAnimating) {
650
- e.preventDefault();
651
- announce('Reorder cancelled');
652
- resetState();
653
- }
654
- }
655
-
656
688
  setDragAndDropCtx({
657
689
  get draggedIndex() {
658
690
  return draggedIndex;
@@ -672,6 +704,12 @@
672
704
  get foreignOverIndex() {
673
705
  return foreignOverIndex;
674
706
  },
707
+ get instructionsId() {
708
+ return instructionsId;
709
+ },
710
+ get itemCount() {
711
+ return items.length;
712
+ },
675
713
  registerHandle() {
676
714
  hasHandle = true;
677
715
  },
@@ -711,6 +749,7 @@
711
749
  if (toGap !== fromIndex && toGap !== fromIndex + 1) {
712
750
  reorder(fromIndex, toGap);
713
751
  const insertAt = toGap > fromIndex ? toGap - 1 : toGap;
752
+ focusReorderedControl(insertAt);
714
753
  announce(`Item moved to position ${insertAt + 1} of ${items.length}`);
715
754
  }
716
755
  },
@@ -729,10 +768,13 @@
729
768
 
730
769
  <div
731
770
  bind:this={rootElement}
732
- role="listbox"
771
+ role="list"
772
+ aria-roledescription="sortable list"
773
+ aria-describedby={mergeIds(ariaDescribedBy, instructionsId)}
733
774
  data-dnd-root
734
775
  data-orientation={orientation}
735
776
  data-dragging={isDragging ? '' : undefined}
777
+ data-handle={hasHandle ? '' : undefined}
736
778
  data-over-end={(isDragging && overIndex !== null && overIndex >= items.length) ||
737
779
  (foreignOverIndex !== null && foreignOverIndex >= items.length)
738
780
  ? ''
@@ -740,18 +782,20 @@
740
782
  onpointermove={handlePointerMove}
741
783
  onpointerup={handlePointerUp}
742
784
  onpointercancel={handlePointerCancel}
743
- onkeydown={handleKeydown}
744
785
  {...rest}
745
786
  class={className}
746
787
  >
747
788
  {@render children()}
748
- <div aria-live="assertive" aria-atomic="true" class="liveRegion">
789
+ <div id={instructionsId} data-dnd-instructions class="assistiveText">
790
+ {keyboardInstructions}
791
+ </div>
792
+ <div data-dnd-live-region aria-live="assertive" aria-atomic="true" class="assistiveText">
749
793
  {liveRegionMessage}
750
794
  </div>
751
795
  </div>
752
796
 
753
797
  <style>
754
- .liveRegion {
798
+ .assistiveText {
755
799
  position: absolute;
756
800
  aspect-ratio: 1;
757
801
  height: 1px;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
+ import { observeOffscreenState } from '@dryui/primitives/internal/motion';
4
5
 
5
6
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
6
7
  color?: string | undefined;
@@ -55,6 +56,8 @@
55
56
  node.style.setProperty('--dry-rays-blend', blendMode);
56
57
  node.style.setProperty('--dry-rays-speed', speedValue);
57
58
  });
59
+
60
+ $effect(() => observeOffscreenState(node, { rootMargin: '200px' }));
58
61
  }
59
62
 
60
63
  // Flash-on-load: background is a computed conic-gradient with a dynamic number of stops
@@ -107,6 +110,14 @@
107
110
  animation: god-rays-rotate var(--dry-rays-speed, 30s) linear infinite;
108
111
  }
109
112
 
113
+ [data-god-rays][data-animated]:not([data-offscreen]) [data-god-rays-layer] {
114
+ will-change: transform, filter;
115
+ }
116
+
117
+ [data-god-rays][data-offscreen] [data-god-rays-layer] {
118
+ animation-play-state: paused;
119
+ }
120
+
110
121
  @keyframes god-rays-rotate {
111
122
  to {
112
123
  transform: rotate(360deg);
@@ -4,6 +4,8 @@
4
4
  import type { HTMLAttributes } from 'svelte/elements';
5
5
  import {
6
6
  getReducedMotionPreference,
7
+ observeInViewport,
8
+ observePageVisibility,
7
9
  observeReducedMotionPreference,
8
10
  registerPropertyOnce,
9
11
  supportsPropertyRegistration,
@@ -30,6 +32,9 @@
30
32
  let element = $state<HTMLDivElement | null>(null);
31
33
  let prefersReducedMotion = $state(false);
32
34
  let animated = $state(false);
35
+ let onScreen = $state(true);
36
+ let tabVisible = $state(true);
37
+ const paused = $derived(!onScreen || !tabVisible);
33
38
  let pointerX = $state('50%');
34
39
  let pointerY = $state('50%');
35
40
  let pointerFrame = $state<number | null>(null);
@@ -126,19 +131,31 @@
126
131
  };
127
132
 
128
133
  const stopMotionObserver = observeReducedMotionPreference(updateAnimatedState);
134
+ const stopVisibility = observePageVisibility((visible) => {
135
+ tabVisible = visible;
136
+ });
137
+ let stopViewport = () => {};
138
+ if (element) {
139
+ stopViewport = observeInViewport(
140
+ element,
141
+ (inView) => {
142
+ onScreen = inView;
143
+ },
144
+ { rootMargin: '200px' }
145
+ );
146
+ }
129
147
 
130
148
  if (!supportsPropertyRegistration() || getReducedMotionPreference()) {
131
149
  animated = false;
132
- return () => {
133
- cancelQueuedPointerPosition();
134
- stopMotionObserver();
135
- };
150
+ } else {
151
+ animated = true;
136
152
  }
137
153
 
138
- animated = true;
139
154
  return () => {
140
155
  cancelQueuedPointerPosition();
141
156
  stopMotionObserver();
157
+ stopVisibility();
158
+ stopViewport();
142
159
  };
143
160
  });
144
161
 
@@ -164,6 +181,7 @@
164
181
  data-animated={animated || undefined}
165
182
  data-interactive={interactive || undefined}
166
183
  data-reduced-motion={prefersReducedMotion || undefined}
184
+ data-paused={paused || undefined}
167
185
  onpointermove={interactive ? handlePointerMove : undefined}
168
186
  onpointerleave={interactive ? handlePointerLeave : undefined}
169
187
  {...rest}
@@ -213,6 +231,10 @@
213
231
  animation: mesh-cycle var(--dry-mesh-duration) ease-in-out infinite alternate;
214
232
  }
215
233
 
234
+ [data-gradient-mesh][data-animated][data-paused] {
235
+ animation-play-state: paused;
236
+ }
237
+
216
238
  [data-gradient-mesh][data-interactive] {
217
239
  background:
218
240
  radial-gradient(
@@ -1,10 +1 @@
1
- interface HoverCardContext {
2
- readonly open: boolean;
3
- readonly triggerId: string;
4
- readonly contentId: string;
5
- triggerEl: HTMLElement | null;
6
- show: () => void;
7
- close: () => void;
8
- }
9
- export declare const setHoverCardCtx: (ctx: HoverCardContext) => HoverCardContext, getHoverCardCtx: () => HoverCardContext;
10
- export {};
1
+ export { setHoverCardCtx, getHoverCardCtx, type HoverCardContext } from '@dryui/primitives';
@@ -1,2 +1 @@
1
- import { createContext } from '@dryui/primitives';
2
- export const [setHoverCardCtx, getHoverCardCtx] = createContext('hover-card');
1
+ export { setHoverCardCtx, getHoverCardCtx } from '@dryui/primitives';
@@ -24,6 +24,18 @@
24
24
 
25
25
  let contentEl = $state<HTMLDivElement>();
26
26
 
27
+ $effect(() => {
28
+ if (contentEl) {
29
+ ctx.contentEl = contentEl;
30
+
31
+ return () => {
32
+ if (ctx.contentEl === contentEl) {
33
+ ctx.contentEl = null;
34
+ }
35
+ };
36
+ }
37
+ });
38
+
27
39
  const popover = createAnchoredPopover({
28
40
  triggerEl: () => ctx.triggerEl,
29
41
  contentEl: () => contentEl ?? null,
@@ -35,20 +47,46 @@
35
47
  function handleKeydown(e: KeyboardEvent) {
36
48
  if (e.key === 'Escape') {
37
49
  e.preventDefault();
38
- ctx.close();
50
+ ctx.forceClose();
51
+ ctx.ignoreNextTriggerFocusOpen = true;
52
+ ctx.triggerEl?.focus();
39
53
  }
40
54
  }
55
+
56
+ function handlePointerEnter() {
57
+ ctx.contentHovered = true;
58
+ ctx.showImmediate();
59
+ }
60
+
61
+ function handlePointerLeave() {
62
+ ctx.contentHovered = false;
63
+ ctx.close();
64
+ }
65
+
66
+ function handleFocusIn() {
67
+ ctx.contentFocused = true;
68
+ ctx.showImmediate();
69
+ }
70
+
71
+ function handleFocusOut() {
72
+ ctx.contentFocused = false;
73
+ ctx.close();
74
+ }
41
75
  </script>
42
76
 
43
77
  <div
44
78
  bind:this={contentEl}
45
79
  id={ctx.contentId}
46
80
  popover="manual"
81
+ role="dialog"
82
+ aria-labelledby={ctx.triggerId}
47
83
  data-hover-card-content
48
84
  data-state={ctx.open ? 'open' : 'closed'}
49
85
  use:popover.applyPosition={style}
50
- onpointerenter={() => ctx.show()}
51
- onpointerleave={() => ctx.close()}
86
+ onpointerenter={handlePointerEnter}
87
+ onpointerleave={handlePointerLeave}
88
+ onfocusin={handleFocusIn}
89
+ onfocusout={handleFocusOut}
52
90
  onkeydown={handleKeydown}
53
91
  class={className}
54
92
  {...rest}
@@ -1,59 +1,11 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { generateFormId } from '@dryui/primitives';
4
- import { setHoverCardCtx } from './context.svelte.js';
2
+ import type { ComponentProps } from 'svelte';
3
+ import { HoverCard as PrimitiveHoverCard } from '@dryui/primitives';
5
4
 
6
- interface Props {
7
- openDelay?: number;
8
- closeDelay?: number;
9
- children: Snippet;
10
- }
11
-
12
- let { openDelay = 700, closeDelay = 300, children }: Props = $props();
13
-
14
- let open = $state(false);
15
-
16
- const triggerId = generateFormId('hovercard-trigger');
17
- const contentId = generateFormId('hovercard-content');
18
-
19
- let openTimeout: ReturnType<typeof setTimeout>;
20
- let closeTimeout: ReturnType<typeof setTimeout>;
21
-
22
- function show() {
23
- clearTimeout(closeTimeout);
24
- openTimeout = setTimeout(() => {
25
- open = true;
26
- }, openDelay);
27
- }
28
-
29
- function close() {
30
- clearTimeout(openTimeout);
31
- closeTimeout = setTimeout(() => {
32
- open = false;
33
- }, closeDelay);
34
- }
35
-
36
- setHoverCardCtx({
37
- get open() {
38
- return open;
39
- },
40
- get triggerId() {
41
- return triggerId;
42
- },
43
- get contentId() {
44
- return contentId;
45
- },
46
- triggerEl: null,
47
- show,
48
- close
49
- });
50
-
51
- $effect(() => {
52
- return () => {
53
- clearTimeout(openTimeout);
54
- clearTimeout(closeTimeout);
55
- };
56
- });
5
+ // The styled hover-card only adds theme tokens to trigger/content, so root
6
+ // is a pure re-export of the primitives implementation. The ctx set here is
7
+ // consumed by the ui trigger/content via the shared primitives ctx key.
8
+ let props: ComponentProps<typeof PrimitiveHoverCard.Root> = $props();
57
9
  </script>
58
10
 
59
- {@render children()}
11
+ <PrimitiveHoverCard.Root {...props} />