@lyeve-labs/ui-kit 0.12.1 → 0.13.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/dist/components/Autocomplete.svelte +191 -125
- package/dist/components/Autocomplete.svelte.d.ts +29 -8
- package/dist/components/Card.svelte +43 -2
- package/dist/components/Card.svelte.d.ts +24 -2
- package/dist/components/Checkbox.svelte +174 -63
- package/dist/components/Checkbox.svelte.d.ts +20 -3
- package/dist/components/CheckboxGroup.svelte +162 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +51 -0
- package/dist/components/Collapsible.svelte +142 -0
- package/dist/components/Collapsible.svelte.d.ts +32 -0
- package/dist/components/CopyButton.svelte +126 -0
- package/dist/components/CopyButton.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +48 -6
- package/dist/components/DateTimePicker.svelte +337 -0
- package/dist/components/DateTimePicker.svelte.d.ts +26 -0
- package/dist/components/DescriptionList.svelte +78 -0
- package/dist/components/DescriptionList.svelte.d.ts +34 -0
- package/dist/components/Field.svelte +104 -0
- package/dist/components/Field.svelte.d.ts +46 -0
- package/dist/components/FileInput.svelte +5 -2
- package/dist/components/FormMessage.svelte +85 -0
- package/dist/components/FormMessage.svelte.d.ts +11 -0
- package/dist/components/Input.svelte +1 -1
- package/dist/components/Label.svelte +7 -1
- package/dist/components/Label.svelte.d.ts +6 -0
- package/dist/components/MultiSelect.svelte +199 -109
- package/dist/components/MultiSelect.svelte.d.ts +22 -9
- package/dist/components/NumberInput.svelte +8 -4
- package/dist/components/PageHeader.svelte +37 -4
- package/dist/components/PageHeader.svelte.d.ts +15 -0
- package/dist/components/PageShell.svelte +85 -0
- package/dist/components/PageShell.svelte.d.ts +38 -0
- package/dist/components/Pagination.svelte +43 -7
- package/dist/components/Panel.svelte +101 -0
- package/dist/components/Panel.svelte.d.ts +39 -0
- package/dist/components/PasswordInput.svelte +139 -0
- package/dist/components/PasswordInput.svelte.d.ts +29 -0
- package/dist/components/Radio.svelte +152 -35
- package/dist/components/Radio.svelte.d.ts +16 -1
- package/dist/components/RadioGroup.svelte +118 -71
- package/dist/components/RadioGroup.svelte.d.ts +39 -9
- package/dist/components/SectionHeading.svelte +39 -0
- package/dist/components/SectionHeading.svelte.d.ts +21 -0
- package/dist/components/SegmentedControl.svelte +194 -0
- package/dist/components/SegmentedControl.svelte.d.ts +55 -0
- package/dist/components/Select.svelte +471 -46
- package/dist/components/Select.svelte.d.ts +95 -6
- package/dist/components/SidebarNav.svelte +259 -0
- package/dist/components/SidebarNav.svelte.d.ts +17 -0
- package/dist/components/Stat.svelte +53 -2
- package/dist/components/Stat.svelte.d.ts +31 -0
- package/dist/components/Textarea.svelte +1 -1
- package/dist/components/TimePicker.svelte +480 -0
- package/dist/components/TimePicker.svelte.d.ts +23 -0
- package/dist/components/Toolbar.svelte +39 -0
- package/dist/components/Toolbar.svelte.d.ts +26 -0
- package/dist/components/TreeView.svelte +339 -0
- package/dist/components/TreeView.svelte.d.ts +37 -0
- package/dist/index.d.ts +25 -1
- package/dist/index.js +20 -1
- package/dist/internal/calendar.d.ts +119 -0
- package/dist/internal/calendar.js +225 -0
- package/dist/internal/choice.d.ts +136 -0
- package/dist/internal/choice.js +179 -0
- package/dist/internal/field.d.ts +31 -0
- package/dist/internal/field.js +38 -0
- package/dist/internal/filter.d.ts +80 -0
- package/dist/internal/filter.js +80 -0
- package/dist/internal/layout.d.ts +119 -0
- package/dist/internal/layout.js +132 -0
- package/dist/internal/listbox.svelte.d.ts +77 -0
- package/dist/internal/listbox.svelte.js +438 -0
- package/dist/internal/nav-expansion.svelte.d.ts +36 -0
- package/dist/internal/nav-expansion.svelte.js +144 -0
- package/dist/internal/nav-tree.d.ts +68 -0
- package/dist/internal/nav-tree.js +102 -0
- package/dist/internal/panel.d.ts +100 -0
- package/dist/internal/panel.js +109 -0
- package/dist/internal/rollup.d.ts +52 -0
- package/dist/internal/rollup.js +67 -0
- package/dist/internal/time.d.ts +103 -0
- package/dist/internal/time.js +166 -0
- package/dist/internal/tree.d.ts +86 -0
- package/dist/internal/tree.js +111 -0
- package/dist/styles/theme.css +18 -10
- package/package.json +4 -2
- package/src/lib/styles/theme.css +18 -10
|
@@ -1,4 +1,36 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { ListboxItem } from '../internal/listbox.svelte.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One row of a filterable option list.
|
|
6
|
+
*
|
|
7
|
+
* Declared here and imported by MultiSelect, which carried the same interface
|
|
8
|
+
* written out character for character. Two declarations are two chances to
|
|
9
|
+
* answer the same question differently, and these two had already drifted
|
|
10
|
+
* apart in what they let a consumer say: neither could describe a row by
|
|
11
|
+
* anything but its label, so an option a user knows by a code or a synonym
|
|
12
|
+
* could not be found by typing it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ListOption extends ListboxItem {
|
|
15
|
+
/**
|
|
16
|
+
* Extra words the default matcher searches alongside the label. Absent on
|
|
17
|
+
* every option that ships today, so adopting it narrows no existing list.
|
|
18
|
+
*/
|
|
19
|
+
keywords?: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
1
23
|
<script lang="ts">
|
|
24
|
+
/**
|
|
25
|
+
* A combobox: type to narrow the list, arrow through what is left, Enter to
|
|
26
|
+
* choose.
|
|
27
|
+
*
|
|
28
|
+
* The open state, the active row, the keyboard model and the dismissal come
|
|
29
|
+
* from internal/listbox, the matching from internal/filter and every class in
|
|
30
|
+
* the panel from internal/panel. What stays here is the one thing a combobox
|
|
31
|
+
* owns: whether the input is showing a query being typed or the label of the
|
|
32
|
+
* row already chosen.
|
|
33
|
+
*/
|
|
2
34
|
import {
|
|
3
35
|
CONTROL_BASE,
|
|
4
36
|
FIELD_ERROR,
|
|
@@ -8,12 +40,31 @@
|
|
|
8
40
|
controlBorder,
|
|
9
41
|
describedBy,
|
|
10
42
|
} from '../internal/field.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
43
|
+
import { applyFilter, type FilterInput } from '../internal/filter.js';
|
|
44
|
+
import { createListbox } from '../internal/listbox.svelte.js';
|
|
45
|
+
import { PANEL_EMPTY, PANEL_LIST, PANEL_SURFACE, panelOption } from '../internal/panel.js';
|
|
46
|
+
|
|
47
|
+
interface Props {
|
|
48
|
+
/** The chosen option's value. Empty for none. */
|
|
49
|
+
value?: string;
|
|
50
|
+
options: ListOption[];
|
|
51
|
+
label?: string;
|
|
52
|
+
hint?: string;
|
|
53
|
+
error?: string;
|
|
54
|
+
placeholder?: string;
|
|
16
55
|
disabled?: boolean;
|
|
56
|
+
required?: boolean;
|
|
57
|
+
id?: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
/** Offers a button that empties the field. */
|
|
60
|
+
allowClear?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Replaces the default matcher, or false to switch local filtering off
|
|
63
|
+
* because the list arrived already narrowed, by a server query for instance.
|
|
64
|
+
*/
|
|
65
|
+
filter?: FilterInput<ListOption>;
|
|
66
|
+
class?: string;
|
|
67
|
+
onchange?: (value: string) => void;
|
|
17
68
|
}
|
|
18
69
|
|
|
19
70
|
let {
|
|
@@ -28,126 +79,123 @@
|
|
|
28
79
|
id = undefined,
|
|
29
80
|
name = undefined,
|
|
30
81
|
allowClear = true,
|
|
82
|
+
filter = undefined,
|
|
31
83
|
class: cls = '',
|
|
32
84
|
onchange = undefined,
|
|
33
|
-
}:
|
|
34
|
-
value?: string;
|
|
35
|
-
options: Option[];
|
|
36
|
-
label?: string;
|
|
37
|
-
hint?: string;
|
|
38
|
-
error?: string;
|
|
39
|
-
placeholder?: string;
|
|
40
|
-
disabled?: boolean;
|
|
41
|
-
required?: boolean;
|
|
42
|
-
id?: string;
|
|
43
|
-
name?: string;
|
|
44
|
-
allowClear?: boolean;
|
|
45
|
-
class?: string;
|
|
46
|
-
onchange?: (value: string) => void;
|
|
47
|
-
} = $props();
|
|
85
|
+
}: Props = $props();
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
/*
|
|
88
|
+
* $props.id() and not a random string: a random id differs between the server
|
|
89
|
+
* render and hydration, so the aria-controls this field emitted named an
|
|
90
|
+
* element the client had never rendered and the relationship was dropped in
|
|
91
|
+
* silence. Every idref in the panel is built from this one.
|
|
92
|
+
*/
|
|
93
|
+
const uid = $props.id();
|
|
94
|
+
const fieldId = $derived(id ?? uid);
|
|
51
95
|
|
|
52
|
-
let open = $state(false);
|
|
53
96
|
let query = $state('');
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
let
|
|
97
|
+
/** Whether the input holds a query being typed or the chosen row's label. */
|
|
98
|
+
let dirty = $state(false);
|
|
99
|
+
let inputEl = $state<HTMLInputElement | undefined>();
|
|
100
|
+
let fieldEl = $state<HTMLDivElement | undefined>();
|
|
57
101
|
|
|
58
|
-
const selectedLabel = $derived(options.find((
|
|
102
|
+
const selectedLabel = $derived(options.find((option) => option.value === value)?.label ?? '');
|
|
59
103
|
const display = $derived(dirty ? query : selectedLabel);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
// Not narrowed until the user types: opening the field shows the whole list,
|
|
105
|
+
// with the row already chosen under the ring.
|
|
106
|
+
const rows = $derived(applyFilter(options, dirty ? query : '', filter));
|
|
107
|
+
|
|
108
|
+
// No typeahead: every letter here is query text, and a second matcher jumping
|
|
109
|
+
// the active row would fight the filter for the same keys.
|
|
110
|
+
const box = createListbox<ListOption>({
|
|
111
|
+
items: () => rows,
|
|
112
|
+
baseId: () => fieldId,
|
|
113
|
+
onSelect: (option) => choose(option),
|
|
114
|
+
onClose: () => {
|
|
115
|
+
// The input goes back to showing what is chosen. A dismissed query left
|
|
116
|
+
// on screen reads as a value the field does not hold.
|
|
117
|
+
query = '';
|
|
118
|
+
dirty = false;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const anchor = box.anchor;
|
|
123
|
+
const panel = box.panel;
|
|
124
|
+
|
|
125
|
+
function choose(option: ListOption): void {
|
|
126
|
+
if (option.disabled === true) return;
|
|
127
|
+
value = option.value;
|
|
128
|
+
onchange?.(option.value);
|
|
129
|
+
// The factory leaves the list open, because a multi-value control collects
|
|
130
|
+
// several picks in one pass. This one takes a single value.
|
|
131
|
+
box.close('select');
|
|
73
132
|
}
|
|
74
133
|
|
|
75
|
-
function clear() {
|
|
134
|
+
function clear(): void {
|
|
76
135
|
value = '';
|
|
77
136
|
query = '';
|
|
78
137
|
dirty = false;
|
|
79
138
|
onchange?.('');
|
|
139
|
+
// The button is about to unmount with focus on it, which would drop focus
|
|
140
|
+
// to the body and leave a keyboard user at the top of the page.
|
|
141
|
+
inputEl?.focus();
|
|
80
142
|
}
|
|
81
143
|
|
|
82
|
-
function
|
|
83
|
-
query =
|
|
144
|
+
function search(event: Event & { currentTarget: HTMLInputElement }): void {
|
|
145
|
+
query = event.currentTarget.value;
|
|
84
146
|
dirty = true;
|
|
85
|
-
|
|
86
|
-
|
|
147
|
+
// The keyboard must not rest on a row the new query pushed out from under
|
|
148
|
+
// it: aria-activedescendant would name one option while the ring is drawn
|
|
149
|
+
// on another. Cleared first, so openList lands on the first row that still
|
|
150
|
+
// matches rather than on whatever now sits at the old index.
|
|
151
|
+
box.setActive(-1);
|
|
152
|
+
box.openList();
|
|
87
153
|
}
|
|
88
154
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
active = Math.max(active - 1, 0);
|
|
97
|
-
} else if (e.key === 'Enter') {
|
|
98
|
-
if (open && filtered[active]) {
|
|
99
|
-
e.preventDefault();
|
|
100
|
-
choose(filtered[active]);
|
|
101
|
-
}
|
|
102
|
-
} else if (e.key === 'Escape') {
|
|
103
|
-
open = false;
|
|
104
|
-
dirty = false;
|
|
105
|
-
query = '';
|
|
106
|
-
}
|
|
107
|
-
}
|
|
155
|
+
function onFocus(event: FocusEvent): void {
|
|
156
|
+
// Focus arriving from inside the field is focus coming back, not the user
|
|
157
|
+
// entering it. Escape hands focus from a row to the input, and reopening on
|
|
158
|
+
// that would undo the dismissal the user had just asked for.
|
|
159
|
+
const from = event.relatedTarget;
|
|
160
|
+
if (from instanceof Node && fieldEl?.contains(from) === true) return;
|
|
108
161
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
query = '';
|
|
114
|
-
}
|
|
162
|
+
// Opening on the row the field already holds, so the first ArrowDown moves
|
|
163
|
+
// from the current value rather than from the top of the list.
|
|
164
|
+
const index = rows.findIndex((option) => option.value === value);
|
|
165
|
+
box.openList(index >= 0 ? index : undefined);
|
|
115
166
|
}
|
|
116
|
-
|
|
117
|
-
$effect(() => {
|
|
118
|
-
if (open) document.addEventListener('click', handleOutside, { capture: true });
|
|
119
|
-
return () => document.removeEventListener('click', handleOutside, { capture: true });
|
|
120
|
-
});
|
|
121
167
|
</script>
|
|
122
168
|
|
|
123
|
-
<div class="{FIELD_WRAP} {cls}"
|
|
169
|
+
<div class="{FIELD_WRAP} {cls}">
|
|
124
170
|
{#if label}
|
|
125
|
-
<label for={
|
|
126
|
-
{label}{#if required}<span class="
|
|
171
|
+
<label for={fieldId} class={FIELD_LABEL}>
|
|
172
|
+
{label}{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
|
|
127
173
|
</label>
|
|
128
174
|
{/if}
|
|
129
175
|
|
|
130
|
-
<div class="relative">
|
|
176
|
+
<div class="relative" bind:this={fieldEl} use:anchor>
|
|
131
177
|
<input
|
|
132
|
-
{
|
|
178
|
+
bind:this={inputEl}
|
|
179
|
+
id={fieldId}
|
|
133
180
|
{name}
|
|
134
181
|
{placeholder}
|
|
135
182
|
{disabled}
|
|
136
183
|
{required}
|
|
137
184
|
role="combobox"
|
|
138
|
-
aria-expanded={open}
|
|
139
|
-
aria-controls={listId}
|
|
140
185
|
aria-autocomplete="list"
|
|
141
186
|
autocomplete="off"
|
|
142
187
|
value={display}
|
|
143
|
-
oninput={
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
188
|
+
oninput={search}
|
|
189
|
+
onfocus={onFocus}
|
|
190
|
+
onkeydown={(event) => {
|
|
191
|
+
box.onkeydown(event);
|
|
192
|
+
}}
|
|
147
193
|
aria-invalid={error ? 'true' : undefined}
|
|
148
|
-
aria-describedby={describedBy(
|
|
194
|
+
aria-describedby={describedBy(fieldId, error, hint)}
|
|
195
|
+
{...box.triggerAttrs}
|
|
149
196
|
class="{CONTROL_BASE} {controlBorder(!!error)} {allowClear && value ? 'pr-8' : 'pr-3'}"
|
|
150
197
|
/>
|
|
198
|
+
|
|
151
199
|
{#if allowClear && value && !disabled}
|
|
152
200
|
<button
|
|
153
201
|
type="button"
|
|
@@ -170,49 +218,67 @@
|
|
|
170
218
|
</button>
|
|
171
219
|
{/if}
|
|
172
220
|
|
|
173
|
-
{#if open}
|
|
174
|
-
<div
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
</
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
221
|
+
{#if box.open}
|
|
222
|
+
<div class="{PANEL_SURFACE} w-full">
|
|
223
|
+
<div class={PANEL_LIST} use:panel {...box.listAttrs}>
|
|
224
|
+
{#each rows as option, index (option.value)}
|
|
225
|
+
{@const isSelected = option.value === value}
|
|
226
|
+
<!--
|
|
227
|
+
role is stated here as well as spread. The compiler checks
|
|
228
|
+
aria-selected against the role it can see in the source and it
|
|
229
|
+
cannot see into a spread, so without this the row reads to it as a
|
|
230
|
+
plain button carrying an attribute a button does not support.
|
|
231
|
+
-->
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
role="option"
|
|
235
|
+
onclick={() => choose(option)}
|
|
236
|
+
onmouseenter={() => box.setActive(index)}
|
|
237
|
+
onmousedown={(event) => {
|
|
238
|
+
// Pressing a row must not pull focus out of the input: the
|
|
239
|
+
// caret stays where the user is typing, and the field never
|
|
240
|
+
// has to be given focus back after a pick.
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
}}
|
|
243
|
+
aria-selected={isSelected ? 'true' : 'false'}
|
|
244
|
+
{...box.optionAttrs(index)}
|
|
245
|
+
class={panelOption({
|
|
246
|
+
active: box.activeIndex === index,
|
|
247
|
+
selected: isSelected,
|
|
248
|
+
disabled: option.disabled === true,
|
|
249
|
+
})}
|
|
250
|
+
>
|
|
251
|
+
<span class="min-w-0 truncate">{option.label}</span>
|
|
252
|
+
{#if isSelected}
|
|
253
|
+
<svg
|
|
254
|
+
width="12"
|
|
255
|
+
height="10"
|
|
256
|
+
viewBox="0 0 12 10"
|
|
257
|
+
fill="none"
|
|
258
|
+
class="ml-auto shrink-0"
|
|
259
|
+
aria-hidden="true"
|
|
260
|
+
>
|
|
261
|
+
<path
|
|
262
|
+
d="M1 5l3.5 3.5L11 1.5"
|
|
263
|
+
stroke="currentColor"
|
|
264
|
+
stroke-width="1.5"
|
|
265
|
+
stroke-linecap="round"
|
|
266
|
+
stroke-linejoin="round"
|
|
267
|
+
/>
|
|
268
|
+
</svg>
|
|
269
|
+
{/if}
|
|
270
|
+
</button>
|
|
271
|
+
{:else}
|
|
272
|
+
<p class={PANEL_EMPTY}>No matches</p>
|
|
273
|
+
{/each}
|
|
274
|
+
</div>
|
|
209
275
|
</div>
|
|
210
276
|
{/if}
|
|
211
277
|
</div>
|
|
212
278
|
|
|
213
279
|
{#if error}
|
|
214
|
-
<p id={
|
|
280
|
+
<p id="{fieldId}-error" class={FIELD_ERROR}>{error}</p>
|
|
215
281
|
{:else if hint}
|
|
216
|
-
<p id={
|
|
282
|
+
<p id="{fieldId}-hint" class={FIELD_HINT}>{hint}</p>
|
|
217
283
|
{/if}
|
|
218
284
|
</div>
|
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { ListboxItem } from '../internal/listbox.svelte.js';
|
|
2
|
+
/**
|
|
3
|
+
* One row of a filterable option list.
|
|
4
|
+
*
|
|
5
|
+
* Declared here and imported by MultiSelect, which carried the same interface
|
|
6
|
+
* written out character for character. Two declarations are two chances to
|
|
7
|
+
* answer the same question differently, and these two had already drifted
|
|
8
|
+
* apart in what they let a consumer say: neither could describe a row by
|
|
9
|
+
* anything but its label, so an option a user knows by a code or a synonym
|
|
10
|
+
* could not be found by typing it.
|
|
11
|
+
*/
|
|
12
|
+
export interface ListOption extends ListboxItem {
|
|
13
|
+
/**
|
|
14
|
+
* Extra words the default matcher searches alongside the label. Absent on
|
|
15
|
+
* every option that ships today, so adopting it narrows no existing list.
|
|
16
|
+
*/
|
|
17
|
+
keywords?: readonly string[];
|
|
5
18
|
}
|
|
6
|
-
type
|
|
19
|
+
import { type FilterInput } from '../internal/filter.js';
|
|
20
|
+
interface Props {
|
|
21
|
+
/** The chosen option's value. Empty for none. */
|
|
7
22
|
value?: string;
|
|
8
|
-
options:
|
|
23
|
+
options: ListOption[];
|
|
9
24
|
label?: string;
|
|
10
25
|
hint?: string;
|
|
11
26
|
error?: string;
|
|
@@ -14,10 +29,16 @@ type $$ComponentProps = {
|
|
|
14
29
|
required?: boolean;
|
|
15
30
|
id?: string;
|
|
16
31
|
name?: string;
|
|
32
|
+
/** Offers a button that empties the field. */
|
|
17
33
|
allowClear?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Replaces the default matcher, or false to switch local filtering off
|
|
36
|
+
* because the list arrived already narrowed, by a server query for instance.
|
|
37
|
+
*/
|
|
38
|
+
filter?: FilterInput<ListOption>;
|
|
18
39
|
class?: string;
|
|
19
40
|
onchange?: (value: string) => void;
|
|
20
|
-
}
|
|
21
|
-
declare const Autocomplete: import("svelte").Component
|
|
41
|
+
}
|
|
42
|
+
declare const Autocomplete: import("svelte").Component<Props, {}, "value">;
|
|
22
43
|
type Autocomplete = ReturnType<typeof Autocomplete>;
|
|
23
44
|
export default Autocomplete;
|
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
|
+
import { sectionHeading } from '../internal/layout.js';
|
|
3
4
|
|
|
4
5
|
type Pad = 'none' | 'sm' | 'md' | 'lg';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
title?: string;
|
|
8
9
|
description?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The card's own section heading.
|
|
12
|
+
*
|
|
13
|
+
* The header band has three ways in, and they resolve in this order:
|
|
14
|
+
* `header` wins over everything, then `heading`, then `title`. A caller who
|
|
15
|
+
* hands a snippet has already decided what the band contains, so the
|
|
16
|
+
* component adds nothing to it. `heading` renders the icon, a real heading
|
|
17
|
+
* element at the level the caller names, and the meta row that seven
|
|
18
|
+
* hand-rolled section shells wrote out for themselves. `title` predates it
|
|
19
|
+
* and still renders exactly what it always did, so the cards already
|
|
20
|
+
* shipping do not move. Set one of the three.
|
|
21
|
+
*/
|
|
22
|
+
heading?: string;
|
|
23
|
+
/** 2 under a page title, 3 inside another card. */
|
|
24
|
+
headingLevel?: 2 | 3;
|
|
25
|
+
/** Drawn before the heading. Decorative, so it carries no name of its own. */
|
|
26
|
+
icon?: Component<{ size?: number; class?: string }>;
|
|
27
|
+
/** A count, a status or a timestamp, at the end of the heading row. */
|
|
28
|
+
meta?: string;
|
|
9
29
|
pad?: Pad;
|
|
10
30
|
hover?: boolean;
|
|
11
31
|
class?: string;
|
|
@@ -19,6 +39,10 @@
|
|
|
19
39
|
let {
|
|
20
40
|
title = undefined,
|
|
21
41
|
description = undefined,
|
|
42
|
+
heading = undefined,
|
|
43
|
+
headingLevel = 3,
|
|
44
|
+
icon = undefined,
|
|
45
|
+
meta = undefined,
|
|
22
46
|
pad = 'md',
|
|
23
47
|
hover = false,
|
|
24
48
|
class: klass = '',
|
|
@@ -66,10 +90,27 @@
|
|
|
66
90
|
{onclick}
|
|
67
91
|
onkeydown={onclick ? activate : onkeydown}
|
|
68
92
|
>
|
|
69
|
-
{#if header || title}
|
|
93
|
+
{#if header || heading || title}
|
|
70
94
|
<div class="px-5 py-4 border-b border-line">
|
|
71
95
|
{#if header}
|
|
72
96
|
{@render header()}
|
|
97
|
+
{:else if heading}
|
|
98
|
+
<div class="flex items-center gap-2">
|
|
99
|
+
{#if icon}
|
|
100
|
+
{@const Icon = icon}
|
|
101
|
+
<Icon size={16} class="shrink-0 text-faint" />
|
|
102
|
+
{/if}
|
|
103
|
+
<!-- Two branches rather than one dynamic element: the level is a
|
|
104
|
+
document structure decision, and a reader of this file should be
|
|
105
|
+
able to see both headings the card can produce. -->
|
|
106
|
+
{#if headingLevel === 2}
|
|
107
|
+
<h2 class={sectionHeading(2)}>{heading}</h2>
|
|
108
|
+
{:else}
|
|
109
|
+
<h3 class={sectionHeading(3)}>{heading}</h3>
|
|
110
|
+
{/if}
|
|
111
|
+
{#if meta}<span class="ml-auto shrink-0 text-xs text-faint">{meta}</span>{/if}
|
|
112
|
+
</div>
|
|
113
|
+
{#if description}<p class="text-sm text-muted mt-0.5">{description}</p>{/if}
|
|
73
114
|
{:else}
|
|
74
115
|
<h3 class="font-semibold text-fg">{title}</h3>
|
|
75
116
|
{#if description}<p class="text-sm text-muted mt-0.5">{description}</p>{/if}
|
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
import type { Snippet } from 'svelte';
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
2
|
type Pad = 'none' | 'sm' | 'md' | 'lg';
|
|
3
3
|
interface Props {
|
|
4
4
|
title?: string;
|
|
5
5
|
description?: string;
|
|
6
|
+
/**
|
|
7
|
+
* The card's own section heading.
|
|
8
|
+
*
|
|
9
|
+
* The header band has three ways in, and they resolve in this order:
|
|
10
|
+
* `header` wins over everything, then `heading`, then `title`. A caller who
|
|
11
|
+
* hands a snippet has already decided what the band contains, so the
|
|
12
|
+
* component adds nothing to it. `heading` renders the icon, a real heading
|
|
13
|
+
* element at the level the caller names, and the meta row that seven
|
|
14
|
+
* hand-rolled section shells wrote out for themselves. `title` predates it
|
|
15
|
+
* and still renders exactly what it always did, so the cards already
|
|
16
|
+
* shipping do not move. Set one of the three.
|
|
17
|
+
*/
|
|
18
|
+
heading?: string;
|
|
19
|
+
/** 2 under a page title, 3 inside another card. */
|
|
20
|
+
headingLevel?: 2 | 3;
|
|
21
|
+
/** Drawn before the heading. Decorative, so it carries no name of its own. */
|
|
22
|
+
icon?: Component<{
|
|
23
|
+
size?: number;
|
|
24
|
+
class?: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** A count, a status or a timestamp, at the end of the heading row. */
|
|
27
|
+
meta?: string;
|
|
6
28
|
pad?: Pad;
|
|
7
29
|
hover?: boolean;
|
|
8
30
|
class?: string;
|
|
@@ -12,6 +34,6 @@ interface Props {
|
|
|
12
34
|
onkeydown?: (e: KeyboardEvent) => void;
|
|
13
35
|
children: Snippet;
|
|
14
36
|
}
|
|
15
|
-
declare const Card:
|
|
37
|
+
declare const Card: Component<Props, {}, "">;
|
|
16
38
|
type Card = ReturnType<typeof Card>;
|
|
17
39
|
export default Card;
|