@viniaraujo68/plinth 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/anchoring.d.ts +61 -0
- package/dist/anchoring.js +151 -0
- package/dist/async-action.svelte.d.ts +37 -0
- package/dist/async-action.svelte.js +64 -0
- package/dist/attachments/index.d.ts +2 -0
- package/dist/attachments/index.js +2 -0
- package/dist/attachments/sortable.d.ts +6 -0
- package/dist/attachments/sortable.js +227 -0
- package/dist/attachments/tooltip.d.ts +37 -0
- package/dist/attachments/tooltip.js +166 -0
- package/dist/components/AsyncButton.svelte +49 -0
- package/dist/components/AsyncButton.svelte.d.ts +32 -0
- package/dist/components/Combobox.svelte +380 -0
- package/dist/components/Combobox.svelte.d.ts +74 -0
- package/dist/components/Copyable.svelte +116 -0
- package/dist/components/Copyable.svelte.d.ts +37 -0
- package/dist/components/DateRangePicker.svelte +266 -0
- package/dist/components/DateRangePicker.svelte.d.ts +66 -0
- package/dist/components/Dialog.svelte +102 -0
- package/dist/components/Dialog.svelte.d.ts +38 -0
- package/dist/components/DragHandle.svelte +26 -0
- package/dist/components/DragHandle.svelte.d.ts +7 -0
- package/dist/components/Dropdown.svelte +204 -0
- package/dist/components/Dropdown.svelte.d.ts +49 -0
- package/dist/components/ErrorDisplay.svelte +59 -0
- package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
- package/dist/components/ErrorPage.svelte +119 -0
- package/dist/components/ErrorPage.svelte.d.ts +52 -0
- package/dist/components/JsonInput.svelte +106 -0
- package/dist/components/JsonInput.svelte.d.ts +40 -0
- package/dist/components/Listbox.svelte +257 -0
- package/dist/components/Listbox.svelte.d.ts +78 -0
- package/dist/components/LoadingButton.svelte +36 -0
- package/dist/components/LoadingButton.svelte.d.ts +22 -0
- package/dist/components/Logo.svelte +112 -0
- package/dist/components/Logo.svelte.d.ts +29 -0
- package/dist/components/Modal.svelte +187 -0
- package/dist/components/Modal.svelte.d.ts +62 -0
- package/dist/components/MultiSelect.svelte +256 -0
- package/dist/components/MultiSelect.svelte.d.ts +89 -0
- package/dist/components/OptionPanel.svelte +241 -0
- package/dist/components/OptionPanel.svelte.d.ts +51 -0
- package/dist/components/Paginator.svelte +205 -0
- package/dist/components/Paginator.svelte.d.ts +48 -0
- package/dist/components/RefreshButton.svelte +246 -0
- package/dist/components/RefreshButton.svelte.d.ts +46 -0
- package/dist/components/SegmentedControl.svelte +147 -0
- package/dist/components/SegmentedControl.svelte.d.ts +71 -0
- package/dist/components/Select.svelte +312 -0
- package/dist/components/Select.svelte.d.ts +68 -0
- package/dist/components/Skeleton.svelte +97 -0
- package/dist/components/Skeleton.svelte.d.ts +32 -0
- package/dist/components/Tooltip.svelte +60 -0
- package/dist/components/Tooltip.svelte.d.ts +58 -0
- package/dist/components/date-range.d.ts +101 -0
- package/dist/components/date-range.js +144 -0
- package/dist/components/index.d.ts +26 -0
- package/dist/components/index.js +38 -0
- package/dist/components/json-text.d.ts +22 -0
- package/dist/components/json-text.js +22 -0
- package/dist/components/pick.d.ts +18 -0
- package/dist/components/pick.js +18 -0
- package/dist/components/segmented.d.ts +7 -0
- package/dist/components/segmented.js +1 -0
- package/dist/components/select.d.ts +66 -0
- package/dist/components/select.js +90 -0
- package/dist/confirm/Confirmer.svelte +194 -0
- package/dist/confirm/Confirmer.svelte.d.ts +45 -0
- package/dist/confirm/challenge.d.ts +16 -0
- package/dist/confirm/challenge.js +16 -0
- package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
- package/dist/confirm/confirm-manager.svelte.js +149 -0
- package/dist/confirm/index.d.ts +3 -0
- package/dist/confirm/index.js +3 -0
- package/dist/error.d.ts +25 -0
- package/dist/error.js +39 -0
- package/dist/formatters.d.ts +95 -0
- package/dist/formatters.js +146 -0
- package/dist/http.d.ts +68 -0
- package/dist/http.js +138 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +14 -0
- package/dist/routing.svelte.d.ts +169 -0
- package/dist/routing.svelte.js +208 -0
- package/dist/shell/AppShell.svelte +874 -0
- package/dist/shell/AppShell.svelte.d.ts +93 -0
- package/dist/shell/Breadcrumbs.svelte +78 -0
- package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
- package/dist/shell/crumbs.d.ts +23 -0
- package/dist/shell/crumbs.js +1 -0
- package/dist/shell/index.d.ts +3 -0
- package/dist/shell/index.js +6 -0
- package/dist/table/ColumnMenu.svelte +249 -0
- package/dist/table/ColumnMenu.svelte.d.ts +47 -0
- package/dist/table/DataTable.svelte +643 -0
- package/dist/table/DataTable.svelte.d.ts +153 -0
- package/dist/table/columns.d.ts +55 -0
- package/dist/table/columns.js +153 -0
- package/dist/table/index.d.ts +4 -0
- package/dist/table/index.js +8 -0
- package/dist/table/sort.d.ts +48 -0
- package/dist/table/sort.js +139 -0
- package/dist/table/types.d.ts +93 -0
- package/dist/table/types.js +1 -0
- package/dist/theme/ThemeController.svelte +54 -0
- package/dist/theme/ThemeController.svelte.d.ts +27 -0
- package/dist/theme/ThemeToggle.svelte +119 -0
- package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
- package/dist/theme/css-color.d.ts +23 -0
- package/dist/theme/css-color.js +31 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +5 -0
- package/dist/theme/theme.css +410 -0
- package/dist/theme/theme.svelte.d.ts +54 -0
- package/dist/theme/theme.svelte.js +58 -0
- package/dist/toast/Toaster.svelte +257 -0
- package/dist/toast/Toaster.svelte.d.ts +35 -0
- package/dist/toast/index.d.ts +2 -0
- package/dist/toast/index.js +2 -0
- package/dist/toast/toast-manager.svelte.d.ts +99 -0
- package/dist/toast/toast-manager.svelte.js +176 -0
- package/dist/user/ScopedComponent.svelte +47 -0
- package/dist/user/ScopedComponent.svelte.d.ts +29 -0
- package/dist/user/anonymous.d.ts +21 -0
- package/dist/user/anonymous.js +26 -0
- package/dist/user/context.d.ts +55 -0
- package/dist/user/context.js +13 -0
- package/dist/user/index.d.ts +3 -0
- package/dist/user/index.js +6 -0
- package/package.json +133 -4
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<script lang="ts" generics="O extends SelectOption = SelectOption">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { Attachment } from "svelte/attachments";
|
|
4
|
+
import type { ClassValue, HTMLAttributes } from "svelte/elements";
|
|
5
|
+
import {
|
|
6
|
+
filterOptions,
|
|
7
|
+
isTypedKey,
|
|
8
|
+
nearestSelectableIndex,
|
|
9
|
+
nextSelectableIndex,
|
|
10
|
+
Typeahead,
|
|
11
|
+
typeaheadMatch,
|
|
12
|
+
type SelectOption,
|
|
13
|
+
} from "./select.js";
|
|
14
|
+
|
|
15
|
+
type Props = Omit<
|
|
16
|
+
HTMLAttributes<HTMLDivElement>,
|
|
17
|
+
"children" | "class" | "onkeydown" | "role" | "tabindex"
|
|
18
|
+
> & {
|
|
19
|
+
/** The rows, in order. A new list keeps the highlighted row, or its position once it is gone. */
|
|
20
|
+
options: readonly O[];
|
|
21
|
+
/**
|
|
22
|
+
* The checked rows: one value, or an array for a list that picks several. A single value is
|
|
23
|
+
* where the highlight starts, and it goes back there whenever the value changes.
|
|
24
|
+
*/
|
|
25
|
+
value?: string | readonly string[] | null;
|
|
26
|
+
/** A row was picked, by a click, Enter or Space. A disabled row is never reported. */
|
|
27
|
+
onpick: (option: O) => void;
|
|
28
|
+
/** Adds a search box above the list, which then holds the focus. */
|
|
29
|
+
searchable?: boolean;
|
|
30
|
+
/** The search text. Bindable. */
|
|
31
|
+
query?: string;
|
|
32
|
+
/** What the search reads for a row. Defaults to its label. */
|
|
33
|
+
searchText?: (option: O) => string;
|
|
34
|
+
searchPlaceholder?: string;
|
|
35
|
+
/** Shown in place of the rows when there are none to show. */
|
|
36
|
+
emptyLabel?: string;
|
|
37
|
+
/** Marks the list busy, and shows `loadingLabel` while it has no rows. */
|
|
38
|
+
loading?: boolean;
|
|
39
|
+
loadingLabel?: string;
|
|
40
|
+
/** Takes the focus as the popover or dialog holding the list opens. */
|
|
41
|
+
autofocus?: boolean;
|
|
42
|
+
/** Marks the checked rows with a ✓. Turn it off for a list of actions rather than a choice. */
|
|
43
|
+
checkmarks?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Renders a row in place of its label. The row is already a `role="option"` button, so the
|
|
46
|
+
* snippet must not hold buttons, links or anything else interactive.
|
|
47
|
+
*/
|
|
48
|
+
option?: Snippet<[O]>;
|
|
49
|
+
/** Classes for the whole control. The list is where every other attribute goes. */
|
|
50
|
+
class?: ClassValue;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
options,
|
|
55
|
+
value = null,
|
|
56
|
+
onpick,
|
|
57
|
+
searchable = false,
|
|
58
|
+
query = $bindable(""),
|
|
59
|
+
searchText = (candidate) => candidate.label,
|
|
60
|
+
searchPlaceholder = "Search…",
|
|
61
|
+
emptyLabel = "No matches",
|
|
62
|
+
loading = false,
|
|
63
|
+
loadingLabel = "Loading…",
|
|
64
|
+
autofocus = false,
|
|
65
|
+
checkmarks = true,
|
|
66
|
+
option,
|
|
67
|
+
class: className,
|
|
68
|
+
...rest
|
|
69
|
+
}: Props = $props();
|
|
70
|
+
|
|
71
|
+
const uid = $props.id();
|
|
72
|
+
const listId = $derived(rest.id ?? `plinth-listbox-${uid}`);
|
|
73
|
+
const optionId = (index: number) => `${listId}-option-${index}`;
|
|
74
|
+
|
|
75
|
+
const visible = $derived(searchable ? filterOptions(options, query, searchText) : options);
|
|
76
|
+
const multiple = $derived(Array.isArray(value));
|
|
77
|
+
const checked = $derived(new Set<string | null>([value].flat()));
|
|
78
|
+
|
|
79
|
+
let highlighted = $state.raw<{ value: string; index: number; from: typeof value }>();
|
|
80
|
+
|
|
81
|
+
const activeIndex = $derived.by(() => {
|
|
82
|
+
if (highlighted && (multiple || highlighted.from === value)) {
|
|
83
|
+
const kept = visible.findIndex(
|
|
84
|
+
(candidate) => candidate.value === highlighted!.value && !candidate.disabled,
|
|
85
|
+
);
|
|
86
|
+
return kept >= 0 ? kept : nearestSelectableIndex(visible, highlighted.index);
|
|
87
|
+
}
|
|
88
|
+
const start = multiple
|
|
89
|
+
? -1
|
|
90
|
+
: visible.findIndex((candidate) => candidate.value === value && !candidate.disabled);
|
|
91
|
+
return start >= 0 ? start : nearestSelectableIndex(visible, 0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const activeId = $derived(activeIndex >= 0 ? optionId(activeIndex) : undefined);
|
|
95
|
+
|
|
96
|
+
const scrollToActive: Attachment<HTMLElement> = (list) => {
|
|
97
|
+
const row = list.children[activeIndex] as HTMLElement | undefined;
|
|
98
|
+
if (!row) return;
|
|
99
|
+
const bottom = row.offsetTop + row.offsetHeight - list.clientHeight;
|
|
100
|
+
if (row.offsetTop < list.scrollTop) list.scrollTop = row.offsetTop;
|
|
101
|
+
else if (bottom > list.scrollTop) list.scrollTop = bottom;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const scrollOnResize: Attachment<HTMLElement> = (list) => {
|
|
105
|
+
const observer = new ResizeObserver(() => scrollToActive(list));
|
|
106
|
+
observer.observe(list);
|
|
107
|
+
return () => observer.disconnect();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const highlight = (index: number) => {
|
|
111
|
+
const candidate = visible[index];
|
|
112
|
+
if (candidate) highlighted = { value: candidate.value, index, from: value };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const pick = (index: number) => {
|
|
116
|
+
const candidate = visible[index];
|
|
117
|
+
if (!candidate || candidate.disabled) return;
|
|
118
|
+
highlight(index);
|
|
119
|
+
onpick(candidate);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const typeahead = new Typeahead();
|
|
123
|
+
const jumpTo = (char: string) =>
|
|
124
|
+
highlight(typeaheadMatch(visible, activeIndex, typeahead.add(char)));
|
|
125
|
+
|
|
126
|
+
const onkeydown = (event: KeyboardEvent) => {
|
|
127
|
+
if (event.isComposing) return;
|
|
128
|
+
switch (event.key) {
|
|
129
|
+
case "ArrowDown":
|
|
130
|
+
highlight(nextSelectableIndex(visible, activeIndex + 1, 1));
|
|
131
|
+
break;
|
|
132
|
+
case "ArrowUp":
|
|
133
|
+
highlight(nextSelectableIndex(visible, activeIndex - 1, -1));
|
|
134
|
+
break;
|
|
135
|
+
case "Home":
|
|
136
|
+
if (searchable) return;
|
|
137
|
+
highlight(nextSelectableIndex(visible, 0, 1));
|
|
138
|
+
break;
|
|
139
|
+
case "End":
|
|
140
|
+
if (searchable) return;
|
|
141
|
+
highlight(nextSelectableIndex(visible, visible.length - 1, -1));
|
|
142
|
+
break;
|
|
143
|
+
case " ":
|
|
144
|
+
if (searchable) return;
|
|
145
|
+
if (typeahead.typing) {
|
|
146
|
+
jumpTo(" ");
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
pick(activeIndex);
|
|
150
|
+
break;
|
|
151
|
+
case "Enter":
|
|
152
|
+
pick(activeIndex);
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
if (searchable || !isTypedKey(event)) return;
|
|
156
|
+
jumpTo(event.key);
|
|
157
|
+
}
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
};
|
|
160
|
+
</script>
|
|
161
|
+
|
|
162
|
+
<!--
|
|
163
|
+
@component
|
|
164
|
+
A list to pick from in place: click a row, or walk it with the arrows, Home and End or the first
|
|
165
|
+
letters of a label, and pick with Enter or Space. `value` checks the selected row and the highlight
|
|
166
|
+
starts on it; an array of values checks several, for a list that picks many. Disabled rows show,
|
|
167
|
+
and the keyboard and the pointer step over them.
|
|
168
|
+
|
|
169
|
+
`searchable` adds a search box that keeps the focus while the arrows walk the list under it. It
|
|
170
|
+
folds case and accents, and a row matches when it contains every word typed; `searchText` decides
|
|
171
|
+
what it reads, so a row can be found by text it does not show.
|
|
172
|
+
|
|
173
|
+
The focus stays inside: on the search box, or on the list itself. The highlighted row is pointed at
|
|
174
|
+
with `aria-activedescendant` and never focused. Put the list in a popover with `autofocus` and it
|
|
175
|
+
takes the focus as the popover opens — which is what a `Dropdown` does with it.
|
|
176
|
+
|
|
177
|
+
```svelte
|
|
178
|
+
<Listbox options={statuses} value={status} onpick={(row) => (status = row.value)} aria-label="Status" />
|
|
179
|
+
```
|
|
180
|
+
-->
|
|
181
|
+
|
|
182
|
+
<div class={["plinth-listbox flex flex-col", className]}>
|
|
183
|
+
{#if searchable}
|
|
184
|
+
<input
|
|
185
|
+
bind:value={query}
|
|
186
|
+
type="text"
|
|
187
|
+
class="input mb-1 w-full input-sm"
|
|
188
|
+
placeholder={searchPlaceholder}
|
|
189
|
+
autocomplete="off"
|
|
190
|
+
spellcheck="false"
|
|
191
|
+
{autofocus}
|
|
192
|
+
role="combobox"
|
|
193
|
+
aria-expanded="true"
|
|
194
|
+
aria-controls={listId}
|
|
195
|
+
aria-activedescendant={activeId}
|
|
196
|
+
aria-autocomplete="list"
|
|
197
|
+
aria-label={searchPlaceholder}
|
|
198
|
+
oninput={() => (highlighted = undefined)}
|
|
199
|
+
{onkeydown}
|
|
200
|
+
/>
|
|
201
|
+
{/if}
|
|
202
|
+
|
|
203
|
+
<div
|
|
204
|
+
{...rest}
|
|
205
|
+
{@attach scrollToActive}
|
|
206
|
+
{@attach scrollOnResize}
|
|
207
|
+
id={listId}
|
|
208
|
+
role="listbox"
|
|
209
|
+
tabindex={searchable ? -1 : 0}
|
|
210
|
+
aria-busy={loading || undefined}
|
|
211
|
+
aria-multiselectable={multiple || undefined}
|
|
212
|
+
aria-activedescendant={searchable ? undefined : activeId}
|
|
213
|
+
autofocus={!searchable && autofocus}
|
|
214
|
+
class="pointer-events-auto relative max-h-64 overflow-y-auto overscroll-contain p-1 focus-visible:outline-none"
|
|
215
|
+
onkeydown={searchable ? undefined : onkeydown}
|
|
216
|
+
onmousedown={(event) => {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
if (!searchable) event.currentTarget.focus();
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{#each visible as candidate, index (candidate.value)}
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
id={optionId(index)}
|
|
225
|
+
role="option"
|
|
226
|
+
tabindex="-1"
|
|
227
|
+
disabled={candidate.disabled}
|
|
228
|
+
aria-selected={checked.has(candidate.value)}
|
|
229
|
+
aria-disabled={candidate.disabled}
|
|
230
|
+
class={[
|
|
231
|
+
"flex min-h-11 w-full cursor-pointer items-center gap-2 rounded-field px-3 py-2 text-start text-sm",
|
|
232
|
+
index === activeIndex && "bg-base-content/10",
|
|
233
|
+
candidate.disabled && "cursor-not-allowed opacity-40",
|
|
234
|
+
]}
|
|
235
|
+
onpointermove={() => {
|
|
236
|
+
if (!candidate.disabled && index !== activeIndex) highlight(index);
|
|
237
|
+
}}
|
|
238
|
+
onclick={() => pick(index)}
|
|
239
|
+
>
|
|
240
|
+
<span class="min-w-0 flex-1 wrap-anywhere">
|
|
241
|
+
{#if option}{@render option(candidate)}{:else}{candidate.label}{/if}
|
|
242
|
+
</span>
|
|
243
|
+
{#if checkmarks}
|
|
244
|
+
<span class="flex-none text-xs" aria-hidden="true">
|
|
245
|
+
{checked.has(candidate.value) ? "✓" : ""}
|
|
246
|
+
</span>
|
|
247
|
+
{/if}
|
|
248
|
+
</button>
|
|
249
|
+
{/each}
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{#if visible.length === 0}
|
|
253
|
+
<p role="status" class="px-3 py-4 text-center text-sm text-base-content/60">
|
|
254
|
+
{loading ? loadingLabel : emptyLabel}
|
|
255
|
+
</p>
|
|
256
|
+
{/if}
|
|
257
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ClassValue, HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { type SelectOption } from "./select.js";
|
|
4
|
+
declare function $$render<O extends SelectOption = SelectOption>(): {
|
|
5
|
+
props: Omit<HTMLAttributes<HTMLDivElement>, "children" | "class" | "tabindex" | "role" | "onkeydown"> & {
|
|
6
|
+
/** The rows, in order. A new list keeps the highlighted row, or its position once it is gone. */
|
|
7
|
+
options: readonly O[];
|
|
8
|
+
/**
|
|
9
|
+
* The checked rows: one value, or an array for a list that picks several. A single value is
|
|
10
|
+
* where the highlight starts, and it goes back there whenever the value changes.
|
|
11
|
+
*/
|
|
12
|
+
value?: string | readonly string[] | null;
|
|
13
|
+
/** A row was picked, by a click, Enter or Space. A disabled row is never reported. */
|
|
14
|
+
onpick: (option: O) => void;
|
|
15
|
+
/** Adds a search box above the list, which then holds the focus. */
|
|
16
|
+
searchable?: boolean;
|
|
17
|
+
/** The search text. Bindable. */
|
|
18
|
+
query?: string;
|
|
19
|
+
/** What the search reads for a row. Defaults to its label. */
|
|
20
|
+
searchText?: (option: O) => string;
|
|
21
|
+
searchPlaceholder?: string;
|
|
22
|
+
/** Shown in place of the rows when there are none to show. */
|
|
23
|
+
emptyLabel?: string;
|
|
24
|
+
/** Marks the list busy, and shows `loadingLabel` while it has no rows. */
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
loadingLabel?: string;
|
|
27
|
+
/** Takes the focus as the popover or dialog holding the list opens. */
|
|
28
|
+
autofocus?: boolean;
|
|
29
|
+
/** Marks the checked rows with a ✓. Turn it off for a list of actions rather than a choice. */
|
|
30
|
+
checkmarks?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Renders a row in place of its label. The row is already a `role="option"` button, so the
|
|
33
|
+
* snippet must not hold buttons, links or anything else interactive.
|
|
34
|
+
*/
|
|
35
|
+
option?: Snippet<[O]>;
|
|
36
|
+
/** Classes for the whole control. The list is where every other attribute goes. */
|
|
37
|
+
class?: ClassValue;
|
|
38
|
+
};
|
|
39
|
+
exports: {};
|
|
40
|
+
bindings: "query";
|
|
41
|
+
slots: {};
|
|
42
|
+
events: {};
|
|
43
|
+
};
|
|
44
|
+
declare class __sveltets_Render<O extends SelectOption = SelectOption> {
|
|
45
|
+
props(): ReturnType<typeof $$render<O>>['props'];
|
|
46
|
+
events(): ReturnType<typeof $$render<O>>['events'];
|
|
47
|
+
slots(): ReturnType<typeof $$render<O>>['slots'];
|
|
48
|
+
bindings(): "query";
|
|
49
|
+
exports(): {};
|
|
50
|
+
}
|
|
51
|
+
interface $$IsomorphicComponent {
|
|
52
|
+
new <O extends SelectOption = SelectOption>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<O>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<O>['props']>, ReturnType<__sveltets_Render<O>['events']>, ReturnType<__sveltets_Render<O>['slots']>> & {
|
|
53
|
+
$$bindings?: ReturnType<__sveltets_Render<O>['bindings']>;
|
|
54
|
+
} & ReturnType<__sveltets_Render<O>['exports']>;
|
|
55
|
+
<O extends SelectOption = SelectOption>(internal: unknown, props: ReturnType<__sveltets_Render<O>['props']> & {}): ReturnType<__sveltets_Render<O>['exports']>;
|
|
56
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A list to pick from in place: click a row, or walk it with the arrows, Home and End or the first
|
|
60
|
+
* letters of a label, and pick with Enter or Space. `value` checks the selected row and the highlight
|
|
61
|
+
* starts on it; an array of values checks several, for a list that picks many. Disabled rows show,
|
|
62
|
+
* and the keyboard and the pointer step over them.
|
|
63
|
+
*
|
|
64
|
+
* `searchable` adds a search box that keeps the focus while the arrows walk the list under it. It
|
|
65
|
+
* folds case and accents, and a row matches when it contains every word typed; `searchText` decides
|
|
66
|
+
* what it reads, so a row can be found by text it does not show.
|
|
67
|
+
*
|
|
68
|
+
* The focus stays inside: on the search box, or on the list itself. The highlighted row is pointed at
|
|
69
|
+
* with `aria-activedescendant` and never focused. Put the list in a popover with `autofocus` and it
|
|
70
|
+
* takes the focus as the popover opens — which is what a `Dropdown` does with it.
|
|
71
|
+
*
|
|
72
|
+
* ```svelte
|
|
73
|
+
* <Listbox options={statuses} value={status} onpick={(row) => (status = row.value)} aria-label="Status" />
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare const Listbox: $$IsomorphicComponent;
|
|
77
|
+
type Listbox<O extends SelectOption = SelectOption> = InstanceType<typeof Listbox<O>>;
|
|
78
|
+
export default Listbox;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLButtonAttributes {
|
|
6
|
+
children: Snippet;
|
|
7
|
+
/** Shows the spinner and blocks the button. The caller owns this flag. */
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// `type` defaults to "submit" in HTML, so a button dropped into a form to run a handler also
|
|
12
|
+
// submits that form. This one asks to be told when that is what you meant.
|
|
13
|
+
let { children, loading = false, disabled, type = "button", ...rest }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
@component
|
|
18
|
+
A button that grows a spinner and stops accepting clicks while `loading` is set.
|
|
19
|
+
|
|
20
|
+
It is a plain `<button>` with everything forwarded, so it takes the app's own classes — the
|
|
21
|
+
library never decides that a button is `btn-primary`. The label is kept on screen next to the
|
|
22
|
+
spinner instead of being swapped out, because a button that changes width mid-click moves whatever
|
|
23
|
+
sits next to it.
|
|
24
|
+
|
|
25
|
+
```svelte
|
|
26
|
+
<LoadingButton class="btn btn-primary" loading={saving} onclick={save}>Save</LoadingButton>
|
|
27
|
+
```
|
|
28
|
+
-->
|
|
29
|
+
|
|
30
|
+
<button {...rest} {type} disabled={loading || disabled} aria-busy={loading}>
|
|
31
|
+
{#if loading}
|
|
32
|
+
<!-- Decorative: `aria-busy` above is what a screen reader announces. -->
|
|
33
|
+
<span class="loading loading-xs loading-spinner" aria-hidden="true"></span>
|
|
34
|
+
{/if}
|
|
35
|
+
{@render children()}
|
|
36
|
+
</button>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
3
|
+
interface Props extends HTMLButtonAttributes {
|
|
4
|
+
children: Snippet;
|
|
5
|
+
/** Shows the spinner and blocks the button. The caller owns this flag. */
|
|
6
|
+
loading?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A button that grows a spinner and stops accepting clicks while `loading` is set.
|
|
10
|
+
*
|
|
11
|
+
* It is a plain `<button>` with everything forwarded, so it takes the app's own classes — the
|
|
12
|
+
* library never decides that a button is `btn-primary`. The label is kept on screen next to the
|
|
13
|
+
* spinner instead of being swapped out, because a button that changes width mid-click moves whatever
|
|
14
|
+
* sits next to it.
|
|
15
|
+
*
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <LoadingButton class="btn btn-primary" loading={saving} onclick={save}>Save</LoadingButton>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare const LoadingButton: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type LoadingButton = ReturnType<typeof LoadingButton>;
|
|
22
|
+
export default LoadingButton;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { PICK_PATH } from "./pick.js";
|
|
4
|
+
|
|
5
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
/** Runs the orbit. Leave it off for a static placeholder mark. */
|
|
7
|
+
animated?: boolean;
|
|
8
|
+
/** Stroke color. Defaults to the current theme's ink. */
|
|
9
|
+
color?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let { class: className, animated = false, color, ...rest }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<!--
|
|
16
|
+
@component
|
|
17
|
+
A guitar pick drawn as a single outline stroke — the library's loading indicator, and the mark it
|
|
18
|
+
falls back to when there is nothing to show yet. Animated, a lit third of the outline orbits the
|
|
19
|
+
perimeter; standing still, the outline is unbroken.
|
|
20
|
+
|
|
21
|
+
It sizes itself from the inherited font size, so `class="text-4xl"` is the whole API for making it
|
|
22
|
+
bigger, and it colors itself from `--color-base-content`, so it stays legible in either theme and
|
|
23
|
+
inside a `data-theme` subtree.
|
|
24
|
+
|
|
25
|
+
It renders no text and carries no role, which makes it silent to assistive technology on purpose:
|
|
26
|
+
whatever it is standing in for is what should be announced, so wrap it in the live region or
|
|
27
|
+
`aria-busy` container that owns the loading state.
|
|
28
|
+
|
|
29
|
+
```svelte
|
|
30
|
+
<div class="grid place-items-center" role="status" aria-label="Loading releases">
|
|
31
|
+
<Logo class="text-4xl" animated />
|
|
32
|
+
</div>
|
|
33
|
+
```
|
|
34
|
+
-->
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
{...rest}
|
|
38
|
+
class={["plinth-logo", className]}
|
|
39
|
+
data-animated={animated ? "" : undefined}
|
|
40
|
+
style:--logo-color={color}
|
|
41
|
+
>
|
|
42
|
+
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false">
|
|
43
|
+
<path d={PICK_PATH} />
|
|
44
|
+
</svg>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
@keyframes orbit {
|
|
49
|
+
from {
|
|
50
|
+
stroke-dashoffset: 0px;
|
|
51
|
+
}
|
|
52
|
+
to {
|
|
53
|
+
stroke-dashoffset: -71.648px;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@keyframes pulse {
|
|
58
|
+
0%,
|
|
59
|
+
100% {
|
|
60
|
+
opacity: 1;
|
|
61
|
+
}
|
|
62
|
+
50% {
|
|
63
|
+
opacity: 0.35;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.plinth-logo {
|
|
68
|
+
/* Full ink rather than the faint wash a solid mark could afford: a 3.5-wide stroke in a 32 box
|
|
69
|
+
covers little area, and the animated state lights only a third of that perimeter at a time,
|
|
70
|
+
so a single-digit-percent wash would leave the running dash invisible. The outline stays
|
|
71
|
+
quiet at full strength because it is hollow — the surface reads through the middle, which is
|
|
72
|
+
what keeps it from being mistaken for content. */
|
|
73
|
+
--logo-color: var(--color-base-content);
|
|
74
|
+
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
|
|
79
|
+
svg {
|
|
80
|
+
display: block;
|
|
81
|
+
width: 1em;
|
|
82
|
+
height: 1em;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
path {
|
|
86
|
+
fill: none;
|
|
87
|
+
stroke: var(--logo-color);
|
|
88
|
+
stroke-width: 3.5;
|
|
89
|
+
stroke-linecap: round;
|
|
90
|
+
stroke-linejoin: round;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Presence, not value: `data-animated={false}` would still serialise to the string "false"
|
|
94
|
+
and match an attribute selector, so the attribute is omitted entirely when it is off. */
|
|
95
|
+
&[data-animated] path {
|
|
96
|
+
/* 23.883 + 47.765 = 71.648, the path's measured length via `getTotalLength` in chromium,
|
|
97
|
+
which is also the distance the keyframes travel. The three numbers are one fact stated
|
|
98
|
+
three times and have to move together: a dash period that is not exactly the path length
|
|
99
|
+
makes the pattern restart at the path's start point — the tip — so the dash visibly snaps
|
|
100
|
+
there once per cycle. Change the `d` and all three are wrong until re-measured. */
|
|
101
|
+
stroke-dasharray: 23.883px 47.765px;
|
|
102
|
+
animation: orbit 1.7s linear infinite;
|
|
103
|
+
|
|
104
|
+
/* A fully static mark would remove the only feedback that something is loading, so the
|
|
105
|
+
orbit degrades to a pulse rather than to nothing. */
|
|
106
|
+
@media (prefers-reduced-motion: reduce) {
|
|
107
|
+
stroke-dasharray: none;
|
|
108
|
+
animation: pulse 1.7s ease-in-out infinite;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
/** Runs the orbit. Leave it off for a static placeholder mark. */
|
|
4
|
+
animated?: boolean;
|
|
5
|
+
/** Stroke color. Defaults to the current theme's ink. */
|
|
6
|
+
color?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A guitar pick drawn as a single outline stroke — the library's loading indicator, and the mark it
|
|
10
|
+
* falls back to when there is nothing to show yet. Animated, a lit third of the outline orbits the
|
|
11
|
+
* perimeter; standing still, the outline is unbroken.
|
|
12
|
+
*
|
|
13
|
+
* It sizes itself from the inherited font size, so `class="text-4xl"` is the whole API for making it
|
|
14
|
+
* bigger, and it colors itself from `--color-base-content`, so it stays legible in either theme and
|
|
15
|
+
* inside a `data-theme` subtree.
|
|
16
|
+
*
|
|
17
|
+
* It renders no text and carries no role, which makes it silent to assistive technology on purpose:
|
|
18
|
+
* whatever it is standing in for is what should be announced, so wrap it in the live region or
|
|
19
|
+
* `aria-busy` container that owns the loading state.
|
|
20
|
+
*
|
|
21
|
+
* ```svelte
|
|
22
|
+
* <div class="grid place-items-center" role="status" aria-label="Loading releases">
|
|
23
|
+
* <Logo class="text-4xl" animated />
|
|
24
|
+
* </div>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare const Logo: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type Logo = ReturnType<typeof Logo>;
|
|
29
|
+
export default Logo;
|