@loidolt/theme-svelte 0.5.0 → 0.6.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 (123) hide show
  1. package/README.md +11 -0
  2. package/dist/announcer.svelte.d.ts +36 -0
  3. package/dist/announcer.svelte.js +73 -0
  4. package/dist/attachments.d.ts +83 -0
  5. package/dist/attachments.js +211 -0
  6. package/dist/components/ActiveFilterChips.svelte +80 -0
  7. package/dist/components/ActiveFilterChips.svelte.d.ts +41 -0
  8. package/dist/components/AspectRatio.svelte +32 -0
  9. package/dist/components/AspectRatio.svelte.d.ts +13 -0
  10. package/dist/components/AudioPlayer.svelte +94 -0
  11. package/dist/components/AudioPlayer.svelte.d.ts +24 -0
  12. package/dist/components/Avatar.svelte +85 -0
  13. package/dist/components/Avatar.svelte.d.ts +31 -0
  14. package/dist/components/Badge.svelte +28 -2
  15. package/dist/components/Badge.svelte.d.ts +13 -1
  16. package/dist/components/Combobox.svelte +180 -0
  17. package/dist/components/Combobox.svelte.d.ts +77 -0
  18. package/dist/components/DataTable.svelte +228 -0
  19. package/dist/components/DataTable.svelte.d.ts +62 -0
  20. package/dist/components/DataTableColumnVisibility.svelte +46 -0
  21. package/dist/components/DataTableColumnVisibility.svelte.d.ts +31 -0
  22. package/dist/components/DataTableFacetedFilter.svelte +87 -0
  23. package/dist/components/DataTableFacetedFilter.svelte.d.ts +40 -0
  24. package/dist/components/DataTablePagination.svelte +68 -0
  25. package/dist/components/DataTablePagination.svelte.d.ts +42 -0
  26. package/dist/components/DataTableSearch.svelte +67 -0
  27. package/dist/components/DataTableSearch.svelte.d.ts +37 -0
  28. package/dist/components/Dialog.svelte +32 -11
  29. package/dist/components/Dialog.svelte.d.ts +7 -0
  30. package/dist/components/DropdownMenu.svelte +95 -28
  31. package/dist/components/DropdownMenu.svelte.d.ts +12 -3
  32. package/dist/components/Field.svelte +2 -3
  33. package/dist/components/Fieldset.svelte +2 -3
  34. package/dist/components/Filmstrip.svelte +10 -3
  35. package/dist/components/FilterPanel.svelte +62 -0
  36. package/dist/components/FilterPanel.svelte.d.ts +22 -0
  37. package/dist/components/Lightbox.svelte +239 -0
  38. package/dist/components/Lightbox.svelte.d.ts +40 -0
  39. package/dist/components/LiveRegion.svelte +48 -0
  40. package/dist/components/LiveRegion.svelte.d.ts +20 -0
  41. package/dist/components/MediaCarousel.svelte +177 -0
  42. package/dist/components/MediaCarousel.svelte.d.ts +32 -0
  43. package/dist/components/MediaEmbed.svelte +116 -0
  44. package/dist/components/MediaEmbed.svelte.d.ts +34 -0
  45. package/dist/components/MediaGrid.svelte +91 -0
  46. package/dist/components/MediaGrid.svelte.d.ts +24 -0
  47. package/dist/components/MultiSelect.svelte +232 -0
  48. package/dist/components/MultiSelect.svelte.d.ts +67 -0
  49. package/dist/components/NumberField.svelte +53 -25
  50. package/dist/components/NumberField.svelte.d.ts +47 -18
  51. package/dist/components/OTPInput.svelte +94 -0
  52. package/dist/components/OTPInput.svelte.d.ts +26 -0
  53. package/dist/components/PasswordInput.svelte +93 -0
  54. package/dist/components/PasswordInput.svelte.d.ts +24 -0
  55. package/dist/components/Progress.svelte +57 -14
  56. package/dist/components/Progress.svelte.d.ts +13 -0
  57. package/dist/components/RadioGroup.svelte +19 -5
  58. package/dist/components/RadioGroup.svelte.d.ts +3 -2
  59. package/dist/components/Select.svelte +15 -5
  60. package/dist/components/Select.svelte.d.ts +3 -2
  61. package/dist/components/SignaturePad.svelte +317 -0
  62. package/dist/components/SignaturePad.svelte.d.ts +48 -0
  63. package/dist/components/Skeleton.svelte +40 -9
  64. package/dist/components/Skeleton.svelte.d.ts +8 -0
  65. package/dist/components/SkipLink.svelte +48 -0
  66. package/dist/components/SkipLink.svelte.d.ts +14 -0
  67. package/dist/components/Slider.svelte +148 -0
  68. package/dist/components/Slider.svelte.d.ts +55 -0
  69. package/dist/components/Spinner.svelte +11 -2
  70. package/dist/components/Spinner.svelte.d.ts +3 -0
  71. package/dist/components/Tabs.svelte.d.ts +1 -1
  72. package/dist/components/Thumbnail.svelte +52 -6
  73. package/dist/components/Thumbnail.svelte.d.ts +14 -2
  74. package/dist/components/Toast.svelte +24 -0
  75. package/dist/components/Toast.svelte.d.ts +10 -0
  76. package/dist/components/ToastViewport.svelte +9 -1
  77. package/dist/components/ToastViewport.svelte.d.ts +2 -0
  78. package/dist/components/Toolbar.svelte +49 -0
  79. package/dist/components/Toolbar.svelte.d.ts +23 -0
  80. package/dist/components/Tooltip.svelte +9 -1
  81. package/dist/components/Tooltip.svelte.d.ts +2 -0
  82. package/dist/components/VideoPlayer.svelte +136 -0
  83. package/dist/components/VideoPlayer.svelte.d.ts +38 -0
  84. package/dist/data-table-utils.d.ts +62 -0
  85. package/dist/data-table-utils.js +91 -0
  86. package/dist/data-table.svelte.d.ts +132 -0
  87. package/dist/data-table.svelte.js +292 -0
  88. package/dist/hls.d.ts +35 -0
  89. package/dist/hls.js +37 -0
  90. package/dist/hls.svelte.d.ts +22 -0
  91. package/dist/hls.svelte.js +66 -0
  92. package/dist/index.d.ts +47 -4
  93. package/dist/index.js +35 -2
  94. package/dist/internal/EditableCell.svelte +58 -0
  95. package/dist/internal/EditableCell.svelte.d.ts +8 -0
  96. package/dist/internal/ListboxEntries.svelte +49 -0
  97. package/dist/internal/ListboxEntries.svelte.d.ts +35 -0
  98. package/dist/internal/media/MediaControls.svelte +163 -0
  99. package/dist/internal/media/MediaControls.svelte.d.ts +12 -0
  100. package/dist/internal/media/MediaStage.svelte +49 -0
  101. package/dist/internal/media/MediaStage.svelte.d.ts +11 -0
  102. package/dist/internal/media/MediaTile.svelte +36 -0
  103. package/dist/internal/media/MediaTile.svelte.d.ts +11 -0
  104. package/dist/internal/media/keys.d.ts +7 -0
  105. package/dist/internal/media/keys.js +51 -0
  106. package/dist/internal/media/labels.d.ts +19 -0
  107. package/dist/internal/media/labels.js +15 -0
  108. package/dist/internal/options.d.ts +9 -0
  109. package/dist/internal/options.js +20 -0
  110. package/dist/media-player.svelte.d.ts +61 -0
  111. package/dist/media-player.svelte.js +246 -0
  112. package/dist/media-utils.d.ts +112 -0
  113. package/dist/media-utils.js +205 -0
  114. package/dist/media-zoom.svelte.d.ts +33 -0
  115. package/dist/media-zoom.svelte.js +126 -0
  116. package/dist/signature.d.ts +55 -0
  117. package/dist/signature.js +52 -0
  118. package/dist/toaster.svelte.d.ts +24 -0
  119. package/dist/toaster.svelte.js +23 -0
  120. package/dist/types.d.ts +62 -3
  121. package/dist/utils.d.ts +9 -0
  122. package/dist/utils.js +11 -0
  123. package/package.json +10 -4
package/README.md CHANGED
@@ -19,3 +19,14 @@ Then import components from the package barrel:
19
19
  import { Button, Field, Input } from '@loidolt/theme-svelte';
20
20
  </script>
21
21
  ```
22
+
23
+ `hls.js` is an optional peer dependency, needed only to play HLS (`.m3u8`) streams outside
24
+ Safari. Install it and register it once:
25
+
26
+ ```ts
27
+ import { setHlsLoader } from '@loidolt/theme-svelte';
28
+
29
+ setHlsLoader(() => import('hls.js').then((module) => module.default));
30
+ ```
31
+
32
+ See the [design-system documentation](https://theme.loidolt.space) for every component.
@@ -0,0 +1,36 @@
1
+ export type Politeness = 'polite' | 'assertive';
2
+ export interface AnnouncerOptions {
3
+ /** Default politeness for `announce()`. `'assertive'` interrupts; keep it for errors. */
4
+ politeness?: Politeness;
5
+ /**
6
+ * Milliseconds before a message is cleared, so the region does not hold stale text a screen
7
+ * reader user might land on later. `0` keeps it. Defaults to 5000.
8
+ */
9
+ clearAfter?: number;
10
+ }
11
+ export interface Announcer {
12
+ /** The text to render inside a mounted `LiveRegion`. Reactive. */
13
+ readonly message: string;
14
+ readonly politeness: Politeness;
15
+ /** Speaks `message`. Repeating the same text is announced again, not swallowed. */
16
+ announce(message: string, options?: {
17
+ politeness?: Politeness;
18
+ }): void;
19
+ clear(): void;
20
+ /** Cancels any pending write or clear. Automatic for a component-scoped instance. */
21
+ destroy(): void;
22
+ }
23
+ /**
24
+ * State behind a `LiveRegion`: set a message, and it is spoken and then cleared.
25
+ *
26
+ * ```svelte
27
+ * const announcer = createAnnouncer();
28
+ * <LiveRegion message={announcer.message} politeness={announcer.politeness} />
29
+ * <button onclick={() => announcer.announce('Saved')}>Save</button>
30
+ * ```
31
+ *
32
+ * The region must already be in the document when the message changes — a region mounted
33
+ * together with its text is silent in most screen readers — so render `LiveRegion` once,
34
+ * unconditionally, and let this drive its contents.
35
+ */
36
+ export declare function createAnnouncer(options?: AnnouncerOptions): Announcer;
@@ -0,0 +1,73 @@
1
+ import { onDestroy } from 'svelte';
2
+ /** Milliseconds between emptying the region and writing the new message. */
3
+ const WRITE_DELAY = 50;
4
+ /**
5
+ * State behind a `LiveRegion`: set a message, and it is spoken and then cleared.
6
+ *
7
+ * ```svelte
8
+ * const announcer = createAnnouncer();
9
+ * <LiveRegion message={announcer.message} politeness={announcer.politeness} />
10
+ * <button onclick={() => announcer.announce('Saved')}>Save</button>
11
+ * ```
12
+ *
13
+ * The region must already be in the document when the message changes — a region mounted
14
+ * together with its text is silent in most screen readers — so render `LiveRegion` once,
15
+ * unconditionally, and let this drive its contents.
16
+ */
17
+ export function createAnnouncer(options = {}) {
18
+ const { politeness: defaultPoliteness = 'polite', clearAfter = 5000 } = options;
19
+ if (!(Number.isFinite(clearAfter) && clearAfter >= 0)) {
20
+ throw new RangeError('createAnnouncer: `clearAfter` must be a non-negative number');
21
+ }
22
+ let message = $state('');
23
+ let politeness = $state(defaultPoliteness);
24
+ let clearTimer;
25
+ let pending = 0;
26
+ const cancel = () => {
27
+ clearTimeout(clearTimer);
28
+ clearTimer = undefined;
29
+ };
30
+ const announce = (text, opts = {}) => {
31
+ cancel();
32
+ politeness = opts.politeness ?? defaultPoliteness;
33
+ // Screen readers announce a *change*. Setting the same string again is no change, so empty
34
+ // the region first and write the text a moment later. The gap is a macrotask, not a
35
+ // microtask: both writes inside one task can reach assistive tech as a single, unchanged
36
+ // mutation.
37
+ message = '';
38
+ const token = ++pending;
39
+ setTimeout(() => {
40
+ if (token !== pending)
41
+ return;
42
+ message = text;
43
+ if (clearAfter > 0)
44
+ clearTimer = setTimeout(() => (message = ''), clearAfter);
45
+ }, WRITE_DELAY);
46
+ };
47
+ const clear = () => {
48
+ pending++;
49
+ cancel();
50
+ message = '';
51
+ };
52
+ const destroy = () => {
53
+ pending++;
54
+ cancel();
55
+ };
56
+ try {
57
+ onDestroy(destroy);
58
+ }
59
+ catch {
60
+ /* module scope: the caller owns `destroy()` */
61
+ }
62
+ return {
63
+ get message() {
64
+ return message;
65
+ },
66
+ get politeness() {
67
+ return politeness;
68
+ },
69
+ announce,
70
+ clear,
71
+ destroy,
72
+ };
73
+ }
@@ -0,0 +1,83 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ import type { Orientation } from './types.js';
3
+ export interface EscapeKeyOptions {
4
+ /** Listen only while true. Defaults to `true`. */
5
+ enabled?: boolean;
6
+ preventDefault?: boolean;
7
+ /**
8
+ * Stop the event reaching outer Escape handlers, so only the innermost layer closes. Defaults
9
+ * to `false`.
10
+ */
11
+ stopPropagation?: boolean;
12
+ /**
13
+ * `'document'` (the default) hears Escape wherever focus is — right for a layer that owns the
14
+ * screen. `'node'` hears it only while focus is inside the element.
15
+ */
16
+ scope?: 'document' | 'node';
17
+ }
18
+ /** Calls `handler` when Escape is pressed. */
19
+ export declare function escapeKey(handler: (event: KeyboardEvent) => void, options?: EscapeKeyOptions): Attachment<HTMLElement>;
20
+ export interface ClickOutsideOptions {
21
+ enabled?: boolean;
22
+ /** Defaults to `['pointerdown']`, which covers mouse, pen and touch in one listener. */
23
+ events?: Array<'pointerdown' | 'mousedown' | 'touchstart' | 'click'>;
24
+ /**
25
+ * Presses on these count as inside — typically the trigger that opened the surface, so
26
+ * pressing it toggles rather than closing and instantly reopening.
27
+ */
28
+ ignore?: Array<Element | null | undefined>;
29
+ }
30
+ /** Calls `handler` when a press lands outside the element (and outside `ignore`). */
31
+ export declare function clickOutside(handler: (event: Event) => void, options?: ClickOutsideOptions): Attachment<HTMLElement>;
32
+ export interface AutofocusOptions {
33
+ enabled?: boolean;
34
+ /** Milliseconds to wait — for a surface that animates in. Defaults to the next frame. */
35
+ delay?: number;
36
+ preventScroll?: boolean;
37
+ /** Select an input's text as well as focusing it. */
38
+ select?: boolean;
39
+ }
40
+ /**
41
+ * Moves focus to the element once it mounts. Prefer it to the `autofocus` attribute, which
42
+ * browsers honour only on page load and which Svelte warns about for that reason.
43
+ */
44
+ export declare function autofocus(options?: AutofocusOptions): Attachment<HTMLElement>;
45
+ export interface RovingFocusOptions {
46
+ /**
47
+ * Which arrow keys move focus. `'both'` accepts all four, for a grid you walk in reading order.
48
+ * Defaults to `'horizontal'`.
49
+ */
50
+ orientation?: Orientation | 'both';
51
+ /** Wrap from the last item to the first and back. Defaults to `true`. */
52
+ loop?: boolean;
53
+ /** Which descendants take part. Defaults to `[data-roving-item]`. */
54
+ selector?: string;
55
+ /**
56
+ * Keep exactly one item in the tab order (`tabindex="0"`) and the rest at `-1`, so Tab enters
57
+ * and leaves the group in one step — the WAI-ARIA toolbar and grid pattern. Defaults to `true`.
58
+ */
59
+ manageTabIndex?: boolean;
60
+ onFocusChange?: (index: number, element: HTMLElement) => void;
61
+ }
62
+ /**
63
+ * Arrow-key, Home and End navigation between the matching descendants, with an optional roving
64
+ * tab stop. Items added or removed later are picked up on the next key press.
65
+ */
66
+ export declare function rovingFocus(options?: RovingFocusOptions): Attachment<HTMLElement>;
67
+ export interface FocusTrapOptions {
68
+ enabled?: boolean;
69
+ /** A selector inside the element, or an element, to focus first. Defaults to the first tabbable. */
70
+ initialFocus?: string | HTMLElement;
71
+ /**
72
+ * Where focus goes when the trap is released: `true` (the default) returns it to whatever had
73
+ * it before, an element sends it there, `false` leaves it alone.
74
+ */
75
+ returnFocus?: boolean | HTMLElement;
76
+ preventScroll?: boolean;
77
+ }
78
+ /**
79
+ * Keeps Tab and Shift+Tab inside the element while enabled, and restores focus when released.
80
+ * `Dialog`, `AlertDialog`, `Drawer` and the other Bits overlays already trap focus — this is for
81
+ * a modal surface you render yourself.
82
+ */
83
+ export declare function focusTrap(options?: FocusTrapOptions): Attachment<HTMLElement>;
@@ -0,0 +1,211 @@
1
+ /** Calls `handler` when Escape is pressed. */
2
+ export function escapeKey(handler, options = {}) {
3
+ const { enabled = true, preventDefault = false, stopPropagation = false, scope } = options;
4
+ return (node) => {
5
+ if (!enabled)
6
+ return;
7
+ const target = scope === 'node' ? node : node.ownerDocument;
8
+ const onKeydown = (event) => {
9
+ const key = event;
10
+ // An IME composition uses Escape to cancel the candidate, not the surface.
11
+ if (key.key !== 'Escape' || key.isComposing)
12
+ return;
13
+ if (preventDefault)
14
+ key.preventDefault();
15
+ if (stopPropagation)
16
+ key.stopPropagation();
17
+ handler(key);
18
+ };
19
+ target.addEventListener('keydown', onKeydown);
20
+ return () => target.removeEventListener('keydown', onKeydown);
21
+ };
22
+ }
23
+ /** Calls `handler` when a press lands outside the element (and outside `ignore`). */
24
+ export function clickOutside(handler, options = {}) {
25
+ const { enabled = true, events = ['pointerdown'], ignore = [] } = options;
26
+ return (node) => {
27
+ if (!enabled)
28
+ return;
29
+ const doc = node.ownerDocument;
30
+ const onPress = (event) => {
31
+ // `composedPath` sees through shadow roots and survives the target being removed by an
32
+ // earlier handler in the same dispatch; `contains` on a detached target would not.
33
+ const path = event.composedPath();
34
+ if (path.includes(node) || ignore.some((element) => element && path.includes(element)))
35
+ return;
36
+ handler(event);
37
+ };
38
+ for (const type of events)
39
+ doc.addEventListener(type, onPress, true);
40
+ return () => {
41
+ for (const type of events)
42
+ doc.removeEventListener(type, onPress, true);
43
+ };
44
+ };
45
+ }
46
+ /**
47
+ * Moves focus to the element once it mounts. Prefer it to the `autofocus` attribute, which
48
+ * browsers honour only on page load and which Svelte warns about for that reason.
49
+ */
50
+ export function autofocus(options = {}) {
51
+ const { enabled = true, delay, preventScroll = false, select = false } = options;
52
+ return (node) => {
53
+ if (!enabled)
54
+ return;
55
+ const run = () => {
56
+ node.focus({ preventScroll });
57
+ if (select && (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement))
58
+ node.select();
59
+ };
60
+ if (delay !== undefined) {
61
+ const timer = setTimeout(run, delay);
62
+ return () => clearTimeout(timer);
63
+ }
64
+ const frame = requestAnimationFrame(run);
65
+ return () => cancelAnimationFrame(frame);
66
+ };
67
+ }
68
+ const isUsable = (element) => !element.matches(':disabled, [aria-disabled="true"]') && !element.closest('[hidden], [inert]');
69
+ /**
70
+ * Arrow-key, Home and End navigation between the matching descendants, with an optional roving
71
+ * tab stop. Items added or removed later are picked up on the next key press.
72
+ */
73
+ export function rovingFocus(options = {}) {
74
+ const { orientation = 'horizontal', loop = true, selector = '[data-roving-item]', manageTabIndex = true, onFocusChange, } = options;
75
+ const next = new Set(orientation === 'vertical'
76
+ ? ['ArrowDown']
77
+ : orientation === 'horizontal'
78
+ ? ['ArrowRight']
79
+ : ['ArrowRight', 'ArrowDown']);
80
+ const previous = new Set(orientation === 'vertical'
81
+ ? ['ArrowUp']
82
+ : orientation === 'horizontal'
83
+ ? ['ArrowLeft']
84
+ : ['ArrowLeft', 'ArrowUp']);
85
+ return (node) => {
86
+ const items = () => [...node.querySelectorAll(selector)].filter((item) => isUsable(item));
87
+ const setTabStop = (active) => {
88
+ if (!manageTabIndex)
89
+ return;
90
+ for (const item of node.querySelectorAll(selector)) {
91
+ item.tabIndex = item === active ? 0 : -1;
92
+ }
93
+ };
94
+ const move = (target, index) => {
95
+ setTabStop(target);
96
+ target.focus();
97
+ onFocusChange?.(index, target);
98
+ };
99
+ // Start with one tab stop: the first item already marked current, else the first usable one.
100
+ const initial = items();
101
+ setTabStop(initial.find((item) => item.matches('[aria-current], [aria-selected="true"]')) ?? initial[0]);
102
+ const onKeydown = (event) => {
103
+ const list = items();
104
+ if (list.length === 0)
105
+ return;
106
+ const current = list.findIndex((item) => item === event.target || item.contains(event.target));
107
+ if (current === -1)
108
+ return;
109
+ let index;
110
+ if (next.has(event.key)) {
111
+ index = current + 1 < list.length ? current + 1 : loop ? 0 : current;
112
+ }
113
+ else if (previous.has(event.key)) {
114
+ index = current > 0 ? current - 1 : loop ? list.length - 1 : current;
115
+ }
116
+ else if (event.key === 'Home') {
117
+ index = 0;
118
+ }
119
+ else if (event.key === 'End') {
120
+ index = list.length - 1;
121
+ }
122
+ else {
123
+ return;
124
+ }
125
+ event.preventDefault();
126
+ move(list[index], index);
127
+ };
128
+ // A click or programmatic focus moves the tab stop too, so Tab returns to the last item used.
129
+ const onFocusin = (event) => {
130
+ const item = items().find((one) => one === event.target);
131
+ if (item)
132
+ setTabStop(item);
133
+ };
134
+ node.addEventListener('keydown', onKeydown);
135
+ node.addEventListener('focusin', onFocusin);
136
+ return () => {
137
+ node.removeEventListener('keydown', onKeydown);
138
+ node.removeEventListener('focusin', onFocusin);
139
+ };
140
+ };
141
+ }
142
+ const TABBABLE = [
143
+ 'a[href]',
144
+ 'area[href]',
145
+ 'button:not([disabled])',
146
+ 'input:not([disabled]):not([type="hidden"])',
147
+ 'select:not([disabled])',
148
+ 'textarea:not([disabled])',
149
+ 'iframe',
150
+ 'audio[controls]',
151
+ 'video[controls]',
152
+ '[contenteditable]:not([contenteditable="false"])',
153
+ '[tabindex]',
154
+ ].join(',');
155
+ const tabbables = (container) => [...container.querySelectorAll(TABBABLE)].filter((element) => element.tabIndex >= 0 &&
156
+ !element.closest('[hidden], [inert]') &&
157
+ getComputedStyle(element).visibility !== 'hidden' &&
158
+ getComputedStyle(element).display !== 'none');
159
+ /**
160
+ * Keeps Tab and Shift+Tab inside the element while enabled, and restores focus when released.
161
+ * `Dialog`, `AlertDialog`, `Drawer` and the other Bits overlays already trap focus — this is for
162
+ * a modal surface you render yourself.
163
+ */
164
+ export function focusTrap(options = {}) {
165
+ const { enabled = true, initialFocus, returnFocus = true, preventScroll = false } = options;
166
+ return (node) => {
167
+ if (!enabled)
168
+ return;
169
+ const doc = node.ownerDocument;
170
+ const previous = doc.activeElement;
171
+ const first = typeof initialFocus === 'string'
172
+ ? node.querySelector(initialFocus)
173
+ : (initialFocus ?? null);
174
+ const frame = requestAnimationFrame(() => {
175
+ if (node.contains(doc.activeElement))
176
+ return;
177
+ const target = first ?? tabbables(node)[0];
178
+ // With nothing tabbable inside, hold focus on the container itself so Tab has nowhere to go.
179
+ if (!target && !node.hasAttribute('tabindex'))
180
+ node.tabIndex = -1;
181
+ (target ?? node).focus({ preventScroll });
182
+ });
183
+ const onKeydown = (event) => {
184
+ if (event.key !== 'Tab')
185
+ return;
186
+ const list = tabbables(node);
187
+ if (list.length === 0) {
188
+ event.preventDefault();
189
+ return;
190
+ }
191
+ const active = doc.activeElement;
192
+ const [head, tail] = [list[0], list[list.length - 1]];
193
+ if (event.shiftKey && (active === head || !node.contains(active))) {
194
+ event.preventDefault();
195
+ tail.focus({ preventScroll });
196
+ }
197
+ else if (!event.shiftKey && (active === tail || !node.contains(active))) {
198
+ event.preventDefault();
199
+ head.focus({ preventScroll });
200
+ }
201
+ };
202
+ doc.addEventListener('keydown', onKeydown, true);
203
+ return () => {
204
+ cancelAnimationFrame(frame);
205
+ doc.removeEventListener('keydown', onKeydown, true);
206
+ const destination = returnFocus === true ? previous : returnFocus || null;
207
+ if (destination?.isConnected)
208
+ destination.focus({ preventScroll });
209
+ };
210
+ };
211
+ }
@@ -0,0 +1,80 @@
1
+ <script lang="ts" generics="T extends string = string">
2
+ import { tick, type Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import type { Option } from '../types.js';
5
+ import { cx } from '../utils.js';
6
+ import Button from './Button.svelte';
7
+
8
+ interface Props extends HTMLAttributes<HTMLDivElement> {
9
+ /** One chip per filter narrowing the results, labelled the way the user would say it. */
10
+ items: Option<T>[];
11
+ /** Clear one filter. Remove it from `items` in response. */
12
+ onRemove: (value: T) => void;
13
+ /** Clear every filter; shows a trailing "Clear all" when set. */
14
+ onClearAll?: () => void;
15
+ label?: string;
16
+ removeLabel?: (label: string) => string;
17
+ clearAllLabel?: string;
18
+ /** Notes after the chips, e.g. a filter that does not apply to this view. */
19
+ children?: Snippet;
20
+ class?: string;
21
+ ref?: HTMLDivElement | null;
22
+ }
23
+
24
+ let {
25
+ items,
26
+ onRemove,
27
+ onClearAll,
28
+ label = 'Active filters',
29
+ removeLabel = (text) => `Remove filter ${text}`,
30
+ clearAllLabel = 'Clear all',
31
+ children,
32
+ class: className,
33
+ ref = $bindable(null),
34
+ ...rest
35
+ }: Props = $props();
36
+
37
+ async function remove(value: T, index: number) {
38
+ onRemove(value);
39
+ await tick();
40
+ // Focus stays in the group: the chip that took this one's place, else the one before it,
41
+ // else the group itself — never dropped to <body>.
42
+ const buttons = ref?.querySelectorAll<HTMLButtonElement>('.ldt-chip__remove') ?? [];
43
+ (buttons[index] ?? buttons[index - 1] ?? ref)?.focus();
44
+ }
45
+ </script>
46
+
47
+ {#if items.length || children}
48
+ <div
49
+ bind:this={ref}
50
+ class={cx('ldt-filter-chips', className)}
51
+ role="group"
52
+ aria-label={label}
53
+ tabindex="-1"
54
+ {...rest}
55
+ >
56
+ {#if items.length}
57
+ <ul class="ldt-filter-chips__list">
58
+ {#each items as item, index (item.value)}
59
+ <li class="ldt-chip">
60
+ <span>{item.label}</span>
61
+ <button
62
+ type="button"
63
+ class="ldt-chip__remove"
64
+ aria-label={removeLabel(item.label)}
65
+ disabled={item.disabled}
66
+ onclick={() => remove(item.value, index)}
67
+ ><svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true" focusable="false"
68
+ ><path d="m2 2 6 6M8 2 2 8" stroke="currentColor" stroke-width="1.4" /></svg
69
+ ></button
70
+ >
71
+ </li>
72
+ {/each}
73
+ </ul>
74
+ {/if}
75
+ {#if children}{@render children()}{/if}
76
+ {#if onClearAll && items.length}<Button variant="text" size="sm" onclick={onClearAll}
77
+ >{clearAllLabel}</Button
78
+ >{/if}
79
+ </div>
80
+ {/if}
@@ -0,0 +1,41 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { Option } from '../types.js';
4
+ declare function $$render<T extends string = string>(): {
5
+ props: HTMLAttributes<HTMLDivElement> & {
6
+ /** One chip per filter narrowing the results, labelled the way the user would say it. */
7
+ items: Option<T>[];
8
+ /** Clear one filter. Remove it from `items` in response. */
9
+ onRemove: (value: T) => void;
10
+ /** Clear every filter; shows a trailing "Clear all" when set. */
11
+ onClearAll?: () => void;
12
+ label?: string;
13
+ removeLabel?: (label: string) => string;
14
+ clearAllLabel?: string;
15
+ /** Notes after the chips, e.g. a filter that does not apply to this view. */
16
+ children?: Snippet;
17
+ class?: string;
18
+ ref?: HTMLDivElement | null;
19
+ };
20
+ exports: {};
21
+ bindings: "ref";
22
+ slots: {};
23
+ events: {};
24
+ };
25
+ declare class __sveltets_Render<T extends string = string> {
26
+ props(): ReturnType<typeof $$render<T>>['props'];
27
+ events(): ReturnType<typeof $$render<T>>['events'];
28
+ slots(): ReturnType<typeof $$render<T>>['slots'];
29
+ bindings(): "ref";
30
+ exports(): {};
31
+ }
32
+ interface $$IsomorphicComponent {
33
+ new <T extends string = string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
34
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
35
+ } & ReturnType<__sveltets_Render<T>['exports']>;
36
+ <T extends string = string>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
37
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
38
+ }
39
+ declare const ActiveFilterChips: $$IsomorphicComponent;
40
+ type ActiveFilterChips<T extends string = string> = InstanceType<typeof ActiveFilterChips<T>>;
41
+ export default ActiveFilterChips;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { resolveAspectRatio, type AspectRatioName } from '../media-utils.js';
5
+ import { cx } from '../utils.js';
6
+
7
+ interface Props extends HTMLAttributes<HTMLDivElement> {
8
+ /** A named frame (`video`, `photo`, `square`, `portrait`, `wide`), a number, or `'4 / 3'`. */
9
+ ratio?: AspectRatioName | number | string;
10
+ children?: Snippet;
11
+ class?: string;
12
+ ref?: HTMLDivElement | null;
13
+ }
14
+
15
+ let {
16
+ ratio = 'video',
17
+ children,
18
+ class: className,
19
+ ref = $bindable(null),
20
+ ...rest
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ <!-- Native `aspect-ratio`: the box keeps its shape before anything inside it has loaded. -->
25
+ <div
26
+ bind:this={ref}
27
+ class={cx('ldt-aspect-ratio', className)}
28
+ style:--ldt-aspect-ratio={resolveAspectRatio(ratio)}
29
+ {...rest}
30
+ >
31
+ {#if children}{@render children()}{/if}
32
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type AspectRatioName } from '../media-utils.js';
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
+ /** A named frame (`video`, `photo`, `square`, `portrait`, `wide`), a number, or `'4 / 3'`. */
6
+ ratio?: AspectRatioName | number | string;
7
+ children?: Snippet;
8
+ class?: string;
9
+ ref?: HTMLDivElement | null;
10
+ }
11
+ declare const AspectRatio: import("svelte").Component<Props, {}, "ref">;
12
+ type AspectRatio = ReturnType<typeof AspectRatio>;
13
+ export default AspectRatio;
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { HlsLoader } from '../hls.js';
4
+ import { createHlsSource } from '../hls.svelte.js';
5
+ import { defaultMediaLabels, type MediaPlayerLabels } from '../internal/media/labels.js';
6
+ import { mediaKeydown } from '../internal/media/keys.js';
7
+ import MediaControls from '../internal/media/MediaControls.svelte';
8
+ import { createMediaPlayer } from '../media-player.svelte.js';
9
+ import { isHlsSource, toMediaSources, type MediaSourceEntry } from '../media-utils.js';
10
+ import { cx } from '../utils.js';
11
+
12
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {
13
+ src: string | MediaSourceEntry[];
14
+ /** The track or recording title; also names the player. */
15
+ title: string;
16
+ artist?: string;
17
+ /** Cover art. Decorative: the title already names the recording. */
18
+ artwork?: string;
19
+ loop?: boolean;
20
+ preload?: 'none' | 'metadata' | 'auto';
21
+ hlsLoader?: HlsLoader;
22
+ labels?: Partial<MediaPlayerLabels>;
23
+ onEnded?: () => void;
24
+ onTimeUpdate?: (seconds: number) => void;
25
+ class?: string;
26
+ ref?: HTMLDivElement | null;
27
+ mediaRef?: HTMLAudioElement | null;
28
+ }
29
+
30
+ let {
31
+ src,
32
+ title,
33
+ artist,
34
+ artwork,
35
+ loop = false,
36
+ preload = 'metadata',
37
+ hlsLoader,
38
+ labels,
39
+ onEnded,
40
+ onTimeUpdate,
41
+ class: className,
42
+ ref = $bindable(null),
43
+ mediaRef = $bindable(null),
44
+ ...rest
45
+ }: Props = $props();
46
+
47
+ const text = $derived({ ...defaultMediaLabels, ...labels });
48
+ const sources = $derived(toMediaSources(src));
49
+ const stream = $derived(sources.find((source) => isHlsSource(source)));
50
+ const player = createMediaPlayer({
51
+ onEnded: () => onEnded?.(),
52
+ onTimeUpdate: (seconds) => onTimeUpdate?.(seconds),
53
+ });
54
+ const hls = createHlsSource({
55
+ get loader() {
56
+ return hlsLoader;
57
+ },
58
+ });
59
+ </script>
60
+
61
+ <div
62
+ bind:this={ref}
63
+ class={cx('ldt-media-player', 'ldt-media-player--audio', className)}
64
+ data-playing={player.playing ? '' : undefined}
65
+ role="group"
66
+ aria-label={title}
67
+ onkeydown={mediaKeydown(player, () => null)}
68
+ {...rest}
69
+ >
70
+ {#if artwork}<img class="ldt-media-player__artwork" src={artwork} alt="" />{/if}
71
+ <div class="ldt-media-player__meta">
72
+ <strong class="ldt-media-player__title">{title}</strong>
73
+ {#if artist}<span class="ldt-media-player__artist">{artist}</span>{/if}
74
+ </div>
75
+ <audio
76
+ bind:this={mediaRef}
77
+ {preload}
78
+ {loop}
79
+ {@attach player.attach}
80
+ {@attach stream ? hls.source(stream.src) : undefined}
81
+ >
82
+ {#if !stream}{#each sources as source (source.src)}<source
83
+ src={source.src}
84
+ type={source.type}
85
+ />{/each}{/if}
86
+ </audio>
87
+ {#if player.error || hls.status === 'unsupported' || hls.status === 'error'}<p
88
+ class="ldt-media-player__error"
89
+ role="alert"
90
+ >
91
+ {text.error}
92
+ </p>{/if}
93
+ <MediaControls {player} labels={text} />
94
+ </div>