@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.
- package/README.md +11 -0
- package/dist/announcer.svelte.d.ts +36 -0
- package/dist/announcer.svelte.js +73 -0
- package/dist/attachments.d.ts +83 -0
- package/dist/attachments.js +211 -0
- package/dist/components/ActiveFilterChips.svelte +80 -0
- package/dist/components/ActiveFilterChips.svelte.d.ts +41 -0
- package/dist/components/AspectRatio.svelte +32 -0
- package/dist/components/AspectRatio.svelte.d.ts +13 -0
- package/dist/components/AudioPlayer.svelte +94 -0
- package/dist/components/AudioPlayer.svelte.d.ts +24 -0
- package/dist/components/Avatar.svelte +85 -0
- package/dist/components/Avatar.svelte.d.ts +31 -0
- package/dist/components/Badge.svelte +28 -2
- package/dist/components/Badge.svelte.d.ts +13 -1
- package/dist/components/Combobox.svelte +180 -0
- package/dist/components/Combobox.svelte.d.ts +77 -0
- package/dist/components/DataTable.svelte +228 -0
- package/dist/components/DataTable.svelte.d.ts +62 -0
- package/dist/components/DataTableColumnVisibility.svelte +46 -0
- package/dist/components/DataTableColumnVisibility.svelte.d.ts +31 -0
- package/dist/components/DataTableFacetedFilter.svelte +87 -0
- package/dist/components/DataTableFacetedFilter.svelte.d.ts +40 -0
- package/dist/components/DataTablePagination.svelte +68 -0
- package/dist/components/DataTablePagination.svelte.d.ts +42 -0
- package/dist/components/DataTableSearch.svelte +67 -0
- package/dist/components/DataTableSearch.svelte.d.ts +37 -0
- package/dist/components/Dialog.svelte +32 -11
- package/dist/components/Dialog.svelte.d.ts +7 -0
- package/dist/components/DropdownMenu.svelte +95 -28
- package/dist/components/DropdownMenu.svelte.d.ts +12 -3
- package/dist/components/Field.svelte +2 -3
- package/dist/components/Fieldset.svelte +2 -3
- package/dist/components/Filmstrip.svelte +10 -3
- package/dist/components/FilterPanel.svelte +62 -0
- package/dist/components/FilterPanel.svelte.d.ts +22 -0
- package/dist/components/Lightbox.svelte +239 -0
- package/dist/components/Lightbox.svelte.d.ts +40 -0
- package/dist/components/LiveRegion.svelte +48 -0
- package/dist/components/LiveRegion.svelte.d.ts +20 -0
- package/dist/components/MediaCarousel.svelte +177 -0
- package/dist/components/MediaCarousel.svelte.d.ts +32 -0
- package/dist/components/MediaEmbed.svelte +116 -0
- package/dist/components/MediaEmbed.svelte.d.ts +34 -0
- package/dist/components/MediaGrid.svelte +91 -0
- package/dist/components/MediaGrid.svelte.d.ts +24 -0
- package/dist/components/MultiSelect.svelte +232 -0
- package/dist/components/MultiSelect.svelte.d.ts +67 -0
- package/dist/components/NumberField.svelte +53 -25
- package/dist/components/NumberField.svelte.d.ts +47 -18
- package/dist/components/OTPInput.svelte +94 -0
- package/dist/components/OTPInput.svelte.d.ts +26 -0
- package/dist/components/PasswordInput.svelte +93 -0
- package/dist/components/PasswordInput.svelte.d.ts +24 -0
- package/dist/components/Progress.svelte +57 -14
- package/dist/components/Progress.svelte.d.ts +13 -0
- package/dist/components/RadioGroup.svelte +19 -5
- package/dist/components/RadioGroup.svelte.d.ts +3 -2
- package/dist/components/Select.svelte +15 -5
- package/dist/components/Select.svelte.d.ts +3 -2
- package/dist/components/SignaturePad.svelte +317 -0
- package/dist/components/SignaturePad.svelte.d.ts +48 -0
- package/dist/components/Skeleton.svelte +40 -9
- package/dist/components/Skeleton.svelte.d.ts +8 -0
- package/dist/components/SkipLink.svelte +48 -0
- package/dist/components/SkipLink.svelte.d.ts +14 -0
- package/dist/components/Slider.svelte +148 -0
- package/dist/components/Slider.svelte.d.ts +55 -0
- package/dist/components/Spinner.svelte +11 -2
- package/dist/components/Spinner.svelte.d.ts +3 -0
- package/dist/components/Tabs.svelte.d.ts +1 -1
- package/dist/components/Thumbnail.svelte +52 -6
- package/dist/components/Thumbnail.svelte.d.ts +14 -2
- package/dist/components/Toast.svelte +24 -0
- package/dist/components/Toast.svelte.d.ts +10 -0
- package/dist/components/ToastViewport.svelte +9 -1
- package/dist/components/ToastViewport.svelte.d.ts +2 -0
- package/dist/components/Toolbar.svelte +49 -0
- package/dist/components/Toolbar.svelte.d.ts +23 -0
- package/dist/components/Tooltip.svelte +9 -1
- package/dist/components/Tooltip.svelte.d.ts +2 -0
- package/dist/components/VideoPlayer.svelte +136 -0
- package/dist/components/VideoPlayer.svelte.d.ts +38 -0
- package/dist/data-table-utils.d.ts +62 -0
- package/dist/data-table-utils.js +91 -0
- package/dist/data-table.svelte.d.ts +132 -0
- package/dist/data-table.svelte.js +292 -0
- package/dist/hls.d.ts +35 -0
- package/dist/hls.js +37 -0
- package/dist/hls.svelte.d.ts +22 -0
- package/dist/hls.svelte.js +66 -0
- package/dist/index.d.ts +47 -4
- package/dist/index.js +35 -2
- package/dist/internal/EditableCell.svelte +58 -0
- package/dist/internal/EditableCell.svelte.d.ts +8 -0
- package/dist/internal/ListboxEntries.svelte +49 -0
- package/dist/internal/ListboxEntries.svelte.d.ts +35 -0
- package/dist/internal/media/MediaControls.svelte +163 -0
- package/dist/internal/media/MediaControls.svelte.d.ts +12 -0
- package/dist/internal/media/MediaStage.svelte +49 -0
- package/dist/internal/media/MediaStage.svelte.d.ts +11 -0
- package/dist/internal/media/MediaTile.svelte +36 -0
- package/dist/internal/media/MediaTile.svelte.d.ts +11 -0
- package/dist/internal/media/keys.d.ts +7 -0
- package/dist/internal/media/keys.js +51 -0
- package/dist/internal/media/labels.d.ts +19 -0
- package/dist/internal/media/labels.js +15 -0
- package/dist/internal/options.d.ts +9 -0
- package/dist/internal/options.js +20 -0
- package/dist/media-player.svelte.d.ts +61 -0
- package/dist/media-player.svelte.js +246 -0
- package/dist/media-utils.d.ts +112 -0
- package/dist/media-utils.js +205 -0
- package/dist/media-zoom.svelte.d.ts +33 -0
- package/dist/media-zoom.svelte.js +126 -0
- package/dist/signature.d.ts +55 -0
- package/dist/signature.js +52 -0
- package/dist/toaster.svelte.d.ts +24 -0
- package/dist/toaster.svelte.js +23 -0
- package/dist/types.d.ts +62 -3
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +11 -0
- 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>
|