@dryui/ui 1.3.1 → 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 (91) 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 +42 -5
  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/transfer/transfer-item.svelte +0 -3
  82. package/dist/transfer/transfer-list-input.svelte +1 -6
  83. package/dist/tree/context.svelte.d.ts +7 -1
  84. package/dist/tree/tree-item-children.svelte +12 -10
  85. package/dist/tree/tree-item-label.svelte +6 -17
  86. package/dist/tree/tree-item-label.svelte.d.ts +2 -2
  87. package/dist/tree/tree-item.svelte +28 -1
  88. package/dist/tree/tree-root.svelte +135 -59
  89. package/package.json +8 -2
  90. package/skills/dryui/SKILL.md +1 -0
  91. package/dist/hover-card/hover-card-root.svelte.d.ts +0 -9
@@ -10,6 +10,7 @@
10
10
  let { class: className, children, ...rest }: Props = $props();
11
11
  const ctx = getCarouselCtx();
12
12
  const index = ctx.registerSlide();
13
+ const slideId = ctx.getSlideId(index);
13
14
 
14
15
  $effect(() => {
15
16
  return () => ctx.unregisterSlide();
@@ -17,11 +18,14 @@
17
18
  </script>
18
19
 
19
20
  <div
21
+ id={slideId}
20
22
  role="group"
21
23
  aria-roledescription="slide"
22
- aria-label="Slide {index + 1} of {ctx.totalSlides}"
24
+ aria-hidden={ctx.activeIndex === index ? undefined : true}
25
+ aria-label={`${index + 1} of ${ctx.totalSlides}`}
23
26
  data-carousel-slide=""
24
27
  data-active={ctx.activeIndex === index ? '' : undefined}
28
+ inert={ctx.activeIndex !== index}
25
29
  class={className}
26
30
  {...rest}
27
31
  >
@@ -35,6 +35,8 @@
35
35
 
36
36
  <div
37
37
  bind:this={el}
38
+ aria-atomic="false"
39
+ aria-live={ctx.autoplayRunning ? 'off' : 'polite'}
38
40
  data-carousel-viewport=""
39
41
  data-orientation={ctx.orientation}
40
42
  class={className}
@@ -4,10 +4,15 @@ interface CarouselContext {
4
4
  readonly orientation: 'horizontal' | 'vertical';
5
5
  readonly canScrollPrev: boolean;
6
6
  readonly canScrollNext: boolean;
7
+ readonly autoplayEnabled: boolean;
8
+ readonly autoplayPaused: boolean;
9
+ readonly autoplayRunning: boolean;
7
10
  scrollTo: (index: number) => void;
8
11
  syncActiveIndex: (index: number) => void;
9
12
  scrollPrev: () => void;
10
13
  scrollNext: () => void;
14
+ toggleAutoplay: () => void;
15
+ getSlideId: (index: number) => string;
11
16
  registerViewport: (el: HTMLElement) => void;
12
17
  registerSlide: () => number;
13
18
  unregisterSlide: () => void;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { SVGAttributes } from 'svelte/elements';
3
- import { getChartCtx } from './context.svelte.js';
3
+ import { getChartCtx, registerChartInteractive } from './context.svelte.js';
4
4
 
5
5
  interface Props extends Omit<SVGAttributes<SVGGElement>, 'onclick'> {
6
6
  radius?: number;
@@ -31,12 +31,32 @@
31
31
  function handleClick(bar: (typeof bars)[number]) {
32
32
  onclick?.({ label: bar.point.label, value: bar.point.value, index: bar.index });
33
33
  }
34
+
35
+ function attachBarClick(bar: (typeof bars)[number]) {
36
+ if (!onclick) return undefined;
37
+
38
+ return (node: SVGRectElement) => {
39
+ const handleNodeClick = () => handleClick(bar);
40
+ node.addEventListener('click', handleNodeClick);
41
+ return () => node.removeEventListener('click', handleNodeClick);
42
+ };
43
+ }
44
+
45
+ const registeredHandler = $derived(
46
+ onclick
47
+ ? (index: number) => {
48
+ const point = ctx.data[index];
49
+ if (!point) return;
50
+ onclick({ label: point.label, value: point.value, index });
51
+ }
52
+ : undefined
53
+ );
54
+
55
+ registerChartInteractive(ctx, () => registeredHandler);
34
56
  </script>
35
57
 
36
58
  <g role="list" aria-label="Bar chart data" data-chart-bars class={className} {...rest}>
37
- {#each bars as bar}
38
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
39
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
59
+ {#each bars as bar (bar.index)}
40
60
  <rect
41
61
  x={bar.x}
42
62
  y={bar.y}
@@ -44,20 +64,9 @@
44
64
  height={bar.height}
45
65
  rx={radius}
46
66
  fill={bar.point.color ?? 'currentColor'}
47
- role={onclick ? 'button' : 'listitem'}
48
- tabindex={onclick ? 0 : undefined}
49
- aria-label="{bar.point.label}: {bar.point.value}"
50
67
  data-part="bar"
51
68
  data-clickable={onclick ? '' : undefined}
52
- onclick={onclick ? () => handleClick(bar) : undefined}
53
- onkeydown={onclick
54
- ? (e: KeyboardEvent) => {
55
- if (e.key === 'Enter' || e.key === ' ') {
56
- e.preventDefault();
57
- handleClick(bar);
58
- }
59
- }
60
- : undefined}
69
+ {@attach attachBarClick(bar)}
61
70
  />
62
71
  {/each}
63
72
  </g>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { SVGAttributes } from 'svelte/elements';
4
- import { getChartCtx } from './context.svelte.js';
4
+ import { getChartCtx, registerChartInteractive } from './context.svelte.js';
5
5
 
6
6
  interface Props extends Omit<SVGAttributes<SVGGElement>, 'onclick'> {
7
7
  innerRadius?: number;
@@ -50,12 +50,32 @@
50
50
  function handleClick(seg: (typeof segments)[number]) {
51
51
  onclick?.({ label: seg.point.label, value: seg.point.value, index: seg.index });
52
52
  }
53
+
54
+ function attachSegmentClick(seg: (typeof segments)[number]) {
55
+ if (!onclick) return undefined;
56
+
57
+ return (node: SVGCircleElement) => {
58
+ const handleNodeClick = () => handleClick(seg);
59
+ node.addEventListener('click', handleNodeClick);
60
+ return () => node.removeEventListener('click', handleNodeClick);
61
+ };
62
+ }
63
+
64
+ const registeredHandler = $derived(
65
+ onclick
66
+ ? (index: number) => {
67
+ const point = ctx.data[index];
68
+ if (!point) return;
69
+ onclick({ label: point.label, value: point.value, index });
70
+ }
71
+ : undefined
72
+ );
73
+
74
+ registerChartInteractive(ctx, () => registeredHandler);
53
75
  </script>
54
76
 
55
77
  <g role="list" aria-label="Donut chart data" data-chart-donut class={className} {...rest}>
56
- {#each segments as seg}
57
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
58
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
78
+ {#each segments as seg (seg.index)}
59
79
  <circle
60
80
  {cx}
61
81
  {cy}
@@ -66,20 +86,9 @@
66
86
  stroke-dasharray={seg.dasharray}
67
87
  stroke-dashoffset={seg.dashoffset}
68
88
  transform="rotate(-90 {cx} {cy})"
69
- role={onclick ? 'button' : 'listitem'}
70
- tabindex={onclick ? 0 : undefined}
71
- aria-label="{seg.point.label}: {seg.point.value}"
72
89
  data-part="donut-segment"
73
90
  data-clickable={onclick ? '' : undefined}
74
- onclick={onclick ? () => handleClick(seg) : undefined}
75
- onkeydown={onclick
76
- ? (e: KeyboardEvent) => {
77
- if (e.key === 'Enter' || e.key === ' ') {
78
- e.preventDefault();
79
- handleClick(seg);
80
- }
81
- }
82
- : undefined}
91
+ {@attach attachSegmentClick(seg)}
83
92
  />
84
93
  {/each}
85
94
  {#if label}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { SVGAttributes } from 'svelte/elements';
4
+ import Button from '../button/button.svelte';
4
5
  import { setChartCtx, type ChartDataPoint } from './context.svelte.js';
5
6
 
6
7
  interface Props extends SVGAttributes<SVGSVGElement> {
@@ -8,6 +9,7 @@
8
9
  width?: number;
9
10
  height?: number;
10
11
  padding?: { top?: number; right?: number; bottom?: number; left?: number };
12
+ summary?: string;
11
13
  children: Snippet;
12
14
  }
13
15
 
@@ -16,22 +18,24 @@
16
18
  width: widthProp,
17
19
  height: heightProp,
18
20
  padding: paddingProp,
21
+ summary,
22
+ 'aria-label': ariaLabel = 'Chart',
19
23
  class: className,
20
24
  children,
21
25
  ...rest
22
26
  }: Props = $props();
23
27
 
24
- let containerEl: HTMLDivElement | undefined = $state();
28
+ const chartId = $props.id();
29
+
25
30
  let observedWidth = $state(400);
26
31
  let observedHeight = $state(300);
32
+ let interactiveHandler: ((index: number) => void) | undefined = $state();
33
+ let interactiveOwner: symbol | undefined = $state();
27
34
 
28
35
  const width = $derived(widthProp ?? observedWidth);
29
36
  const height = $derived(heightProp ?? observedHeight);
30
37
 
31
- $effect(() => {
32
- if (widthProp !== undefined && heightProp !== undefined) return;
33
- if (!containerEl) return;
34
-
38
+ function observeSize(node: HTMLDivElement) {
35
39
  const ro = new ResizeObserver((entries) => {
36
40
  for (const entry of entries) {
37
41
  const cr = entry.contentRect;
@@ -40,9 +44,9 @@
40
44
  }
41
45
  });
42
46
 
43
- ro.observe(containerEl);
47
+ ro.observe(node);
44
48
  return () => ro.disconnect();
45
- });
49
+ }
46
50
 
47
51
  const padding = $derived({
48
52
  top: paddingProp?.top ?? 20,
@@ -51,9 +55,34 @@
51
55
  left: paddingProp?.left ?? 50
52
56
  });
53
57
 
54
- const minValue = $derived(Math.min(0, ...data.map((d) => d.value)));
55
- const maxValue = $derived(Math.max(...data.map((d) => d.value)));
56
- const total = $derived(data.reduce((sum, d) => sum + d.value, 0));
58
+ const stats = $derived.by(() => {
59
+ let min = 0;
60
+ let max = -Infinity;
61
+ let total = 0;
62
+ let highest: ChartDataPoint | undefined;
63
+ let lowest: ChartDataPoint | undefined;
64
+ for (const point of data) {
65
+ const value = point.value;
66
+ if (value < min) min = value;
67
+ if (value > max) max = value;
68
+ total += value;
69
+ if (!highest || value > highest.value) highest = point;
70
+ if (!lowest || value < lowest.value) lowest = point;
71
+ }
72
+ return { min, max, total, highest, lowest };
73
+ });
74
+ const minValue = $derived(stats.min);
75
+ const maxValue = $derived(stats.max);
76
+ const total = $derived(stats.total);
77
+ const summaryText = $derived.by(() => {
78
+ if (summary?.trim()) return summary;
79
+ if (data.length === 0) return 'No data points available.';
80
+ if (data.length === 1) return `${data[0]!.label}: ${data[0]!.value}.`;
81
+ const { highest, lowest } = stats;
82
+ if (!highest || !lowest) return `${data.length} data points available.`;
83
+ return `${data.length} data points. Highest is ${highest.label} at ${highest.value}. Lowest is ${lowest.label} at ${lowest.value}. Total is ${stats.total}.`;
84
+ });
85
+ const isInteractive = $derived(Boolean(interactiveHandler));
57
86
 
58
87
  setChartCtx({
59
88
  get data() {
@@ -78,41 +107,116 @@
78
107
  return padding;
79
108
  },
80
109
  hasBars: false,
81
- hasHorizontalBars: false
110
+ hasHorizontalBars: false,
111
+ get interactiveHandler() {
112
+ return interactiveHandler;
113
+ },
114
+ set interactiveHandler(handler) {
115
+ interactiveHandler = handler;
116
+ },
117
+ get interactiveOwner() {
118
+ return interactiveOwner;
119
+ },
120
+ set interactiveOwner(owner) {
121
+ interactiveOwner = owner;
122
+ }
82
123
  });
83
124
  </script>
84
125
 
85
- {#if widthProp === undefined || heightProp === undefined}
86
- <div bind:this={containerEl} data-chart-container>
126
+ <div data-chart-root>
127
+ <div data-chart-a11y data-interactive={isInteractive || undefined}>
128
+ <p id={`${chartId}-summary`} data-chart-summary>
129
+ <strong>{ariaLabel}</strong>
130
+ <span>{summaryText}</span>
131
+ </p>
132
+ <ul data-chart-data-list>
133
+ {#each data as point, index (point.label)}
134
+ <li>
135
+ {#if interactiveHandler}
136
+ <Button variant="bare" type="button" onclick={() => interactiveHandler?.(index)}>
137
+ {point.label}: {point.value}
138
+ </Button>
139
+ {:else}
140
+ <span>{point.label}: {point.value}</span>
141
+ {/if}
142
+ </li>
143
+ {/each}
144
+ </ul>
145
+ </div>
146
+
147
+ {#if widthProp === undefined || heightProp === undefined}
148
+ <div data-chart-container {@attach observeSize}>
149
+ <svg
150
+ viewBox="0 0 {width} {height}"
151
+ {width}
152
+ {height}
153
+ data-chart
154
+ class={className}
155
+ {...rest}
156
+ aria-hidden="true"
157
+ focusable="false"
158
+ >
159
+ {@render children()}
160
+ </svg>
161
+ </div>
162
+ {:else}
87
163
  <svg
88
164
  viewBox="0 0 {width} {height}"
89
- role="img"
90
165
  {width}
91
166
  {height}
92
- aria-label="Chart"
93
167
  data-chart
94
168
  class={className}
95
169
  {...rest}
170
+ aria-hidden="true"
171
+ focusable="false"
96
172
  >
97
173
  {@render children()}
98
174
  </svg>
99
- </div>
100
- {:else}
101
- <svg
102
- viewBox="0 0 {width} {height}"
103
- role="img"
104
- {width}
105
- {height}
106
- aria-label="Chart"
107
- data-chart
108
- class={className}
109
- {...rest}
110
- >
111
- {@render children()}
112
- </svg>
113
- {/if}
175
+ {/if}
176
+ </div>
114
177
 
115
178
  <style>
179
+ [data-chart-root] {
180
+ display: grid;
181
+ }
182
+
183
+ [data-chart-a11y] {
184
+ position: absolute;
185
+ height: 1px;
186
+ aspect-ratio: 1;
187
+ padding: 0;
188
+ margin: -1px;
189
+ overflow: hidden;
190
+ clip: rect(0 0 0 0);
191
+ clip-path: inset(50%);
192
+ border: 0;
193
+ white-space: nowrap;
194
+ }
195
+
196
+ [data-chart-a11y][data-interactive]:focus-within {
197
+ position: static;
198
+ height: auto;
199
+ aspect-ratio: auto;
200
+ margin: 0 0 var(--dry-space-3);
201
+ overflow: visible;
202
+ clip: auto;
203
+ clip-path: none;
204
+ white-space: normal;
205
+ }
206
+
207
+ [data-chart-summary] {
208
+ margin: 0;
209
+ }
210
+
211
+ [data-chart-summary] strong {
212
+ margin-inline-end: var(--dry-space-1);
213
+ }
214
+
215
+ [data-chart-data-list] {
216
+ margin: var(--dry-space-2) 0 0;
217
+ padding-inline-start: 1.25rem;
218
+ }
219
+
116
220
  [data-chart-container] {
117
221
  display: grid;
118
222
  min-height: 200px;
@@ -11,6 +11,7 @@ interface Props extends SVGAttributes<SVGSVGElement> {
11
11
  bottom?: number;
12
12
  left?: number;
13
13
  };
14
+ summary?: string;
14
15
  children: Snippet;
15
16
  }
16
17
  declare const ChartRoot: import("svelte").Component<Props, {}, "">;
@@ -1,3 +1,4 @@
1
+ export { registerChartInteractive } from '@dryui/primitives';
1
2
  export interface ChartDataPoint {
2
3
  label: string;
3
4
  value: number;
@@ -25,6 +26,7 @@ interface ChartContext {
25
26
  };
26
27
  hasBars: boolean;
27
28
  hasHorizontalBars: boolean;
29
+ interactiveHandler?: (index: number) => void;
30
+ interactiveOwner?: symbol;
28
31
  }
29
32
  export declare const setChartCtx: (ctx: ChartContext) => ChartContext, getChartCtx: () => ChartContext;
30
- export {};
@@ -1,2 +1,3 @@
1
1
  import { createContext } from '@dryui/primitives';
2
+ export { registerChartInteractive } from '@dryui/primitives';
2
3
  export const [setChartCtx, getChartCtx] = createContext('chart');
@@ -11,6 +11,7 @@ export interface ChartRootProps extends SVGAttributes<SVGSVGElement> {
11
11
  bottom?: number;
12
12
  left?: number;
13
13
  };
14
+ summary?: string;
14
15
  children: Snippet;
15
16
  }
16
17
  export interface ChartBarsProps extends Omit<SVGAttributes<SVGGElement>, 'onclick'> {
@@ -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
  offset?: number;
@@ -24,6 +29,9 @@
24
29
 
25
30
  let active = $state(false);
26
31
  let prefersReducedMotion = $state(false);
32
+ let onScreen = $state(true);
33
+ let tabVisible = $state(true);
34
+ const paused = $derived(!onScreen || !tabVisible);
27
35
 
28
36
  const shouldActivate = $derived.by(() => {
29
37
  if (prefersReducedMotion) return false;
@@ -42,21 +50,34 @@
42
50
 
43
51
  const offsetValue = $derived(`${Math.max(0, offset)}px`);
44
52
 
45
- onMount(() => {
46
- const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
47
- prefersReducedMotion = mql.matches;
48
- const handler = (e: MediaQueryListEvent) => {
49
- prefersReducedMotion = e.matches;
50
- };
51
- mql.addEventListener('change', handler);
52
- return () => mql.removeEventListener('change', handler);
53
- });
53
+ onMount(() =>
54
+ observeReducedMotionPreference((matches) => {
55
+ prefersReducedMotion = matches;
56
+ })
57
+ );
54
58
 
55
59
  function applyStyles(node: HTMLElement) {
56
60
  $effect(() => {
57
61
  node.style.cssText = style || '';
58
62
  node.style.setProperty('--dry-chromatic-offset', offsetValue);
59
63
  });
64
+
65
+ $effect(() => {
66
+ const stopViewport = observeInViewport(
67
+ node,
68
+ (inView) => {
69
+ onScreen = inView;
70
+ },
71
+ { rootMargin: '200px' }
72
+ );
73
+ const stopVisibility = observePageVisibility((visible) => {
74
+ tabVisible = visible;
75
+ });
76
+ return () => {
77
+ stopViewport();
78
+ stopVisibility();
79
+ };
80
+ });
60
81
  }
61
82
  </script>
62
83
 
@@ -67,6 +88,7 @@
67
88
  data-channels={channels}
68
89
  data-animated={(animated && !prefersReducedMotion) || undefined}
69
90
  data-reduced-motion={prefersReducedMotion || undefined}
91
+ data-paused={paused || undefined}
70
92
  onpointerenter={trigger === 'hover' ? handlePointerEnter : undefined}
71
93
  onpointerleave={trigger === 'hover' ? handlePointerLeave : undefined}
72
94
  {...rest}
@@ -143,6 +165,11 @@
143
165
  animation: chromatic-drift-b 4s ease-in-out infinite alternate-reverse;
144
166
  }
145
167
 
168
+ [data-chromatic-shift][data-animated][data-paused]::before,
169
+ [data-chromatic-shift][data-animated][data-paused]::after {
170
+ animation-play-state: paused;
171
+ }
172
+
146
173
  @keyframes chromatic-drift-r {
147
174
  0% {
148
175
  transform: translate(var(--dry-chromatic-offset), 0);
@@ -22,7 +22,8 @@
22
22
  <div
23
23
  class={className}
24
24
  id={ctx.contentId}
25
- role="region"
25
+ aria-hidden={!ctx.open}
26
+ inert={!ctx.open}
26
27
  data-state={ctx.open ? 'open' : 'closed'}
27
28
  data-collapsible-content
28
29
  {...rest}
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
+ import { fromAction } from 'svelte/attachments';
2
3
  import type { Snippet } from 'svelte';
3
4
  import type { HTMLAttributes } from 'svelte/elements';
4
- import { createAnchorPosition } from '@dryui/primitives';
5
- import type { Placement } from '@dryui/primitives';
5
+ import { createAnchoredPopover, createDismiss, type Placement } from '@dryui/primitives';
6
6
  import { getComboboxCtx } from './context.svelte.js';
7
7
 
8
8
  interface Props extends HTMLAttributes<HTMLDivElement> {
@@ -26,56 +26,38 @@
26
26
 
27
27
  const ctx = getComboboxCtx();
28
28
 
29
- let el = $state<HTMLDivElement>();
30
-
31
- const anchor = createAnchorPosition(
32
- () => ctx.inputEl,
33
- () => el ?? null,
34
- {
35
- get placement() {
36
- return placement;
37
- },
38
- get offset() {
39
- return offset;
40
- }
41
- }
42
- );
29
+ let el = $state<HTMLDivElement | null>(null);
43
30
 
44
- $effect(() => {
45
- if (!el) return;
31
+ function attachContent(node: HTMLDivElement) {
32
+ el = node;
46
33
 
47
- el.style.cssText = typeof style === 'string' ? style : '';
48
- const positionStyles = anchor.styles;
49
- for (const [key, value] of Object.entries(positionStyles)) {
50
- el.style.setProperty(key, value);
51
- }
52
- });
34
+ return () => {
35
+ if (el === node) {
36
+ el = null;
37
+ }
38
+ };
39
+ }
53
40
 
54
- $effect(() => {
55
- if (ctx.open && el && !el.matches(':popover-open')) {
56
- el.showPopover();
57
- } else if (!ctx.open && el?.matches(':popover-open')) {
58
- el.hidePopover();
59
- }
41
+ const popover = createAnchoredPopover({
42
+ triggerEl: () => ctx.inputEl,
43
+ contentEl: () => el ?? null,
44
+ open: () => ctx.open,
45
+ placement: () => placement,
46
+ offset: () => offset
60
47
  });
61
48
 
62
- // Manual dismiss: close on click outside input+popover
63
- $effect(() => {
64
- if (!ctx.open) return;
65
-
66
- function handlePointerDown(e: PointerEvent) {
67
- const target = e.target as Node;
68
- if (ctx.inputEl?.contains(target) || el?.contains(target)) return;
69
- ctx.close();
70
- }
71
-
72
- document.addEventListener('pointerdown', handlePointerDown);
73
- return () => document.removeEventListener('pointerdown', handlePointerDown);
49
+ createDismiss({
50
+ enabled: () => ctx.open,
51
+ escapeKey: false,
52
+ onDismiss: () => ctx.close(),
53
+ contentEl: () => el ?? null,
54
+ triggerEl: () => ctx.inputEl
74
55
  });
75
56
  </script>
76
57
 
77
58
  <div
78
- bind:this={el}
59
+ {@attach attachContent}
60
+ {@attach fromAction(popover.applyPosition, () => style)}
79
61
  popover="manual"
80
62
  role="listbox"
81
63
  id={ctx.contentId}
@@ -105,7 +87,7 @@
105
87
  margin: 0;
106
88
 
107
89
  display: grid;
108
- grid-template-columns: minmax(12rem, max-content);
90
+ grid-template-columns: minmax(max(12rem, anchor-size(inline)), max-content);
109
91
  background: var(--dry-color-bg-overlay);
110
92
  border: 1px solid var(--dry-color-stroke-weak);
111
93
  border-radius: var(--dry-radius-md);
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- import type { Placement } from '@dryui/primitives';
3
+ import { type Placement } from '@dryui/primitives';
4
4
  interface Props extends HTMLAttributes<HTMLDivElement> {
5
5
  placement?: Placement;
6
6
  offset?: number;
@@ -25,6 +25,7 @@
25
25
  let displayText = $state('');
26
26
  let inputValue = $state('');
27
27
  let activeIndex = $state(-1);
28
+ let inputEl = $state<HTMLInputElement | null>(null);
28
29
 
29
30
  setComboboxCtx({
30
31
  get open() {
@@ -47,7 +48,12 @@
47
48
  },
48
49
  inputId,
49
50
  contentId,
50
- inputEl: null,
51
+ get inputEl() {
52
+ return inputEl;
53
+ },
54
+ set inputEl(element: HTMLInputElement | null) {
55
+ inputEl = element;
56
+ },
51
57
  show() {
52
58
  if (!disabled) open = true;
53
59
  },