@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
@@ -0,0 +1,317 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import type { Attachment } from 'svelte/attachments';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ import {
6
+ strokesToSvg,
7
+ validateSignature,
8
+ type SignaturePoint,
9
+ type SignatureStroke,
10
+ type SignatureValue,
11
+ } from '../signature.js';
12
+ import { cx } from '../utils.js';
13
+ import Button from './Button.svelte';
14
+ import Input from './Input.svelte';
15
+ import RadioGroup from './RadioGroup.svelte';
16
+ import ToggleGroup from './ToggleGroup.svelte';
17
+
18
+ type Mode = 'draw' | 'type';
19
+
20
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
21
+ /** The accepted signature, or `null` while there is none. Bindable. */
22
+ value?: SignatureValue | null;
23
+ onValueChange?: (value: SignatureValue | null) => void;
24
+ /** Names the pad for assistive tech, e.g. "Customer signature". */
25
+ label: string;
26
+ /**
27
+ * Ways to sign. Typing is the keyboard- and switch-accessible alternative to drawing (WCAG
28
+ * 2.1.1, 2.5.1); drop it only if you offer another route to the same outcome.
29
+ */
30
+ modes?: Mode[];
31
+ mode?: Mode;
32
+ strokeWidth?: number;
33
+ /** Ink colour. Defaults to the text colour, so it follows the theme. */
34
+ strokeColor?: string;
35
+ /** Fonts offered in `type` mode. Load them yourself; the default is the system script face. */
36
+ fonts?: Array<{ family: string; label: string }>;
37
+ /** Drawn length, in pixels, below which a signature does not count. */
38
+ minStrokeLength?: number;
39
+ minTypedLength?: number;
40
+ /** Submits the PNG data URL with a form. */
41
+ name?: string;
42
+ required?: boolean;
43
+ disabled?: boolean;
44
+ modeLabel?: string;
45
+ drawLabel?: string;
46
+ typeLabel?: string;
47
+ typePlaceholder?: string;
48
+ fontLabel?: string;
49
+ clearLabel?: string;
50
+ undoLabel?: string;
51
+ /** Guidance under the drawing area. */
52
+ drawHint?: string;
53
+ class?: string;
54
+ ref?: HTMLDivElement | null;
55
+ }
56
+
57
+ const generatedId = $props.id();
58
+
59
+ let {
60
+ value = $bindable(null),
61
+ onValueChange,
62
+ label,
63
+ modes = ['draw', 'type'],
64
+ mode = $bindable(modes[0] ?? 'draw'),
65
+ strokeWidth = 2.5,
66
+ strokeColor,
67
+ fonts = [{ family: 'cursive', label: 'Script' }],
68
+ minStrokeLength = 20,
69
+ minTypedLength = 2,
70
+ name,
71
+ required = false,
72
+ disabled = false,
73
+ modeLabel = 'Signature method',
74
+ drawLabel = 'Draw',
75
+ typeLabel = 'Type',
76
+ typePlaceholder = 'Type your full name',
77
+ fontLabel = 'Style',
78
+ clearLabel = 'Clear',
79
+ undoLabel = 'Undo',
80
+ drawHint = 'Sign with a mouse, pen or finger.',
81
+ class: className,
82
+ ref = $bindable(null),
83
+ ...rest
84
+ }: Props = $props();
85
+
86
+ // Raw, not a deep proxy: points are pushed onto the live stroke while drawing, and the array
87
+ // is reassigned to publish each change.
88
+ let strokes = $state.raw<SignatureStroke[]>([]);
89
+ let typedName = $state('');
90
+ let font = $state(untrack(() => fonts[0]?.family ?? 'cursive'));
91
+ let canvas = $state<HTMLCanvasElement | null>(null);
92
+ let size = { width: 0, height: 0 };
93
+
94
+ const ink = () => strokeColor ?? (canvas ? getComputedStyle(canvas).color : '') ?? '#20231d';
95
+
96
+ function paint() {
97
+ const context = canvas?.getContext('2d');
98
+ if (!canvas || !context) return;
99
+ const ratio = canvas.width / (size.width || 1);
100
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
101
+ context.clearRect(0, 0, size.width, size.height);
102
+ context.strokeStyle = ink();
103
+ context.lineCap = 'round';
104
+ context.lineJoin = 'round';
105
+ for (const stroke of strokes) {
106
+ const { points } = stroke;
107
+ if (!points.length) continue;
108
+ context.lineWidth = stroke.width;
109
+ context.beginPath();
110
+ context.moveTo(points[0].x, points[0].y);
111
+ for (let index = 1; index < points.length - 1; index++) {
112
+ const [point, next] = [points[index], points[index + 1]];
113
+ context.quadraticCurveTo(point.x, point.y, (point.x + next.x) / 2, (point.y + next.y) / 2);
114
+ }
115
+ const last = points[points.length - 1];
116
+ context.lineTo(last.x + (points.length === 1 ? 0.01 : 0), last.y);
117
+ context.stroke();
118
+ }
119
+ }
120
+
121
+ function snapshot(target: HTMLCanvasElement | null): string {
122
+ try {
123
+ return target?.toDataURL('image/png') ?? '';
124
+ } catch {
125
+ return '';
126
+ }
127
+ }
128
+
129
+ function emit(next: SignatureValue | null) {
130
+ value = next;
131
+ onValueChange?.(next);
132
+ }
133
+
134
+ function commitDrawing() {
135
+ const check = validateSignature({ mode: 'draw', strokes }, { minStrokeLength });
136
+ if (!check.valid) return emit(null);
137
+ emit({
138
+ mode: 'draw',
139
+ strokes: strokes.map((stroke) => ({ ...stroke, points: [...stroke.points] })),
140
+ dataUrl: snapshot(canvas),
141
+ svg: strokesToSvg(strokes, { ...size, color: ink() }),
142
+ ...size,
143
+ });
144
+ }
145
+
146
+ function commitTyped() {
147
+ const check = validateSignature({ mode: 'type', typedName }, { minTypedLength });
148
+ if (!check.valid) return emit(null);
149
+ // Set the name in the chosen face on an offscreen canvas, so both modes yield a PNG.
150
+ const width = 480;
151
+ const height = 140;
152
+ let dataUrl = '';
153
+ if (typeof document !== 'undefined') {
154
+ const offscreen = document.createElement('canvas');
155
+ offscreen.width = width * 2;
156
+ offscreen.height = height * 2;
157
+ const context = offscreen.getContext('2d');
158
+ if (context) {
159
+ context.scale(2, 2);
160
+ context.fillStyle = ink();
161
+ context.textBaseline = 'middle';
162
+ context.font = `48px ${font}`;
163
+ context.fillText(typedName.trim(), 16, height / 2, width - 32);
164
+ dataUrl = snapshot(offscreen);
165
+ }
166
+ }
167
+ emit({ mode: 'type', typedName: typedName.trim(), font, dataUrl, width, height });
168
+ }
169
+
170
+ /** Sizes the backing store to the element and device pixels, and wires the pointer. */
171
+ const pad: Attachment<HTMLCanvasElement> = (node) => {
172
+ const resize = () => {
173
+ const box = node.getBoundingClientRect();
174
+ size = { width: box.width, height: box.height };
175
+ const ratio = window.devicePixelRatio || 1;
176
+ node.width = Math.max(1, Math.round(box.width * ratio));
177
+ node.height = Math.max(1, Math.round(box.height * ratio));
178
+ paint();
179
+ };
180
+ // Untracked: painting reads the strokes, and an attachment that depends on them would tear
181
+ // down and re-wire the pointer on every stroke.
182
+ untrack(resize);
183
+ const observer = new ResizeObserver(resize);
184
+ observer.observe(node);
185
+
186
+ let current: SignatureStroke | null = null;
187
+ const point = (event: PointerEvent): SignaturePoint => {
188
+ const box = node.getBoundingClientRect();
189
+ return {
190
+ x: event.clientX - box.left,
191
+ y: event.clientY - box.top,
192
+ pressure: event.pressure || 0.5,
193
+ };
194
+ };
195
+ const down = (event: PointerEvent) => {
196
+ if (disabled || event.button > 0) return;
197
+ event.preventDefault();
198
+ node.setPointerCapture?.(event.pointerId);
199
+ current = { points: [point(event)], width: strokeWidth };
200
+ strokes = [...strokes, current];
201
+ paint();
202
+ };
203
+ const move = (event: PointerEvent) => {
204
+ if (!current) return;
205
+ current.points.push(point(event));
206
+ strokes = [...strokes];
207
+ paint();
208
+ };
209
+ const up = () => {
210
+ if (!current) return;
211
+ current = null;
212
+ commitDrawing();
213
+ };
214
+ node.addEventListener('pointerdown', down);
215
+ node.addEventListener('pointermove', move);
216
+ node.addEventListener('pointerup', up);
217
+ node.addEventListener('pointercancel', up);
218
+ return () => {
219
+ observer.disconnect();
220
+ node.removeEventListener('pointerdown', down);
221
+ node.removeEventListener('pointermove', move);
222
+ node.removeEventListener('pointerup', up);
223
+ node.removeEventListener('pointercancel', up);
224
+ };
225
+ };
226
+
227
+ /** Removes everything drawn or typed. */
228
+ export function clear() {
229
+ strokes = [];
230
+ typedName = '';
231
+ paint();
232
+ emit(null);
233
+ }
234
+
235
+ /** Removes the last stroke. */
236
+ export function undo() {
237
+ strokes = strokes.slice(0, -1);
238
+ paint();
239
+ commitDrawing();
240
+ }
241
+
242
+ function switchMode(next: Mode) {
243
+ mode = next;
244
+ if (next === 'draw') commitDrawing();
245
+ else commitTyped();
246
+ }
247
+
248
+ const hintId = `${generatedId}-hint`;
249
+ const hasInk = $derived(strokes.some((stroke) => stroke.points.length));
250
+ </script>
251
+
252
+ <div
253
+ bind:this={ref}
254
+ class={cx('ldt-signature', className)}
255
+ role="group"
256
+ aria-label={label}
257
+ data-disabled={disabled ? '' : undefined}
258
+ {...rest}
259
+ >
260
+ {#if modes.length > 1}
261
+ <ToggleGroup
262
+ label={modeLabel}
263
+ size="sm"
264
+ value={mode}
265
+ {disabled}
266
+ options={modes.map((one) => ({ value: one, label: one === 'draw' ? drawLabel : typeLabel }))}
267
+ onValueChange={(next) => typeof next === 'string' && next && switchMode(next as Mode)}
268
+ />
269
+ {/if}
270
+
271
+ {#if mode === 'draw'}
272
+ <!-- The picture is named on a wrapper: a canvas cannot take an image role itself. -->
273
+ <div class="ldt-signature__surface" role="img" aria-label={label} aria-describedby={hintId}>
274
+ <canvas bind:this={canvas} class="ldt-signature__canvas" {@attach pad}></canvas>
275
+ </div>
276
+ <p class="ldt-signature__hint" id={hintId}>{drawHint}</p>
277
+ {:else}
278
+ <div class="ldt-signature__typed">
279
+ <Input
280
+ boxed
281
+ aria-label={label}
282
+ placeholder={typePlaceholder}
283
+ autocomplete="name"
284
+ {disabled}
285
+ bind:value={typedName}
286
+ oninput={commitTyped}
287
+ />
288
+ <p class="ldt-signature__preview" style:font-family={font} aria-hidden="true">
289
+ {typedName || ' '}
290
+ </p>
291
+ {#if fonts.length > 1}
292
+ <RadioGroup
293
+ label={fontLabel}
294
+ options={fonts.map((one) => ({ value: one.family, label: one.label }))}
295
+ bind:value={font}
296
+ onValueChange={commitTyped}
297
+ />
298
+ {/if}
299
+ </div>
300
+ {/if}
301
+
302
+ <div class="ldt-signature__actions">
303
+ {#if mode === 'draw'}<Button
304
+ size="sm"
305
+ variant="quiet"
306
+ disabled={disabled || !hasInk}
307
+ onclick={undo}>{undoLabel}</Button
308
+ >{/if}
309
+ <Button
310
+ size="sm"
311
+ variant="quiet"
312
+ disabled={disabled || (mode === 'draw' ? !hasInk : !typedName)}
313
+ onclick={clear}>{clearLabel}</Button
314
+ >
315
+ </div>
316
+ {#if name}<input type="hidden" {name} value={value?.dataUrl ?? ''} {required} />{/if}
317
+ </div>
@@ -0,0 +1,48 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type SignatureValue } from '../signature.js';
3
+ type Mode = 'draw' | 'type';
4
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ /** The accepted signature, or `null` while there is none. Bindable. */
6
+ value?: SignatureValue | null;
7
+ onValueChange?: (value: SignatureValue | null) => void;
8
+ /** Names the pad for assistive tech, e.g. "Customer signature". */
9
+ label: string;
10
+ /**
11
+ * Ways to sign. Typing is the keyboard- and switch-accessible alternative to drawing (WCAG
12
+ * 2.1.1, 2.5.1); drop it only if you offer another route to the same outcome.
13
+ */
14
+ modes?: Mode[];
15
+ mode?: Mode;
16
+ strokeWidth?: number;
17
+ /** Ink colour. Defaults to the text colour, so it follows the theme. */
18
+ strokeColor?: string;
19
+ /** Fonts offered in `type` mode. Load them yourself; the default is the system script face. */
20
+ fonts?: Array<{
21
+ family: string;
22
+ label: string;
23
+ }>;
24
+ /** Drawn length, in pixels, below which a signature does not count. */
25
+ minStrokeLength?: number;
26
+ minTypedLength?: number;
27
+ /** Submits the PNG data URL with a form. */
28
+ name?: string;
29
+ required?: boolean;
30
+ disabled?: boolean;
31
+ modeLabel?: string;
32
+ drawLabel?: string;
33
+ typeLabel?: string;
34
+ typePlaceholder?: string;
35
+ fontLabel?: string;
36
+ clearLabel?: string;
37
+ undoLabel?: string;
38
+ /** Guidance under the drawing area. */
39
+ drawHint?: string;
40
+ class?: string;
41
+ ref?: HTMLDivElement | null;
42
+ }
43
+ declare const SignaturePad: import("svelte").Component<Props, {
44
+ clear: () => void;
45
+ undo: () => void;
46
+ }, "value" | "ref" | "mode">;
47
+ type SignaturePad = ReturnType<typeof SignaturePad>;
48
+ export default SignaturePad;
@@ -3,6 +3,14 @@
3
3
  import { cx } from '../utils.js';
4
4
 
5
5
  interface Props extends HTMLAttributes<HTMLSpanElement> {
6
+ /**
7
+ * The shape standing in for content. `block` (the default) is sized by `width` and `height`;
8
+ * the others take the metrics of what they replace — a line of body text, a heading, an
9
+ * avatar, a button, a card.
10
+ */
11
+ variant?: 'block' | 'text' | 'title' | 'avatar' | 'button' | 'card';
12
+ /** For `text`: how many lines. The last one is shorter, as a paragraph's usually is. */
13
+ lines?: number;
6
14
  /** Number is treated as pixels; strings pass through as CSS. */
7
15
  width?: string | number;
8
16
  height?: string | number;
@@ -10,17 +18,40 @@
10
18
  ref?: HTMLSpanElement | null;
11
19
  }
12
20
 
13
- let { width, height, class: className, ref = $bindable(null), ...rest }: Props = $props();
21
+ let {
22
+ variant = 'block',
23
+ lines = 1,
24
+ width,
25
+ height,
26
+ class: className,
27
+ ref = $bindable(null),
28
+ ...rest
29
+ }: Props = $props();
14
30
 
15
31
  const size = (value: string | number | undefined) =>
16
32
  typeof value === 'number' ? `${value}px` : value;
17
33
  </script>
18
34
 
19
- <span
20
- bind:this={ref}
21
- class={cx('ldt-skeleton', className)}
22
- style:width={size(width)}
23
- style:height={size(height)}
24
- aria-hidden="true"
25
- {...rest}
26
- ></span>
35
+ {#if variant === 'text' && lines > 1}
36
+ <span
37
+ bind:this={ref}
38
+ class={cx('ldt-skeleton-lines', className)}
39
+ style:width={size(width)}
40
+ aria-hidden="true"
41
+ {...rest}
42
+ >
43
+ {#each { length: lines }, index (index)}<span
44
+ class="ldt-skeleton ldt-skeleton--text"
45
+ style:width={index === lines - 1 ? '60%' : undefined}
46
+ ></span>{/each}
47
+ </span>
48
+ {:else}
49
+ <span
50
+ bind:this={ref}
51
+ class={cx('ldt-skeleton', variant !== 'block' && `ldt-skeleton--${variant}`, className)}
52
+ style:width={size(width)}
53
+ style:height={size(height)}
54
+ aria-hidden="true"
55
+ {...rest}
56
+ ></span>
57
+ {/if}
@@ -1,5 +1,13 @@
1
1
  import type { HTMLAttributes } from 'svelte/elements';
2
2
  interface Props extends HTMLAttributes<HTMLSpanElement> {
3
+ /**
4
+ * The shape standing in for content. `block` (the default) is sized by `width` and `height`;
5
+ * the others take the metrics of what they replace — a line of body text, a heading, an
6
+ * avatar, a button, a card.
7
+ */
8
+ variant?: 'block' | 'text' | 'title' | 'avatar' | 'button' | 'card';
9
+ /** For `text`: how many lines. The last one is shorter, as a paragraph's usually is. */
10
+ lines?: number;
3
11
  /** Number is treated as pixels; strings pass through as CSS. */
4
12
  width?: string | number;
5
13
  height?: string | number;
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAnchorAttributes } from 'svelte/elements';
4
+ import { cx } from '../utils.js';
5
+
6
+ interface Props extends Omit<HTMLAnchorAttributes, 'href'> {
7
+ /** Id of the element to skip to — usually `<main>`. */
8
+ targetId: string;
9
+ /** Link text, when `children` is not given. */
10
+ label?: string;
11
+ children?: Snippet;
12
+ class?: string;
13
+ ref?: HTMLAnchorElement | null;
14
+ }
15
+
16
+ let {
17
+ targetId,
18
+ label = 'Skip to main content',
19
+ children,
20
+ onclick,
21
+ class: className,
22
+ ref = $bindable(null),
23
+ ...rest
24
+ }: Props = $props();
25
+
26
+ function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLAnchorElement }) {
27
+ onclick?.(event);
28
+ if (event.defaultPrevented) return;
29
+ const destination = document.getElementById(targetId);
30
+ if (!destination) return;
31
+ // Following the fragment scrolls, but only moves focus if the target is focusable. A
32
+ // `<main>` is not, so without this the next Tab starts again from the top of the page.
33
+ if (!destination.matches('a[href], button, input, select, textarea, [tabindex]')) {
34
+ destination.tabIndex = -1;
35
+ }
36
+ destination.focus();
37
+ }
38
+ </script>
39
+
40
+ <a
41
+ bind:this={ref}
42
+ class={cx('ldt-skip-link', className)}
43
+ href={`#${targetId}`}
44
+ {...rest}
45
+ onclick={handleClick}
46
+ >
47
+ {#if children}{@render children()}{:else}{label}{/if}
48
+ </a>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAnchorAttributes } from 'svelte/elements';
3
+ interface Props extends Omit<HTMLAnchorAttributes, 'href'> {
4
+ /** Id of the element to skip to — usually `<main>`. */
5
+ targetId: string;
6
+ /** Link text, when `children` is not given. */
7
+ label?: string;
8
+ children?: Snippet;
9
+ class?: string;
10
+ ref?: HTMLAnchorElement | null;
11
+ }
12
+ declare const SkipLink: import("svelte").Component<Props, {}, "ref">;
13
+ type SkipLink = ReturnType<typeof SkipLink>;
14
+ export default SkipLink;
@@ -0,0 +1,148 @@
1
+ <script lang="ts" generics="V extends number | number[] = number">
2
+ import { Slider as SliderPrimitive } from 'bits-ui';
3
+ import type { Snippet } from 'svelte';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ import type { Orientation } from '../types.js';
6
+ import { cx } from '../utils.js';
7
+
8
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
9
+ /** A number for one thumb, an array for a range — one thumb per entry. */
10
+ value?: V;
11
+ min?: number;
12
+ max?: number;
13
+ /** The increment, or an explicit list of the only values allowed. */
14
+ step?: number | number[];
15
+ orientation?: Orientation;
16
+ disabled?: boolean;
17
+ /** Accessible name. With two or more thumbs each is named from it by `thumbLabel`. */
18
+ label: string;
19
+ thumbLabel?: (index: number, count: number) => string;
20
+ /** Spoken (`aria-valuetext`) and shown for each value — add units here: `(v) => \`${v} mm\``. */
21
+ formatValue?: (value: number) => string;
22
+ /** Show the formatted value beside the track. */
23
+ showValue?: boolean;
24
+ /** Mark every step along the track. Sensible only for a coarse `step`. */
25
+ ticks?: boolean;
26
+ /** Submits each value as a hidden input under this name. */
27
+ name?: string;
28
+ /** Every change, while dragging too. */
29
+ onValueChange?: (value: V) => void;
30
+ /** Only when the user lets go or finishes a key press — the moment to save. */
31
+ onValueCommit?: (value: V) => void;
32
+ /** Rendered after the track, e.g. min/max captions. */
33
+ children?: Snippet;
34
+ class?: string;
35
+ ref?: HTMLDivElement | null;
36
+ }
37
+
38
+ let {
39
+ value = $bindable(0 as V),
40
+ min = 0,
41
+ max = 100,
42
+ step = 1,
43
+ orientation = 'horizontal',
44
+ disabled = false,
45
+ label,
46
+ thumbLabel = (index, count) =>
47
+ count === 1 ? label : `${label}, ${index === 0 ? 'minimum' : 'maximum'}`,
48
+ formatValue = String,
49
+ showValue = false,
50
+ ticks = false,
51
+ name,
52
+ onValueChange,
53
+ onValueCommit,
54
+ children,
55
+ class: className,
56
+ ref = $bindable(null),
57
+ ...rest
58
+ }: Props = $props();
59
+
60
+ const values = $derived<number[]>(Array.isArray(value) ? value : [value as number]);
61
+ const multiple = $derived(Array.isArray(value));
62
+ </script>
63
+
64
+ {#snippet parts({
65
+ thumbItems,
66
+ tickItems,
67
+ }: {
68
+ thumbItems: { index: number; value: number }[];
69
+ tickItems: { index: number }[];
70
+ })}
71
+ <span class="ldt-slider__track"><SliderPrimitive.Range class="ldt-slider__range" /></span>
72
+ {#if ticks}
73
+ {#each tickItems as tick (tick.index)}<SliderPrimitive.Tick
74
+ class="ldt-slider__tick"
75
+ index={tick.index}
76
+ />{/each}
77
+ {/if}
78
+ {#each thumbItems as thumb (thumb.index)}
79
+ <SliderPrimitive.Thumb
80
+ class="ldt-slider__thumb"
81
+ index={thumb.index}
82
+ aria-label={thumbLabel(thumb.index, thumbItems.length)}
83
+ aria-valuetext={formatValue(thumb.value)}
84
+ />
85
+ {/each}
86
+ {/snippet}
87
+
88
+ <div
89
+ bind:this={ref}
90
+ class={cx('ldt-slider', orientation === 'vertical' && 'ldt-slider--vertical', className)}
91
+ data-disabled={disabled ? '' : undefined}
92
+ {...rest}
93
+ >
94
+ <!--
95
+ `exact` positioning: the thumb's centre sits on the value and the wrapper's padding keeps it
96
+ inside the box. The default measures the thumb in the browser, which shifts it after
97
+ hydration.
98
+ -->
99
+ {#if multiple}
100
+ <SliderPrimitive.Root
101
+ type="multiple"
102
+ bind:value={() => value as number[], (next) => (value = next as V)}
103
+ {min}
104
+ {max}
105
+ {step}
106
+ {orientation}
107
+ {disabled}
108
+ thumbPositioning="exact"
109
+ class="ldt-slider__control"
110
+ onValueChange={(next) => onValueChange?.(next as V)}
111
+ onValueCommit={(next) => onValueCommit?.(next as V)}
112
+ >
113
+ {#snippet children({ thumbItems, tickItems })}{@render parts({
114
+ thumbItems,
115
+ tickItems,
116
+ })}{/snippet}
117
+ </SliderPrimitive.Root>
118
+ {:else}
119
+ <SliderPrimitive.Root
120
+ type="single"
121
+ bind:value={() => value as number, (next) => (value = next as V)}
122
+ {min}
123
+ {max}
124
+ {step}
125
+ {orientation}
126
+ {disabled}
127
+ thumbPositioning="exact"
128
+ class="ldt-slider__control"
129
+ onValueChange={(next) => onValueChange?.(next as V)}
130
+ onValueCommit={(next) => onValueCommit?.(next as V)}
131
+ >
132
+ {#snippet children({ thumbItems, tickItems })}{@render parts({
133
+ thumbItems,
134
+ tickItems,
135
+ })}{/snippet}
136
+ </SliderPrimitive.Root>
137
+ {/if}
138
+ {#if showValue}
139
+ <!-- The thumbs already speak their values; this is the sighted copy. -->
140
+ <output class="ldt-slider__value" aria-hidden="true"
141
+ >{values.map((one) => formatValue(one)).join(' – ')}</output
142
+ >
143
+ {/if}
144
+ {#if name}
145
+ {#each values as one, index (index)}<input type="hidden" {name} value={one} />{/each}
146
+ {/if}
147
+ {#if children}{@render children()}{/if}
148
+ </div>