@loidolt/theme-svelte 0.4.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/AlertDialog.svelte +4 -4
- package/dist/components/AppShell.svelte +9 -2
- package/dist/components/AppShell.svelte.d.ts +3 -1
- 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 +35 -14
- package/dist/components/Dialog.svelte.d.ts +7 -0
- package/dist/components/Drawer.svelte +3 -3
- 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 +10 -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 +31 -2
- package/dist/toaster.svelte.js +30 -2
- 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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Dialog as DialogPrimitive } from 'bits-ui';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import MediaStage from '../internal/media/MediaStage.svelte';
|
|
5
|
+
import { createMediaZoom } from '../media-zoom.svelte.js';
|
|
6
|
+
import { mediaKey, mediaLabel, mediaThumbnail, type MediaItem } from '../media-utils.js';
|
|
7
|
+
import type { TriggerChildProps } from '../types.js';
|
|
8
|
+
import { cx } from '../utils.js';
|
|
9
|
+
import Filmstrip from './Filmstrip.svelte';
|
|
10
|
+
import LiveRegion from './LiveRegion.svelte';
|
|
11
|
+
import Thumbnail from './Thumbnail.svelte';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
items: MediaItem[];
|
|
15
|
+
open?: boolean;
|
|
16
|
+
/** The item showing, 0-based. */
|
|
17
|
+
index?: number;
|
|
18
|
+
/** Accessible name of the dialog. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** Wrap from the last item to the first. */
|
|
21
|
+
loop?: boolean;
|
|
22
|
+
showThumbnails?: boolean;
|
|
23
|
+
showCaption?: boolean;
|
|
24
|
+
showCounter?: boolean;
|
|
25
|
+
/** Let images be zoomed and panned: pinch, Ctrl + wheel, double-click, or + and −. */
|
|
26
|
+
zoomable?: boolean;
|
|
27
|
+
/** Offer a download link for images that set `download`. */
|
|
28
|
+
downloadable?: boolean;
|
|
29
|
+
/** Opens the lightbox. Leave it out to drive `open` yourself, as `MediaGrid` does. */
|
|
30
|
+
trigger?: Snippet;
|
|
31
|
+
triggerChild?: Snippet<[TriggerChildProps]>;
|
|
32
|
+
triggerClass?: string;
|
|
33
|
+
closeLabel?: string;
|
|
34
|
+
previousLabel?: string;
|
|
35
|
+
nextLabel?: string;
|
|
36
|
+
zoomInLabel?: string;
|
|
37
|
+
zoomOutLabel?: string;
|
|
38
|
+
downloadLabel?: string;
|
|
39
|
+
thumbnailsLabel?: string;
|
|
40
|
+
counterText?: (position: number, count: number) => string;
|
|
41
|
+
onOpenChange?: (open: boolean) => void;
|
|
42
|
+
onIndexChange?: (index: number) => void;
|
|
43
|
+
/** Lands on the dialog content. */
|
|
44
|
+
class?: string;
|
|
45
|
+
ref?: HTMLElement | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let {
|
|
49
|
+
items,
|
|
50
|
+
open = $bindable(false),
|
|
51
|
+
index = $bindable(0),
|
|
52
|
+
title = 'Media viewer',
|
|
53
|
+
loop = true,
|
|
54
|
+
showThumbnails = false,
|
|
55
|
+
showCaption = true,
|
|
56
|
+
showCounter = true,
|
|
57
|
+
zoomable = true,
|
|
58
|
+
downloadable = false,
|
|
59
|
+
trigger,
|
|
60
|
+
triggerChild,
|
|
61
|
+
triggerClass = 'ldt-button',
|
|
62
|
+
closeLabel = 'Close viewer',
|
|
63
|
+
previousLabel = 'Previous',
|
|
64
|
+
nextLabel = 'Next',
|
|
65
|
+
zoomInLabel = 'Zoom in',
|
|
66
|
+
zoomOutLabel = 'Zoom out',
|
|
67
|
+
downloadLabel = 'Download',
|
|
68
|
+
thumbnailsLabel = 'All items',
|
|
69
|
+
counterText = (position, count) => `${position} of ${count}`,
|
|
70
|
+
onOpenChange,
|
|
71
|
+
onIndexChange,
|
|
72
|
+
class: className,
|
|
73
|
+
ref = $bindable(null),
|
|
74
|
+
}: Props = $props();
|
|
75
|
+
|
|
76
|
+
const zoom = createMediaZoom();
|
|
77
|
+
const count = $derived(items.length);
|
|
78
|
+
const current = $derived(items[Math.min(Math.max(0, index), Math.max(0, count - 1))]);
|
|
79
|
+
const ids = $derived(items.map((item, position) => mediaKey(item, position)));
|
|
80
|
+
const canPrevious = $derived(loop ? count > 1 : index > 0);
|
|
81
|
+
const canNext = $derived(loop ? count > 1 : index < count - 1);
|
|
82
|
+
const isImage = $derived(current?.kind === 'image');
|
|
83
|
+
|
|
84
|
+
function go(next: number) {
|
|
85
|
+
if (count === 0) return;
|
|
86
|
+
const target = loop ? (next + count) % count : Math.min(Math.max(0, next), count - 1);
|
|
87
|
+
if (target === index) return;
|
|
88
|
+
zoom.reset();
|
|
89
|
+
index = target;
|
|
90
|
+
onIndexChange?.(target);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
94
|
+
// Keys aimed at a control inside the stage (a player's range, a select) stay with it.
|
|
95
|
+
if ((event.target as HTMLElement).matches('input, select, textarea')) return;
|
|
96
|
+
const handled = (() => {
|
|
97
|
+
if (event.key === 'ArrowRight') go(index + 1);
|
|
98
|
+
else if (event.key === 'ArrowLeft') go(index - 1);
|
|
99
|
+
else if (event.key === 'Home') go(0);
|
|
100
|
+
else if (event.key === 'End') go(count - 1);
|
|
101
|
+
else if (zoomable && isImage && (event.key === '+' || event.key === '=')) zoom.zoomIn();
|
|
102
|
+
else if (zoomable && isImage && event.key === '-') zoom.zoomOut();
|
|
103
|
+
else if (zoomable && isImage && event.key === '0') zoom.reset();
|
|
104
|
+
else return false;
|
|
105
|
+
return true;
|
|
106
|
+
})();
|
|
107
|
+
if (handled) event.preventDefault();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Swipe to navigate when the image is not zoomed (a zoomed image pans instead).
|
|
111
|
+
let swipeStart: number | null = null;
|
|
112
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
113
|
+
swipeStart = zoom.zoomed ? null : event.clientX;
|
|
114
|
+
};
|
|
115
|
+
const onPointerUp = (event: PointerEvent) => {
|
|
116
|
+
if (swipeStart === null) return;
|
|
117
|
+
const delta = event.clientX - swipeStart;
|
|
118
|
+
swipeStart = null;
|
|
119
|
+
if (Math.abs(delta) > 50) go(delta < 0 ? index + 1 : index - 1);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Warm the neighbours, so moving on shows the next image at once.
|
|
123
|
+
$effect(() => {
|
|
124
|
+
if (!open || count < 2) return;
|
|
125
|
+
for (const offset of [-1, 1]) {
|
|
126
|
+
const neighbour = items[(index + offset + count) % count];
|
|
127
|
+
if (neighbour?.kind === 'image') new Image().src = neighbour.src;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
function handleOpenChange(next: boolean) {
|
|
132
|
+
if (!next) zoom.reset();
|
|
133
|
+
onOpenChange?.(next);
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<DialogPrimitive.Root bind:open onOpenChange={handleOpenChange}>
|
|
138
|
+
{#if triggerChild}<DialogPrimitive.Trigger
|
|
139
|
+
>{#snippet child(childProps)}{@render triggerChild(
|
|
140
|
+
childProps
|
|
141
|
+
)}{/snippet}</DialogPrimitive.Trigger
|
|
142
|
+
>{:else if trigger}<DialogPrimitive.Trigger class={triggerClass}
|
|
143
|
+
>{@render trigger()}</DialogPrimitive.Trigger
|
|
144
|
+
>{/if}
|
|
145
|
+
<DialogPrimitive.Portal>
|
|
146
|
+
<DialogPrimitive.Overlay class="ldt-overlay ldt-lightbox__overlay" />
|
|
147
|
+
<DialogPrimitive.Content
|
|
148
|
+
bind:ref
|
|
149
|
+
class={cx('ldt-lightbox', className)}
|
|
150
|
+
onkeydown={handleKeydown}
|
|
151
|
+
>
|
|
152
|
+
<DialogPrimitive.Title class="ldt-sr-only">{title}</DialogPrimitive.Title>
|
|
153
|
+
<div class="ldt-lightbox__bar">
|
|
154
|
+
{#if showCounter && count}<span class="ldt-lightbox__counter" aria-hidden="true"
|
|
155
|
+
>{counterText(index + 1, count)}</span
|
|
156
|
+
>{/if}
|
|
157
|
+
<div class="ldt-lightbox__tools">
|
|
158
|
+
{#if zoomable && isImage}
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
class="ldt-lightbox__control"
|
|
162
|
+
aria-label={zoomOutLabel}
|
|
163
|
+
disabled={zoom.scale <= 1}
|
|
164
|
+
onclick={zoom.zoomOut}>−</button
|
|
165
|
+
>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class="ldt-lightbox__control"
|
|
169
|
+
aria-label={zoomInLabel}
|
|
170
|
+
onclick={zoom.zoomIn}>+</button
|
|
171
|
+
>
|
|
172
|
+
{/if}
|
|
173
|
+
{#if downloadable && current?.kind === 'image' && current.download}<a
|
|
174
|
+
class="ldt-lightbox__control ldt-lightbox__download"
|
|
175
|
+
href={current.download}
|
|
176
|
+
download>{downloadLabel}</a
|
|
177
|
+
>{/if}
|
|
178
|
+
<DialogPrimitive.Close class="ldt-lightbox__control" aria-label={closeLabel}
|
|
179
|
+
>×</DialogPrimitive.Close
|
|
180
|
+
>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
184
|
+
<div
|
|
185
|
+
class="ldt-lightbox__stage"
|
|
186
|
+
data-zoomed={zoom.zoomed ? '' : undefined}
|
|
187
|
+
onpointerdown={onPointerDown}
|
|
188
|
+
onpointerup={onPointerUp}
|
|
189
|
+
>
|
|
190
|
+
{#if current}
|
|
191
|
+
{#key ids[index]}<MediaStage item={current} zoom={zoomable ? zoom : undefined} />{/key}
|
|
192
|
+
{/if}
|
|
193
|
+
{#if canPrevious}<button
|
|
194
|
+
type="button"
|
|
195
|
+
class="ldt-lightbox__control ldt-lightbox__nav ldt-lightbox__nav--previous"
|
|
196
|
+
aria-label={previousLabel}
|
|
197
|
+
onclick={() => go(index - 1)}
|
|
198
|
+
><span aria-hidden="true" class="ldt-lightbox__chevron"></span></button
|
|
199
|
+
>{/if}
|
|
200
|
+
{#if canNext}<button
|
|
201
|
+
type="button"
|
|
202
|
+
class="ldt-lightbox__control ldt-lightbox__nav ldt-lightbox__nav--next"
|
|
203
|
+
aria-label={nextLabel}
|
|
204
|
+
onclick={() => go(index + 1)}
|
|
205
|
+
><span aria-hidden="true" class="ldt-lightbox__chevron"></span></button
|
|
206
|
+
>{/if}
|
|
207
|
+
</div>
|
|
208
|
+
{#if showCaption && current?.caption}<p class="ldt-lightbox__caption">
|
|
209
|
+
{current.caption}
|
|
210
|
+
</p>{/if}
|
|
211
|
+
{#if showThumbnails && count > 1}
|
|
212
|
+
<Filmstrip
|
|
213
|
+
class="ldt-lightbox__strip"
|
|
214
|
+
orientation="horizontal"
|
|
215
|
+
label={thumbnailsLabel}
|
|
216
|
+
items={ids.map((id) => ({ id }))}
|
|
217
|
+
selectedId={ids[index]}
|
|
218
|
+
onSelect={(id) => go(ids.indexOf(id))}
|
|
219
|
+
>
|
|
220
|
+
{#snippet item(entry)}
|
|
221
|
+
{@const position = ids.indexOf(entry.id)}
|
|
222
|
+
<Thumbnail
|
|
223
|
+
src={mediaThumbnail(items[position])}
|
|
224
|
+
alt=""
|
|
225
|
+
ratio="square"
|
|
226
|
+
fit="cover"
|
|
227
|
+
/><span class="ldt-sr-only">{mediaLabel(items[position], position)}</span>
|
|
228
|
+
{/snippet}
|
|
229
|
+
</Filmstrip>
|
|
230
|
+
{/if}
|
|
231
|
+
<!-- The visible counter is hidden from assistive tech; this announces each move once. -->
|
|
232
|
+
<LiveRegion
|
|
233
|
+
message={count && current
|
|
234
|
+
? `${mediaLabel(current, index)}, ${counterText(index + 1, count)}`
|
|
235
|
+
: ''}
|
|
236
|
+
/>
|
|
237
|
+
</DialogPrimitive.Content>
|
|
238
|
+
</DialogPrimitive.Portal>
|
|
239
|
+
</DialogPrimitive.Root>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type MediaItem } from '../media-utils.js';
|
|
3
|
+
import type { TriggerChildProps } from '../types.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
items: MediaItem[];
|
|
6
|
+
open?: boolean;
|
|
7
|
+
/** The item showing, 0-based. */
|
|
8
|
+
index?: number;
|
|
9
|
+
/** Accessible name of the dialog. */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Wrap from the last item to the first. */
|
|
12
|
+
loop?: boolean;
|
|
13
|
+
showThumbnails?: boolean;
|
|
14
|
+
showCaption?: boolean;
|
|
15
|
+
showCounter?: boolean;
|
|
16
|
+
/** Let images be zoomed and panned: pinch, Ctrl + wheel, double-click, or + and −. */
|
|
17
|
+
zoomable?: boolean;
|
|
18
|
+
/** Offer a download link for images that set `download`. */
|
|
19
|
+
downloadable?: boolean;
|
|
20
|
+
/** Opens the lightbox. Leave it out to drive `open` yourself, as `MediaGrid` does. */
|
|
21
|
+
trigger?: Snippet;
|
|
22
|
+
triggerChild?: Snippet<[TriggerChildProps]>;
|
|
23
|
+
triggerClass?: string;
|
|
24
|
+
closeLabel?: string;
|
|
25
|
+
previousLabel?: string;
|
|
26
|
+
nextLabel?: string;
|
|
27
|
+
zoomInLabel?: string;
|
|
28
|
+
zoomOutLabel?: string;
|
|
29
|
+
downloadLabel?: string;
|
|
30
|
+
thumbnailsLabel?: string;
|
|
31
|
+
counterText?: (position: number, count: number) => string;
|
|
32
|
+
onOpenChange?: (open: boolean) => void;
|
|
33
|
+
onIndexChange?: (index: number) => void;
|
|
34
|
+
/** Lands on the dialog content. */
|
|
35
|
+
class?: string;
|
|
36
|
+
ref?: HTMLElement | null;
|
|
37
|
+
}
|
|
38
|
+
declare const Lightbox: import("svelte").Component<Props, {}, "ref" | "open" | "index">;
|
|
39
|
+
type Lightbox = ReturnType<typeof Lightbox>;
|
|
40
|
+
export default Lightbox;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import type { Politeness } from '../announcer.svelte.js';
|
|
5
|
+
import { cx } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
/** Text to announce. Changing it is what triggers speech. */
|
|
9
|
+
message?: string;
|
|
10
|
+
/** `'polite'` waits for a pause (`role="status"`); `'assertive'` interrupts (`role="alert"`). */
|
|
11
|
+
politeness?: Politeness;
|
|
12
|
+
/** Read the whole region on every change, not only the changed part. */
|
|
13
|
+
atomic?: boolean;
|
|
14
|
+
/** Keep the region off-screen. Set `false` to also show the message visually. */
|
|
15
|
+
visuallyHidden?: boolean;
|
|
16
|
+
/** Rendered after `message`, for richer content. */
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
class?: string;
|
|
19
|
+
ref?: HTMLDivElement | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
message = '',
|
|
24
|
+
politeness = 'polite',
|
|
25
|
+
atomic = true,
|
|
26
|
+
visuallyHidden = true,
|
|
27
|
+
children,
|
|
28
|
+
class: className,
|
|
29
|
+
ref = $bindable(null),
|
|
30
|
+
...rest
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<!--
|
|
35
|
+
Must already be in the document before its text changes: a region mounted together with its
|
|
36
|
+
message is silent in most screen readers. Render it once, unconditionally, and change the
|
|
37
|
+
message — `createAnnouncer()` does that bookkeeping.
|
|
38
|
+
-->
|
|
39
|
+
<div
|
|
40
|
+
bind:this={ref}
|
|
41
|
+
class={cx('ldt-live-region', visuallyHidden && 'ldt-sr-only', className)}
|
|
42
|
+
role={politeness === 'assertive' ? 'alert' : 'status'}
|
|
43
|
+
aria-live={politeness}
|
|
44
|
+
aria-atomic={atomic}
|
|
45
|
+
{...rest}
|
|
46
|
+
>
|
|
47
|
+
{message}{#if children}{@render children()}{/if}
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { Politeness } from '../announcer.svelte.js';
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Text to announce. Changing it is what triggers speech. */
|
|
6
|
+
message?: string;
|
|
7
|
+
/** `'polite'` waits for a pause (`role="status"`); `'assertive'` interrupts (`role="alert"`). */
|
|
8
|
+
politeness?: Politeness;
|
|
9
|
+
/** Read the whole region on every change, not only the changed part. */
|
|
10
|
+
atomic?: boolean;
|
|
11
|
+
/** Keep the region off-screen. Set `false` to also show the message visually. */
|
|
12
|
+
visuallyHidden?: boolean;
|
|
13
|
+
/** Rendered after `message`, for richer content. */
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
class?: string;
|
|
16
|
+
ref?: HTMLDivElement | null;
|
|
17
|
+
}
|
|
18
|
+
declare const LiveRegion: import("svelte").Component<Props, {}, "ref">;
|
|
19
|
+
type LiveRegion = ReturnType<typeof LiveRegion>;
|
|
20
|
+
export default LiveRegion;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import MediaStage from '../internal/media/MediaStage.svelte';
|
|
4
|
+
import { createMediaQuery } from '../media.svelte.js';
|
|
5
|
+
import {
|
|
6
|
+
mediaKey,
|
|
7
|
+
mediaLabel,
|
|
8
|
+
mediaThumbnail,
|
|
9
|
+
resolveAspectRatio,
|
|
10
|
+
type AspectRatioName,
|
|
11
|
+
type MediaItem,
|
|
12
|
+
} from '../media-utils.js';
|
|
13
|
+
import { cx } from '../utils.js';
|
|
14
|
+
import Filmstrip from './Filmstrip.svelte';
|
|
15
|
+
import Thumbnail from './Thumbnail.svelte';
|
|
16
|
+
|
|
17
|
+
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
18
|
+
items: MediaItem[];
|
|
19
|
+
/** Accessible name of the carousel. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** The slide showing, 0-based. */
|
|
22
|
+
index?: number;
|
|
23
|
+
loop?: boolean;
|
|
24
|
+
ratio?: AspectRatioName | number | string;
|
|
25
|
+
showThumbnails?: boolean;
|
|
26
|
+
showCaption?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Advance on a timer. It pauses while the pointer or focus is inside, offers a visible
|
|
29
|
+
* pause button (WCAG 2.2.2), and starts paused for people who prefer reduced motion.
|
|
30
|
+
*/
|
|
31
|
+
autoplay?: boolean;
|
|
32
|
+
/** Milliseconds per slide when autoplaying. */
|
|
33
|
+
interval?: number;
|
|
34
|
+
previousLabel?: string;
|
|
35
|
+
nextLabel?: string;
|
|
36
|
+
pauseLabel?: string;
|
|
37
|
+
playLabel?: string;
|
|
38
|
+
thumbnailsLabel?: string;
|
|
39
|
+
slideLabel?: (position: number, count: number) => string;
|
|
40
|
+
onIndexChange?: (index: number) => void;
|
|
41
|
+
class?: string;
|
|
42
|
+
ref?: HTMLElement | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
items,
|
|
47
|
+
label,
|
|
48
|
+
index = $bindable(0),
|
|
49
|
+
loop = true,
|
|
50
|
+
ratio = 'video',
|
|
51
|
+
showThumbnails = false,
|
|
52
|
+
showCaption = true,
|
|
53
|
+
autoplay = false,
|
|
54
|
+
interval = 5000,
|
|
55
|
+
previousLabel = 'Previous slide',
|
|
56
|
+
nextLabel = 'Next slide',
|
|
57
|
+
pauseLabel = 'Pause slideshow',
|
|
58
|
+
playLabel = 'Start slideshow',
|
|
59
|
+
thumbnailsLabel = 'Slides',
|
|
60
|
+
slideLabel = (position, count) => `${position} of ${count}`,
|
|
61
|
+
onIndexChange,
|
|
62
|
+
class: className,
|
|
63
|
+
ref = $bindable(null),
|
|
64
|
+
...rest
|
|
65
|
+
}: Props = $props();
|
|
66
|
+
|
|
67
|
+
const reducedMotion = createMediaQuery('(prefers-reduced-motion: reduce)');
|
|
68
|
+
const count = $derived(items.length);
|
|
69
|
+
const ids = $derived(items.map((item, position) => mediaKey(item, position)));
|
|
70
|
+
const canPrevious = $derived(loop ? count > 1 : index > 0);
|
|
71
|
+
const canNext = $derived(loop ? count > 1 : index < count - 1);
|
|
72
|
+
|
|
73
|
+
// `rotating` is the user's choice; `held` covers hover and focus, which pause without changing it.
|
|
74
|
+
let rotating = $derived(autoplay && !reducedMotion.matches);
|
|
75
|
+
let hovered = $state(false);
|
|
76
|
+
let focused = $state(false);
|
|
77
|
+
const running = $derived(rotating && !hovered && !focused && count > 1);
|
|
78
|
+
|
|
79
|
+
function go(next: number) {
|
|
80
|
+
if (count === 0) return;
|
|
81
|
+
const target = loop ? (next + count) % count : Math.min(Math.max(0, next), count - 1);
|
|
82
|
+
if (target === index) return;
|
|
83
|
+
index = target;
|
|
84
|
+
onIndexChange?.(target);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$effect(() => {
|
|
88
|
+
if (!running) return;
|
|
89
|
+
const timer = setInterval(() => go(index + 1), interval);
|
|
90
|
+
return () => clearInterval(timer);
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<section
|
|
95
|
+
bind:this={ref}
|
|
96
|
+
class={cx('ldt-media-carousel', className)}
|
|
97
|
+
aria-roledescription="carousel"
|
|
98
|
+
aria-label={label}
|
|
99
|
+
onpointerenter={() => (hovered = true)}
|
|
100
|
+
onpointerleave={() => (hovered = false)}
|
|
101
|
+
onfocusin={() => (focused = true)}
|
|
102
|
+
onfocusout={(event) => {
|
|
103
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) focused = false;
|
|
104
|
+
}}
|
|
105
|
+
{...rest}
|
|
106
|
+
>
|
|
107
|
+
<div class="ldt-media-carousel__controls">
|
|
108
|
+
{#if autoplay}<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="ldt-media-carousel__control"
|
|
111
|
+
aria-label={rotating ? pauseLabel : playLabel}
|
|
112
|
+
onclick={() => (rotating = !rotating)}
|
|
113
|
+
><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" focusable="false"
|
|
114
|
+
>{#if rotating}<path d="M4 3h3v10H4zM9 3h3v10H9z" fill="currentColor" />{:else}<path
|
|
115
|
+
d="M4 2.5v11l9-5.5z"
|
|
116
|
+
fill="currentColor"
|
|
117
|
+
/>{/if}</svg
|
|
118
|
+
></button
|
|
119
|
+
>{/if}
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
class="ldt-media-carousel__control"
|
|
123
|
+
aria-label={previousLabel}
|
|
124
|
+
disabled={!canPrevious}
|
|
125
|
+
onclick={() => go(index - 1)}
|
|
126
|
+
><span class="ldt-media-carousel__chevron" aria-hidden="true"></span></button
|
|
127
|
+
>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
class="ldt-media-carousel__control ldt-media-carousel__control--next"
|
|
131
|
+
aria-label={nextLabel}
|
|
132
|
+
disabled={!canNext}
|
|
133
|
+
onclick={() => go(index + 1)}
|
|
134
|
+
><span class="ldt-media-carousel__chevron" aria-hidden="true"></span></button
|
|
135
|
+
>
|
|
136
|
+
</div>
|
|
137
|
+
<!--
|
|
138
|
+
Polite while the user drives it; silent while it rotates on its own, so a screen reader is
|
|
139
|
+
not interrupted every few seconds.
|
|
140
|
+
-->
|
|
141
|
+
<div
|
|
142
|
+
class="ldt-media-carousel__viewport"
|
|
143
|
+
style:--ldt-aspect-ratio={resolveAspectRatio(ratio)}
|
|
144
|
+
aria-live={running ? 'off' : 'polite'}
|
|
145
|
+
>
|
|
146
|
+
{#each items as item, position (ids[position])}
|
|
147
|
+
<div
|
|
148
|
+
class="ldt-media-carousel__slide"
|
|
149
|
+
role="group"
|
|
150
|
+
aria-roledescription="slide"
|
|
151
|
+
aria-label={`${slideLabel(position + 1, count)}: ${mediaLabel(item, position)}`}
|
|
152
|
+
hidden={position !== index}
|
|
153
|
+
>
|
|
154
|
+
{#if position === index}<MediaStage {item} fit="cover" />{/if}
|
|
155
|
+
{#if showCaption && item.caption}<p class="ldt-media-carousel__caption">
|
|
156
|
+
{item.caption}
|
|
157
|
+
</p>{/if}
|
|
158
|
+
</div>
|
|
159
|
+
{/each}
|
|
160
|
+
</div>
|
|
161
|
+
{#if showThumbnails && count > 1}
|
|
162
|
+
<Filmstrip
|
|
163
|
+
orientation="horizontal"
|
|
164
|
+
label={thumbnailsLabel}
|
|
165
|
+
items={ids.map((id) => ({ id }))}
|
|
166
|
+
selectedId={ids[index]}
|
|
167
|
+
onSelect={(id) => go(ids.indexOf(id))}
|
|
168
|
+
>
|
|
169
|
+
{#snippet item(entry)}
|
|
170
|
+
{@const position = ids.indexOf(entry.id)}
|
|
171
|
+
<Thumbnail src={mediaThumbnail(items[position])} alt="" ratio="square" fit="cover" /><span
|
|
172
|
+
class="ldt-sr-only">{mediaLabel(items[position], position)}</span
|
|
173
|
+
>
|
|
174
|
+
{/snippet}
|
|
175
|
+
</Filmstrip>
|
|
176
|
+
{/if}
|
|
177
|
+
</section>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import { type AspectRatioName, type MediaItem } from '../media-utils.js';
|
|
3
|
+
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
4
|
+
items: MediaItem[];
|
|
5
|
+
/** Accessible name of the carousel. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** The slide showing, 0-based. */
|
|
8
|
+
index?: number;
|
|
9
|
+
loop?: boolean;
|
|
10
|
+
ratio?: AspectRatioName | number | string;
|
|
11
|
+
showThumbnails?: boolean;
|
|
12
|
+
showCaption?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Advance on a timer. It pauses while the pointer or focus is inside, offers a visible
|
|
15
|
+
* pause button (WCAG 2.2.2), and starts paused for people who prefer reduced motion.
|
|
16
|
+
*/
|
|
17
|
+
autoplay?: boolean;
|
|
18
|
+
/** Milliseconds per slide when autoplaying. */
|
|
19
|
+
interval?: number;
|
|
20
|
+
previousLabel?: string;
|
|
21
|
+
nextLabel?: string;
|
|
22
|
+
pauseLabel?: string;
|
|
23
|
+
playLabel?: string;
|
|
24
|
+
thumbnailsLabel?: string;
|
|
25
|
+
slideLabel?: (position: number, count: number) => string;
|
|
26
|
+
onIndexChange?: (index: number) => void;
|
|
27
|
+
class?: string;
|
|
28
|
+
ref?: HTMLElement | null;
|
|
29
|
+
}
|
|
30
|
+
declare const MediaCarousel: import("svelte").Component<Props, {}, "ref" | "index">;
|
|
31
|
+
type MediaCarousel = ReturnType<typeof MediaCarousel>;
|
|
32
|
+
export default MediaCarousel;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import {
|
|
4
|
+
buildEmbedSrc,
|
|
5
|
+
parseEmbedUrl,
|
|
6
|
+
resolveAspectRatio,
|
|
7
|
+
type AspectRatioName,
|
|
8
|
+
type EmbedProvider,
|
|
9
|
+
} from '../media-utils.js';
|
|
10
|
+
import { cx } from '../utils.js';
|
|
11
|
+
|
|
12
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {
|
|
13
|
+
/** A YouTube or Vimeo page, share or embed URL. */
|
|
14
|
+
url?: string;
|
|
15
|
+
/** Or name the video directly. */
|
|
16
|
+
provider?: EmbedProvider;
|
|
17
|
+
videoId?: string;
|
|
18
|
+
/** Required: names the iframe and the play button. */
|
|
19
|
+
title: string;
|
|
20
|
+
/**
|
|
21
|
+
* Still shown before the player loads. None is fetched from the provider by default — that
|
|
22
|
+
* request alone would tell them someone opened the page.
|
|
23
|
+
*/
|
|
24
|
+
poster?: string;
|
|
25
|
+
/** No-tracking hosts: `youtube-nocookie.com`, Vimeo's `dnt=1`. */
|
|
26
|
+
privacy?: boolean;
|
|
27
|
+
/** Start offset in seconds. Read from the URL when it carries one. */
|
|
28
|
+
start?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Load the provider's player straight away. By default a local facade stands in until the
|
|
31
|
+
* user presses play, so no third-party request happens before they choose to watch.
|
|
32
|
+
*/
|
|
33
|
+
eager?: boolean;
|
|
34
|
+
ratio?: AspectRatioName | number | string;
|
|
35
|
+
playLabel?: (title: string) => string;
|
|
36
|
+
/** Shown when the URL is not a recognised YouTube or Vimeo link. */
|
|
37
|
+
unsupportedText?: string;
|
|
38
|
+
class?: string;
|
|
39
|
+
ref?: HTMLDivElement | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let {
|
|
43
|
+
url,
|
|
44
|
+
provider,
|
|
45
|
+
videoId,
|
|
46
|
+
title,
|
|
47
|
+
poster,
|
|
48
|
+
privacy = true,
|
|
49
|
+
start,
|
|
50
|
+
eager = false,
|
|
51
|
+
ratio = 'video',
|
|
52
|
+
playLabel = (name) => `Play ${name}`,
|
|
53
|
+
unsupportedText = 'This video link is not supported.',
|
|
54
|
+
class: className,
|
|
55
|
+
ref = $bindable(null),
|
|
56
|
+
...rest
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
|
|
59
|
+
const parsed = $derived(
|
|
60
|
+
provider && videoId ? { provider, videoId, start: undefined } : url ? parseEmbedUrl(url) : null
|
|
61
|
+
);
|
|
62
|
+
let activated = $state(false);
|
|
63
|
+
const loaded = $derived(eager || activated);
|
|
64
|
+
const src = $derived(
|
|
65
|
+
parsed &&
|
|
66
|
+
buildEmbedSrc(parsed.provider, parsed.videoId, {
|
|
67
|
+
privacy,
|
|
68
|
+
autoplay: activated,
|
|
69
|
+
start: start ?? parsed.start,
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
let frame = $state<HTMLIFrameElement | null>(null);
|
|
73
|
+
|
|
74
|
+
function activate() {
|
|
75
|
+
activated = true;
|
|
76
|
+
// Keep keyboard users where the button was: on the player that replaced it.
|
|
77
|
+
queueMicrotask(() => frame?.focus());
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<div
|
|
82
|
+
bind:this={ref}
|
|
83
|
+
class={cx('ldt-media-embed', className)}
|
|
84
|
+
style:--ldt-aspect-ratio={resolveAspectRatio(ratio)}
|
|
85
|
+
{...rest}
|
|
86
|
+
>
|
|
87
|
+
{#if !parsed}
|
|
88
|
+
<p class="ldt-media-embed__unsupported">{unsupportedText}</p>
|
|
89
|
+
{:else if loaded}
|
|
90
|
+
<iframe
|
|
91
|
+
bind:this={frame}
|
|
92
|
+
class="ldt-media-embed__frame"
|
|
93
|
+
{src}
|
|
94
|
+
{title}
|
|
95
|
+
loading="lazy"
|
|
96
|
+
allow="autoplay; encrypted-media; fullscreen; picture-in-picture; clipboard-write"
|
|
97
|
+
allowfullscreen
|
|
98
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
99
|
+
></iframe>
|
|
100
|
+
{:else}
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="ldt-media-embed__facade"
|
|
104
|
+
style:background-image={poster ? `url("${poster}")` : undefined}
|
|
105
|
+
aria-label={playLabel(title)}
|
|
106
|
+
onclick={activate}
|
|
107
|
+
>
|
|
108
|
+
<span class="ldt-media-embed__play" aria-hidden="true">
|
|
109
|
+
<svg viewBox="0 0 16 16" width="22" height="22" focusable="false"
|
|
110
|
+
><path d="M4 2.5v11l9-5.5z" fill="currentColor" /></svg
|
|
111
|
+
>
|
|
112
|
+
</span>
|
|
113
|
+
<span class="ldt-media-embed__title" aria-hidden="true">{title}</span>
|
|
114
|
+
</button>
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|