@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
@@ -15,7 +15,7 @@
15
15
  <div
16
16
  class={className}
17
17
  id={itemCtx.contentId}
18
- role="region"
18
+ aria-hidden={!itemCtx.open}
19
19
  hidden={!itemCtx.open}
20
20
  data-state={itemCtx.open ? 'open' : 'closed'}
21
21
  data-accordion-content
@@ -53,7 +53,7 @@
53
53
 
54
54
  <div data-alert-body>
55
55
  {#if title}
56
- <h5 data-alert-title>{@render title()}</h5>
56
+ <div data-alert-title>{@render title()}</div>
57
57
  {/if}
58
58
  {#if description}
59
59
  <p data-alert-description>{@render description()}</p>
@@ -0,0 +1,125 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ interface Props extends HTMLAttributes<HTMLDivElement> {
6
+ title?: string;
7
+ actions?: Snippet;
8
+ children: Snippet;
9
+ }
10
+
11
+ let { title, actions, children, class: className, ...rest }: Props = $props();
12
+ </script>
13
+
14
+ <div data-app-frame class={className} {...rest}>
15
+ <div data-part="chrome" aria-hidden="true">
16
+ <div data-part="dots">
17
+ <span data-part="dot" data-tone="close"></span>
18
+ <span data-part="dot" data-tone="min"></span>
19
+ <span data-part="dot" data-tone="max"></span>
20
+ </div>
21
+ {#if title}
22
+ <span data-part="title">{title}</span>
23
+ {/if}
24
+ {#if actions}
25
+ <div data-part="actions">
26
+ {@render actions()}
27
+ </div>
28
+ {/if}
29
+ </div>
30
+ <div data-part="content">
31
+ {@render children()}
32
+ </div>
33
+ </div>
34
+
35
+ <style>
36
+ [data-app-frame] {
37
+ --dry-app-frame-bg: var(--dry-color-bg-base);
38
+ --dry-app-frame-chrome-bg: var(--dry-color-bg-raised);
39
+ --dry-app-frame-border: var(--dry-color-stroke-weak);
40
+ --dry-app-frame-radius: var(--dry-radius-xl);
41
+ --dry-app-frame-dot-size: 0.75rem;
42
+ --dry-app-frame-dot-close: #ff5f56;
43
+ --dry-app-frame-dot-min: #ffbd2e;
44
+ --dry-app-frame-dot-max: #27c93f;
45
+
46
+ display: grid;
47
+ grid-template-rows: auto minmax(0, 1fr);
48
+ border: 1px solid var(--dry-app-frame-border);
49
+ border-radius: var(--dry-app-frame-radius);
50
+ background: var(--dry-app-frame-bg);
51
+ overflow: clip;
52
+ }
53
+
54
+ [data-part='chrome'] {
55
+ display: grid;
56
+ grid-template-columns: minmax(0, 1fr);
57
+ grid-template-rows: auto;
58
+ align-items: center;
59
+ padding: var(--dry-app-frame-chrome-padding, var(--dry-space-3) var(--dry-space-4));
60
+ border-block-end: 1px solid var(--dry-app-frame-border);
61
+ background: var(--dry-app-frame-chrome-bg);
62
+ }
63
+
64
+ [data-part='chrome'] > * {
65
+ grid-row: 1;
66
+ grid-column: 1;
67
+ }
68
+
69
+ [data-part='dots'] {
70
+ display: grid;
71
+ grid-auto-flow: column;
72
+ grid-auto-columns: auto;
73
+ gap: var(--dry-space-1_5);
74
+ align-items: center;
75
+ justify-self: start;
76
+ }
77
+
78
+ [data-part='dot'] {
79
+ display: block;
80
+ block-size: var(--dry-app-frame-dot-size);
81
+ aspect-ratio: 1;
82
+ border-radius: var(--dry-radius-full);
83
+ background: var(--dry-color-stroke-weak);
84
+ }
85
+
86
+ [data-part='dot'][data-tone='close'] {
87
+ background: var(--dry-app-frame-dot-close);
88
+ }
89
+
90
+ [data-part='dot'][data-tone='min'] {
91
+ background: var(--dry-app-frame-dot-min);
92
+ }
93
+
94
+ [data-part='dot'][data-tone='max'] {
95
+ background: var(--dry-app-frame-dot-max);
96
+ }
97
+
98
+ [data-part='title'] {
99
+ justify-self: center;
100
+ color: var(--dry-color-text-weak);
101
+ font-size: var(--dry-type-small-size, 0.875rem);
102
+ font-weight: 500;
103
+ letter-spacing: 0.01em;
104
+ text-align: center;
105
+ text-overflow: ellipsis;
106
+ overflow: hidden;
107
+ white-space: nowrap;
108
+ pointer-events: none;
109
+ }
110
+
111
+ [data-part='actions'] {
112
+ display: grid;
113
+ grid-auto-flow: column;
114
+ grid-auto-columns: auto;
115
+ align-items: center;
116
+ gap: var(--dry-space-2);
117
+ justify-self: end;
118
+ }
119
+
120
+ [data-part='content'] {
121
+ display: grid;
122
+ padding: var(--dry-app-frame-content-padding, 0);
123
+ min-block-size: 0;
124
+ }
125
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ title?: string;
5
+ actions?: Snippet;
6
+ children: Snippet;
7
+ }
8
+ declare const AppFrame: import("svelte").Component<Props, {}, "">;
9
+ type AppFrame = ReturnType<typeof AppFrame>;
10
+ export default AppFrame;
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ export interface AppFrameProps extends HTMLAttributes<HTMLDivElement> {
4
+ title?: string;
5
+ actions?: Snippet;
6
+ children: Snippet;
7
+ }
8
+ export { default as AppFrame } from './app-frame.svelte';
@@ -0,0 +1 @@
1
+ export { default as AppFrame } from './app-frame.svelte';
@@ -3,6 +3,14 @@
3
3
  import type { Snippet } from 'svelte';
4
4
  import type { HTMLAttributes } from 'svelte/elements';
5
5
  import type { BlendMode } from '@dryui/primitives/aurora';
6
+ import {
7
+ getReducedMotionPreference,
8
+ observeInViewport,
9
+ observePageVisibility,
10
+ observeReducedMotionPreference,
11
+ registerPropertyOnce,
12
+ supportsPropertyRegistration
13
+ } from '@dryui/primitives/internal/motion';
6
14
 
7
15
  interface Props extends HTMLAttributes<HTMLDivElement> {
8
16
  palette?: 'sunrise' | 'ocean' | 'forest' | 'cosmic' | readonly [string, string, string];
@@ -58,13 +66,6 @@
58
66
  const customPalette = $derived(Array.isArray(palette) ? palette : null);
59
67
 
60
68
  onMount(() => {
61
- const {
62
- registerPropertyOnce,
63
- supportsPropertyRegistration,
64
- observeReducedMotionPreference,
65
- getReducedMotionPreference
66
- } = await_motion();
67
-
68
69
  supportsAnimation = supportsPropertyRegistration();
69
70
  registerPropertyOnce({
70
71
  name: '--dry-aurora-angle',
@@ -79,27 +80,21 @@
79
80
  initialValue: '0%'
80
81
  });
81
82
 
82
- documentVisible = document.visibilityState === 'visible';
83
- const handleVisibilityChange = () => {
84
- documentVisible = document.visibilityState === 'visible';
85
- };
86
- document.addEventListener('visibilitychange', handleVisibilityChange);
87
-
88
- const updateAnimatedState = (matches: boolean) => {
83
+ const stopVisibility = observePageVisibility((visible) => {
84
+ documentVisible = visible;
85
+ });
86
+ const stopMotion = observeReducedMotionPreference((matches) => {
89
87
  prefersReducedMotion = matches;
90
- };
91
- const stopMotionObserver = observeReducedMotionPreference(updateAnimatedState);
92
- let stopViewportObserver = () => {};
93
-
94
- if (rootNode && 'IntersectionObserver' in window) {
95
- const observer = new IntersectionObserver(
96
- ([entry]) => {
97
- inViewport = entry?.isIntersecting ?? true;
88
+ });
89
+ let stopViewport = () => {};
90
+ if (rootNode) {
91
+ stopViewport = observeInViewport(
92
+ rootNode,
93
+ (inView) => {
94
+ inViewport = inView;
98
95
  },
99
96
  { threshold: 0.12 }
100
97
  );
101
- observer.observe(rootNode);
102
- stopViewportObserver = () => observer.disconnect();
103
98
  }
104
99
 
105
100
  if (!supportsAnimation || getReducedMotionPreference()) {
@@ -107,44 +102,12 @@
107
102
  }
108
103
 
109
104
  return () => {
110
- document.removeEventListener('visibilitychange', handleVisibilityChange);
111
- stopMotionObserver();
112
- stopViewportObserver();
105
+ stopVisibility();
106
+ stopMotion();
107
+ stopViewport();
113
108
  };
114
109
  });
115
110
 
116
- function await_motion() {
117
- // Dynamic import would be needed for the motion utils from primitives
118
- // Instead, inline the logic since we're rendering directly
119
- return {
120
- registerPropertyOnce(opts: {
121
- name: string;
122
- syntax: string;
123
- inherits: boolean;
124
- initialValue: string;
125
- }) {
126
- try {
127
- CSS.registerProperty(opts);
128
- } catch {
129
- // already registered or not supported
130
- }
131
- },
132
- supportsPropertyRegistration() {
133
- return typeof CSS !== 'undefined' && 'registerProperty' in CSS;
134
- },
135
- observeReducedMotionPreference(cb: (matches: boolean) => void) {
136
- const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
137
- cb(mql.matches);
138
- const handler = (e: MediaQueryListEvent) => cb(e.matches);
139
- mql.addEventListener('change', handler);
140
- return () => mql.removeEventListener('change', handler);
141
- },
142
- getReducedMotionPreference() {
143
- return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
144
- }
145
- };
146
- }
147
-
148
111
  function applyRootStyles(node: HTMLElement) {
149
112
  $effect(() => {
150
113
  node.style.cssText = style || '';
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import type { BlendMode } from '@dryui/primitives/beam';
5
+ import { observeOffscreenState } from '@dryui/primitives/internal/motion';
5
6
 
6
7
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
7
8
  color?: string;
@@ -40,6 +41,8 @@
40
41
  if (blendMode) node.style.setProperty('--dry-beam-blend', blendMode);
41
42
  else node.style.removeProperty('--dry-beam-blend');
42
43
  });
44
+
45
+ $effect(() => observeOffscreenState(node, { rootMargin: '200px' }));
43
46
  }
44
47
  </script>
45
48
 
@@ -61,12 +64,22 @@
61
64
  position: relative;
62
65
  overflow: hidden;
63
66
  border-radius: inherit;
67
+ contain: content;
64
68
  }
65
69
 
66
70
  [data-beam-layer] {
67
71
  position: absolute;
68
72
  inset: 0;
69
73
  pointer-events: none;
74
+ overflow: hidden;
75
+ opacity: calc(var(--dry-beam-intensity, 70) / 100);
76
+ mix-blend-mode: var(--dry-beam-blend, var(--dry-beam-default-blend, multiply));
77
+ }
78
+
79
+ [data-beam-layer]::before {
80
+ content: '';
81
+ position: absolute;
82
+ inset: -100%;
70
83
  background-image: linear-gradient(
71
84
  var(--dry-beam-angle, 45deg),
72
85
  transparent 0%,
@@ -75,26 +88,32 @@
75
88
  transparent calc(50% + var(--dry-beam-width, 2px)),
76
89
  transparent 100%
77
90
  );
78
- background-size: 300% 300%;
79
- opacity: calc(var(--dry-beam-intensity, 70) / 100);
80
- mix-blend-mode: var(--dry-beam-blend, var(--dry-beam-default-blend, multiply));
81
91
  filter: blur(calc(var(--dry-beam-width, 2px) * 2));
82
92
  animation: beam-sweep var(--dry-beam-speed, 3s) ease-in-out infinite;
93
+ backface-visibility: hidden;
94
+ }
95
+
96
+ [data-beam]:not([data-offscreen]) [data-beam-layer]::before {
97
+ will-change: transform, filter;
98
+ }
99
+
100
+ [data-beam][data-offscreen] [data-beam-layer]::before {
101
+ animation-play-state: paused;
83
102
  }
84
103
 
85
104
  @keyframes beam-sweep {
86
- 0% {
87
- background-position: -50% -50%;
105
+ from {
106
+ transform: translate(-33.333%, -33.333%);
88
107
  }
89
- 100% {
90
- background-position: 150% 150%;
108
+ to {
109
+ transform: translate(33.333%, 33.333%);
91
110
  }
92
111
  }
93
112
 
94
113
  @media (prefers-reduced-motion: reduce) {
95
- [data-beam-layer] {
114
+ [data-beam-layer]::before {
96
115
  animation: none;
97
- background-position: 50% 50%;
116
+ transform: translate(0, 0);
98
117
  }
99
118
  }
100
119
  </style>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import Button from '../button/button.svelte';
4
+ import { VisuallyHidden } from '../visually-hidden/index.js';
4
5
  import { getCarouselCtx } from './context.svelte.js';
5
6
 
6
7
  interface Props extends HTMLAttributes<HTMLDivElement> {}
@@ -9,19 +10,35 @@
9
10
  const ctx = getCarouselCtx();
10
11
  </script>
11
12
 
12
- <div role="tablist" aria-label="Slide indicators" data-carousel-dots class={className} {...rest}>
13
+ <div
14
+ role="group"
15
+ aria-label="Choose slide to display"
16
+ data-carousel-dots
17
+ class={className}
18
+ {...rest}
19
+ >
13
20
  {#each Array(ctx.totalSlides) as _, i (i)}
21
+ {@const isActive = ctx.activeIndex === i}
14
22
  <Button
15
23
  variant="toggle"
16
24
  size="icon-sm"
17
25
  type="button"
18
- role="tab"
19
- aria-selected={ctx.activeIndex === i}
20
- aria-pressed={ctx.activeIndex === i}
21
- aria-label="Go to slide {i + 1}"
22
- onclick={() => ctx.scrollTo(i)}
26
+ aria-controls={ctx.getSlideId(i)}
27
+ aria-disabled={isActive ? true : undefined}
28
+ data-active={isActive ? '' : undefined}
29
+ onclick={() => {
30
+ if (!isActive) {
31
+ ctx.scrollTo(i);
32
+ }
33
+ }}
23
34
  >
24
- <span class="dot"></span>
35
+ <VisuallyHidden>
36
+ Show slide {i + 1} of {ctx.totalSlides}
37
+ {#if isActive}
38
+ (current slide)
39
+ {/if}
40
+ </VisuallyHidden>
41
+ <span class="dot" aria-hidden="true"></span>
25
42
  </Button>
26
43
  {/each}
27
44
  </div>
@@ -45,7 +62,7 @@
45
62
  opacity: 0.4;
46
63
  }
47
64
 
48
- [aria-selected='true'] .dot {
65
+ [data-active] .dot {
49
66
  opacity: 1;
50
67
  }
51
68
  </style>
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import Button from '../button/button.svelte';
5
+ import { VisuallyHidden } from '../visually-hidden/index.js';
5
6
  import { getCarouselCtx } from './context.svelte.js';
6
7
 
7
8
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
@@ -14,24 +15,36 @@
14
15
  </script>
15
16
 
16
17
  <div
17
- role="tablist"
18
- aria-label="Slide thumbnails"
18
+ role="group"
19
+ aria-label="Choose slide to display"
19
20
  data-carousel-thumbnails
20
21
  data-position={position}
21
22
  class={className}
22
23
  {...rest}
23
24
  >
24
25
  {#each Array(ctx.totalSlides) as _, i (i)}
26
+ {@const isActive = ctx.activeIndex === i}
25
27
  <Button
26
28
  variant="bare"
27
29
  type="button"
28
- role="tab"
29
- aria-selected={ctx.activeIndex === i}
30
- aria-label="Go to slide {i + 1}"
31
- data-active={ctx.activeIndex === i ? '' : undefined}
32
- onclick={() => ctx.scrollTo(i)}
30
+ aria-controls={ctx.getSlideId(i)}
31
+ aria-disabled={isActive ? true : undefined}
32
+ data-active={isActive ? '' : undefined}
33
+ onclick={() => {
34
+ if (!isActive) {
35
+ ctx.scrollTo(i);
36
+ }
37
+ }}
33
38
  >
34
- {@render children({ index: i, isActive: ctx.activeIndex === i, scrollTo: ctx.scrollTo })}
39
+ <VisuallyHidden>
40
+ Show slide {i + 1} of {ctx.totalSlides}
41
+ {#if isActive}
42
+ (current slide)
43
+ {/if}
44
+ </VisuallyHidden>
45
+ <span class="thumbnail-preview" aria-hidden="true">
46
+ {@render children({ index: i, isActive, scrollTo: ctx.scrollTo })}
47
+ </span>
35
48
  </Button>
36
49
  {/each}
37
50
  </div>
@@ -62,4 +75,8 @@
62
75
  overflow-y: auto;
63
76
  overflow-x: hidden;
64
77
  }
78
+
79
+ .thumbnail-preview {
80
+ display: inline-grid;
81
+ }
65
82
  </style>
@@ -1,6 +1,15 @@
1
+ <script module lang="ts">
2
+ let carouselIds = 0;
3
+ </script>
4
+
1
5
  <script lang="ts">
2
6
  import type { Snippet } from 'svelte';
3
7
  import type { HTMLAttributes } from 'svelte/elements';
8
+ import Button from '../button/button.svelte';
9
+ import {
10
+ getReducedMotionPreference,
11
+ observeReducedMotionPreference
12
+ } from '../internal/motion.js';
4
13
  import { setCarouselCtx } from './context.svelte.js';
5
14
 
6
15
  interface Props extends HTMLAttributes<HTMLDivElement> {
@@ -23,9 +32,20 @@
23
32
  let totalSlides = $state(0);
24
33
  let slideCounter = $state(0);
25
34
  let viewportEl = $state<HTMLElement | null>(null);
35
+ let rootEl = $state<HTMLDivElement | null>(null);
36
+ let prefersReducedMotion = $state(false);
37
+ let manualPaused = $state(false);
38
+ let pausedByHover = $state(false);
39
+ let pausedByFocus = $state(false);
40
+ const carouselId = `dry-carousel-${++carouselIds}`;
26
41
 
27
42
  const canScrollPrev = $derived(loop || activeIndex > 0);
28
43
  const canScrollNext = $derived(loop || activeIndex < totalSlides - 1);
44
+ const autoplayDelay = $derived(typeof autoplay === 'number' && autoplay > 0 ? autoplay : 0);
45
+ const autoplayConfigured = $derived(autoplayDelay > 0);
46
+ const autoplayEnabled = $derived(autoplayConfigured && totalSlides > 1);
47
+ const autoplayPaused = $derived(manualPaused || pausedByHover || pausedByFocus);
48
+ const autoplayRunning = $derived(autoplayEnabled && !autoplayPaused);
29
49
 
30
50
  function resolveIndex(index: number) {
31
51
  if (totalSlides === 0) return 0;
@@ -40,7 +60,7 @@
40
60
  viewportEl.scrollTo({
41
61
  left: orientation === 'horizontal' ? slide.offsetLeft : 0,
42
62
  top: orientation === 'vertical' ? slide.offsetTop : 0,
43
- behavior: 'smooth'
63
+ behavior: prefersReducedMotion ? 'auto' : 'smooth'
44
64
  });
45
65
  }
46
66
 
@@ -54,6 +74,19 @@
54
74
  scrollViewportTo(target);
55
75
  }
56
76
 
77
+ function toggleAutoplay() {
78
+ if (!autoplayEnabled) return;
79
+
80
+ if (autoplayRunning) {
81
+ manualPaused = true;
82
+ return;
83
+ }
84
+
85
+ manualPaused = false;
86
+ pausedByHover = false;
87
+ pausedByFocus = false;
88
+ }
89
+
57
90
  setCarouselCtx({
58
91
  get activeIndex() {
59
92
  return activeIndex;
@@ -70,6 +103,15 @@
70
103
  get canScrollNext() {
71
104
  return canScrollNext;
72
105
  },
106
+ get autoplayEnabled() {
107
+ return autoplayEnabled;
108
+ },
109
+ get autoplayPaused() {
110
+ return autoplayPaused;
111
+ },
112
+ get autoplayRunning() {
113
+ return autoplayRunning;
114
+ },
73
115
  scrollTo,
74
116
  syncActiveIndex,
75
117
  scrollPrev() {
@@ -78,6 +120,10 @@
78
120
  scrollNext() {
79
121
  scrollTo(activeIndex + 1);
80
122
  },
123
+ toggleAutoplay,
124
+ getSlideId(index) {
125
+ return `${carouselId}-slide-${index + 1}`;
126
+ },
81
127
  registerViewport(el) {
82
128
  viewportEl = el;
83
129
  },
@@ -97,21 +143,67 @@
97
143
  });
98
144
 
99
145
  $effect(() => {
100
- if (!autoplay) return;
146
+ if (!autoplayConfigured) {
147
+ prefersReducedMotion = false;
148
+ manualPaused = false;
149
+ pausedByHover = false;
150
+ pausedByFocus = false;
151
+ return;
152
+ }
153
+
154
+ prefersReducedMotion = getReducedMotionPreference();
155
+ if (prefersReducedMotion) {
156
+ manualPaused = true;
157
+ }
158
+
159
+ return observeReducedMotionPreference((value) => {
160
+ prefersReducedMotion = value;
161
+ if (value) {
162
+ manualPaused = true;
163
+ }
164
+ });
165
+ });
166
+
167
+ $effect(() => {
168
+ if (!autoplayRunning || !autoplayConfigured) return;
101
169
  const interval = setInterval(() => {
102
170
  scrollTo(activeIndex + 1);
103
- }, autoplay);
171
+ }, autoplayDelay);
104
172
  return () => clearInterval(interval);
105
173
  });
106
174
  </script>
107
175
 
108
176
  <div
109
- role="region"
177
+ bind:this={rootEl}
178
+ role="group"
110
179
  aria-roledescription="carousel"
111
180
  data-carousel-root
112
181
  data-orientation={orientation}
182
+ data-autoplay-enabled={autoplayEnabled ? '' : undefined}
183
+ data-autoplay-state={autoplayEnabled ? (autoplayRunning ? 'running' : 'paused') : undefined}
113
184
  class={className}
114
185
  {...rest}
186
+ onfocusin={() => {
187
+ if (autoplayEnabled) {
188
+ pausedByFocus = true;
189
+ }
190
+ }}
191
+ onfocusout={(event) => {
192
+ const nextTarget = event.relatedTarget;
193
+ if (rootEl && nextTarget instanceof Node && rootEl.contains(nextTarget)) {
194
+ return;
195
+ }
196
+
197
+ pausedByFocus = false;
198
+ }}
199
+ onpointerenter={() => {
200
+ if (autoplayEnabled) {
201
+ pausedByHover = true;
202
+ }
203
+ }}
204
+ onpointerleave={() => {
205
+ pausedByHover = false;
206
+ }}
115
207
  onkeydown={(e) => {
116
208
  const prev = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
117
209
  const next = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
@@ -125,6 +217,18 @@
125
217
  }
126
218
  }}
127
219
  >
220
+ {#if autoplayConfigured}
221
+ <Button
222
+ variant="outline"
223
+ size="sm"
224
+ type="button"
225
+ data-carousel-rotation-control
226
+ onclick={() => toggleAutoplay()}
227
+ >
228
+ {autoplayRunning ? 'Stop slide rotation' : 'Start slide rotation'}
229
+ </Button>
230
+ {/if}
231
+
128
232
  {@render children()}
129
233
  </div>
130
234
 
@@ -134,4 +238,11 @@
134
238
 
135
239
  position: relative;
136
240
  }
241
+
242
+ [data-carousel-rotation-control] {
243
+ position: absolute;
244
+ top: var(--dry-space-3);
245
+ left: var(--dry-space-3);
246
+ z-index: 1;
247
+ }
137
248
  </style>