@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,34 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import { type AspectRatioName, type EmbedProvider } from '../media-utils.js';
|
|
3
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {
|
|
4
|
+
/** A YouTube or Vimeo page, share or embed URL. */
|
|
5
|
+
url?: string;
|
|
6
|
+
/** Or name the video directly. */
|
|
7
|
+
provider?: EmbedProvider;
|
|
8
|
+
videoId?: string;
|
|
9
|
+
/** Required: names the iframe and the play button. */
|
|
10
|
+
title: string;
|
|
11
|
+
/**
|
|
12
|
+
* Still shown before the player loads. None is fetched from the provider by default — that
|
|
13
|
+
* request alone would tell them someone opened the page.
|
|
14
|
+
*/
|
|
15
|
+
poster?: string;
|
|
16
|
+
/** No-tracking hosts: `youtube-nocookie.com`, Vimeo's `dnt=1`. */
|
|
17
|
+
privacy?: boolean;
|
|
18
|
+
/** Start offset in seconds. Read from the URL when it carries one. */
|
|
19
|
+
start?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Load the provider's player straight away. By default a local facade stands in until the
|
|
22
|
+
* user presses play, so no third-party request happens before they choose to watch.
|
|
23
|
+
*/
|
|
24
|
+
eager?: boolean;
|
|
25
|
+
ratio?: AspectRatioName | number | string;
|
|
26
|
+
playLabel?: (title: string) => string;
|
|
27
|
+
/** Shown when the URL is not a recognised YouTube or Vimeo link. */
|
|
28
|
+
unsupportedText?: string;
|
|
29
|
+
class?: string;
|
|
30
|
+
ref?: HTMLDivElement | null;
|
|
31
|
+
}
|
|
32
|
+
declare const MediaEmbed: import("svelte").Component<Props, {}, "ref">;
|
|
33
|
+
type MediaEmbed = ReturnType<typeof MediaEmbed>;
|
|
34
|
+
export default MediaEmbed;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { rovingFocus } from '../attachments.js';
|
|
4
|
+
import MediaTile from '../internal/media/MediaTile.svelte';
|
|
5
|
+
import { mediaKey, type AspectRatioName, type MediaItem } from '../media-utils.js';
|
|
6
|
+
import { cx } from '../utils.js';
|
|
7
|
+
import Lightbox from './Lightbox.svelte';
|
|
8
|
+
|
|
9
|
+
interface Props extends Omit<HTMLAttributes<HTMLUListElement>, 'children'> {
|
|
10
|
+
items: MediaItem[];
|
|
11
|
+
/** Accessible name of the collection. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** `grid` crops tiles to `ratio`; `masonry` keeps each image's own shape in columns. */
|
|
14
|
+
variant?: 'grid' | 'masonry';
|
|
15
|
+
/** Narrowest a column may get before the grid drops one. */
|
|
16
|
+
minColumnWidth?: string;
|
|
17
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
18
|
+
ratio?: AspectRatioName | string | number;
|
|
19
|
+
/** Open a `Lightbox` on the pressed item. */
|
|
20
|
+
lightbox?: boolean;
|
|
21
|
+
/** Called when an item is pressed, whether or not the lightbox opens. */
|
|
22
|
+
onSelect?: (index: number, item: MediaItem) => void;
|
|
23
|
+
/** Words for the marker on time-based tiles. */
|
|
24
|
+
kindLabels?: Partial<Record<MediaItem['kind'], string>>;
|
|
25
|
+
class?: string;
|
|
26
|
+
ref?: HTMLUListElement | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
items,
|
|
31
|
+
label,
|
|
32
|
+
variant = 'grid',
|
|
33
|
+
minColumnWidth = '12rem',
|
|
34
|
+
gap = 'md',
|
|
35
|
+
ratio = 'photo',
|
|
36
|
+
lightbox = true,
|
|
37
|
+
onSelect,
|
|
38
|
+
kindLabels,
|
|
39
|
+
class: className,
|
|
40
|
+
ref = $bindable(null),
|
|
41
|
+
...rest
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
|
|
44
|
+
const kinds = $derived({
|
|
45
|
+
image: 'Image',
|
|
46
|
+
video: 'Video',
|
|
47
|
+
audio: 'Audio',
|
|
48
|
+
embed: 'Video',
|
|
49
|
+
...kindLabels,
|
|
50
|
+
});
|
|
51
|
+
let open = $state(false);
|
|
52
|
+
let active = $state(0);
|
|
53
|
+
|
|
54
|
+
function select(index: number) {
|
|
55
|
+
onSelect?.(index, items[index]);
|
|
56
|
+
if (!lightbox) return;
|
|
57
|
+
active = index;
|
|
58
|
+
open = true;
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<!--
|
|
63
|
+
A list with one tab stop: arrow keys, Home and End move between tiles, so a long gallery costs
|
|
64
|
+
a keyboard user one Tab rather than one per image.
|
|
65
|
+
-->
|
|
66
|
+
<ul
|
|
67
|
+
bind:this={ref}
|
|
68
|
+
class={cx(
|
|
69
|
+
'ldt-media-grid',
|
|
70
|
+
variant === 'masonry' && 'ldt-media-grid--masonry',
|
|
71
|
+
gap !== 'md' && `ldt-media-grid--${gap}`,
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
style:--ldt-media-grid-min={minColumnWidth}
|
|
75
|
+
aria-label={label}
|
|
76
|
+
{@attach rovingFocus({ orientation: 'both' })}
|
|
77
|
+
{...rest}
|
|
78
|
+
>
|
|
79
|
+
{#each items as item, index (mediaKey(item, index))}
|
|
80
|
+
<li class="ldt-media-grid__item">
|
|
81
|
+
<MediaTile
|
|
82
|
+
{item}
|
|
83
|
+
{index}
|
|
84
|
+
ratio={variant === 'masonry' ? 'auto' : ratio}
|
|
85
|
+
kindLabels={kinds}
|
|
86
|
+
onSelect={() => select(index)}
|
|
87
|
+
/>
|
|
88
|
+
</li>
|
|
89
|
+
{/each}
|
|
90
|
+
</ul>
|
|
91
|
+
{#if lightbox}<Lightbox {items} bind:open bind:index={active} title={label} showThumbnails />{/if}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import { type AspectRatioName, type MediaItem } from '../media-utils.js';
|
|
3
|
+
interface Props extends Omit<HTMLAttributes<HTMLUListElement>, 'children'> {
|
|
4
|
+
items: MediaItem[];
|
|
5
|
+
/** Accessible name of the collection. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** `grid` crops tiles to `ratio`; `masonry` keeps each image's own shape in columns. */
|
|
8
|
+
variant?: 'grid' | 'masonry';
|
|
9
|
+
/** Narrowest a column may get before the grid drops one. */
|
|
10
|
+
minColumnWidth?: string;
|
|
11
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
12
|
+
ratio?: AspectRatioName | string | number;
|
|
13
|
+
/** Open a `Lightbox` on the pressed item. */
|
|
14
|
+
lightbox?: boolean;
|
|
15
|
+
/** Called when an item is pressed, whether or not the lightbox opens. */
|
|
16
|
+
onSelect?: (index: number, item: MediaItem) => void;
|
|
17
|
+
/** Words for the marker on time-based tiles. */
|
|
18
|
+
kindLabels?: Partial<Record<MediaItem['kind'], string>>;
|
|
19
|
+
class?: string;
|
|
20
|
+
ref?: HTMLUListElement | null;
|
|
21
|
+
}
|
|
22
|
+
declare const MediaGrid: import("svelte").Component<Props, {}, "ref">;
|
|
23
|
+
type MediaGrid = ReturnType<typeof MediaGrid>;
|
|
24
|
+
export default MediaGrid;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends string = string">
|
|
2
|
+
import { Combobox as ComboboxPrimitive, type ComboboxInputProps } from 'bits-ui';
|
|
3
|
+
import { tick, type Snippet } from 'svelte';
|
|
4
|
+
import ListboxEntries from '../internal/ListboxEntries.svelte';
|
|
5
|
+
import {
|
|
6
|
+
filterEntries,
|
|
7
|
+
flattenOptions,
|
|
8
|
+
matchesQuery,
|
|
9
|
+
type OptionEntry,
|
|
10
|
+
} from '../internal/options.js';
|
|
11
|
+
import type { Option } from '../types.js';
|
|
12
|
+
import { cx } from '../utils.js';
|
|
13
|
+
import LiveRegion from './LiveRegion.svelte';
|
|
14
|
+
|
|
15
|
+
type Props = Omit<
|
|
16
|
+
ComboboxInputProps,
|
|
17
|
+
'value' | 'child' | 'children' | 'defaultValue' | 'clearOnDeselect' | 'oninput' | 'ref'
|
|
18
|
+
> & {
|
|
19
|
+
/** The selected values, in the order they were chosen. */
|
|
20
|
+
value?: T[];
|
|
21
|
+
/** Options, optionally in labelled groups. */
|
|
22
|
+
options: OptionEntry<T>[];
|
|
23
|
+
/** Accessible name when the control is not wrapped in a `Field` or `<label>`. */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** Most values that can be chosen. Remaining options disable once it is reached. */
|
|
26
|
+
max?: number;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
/** Submits each selected value under this name. */
|
|
30
|
+
name?: string;
|
|
31
|
+
/** How typing narrows the list. `false` when `onQueryChange` filters on a server. */
|
|
32
|
+
filter?: ((option: Option<T>, query: string) => boolean) | false;
|
|
33
|
+
query?: string;
|
|
34
|
+
onQueryChange?: (query: string) => void;
|
|
35
|
+
queryDebounce?: number;
|
|
36
|
+
loading?: boolean;
|
|
37
|
+
loadingText?: string;
|
|
38
|
+
emptyText?: string;
|
|
39
|
+
resultsText?: (count: number, selected: number) => string;
|
|
40
|
+
maxText?: (max: number) => string;
|
|
41
|
+
/** Accessible name of each chip's remove button. */
|
|
42
|
+
removeLabel?: (label: string) => string;
|
|
43
|
+
open?: boolean;
|
|
44
|
+
onOpenChange?: (open: boolean) => void;
|
|
45
|
+
onValueChange?: (value: T[]) => void;
|
|
46
|
+
/** Draws the full box instead of the default underline. */
|
|
47
|
+
boxed?: boolean;
|
|
48
|
+
triggerLabel?: string;
|
|
49
|
+
item?: Snippet<[Option<T>, { selected: boolean; highlighted: boolean }]>;
|
|
50
|
+
/** Lands on the wrapper; the rest props land on the text input. */
|
|
51
|
+
class?: string;
|
|
52
|
+
contentClass?: string;
|
|
53
|
+
ref?: HTMLElement | null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
value = $bindable([]),
|
|
58
|
+
options,
|
|
59
|
+
label,
|
|
60
|
+
max,
|
|
61
|
+
disabled = false,
|
|
62
|
+
required = false,
|
|
63
|
+
name,
|
|
64
|
+
filter,
|
|
65
|
+
query = $bindable(''),
|
|
66
|
+
onQueryChange,
|
|
67
|
+
queryDebounce = 0,
|
|
68
|
+
loading = false,
|
|
69
|
+
loadingText = 'Loading…',
|
|
70
|
+
emptyText = 'No results',
|
|
71
|
+
resultsText = (count, selected) =>
|
|
72
|
+
`${count} ${count === 1 ? 'result' : 'results'}, ${selected} selected`,
|
|
73
|
+
maxText = (limit) => `Up to ${limit} can be chosen`,
|
|
74
|
+
removeLabel = (text) => `Remove ${text}`,
|
|
75
|
+
open = $bindable(false),
|
|
76
|
+
onOpenChange,
|
|
77
|
+
onValueChange,
|
|
78
|
+
boxed = false,
|
|
79
|
+
triggerLabel = 'Show options',
|
|
80
|
+
item,
|
|
81
|
+
class: className,
|
|
82
|
+
contentClass,
|
|
83
|
+
onkeydown,
|
|
84
|
+
ref = $bindable(null),
|
|
85
|
+
...rest
|
|
86
|
+
}: Props = $props();
|
|
87
|
+
|
|
88
|
+
const flat = $derived(flattenOptions(options));
|
|
89
|
+
const chosen = $derived(
|
|
90
|
+
value.map((one) => flat.find((option) => option.value === one) ?? { value: one, label: one })
|
|
91
|
+
);
|
|
92
|
+
const full = $derived(max !== undefined && value.length >= max);
|
|
93
|
+
const visible = $derived(
|
|
94
|
+
filter === false || !query
|
|
95
|
+
? options
|
|
96
|
+
: filterEntries(options, (option) => (filter ?? matchesQuery)(option, query))
|
|
97
|
+
);
|
|
98
|
+
const count = $derived(flattenOptions(visible).length);
|
|
99
|
+
|
|
100
|
+
let text = $state('');
|
|
101
|
+
let chips = $state<HTMLElement | null>(null);
|
|
102
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
103
|
+
$effect(() => () => clearTimeout(timer));
|
|
104
|
+
|
|
105
|
+
function setValue(next: T[]) {
|
|
106
|
+
value = next;
|
|
107
|
+
onValueChange?.(next);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleInput(event: Event & { currentTarget: HTMLInputElement }) {
|
|
111
|
+
query = event.currentTarget.value;
|
|
112
|
+
text = query;
|
|
113
|
+
if (!onQueryChange) return;
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
const next = query;
|
|
116
|
+
if (queryDebounce > 0) timer = setTimeout(() => onQueryChange(next), queryDebounce);
|
|
117
|
+
else onQueryChange(next);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleValueChange(next: string[]) {
|
|
121
|
+
// Bits writes the chosen label into the input; for a multi-select the input is for searching,
|
|
122
|
+
// so hand it back empty. The two-step write is what makes the prop change reach Bits.
|
|
123
|
+
const option = flat.find((one) => one.value === next[next.length - 1]);
|
|
124
|
+
text = option?.label ?? '';
|
|
125
|
+
onValueChange?.(next as T[]);
|
|
126
|
+
await tick();
|
|
127
|
+
text = '';
|
|
128
|
+
query = '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleOpenChange(next: boolean) {
|
|
132
|
+
if (!next) {
|
|
133
|
+
text = '';
|
|
134
|
+
query = '';
|
|
135
|
+
}
|
|
136
|
+
onOpenChange?.(next);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function remove(target: T, index: number) {
|
|
140
|
+
setValue(value.filter((one) => one !== target));
|
|
141
|
+
await tick();
|
|
142
|
+
// Focus follows the removal: the next chip, else the previous one, else the input.
|
|
143
|
+
const buttons = chips?.querySelectorAll<HTMLButtonElement>('.ldt-chip__remove') ?? [];
|
|
144
|
+
(buttons[index] ?? buttons[index - 1] ?? ref)?.focus();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleKeydown(event: KeyboardEvent & { currentTarget: HTMLInputElement }) {
|
|
148
|
+
onkeydown?.(event);
|
|
149
|
+
if (event.defaultPrevented) return;
|
|
150
|
+
if (event.key === 'Backspace' && event.currentTarget.value === '' && value.length) {
|
|
151
|
+
setValue(value.slice(0, -1));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const announcement = $derived(
|
|
156
|
+
!open
|
|
157
|
+
? ''
|
|
158
|
+
: loading
|
|
159
|
+
? loadingText
|
|
160
|
+
: [count === 0 ? emptyText : resultsText(count, value.length), full && maxText(max!)]
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.join('. ')
|
|
163
|
+
);
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<ComboboxPrimitive.Root
|
|
167
|
+
type="multiple"
|
|
168
|
+
bind:value={() => value, (next) => (value = next as T[])}
|
|
169
|
+
bind:open
|
|
170
|
+
inputValue={text}
|
|
171
|
+
items={flat}
|
|
172
|
+
{disabled}
|
|
173
|
+
{required}
|
|
174
|
+
{name}
|
|
175
|
+
onOpenChange={handleOpenChange}
|
|
176
|
+
onValueChange={handleValueChange}
|
|
177
|
+
>
|
|
178
|
+
<div
|
|
179
|
+
class={cx('ldt-combobox', 'ldt-multi-select', boxed && 'ldt-combobox--boxed', className)}
|
|
180
|
+
data-disabled={disabled ? '' : undefined}
|
|
181
|
+
>
|
|
182
|
+
{#if chosen.length}
|
|
183
|
+
<ul class="ldt-multi-select__chips" bind:this={chips}>
|
|
184
|
+
{#each chosen as option, index (option.value)}
|
|
185
|
+
<li class="ldt-chip">
|
|
186
|
+
<span>{option.label}</span>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
class="ldt-chip__remove"
|
|
190
|
+
aria-label={removeLabel(option.label)}
|
|
191
|
+
{disabled}
|
|
192
|
+
onclick={() => remove(option.value, index)}
|
|
193
|
+
><svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true" focusable="false"
|
|
194
|
+
><path d="m2 2 6 6M8 2 2 8" stroke="currentColor" stroke-width="1.4" /></svg
|
|
195
|
+
></button
|
|
196
|
+
>
|
|
197
|
+
</li>
|
|
198
|
+
{/each}
|
|
199
|
+
</ul>
|
|
200
|
+
{/if}
|
|
201
|
+
<ComboboxPrimitive.Input
|
|
202
|
+
bind:ref
|
|
203
|
+
class="ldt-combobox__input"
|
|
204
|
+
aria-label={label}
|
|
205
|
+
autocomplete="off"
|
|
206
|
+
{...rest}
|
|
207
|
+
oninput={handleInput}
|
|
208
|
+
onkeydown={handleKeydown}
|
|
209
|
+
/>
|
|
210
|
+
<ComboboxPrimitive.Trigger class="ldt-combobox__trigger" aria-label={triggerLabel}>
|
|
211
|
+
<svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false"
|
|
212
|
+
><path d="M2 4.5 6 8.5l4-4" fill="none" stroke="currentColor" stroke-width="1.5" /></svg
|
|
213
|
+
>
|
|
214
|
+
</ComboboxPrimitive.Trigger>
|
|
215
|
+
</div>
|
|
216
|
+
<ComboboxPrimitive.Portal>
|
|
217
|
+
<ComboboxPrimitive.Content class={cx('ldt-listbox', contentClass)} sideOffset={4}>
|
|
218
|
+
{#if loading}
|
|
219
|
+
<div class="ldt-listbox__empty" aria-hidden="true">{loadingText}</div>
|
|
220
|
+
{:else if count === 0}
|
|
221
|
+
<div class="ldt-listbox__empty" aria-hidden="true">{emptyText}</div>
|
|
222
|
+
{:else}
|
|
223
|
+
<ListboxEntries
|
|
224
|
+
entries={visible}
|
|
225
|
+
{item}
|
|
226
|
+
blocked={(option) => full && !value.includes(option.value)}
|
|
227
|
+
/>
|
|
228
|
+
{/if}
|
|
229
|
+
</ComboboxPrimitive.Content>
|
|
230
|
+
</ComboboxPrimitive.Portal>
|
|
231
|
+
</ComboboxPrimitive.Root>
|
|
232
|
+
<LiveRegion message={announcement} />
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Combobox as ComboboxPrimitive } from 'bits-ui';
|
|
2
|
+
import { type Snippet } from 'svelte';
|
|
3
|
+
import { type OptionEntry } from '../internal/options.js';
|
|
4
|
+
import type { Option } from '../types.js';
|
|
5
|
+
declare function $$render<T extends string = string>(): {
|
|
6
|
+
props: Omit<ComboboxPrimitive.InputProps, "value" | "children" | "oninput" | "ref" | "child" | "defaultValue" | "clearOnDeselect"> & {
|
|
7
|
+
/** The selected values, in the order they were chosen. */
|
|
8
|
+
value?: T[];
|
|
9
|
+
/** Options, optionally in labelled groups. */
|
|
10
|
+
options: OptionEntry<T>[];
|
|
11
|
+
/** Accessible name when the control is not wrapped in a `Field` or `<label>`. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Most values that can be chosen. Remaining options disable once it is reached. */
|
|
14
|
+
max?: number;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
/** Submits each selected value under this name. */
|
|
18
|
+
name?: string;
|
|
19
|
+
/** How typing narrows the list. `false` when `onQueryChange` filters on a server. */
|
|
20
|
+
filter?: ((option: Option<T>, query: string) => boolean) | false;
|
|
21
|
+
query?: string;
|
|
22
|
+
onQueryChange?: (query: string) => void;
|
|
23
|
+
queryDebounce?: number;
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
loadingText?: string;
|
|
26
|
+
emptyText?: string;
|
|
27
|
+
resultsText?: (count: number, selected: number) => string;
|
|
28
|
+
maxText?: (max: number) => string;
|
|
29
|
+
/** Accessible name of each chip's remove button. */
|
|
30
|
+
removeLabel?: (label: string) => string;
|
|
31
|
+
open?: boolean;
|
|
32
|
+
onOpenChange?: (open: boolean) => void;
|
|
33
|
+
onValueChange?: (value: T[]) => void;
|
|
34
|
+
/** Draws the full box instead of the default underline. */
|
|
35
|
+
boxed?: boolean;
|
|
36
|
+
triggerLabel?: string;
|
|
37
|
+
item?: Snippet<[Option<T>, {
|
|
38
|
+
selected: boolean;
|
|
39
|
+
highlighted: boolean;
|
|
40
|
+
}]>;
|
|
41
|
+
/** Lands on the wrapper; the rest props land on the text input. */
|
|
42
|
+
class?: string;
|
|
43
|
+
contentClass?: string;
|
|
44
|
+
ref?: HTMLElement | null;
|
|
45
|
+
};
|
|
46
|
+
exports: {};
|
|
47
|
+
bindings: "value" | "ref" | "open" | "query";
|
|
48
|
+
slots: {};
|
|
49
|
+
events: {};
|
|
50
|
+
};
|
|
51
|
+
declare class __sveltets_Render<T extends string = string> {
|
|
52
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
53
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
54
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
55
|
+
bindings(): "value" | "ref" | "open" | "query";
|
|
56
|
+
exports(): {};
|
|
57
|
+
}
|
|
58
|
+
interface $$IsomorphicComponent {
|
|
59
|
+
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']>> & {
|
|
60
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
61
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
62
|
+
<T extends string = string>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
63
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
64
|
+
}
|
|
65
|
+
declare const MultiSelect: $$IsomorphicComponent;
|
|
66
|
+
type MultiSelect<T extends string = string> = InstanceType<typeof MultiSelect<T>>;
|
|
67
|
+
export default MultiSelect;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="V extends number | null = number">
|
|
2
2
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
3
|
import { cx } from '../utils.js';
|
|
4
4
|
|
|
5
5
|
interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'min' | 'max' | 'step'> {
|
|
6
|
-
|
|
6
|
+
/** A number, or `null` for an empty field when `nullable` is set. */
|
|
7
|
+
value?: V;
|
|
7
8
|
min?: number;
|
|
8
9
|
max?: number;
|
|
9
10
|
step?: number;
|
|
@@ -13,7 +14,15 @@
|
|
|
13
14
|
incrementLabel?: (label: string) => string;
|
|
14
15
|
/** Draws the full box instead of the default underline. */
|
|
15
16
|
boxed?: boolean;
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Let the field be empty: clearing it yields `null` instead of snapping to the low bound.
|
|
19
|
+
* For optional quantities where "not set" differs from zero. Type `value` as
|
|
20
|
+
* `number | null` to bind it.
|
|
21
|
+
*/
|
|
22
|
+
nullable?: boolean;
|
|
23
|
+
/** Hide the − and + buttons, for dense tables. The keyboard still steps with the arrows. */
|
|
24
|
+
hideSteppers?: boolean;
|
|
25
|
+
onValueChange?: (value: V) => void;
|
|
17
26
|
/** `class` lands on the wrapper; use `inputClass` to reach the input. */
|
|
18
27
|
class?: string;
|
|
19
28
|
inputClass?: string;
|
|
@@ -21,7 +30,7 @@
|
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
let {
|
|
24
|
-
value = $bindable(0),
|
|
33
|
+
value = $bindable(0 as V),
|
|
25
34
|
min = -Infinity,
|
|
26
35
|
max = Infinity,
|
|
27
36
|
step = 1,
|
|
@@ -29,7 +38,10 @@
|
|
|
29
38
|
decrementLabel = (name) => `Decrease ${name}`,
|
|
30
39
|
incrementLabel = (name) => `Increase ${name}`,
|
|
31
40
|
boxed = false,
|
|
41
|
+
nullable = false,
|
|
42
|
+
hideSteppers = false,
|
|
32
43
|
disabled = false,
|
|
44
|
+
readonly = false,
|
|
33
45
|
onValueChange,
|
|
34
46
|
onchange,
|
|
35
47
|
class: className,
|
|
@@ -41,33 +53,49 @@
|
|
|
41
53
|
/** Clearing the input yields `NaN`; fall back to the low bound rather than wedging the field. */
|
|
42
54
|
const fallback = $derived(Number.isFinite(min) ? min : 0);
|
|
43
55
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
56
|
+
const current = $derived(value === null ? null : (value as number));
|
|
57
|
+
const atMin = $derived(current !== null && current <= min);
|
|
58
|
+
const atMax = $derived(current !== null && current >= max);
|
|
59
|
+
const locked = $derived(Boolean(disabled) || Boolean(readonly));
|
|
46
60
|
|
|
47
61
|
function set(next: number) {
|
|
48
|
-
value =
|
|
62
|
+
value = (
|
|
63
|
+
Number.isFinite(next) ? Math.min(max, Math.max(min, next)) : nullable ? null : fallback
|
|
64
|
+
) as V;
|
|
49
65
|
onValueChange?.(value);
|
|
50
66
|
}
|
|
67
|
+
|
|
68
|
+
// Stepping from an empty field starts at the low bound, the same place clearing lands.
|
|
69
|
+
const nudge = (direction: 1 | -1) =>
|
|
70
|
+
set((current ?? fallback) + (current === null ? 0 : step * direction));
|
|
51
71
|
</script>
|
|
52
72
|
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
<div
|
|
74
|
+
class={cx(
|
|
75
|
+
'ldt-number-field',
|
|
76
|
+
boxed && 'ldt-number-field--boxed',
|
|
77
|
+
hideSteppers && 'ldt-number-field--bare',
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{#if !hideSteppers}<button
|
|
82
|
+
class="ldt-number-field__button"
|
|
83
|
+
type="button"
|
|
84
|
+
{disabled}
|
|
85
|
+
aria-label={decrementLabel(label)}
|
|
86
|
+
aria-disabled={atMin || readonly || undefined}
|
|
87
|
+
onclick={() => !atMin && !locked && nudge(-1)}>−</button
|
|
88
|
+
>{/if}
|
|
62
89
|
<input
|
|
63
90
|
bind:this={ref}
|
|
64
91
|
class={cx('ldt-number-field__input', inputClass)}
|
|
65
92
|
type="number"
|
|
66
|
-
bind:value
|
|
93
|
+
bind:value={() => value, (next) => (value = next as V)}
|
|
67
94
|
min={Number.isFinite(min) ? min : undefined}
|
|
68
95
|
max={Number.isFinite(max) ? max : undefined}
|
|
69
96
|
{step}
|
|
70
97
|
{disabled}
|
|
98
|
+
{readonly}
|
|
71
99
|
aria-label={label}
|
|
72
100
|
{...rest}
|
|
73
101
|
onchange={(event) => {
|
|
@@ -78,12 +106,12 @@
|
|
|
78
106
|
onchange?.(event);
|
|
79
107
|
}}
|
|
80
108
|
/>
|
|
81
|
-
<button
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
{#if !hideSteppers}<button
|
|
110
|
+
class="ldt-number-field__button"
|
|
111
|
+
type="button"
|
|
112
|
+
{disabled}
|
|
113
|
+
aria-label={incrementLabel(label)}
|
|
114
|
+
aria-disabled={atMax || readonly || undefined}
|
|
115
|
+
onclick={() => !atMax && !locked && nudge(1)}>+</button
|
|
116
|
+
>{/if}
|
|
89
117
|
</div>
|
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
-
|
|
3
|
-
value
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
declare function $$render<V extends number | null = number>(): {
|
|
3
|
+
props: Omit<HTMLInputAttributes, "value" | "type" | "step" | "max" | "min"> & {
|
|
4
|
+
/** A number, or `null` for an empty field when `nullable` is set. */
|
|
5
|
+
value?: V;
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
/** Accessible name of the field; also used to name the two spin buttons. */
|
|
10
|
+
label: string;
|
|
11
|
+
decrementLabel?: (label: string) => string;
|
|
12
|
+
incrementLabel?: (label: string) => string;
|
|
13
|
+
/** Draws the full box instead of the default underline. */
|
|
14
|
+
boxed?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Let the field be empty: clearing it yields `null` instead of snapping to the low bound.
|
|
17
|
+
* For optional quantities where "not set" differs from zero. Type `value` as
|
|
18
|
+
* `number | null` to bind it.
|
|
19
|
+
*/
|
|
20
|
+
nullable?: boolean;
|
|
21
|
+
/** Hide the − and + buttons, for dense tables. The keyboard still steps with the arrows. */
|
|
22
|
+
hideSteppers?: boolean;
|
|
23
|
+
onValueChange?: (value: V) => void;
|
|
24
|
+
/** `class` lands on the wrapper; use `inputClass` to reach the input. */
|
|
25
|
+
class?: string;
|
|
26
|
+
inputClass?: string;
|
|
27
|
+
ref?: HTMLInputElement | null;
|
|
28
|
+
};
|
|
29
|
+
exports: {};
|
|
30
|
+
bindings: "value" | "ref";
|
|
31
|
+
slots: {};
|
|
32
|
+
events: {};
|
|
33
|
+
};
|
|
34
|
+
declare class __sveltets_Render<V extends number | null = number> {
|
|
35
|
+
props(): ReturnType<typeof $$render<V>>['props'];
|
|
36
|
+
events(): ReturnType<typeof $$render<V>>['events'];
|
|
37
|
+
slots(): ReturnType<typeof $$render<V>>['slots'];
|
|
38
|
+
bindings(): "value" | "ref";
|
|
39
|
+
exports(): {};
|
|
18
40
|
}
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
interface $$IsomorphicComponent {
|
|
42
|
+
new <V extends number | null = number>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<V>['props']>, ReturnType<__sveltets_Render<V>['events']>, ReturnType<__sveltets_Render<V>['slots']>> & {
|
|
43
|
+
$$bindings?: ReturnType<__sveltets_Render<V>['bindings']>;
|
|
44
|
+
} & ReturnType<__sveltets_Render<V>['exports']>;
|
|
45
|
+
<V extends number | null = number>(internal: unknown, props: ReturnType<__sveltets_Render<V>['props']> & {}): ReturnType<__sveltets_Render<V>['exports']>;
|
|
46
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
47
|
+
}
|
|
48
|
+
declare const NumberField: $$IsomorphicComponent;
|
|
49
|
+
type NumberField<V extends number | null = number> = InstanceType<typeof NumberField<V>>;
|
|
21
50
|
export default NumberField;
|