@mystravil/hardpoint 0.1.0 → 0.2.1
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/CHANGELOG.md +42 -18
- package/LICENSE +25 -25
- package/README.md +67 -64
- package/dist/base.css +625 -346
- package/dist/components/AppShell.svelte +698 -698
- package/dist/components/Button.svelte +174 -174
- package/dist/components/Checkbox.svelte +235 -198
- package/dist/components/Checkbox.svelte.d.ts +9 -2
- package/dist/components/CheckboxGroup.svelte +207 -99
- package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
- package/dist/components/Combobox.svelte +901 -824
- package/dist/components/Combobox.svelte.d.ts +10 -1
- package/dist/components/Dialog.svelte +359 -356
- package/dist/components/Disclosure.svelte +201 -201
- package/dist/components/EmptyState.svelte +88 -88
- package/dist/components/ErrorState.svelte +135 -135
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/KeyValueList.svelte +114 -114
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/LoadRegion.svelte +219 -219
- package/dist/components/MediaFrame.svelte +110 -110
- package/dist/components/MediaImage.svelte +256 -256
- package/dist/components/Menu.svelte +258 -258
- package/dist/components/MenuCheckboxItem.svelte +119 -119
- package/dist/components/MenuGroup.svelte +50 -50
- package/dist/components/MenuItem.svelte +94 -94
- package/dist/components/MenuRadioGroup.svelte +62 -62
- package/dist/components/MenuRadioItem.svelte +110 -110
- package/dist/components/MenuSeparator.svelte +28 -28
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +18 -2
- package/dist/components/NativeSelect.svelte.d.ts +6 -0
- package/dist/components/Notice.svelte +262 -0
- package/dist/components/Notice.svelte.d.ts +30 -0
- package/dist/components/Popover.svelte +222 -222
- package/dist/components/Progress.svelte +157 -157
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +471 -405
- package/dist/components/Select.svelte.d.ts +6 -0
- package/dist/components/Skeleton.svelte +149 -149
- package/dist/components/Spinner.svelte +118 -117
- package/dist/components/Stat.svelte +253 -253
- package/dist/components/Steps.svelte +189 -189
- package/dist/components/Switch.svelte +222 -222
- package/dist/components/Table.svelte +504 -504
- package/dist/components/Tabs.svelte +246 -246
- package/dist/components/Tag.svelte +40 -40
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/TextField.svelte +256 -256
- package/dist/components/Timeline.svelte +156 -156
- package/dist/components/Toaster.svelte +411 -411
- package/dist/components/Tooltip.svelte +200 -200
- package/dist/components/TooltipGroup.svelte +46 -46
- package/dist/components/internal/Scrim.svelte +52 -52
- package/dist/components/internal/Sheet.svelte +263 -263
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/fonts.css +71 -71
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/tokens.css +283 -279
- package/package.json +81 -76
|
@@ -1,405 +1,471 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import { Select as BitsSelect } from 'bits-ui';
|
|
4
|
-
|
|
5
|
-
export type SelectItem = {
|
|
6
|
-
value: string;
|
|
7
|
-
/** The option's text, in the trigger once chosen, and for typeahead. */
|
|
8
|
-
label: string;
|
|
9
|
-
/** A second line in `--muted`. */
|
|
10
|
-
description?: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// The
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
{
|
|
236
|
-
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
min-width:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
.hp-select-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
/*
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
/*
|
|
396
|
-
|
|
397
|
-
.hp-select-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { Select as BitsSelect } from 'bits-ui';
|
|
4
|
+
|
|
5
|
+
export type SelectItem = {
|
|
6
|
+
value: string;
|
|
7
|
+
/** The option's text, in the trigger once chosen, and for typeahead. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** A second line in `--muted`. */
|
|
10
|
+
description?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The heading the option sits under, as written. Options with the same
|
|
13
|
+
* group share one heading, in the order each group first appears;
|
|
14
|
+
* options without one come first.
|
|
15
|
+
*/
|
|
16
|
+
group?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SelectItemSnippetProps = {
|
|
21
|
+
item: SelectItem;
|
|
22
|
+
selected: boolean;
|
|
23
|
+
highlighted: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SelectLabelBeside = boolean | 'medium' | 'expanded' | 'large';
|
|
27
|
+
|
|
28
|
+
/** Attributes and handlers not listed here go to the trigger button. */
|
|
29
|
+
export type SelectProps = Omit<
|
|
30
|
+
BitsSelect.TriggerProps,
|
|
31
|
+
'child' | 'children' | 'value' | 'name' | 'disabled' | 'type' | 'ref'
|
|
32
|
+
> & {
|
|
33
|
+
items: SelectItem[];
|
|
34
|
+
/** The chosen item's `value`; `''` for none. */
|
|
35
|
+
value?: string;
|
|
36
|
+
open?: boolean;
|
|
37
|
+
/** The caps field label above the trigger, and the sheet's title on compact widths. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Shown in `--muted` while nothing is chosen. Never the only label. */
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Submits the value with a form, through a hidden input. */
|
|
43
|
+
name?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The label beside the trigger instead of above it: always (`true`), or
|
|
46
|
+
* from a breakpoint up (`medium` 600px, `expanded` 900px, `large` 1200px).
|
|
47
|
+
*/
|
|
48
|
+
labelBeside?: SelectLabelBeside;
|
|
49
|
+
/** The trigger button. */
|
|
50
|
+
ref?: HTMLElement | null;
|
|
51
|
+
/** Renders an option's content in place of its label and description. */
|
|
52
|
+
item?: Snippet<[SelectItemSnippetProps]>;
|
|
53
|
+
};
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<script lang="ts">
|
|
57
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
58
|
+
import { pop } from '../motion/index.js';
|
|
59
|
+
import { groupOptions } from './internal/groups.js';
|
|
60
|
+
import Sheet from './internal/Sheet.svelte';
|
|
61
|
+
|
|
62
|
+
const uid = $props.id();
|
|
63
|
+
|
|
64
|
+
let {
|
|
65
|
+
items,
|
|
66
|
+
value = $bindable(''),
|
|
67
|
+
open = $bindable(false),
|
|
68
|
+
label,
|
|
69
|
+
placeholder = '',
|
|
70
|
+
disabled = false,
|
|
71
|
+
name,
|
|
72
|
+
labelBeside = false,
|
|
73
|
+
id = `${uid}-trigger`,
|
|
74
|
+
ref = $bindable(null),
|
|
75
|
+
item: itemContent,
|
|
76
|
+
class: className,
|
|
77
|
+
...rest
|
|
78
|
+
}: SelectProps = $props();
|
|
79
|
+
|
|
80
|
+
const labelId = `${uid}-label`;
|
|
81
|
+
|
|
82
|
+
// Compact (< 600px) opens a bottom sheet; medium and up a list under the
|
|
83
|
+
// trigger (BRIEF, Apps and phones: Overlays and Choices).
|
|
84
|
+
const wide = new MediaQuery('min-width: 600px', true);
|
|
85
|
+
|
|
86
|
+
const chosen = $derived(items.find((i) => i.value === value));
|
|
87
|
+
|
|
88
|
+
// The list's order: options without a group, then each group. Bits UI
|
|
89
|
+
// gets it too, so typeahead on the closed trigger follows the list.
|
|
90
|
+
const grouped = $derived(groupOptions(items));
|
|
91
|
+
|
|
92
|
+
// The highlight moved by keys gets the focus contour; a pointer's gets the
|
|
93
|
+
// hover tint alone. Focus itself stays on the trigger.
|
|
94
|
+
let keyboard = $state(false);
|
|
95
|
+
|
|
96
|
+
let sheetNode = $state<HTMLElement | null>(null);
|
|
97
|
+
|
|
98
|
+
function close() {
|
|
99
|
+
open = false;
|
|
100
|
+
ref?.focus();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The sheet's title row and handle sit outside the listbox; pressing
|
|
104
|
+
// them is not an outside press.
|
|
105
|
+
function insideSheet(e: PointerEvent) {
|
|
106
|
+
if (sheetNode && e.target instanceof Node && sheetNode.contains(e.target)) e.preventDefault();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Presses on the sheet keep focus on the trigger.
|
|
110
|
+
function keepFocus(e: PointerEvent) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// In the sheet, Bits UI never learns that its content is placed:
|
|
115
|
+
// ContentStatic with forceMount reports placement once, at mount, while
|
|
116
|
+
// still closed. So Bits neither opens it at the chosen option nor scrolls
|
|
117
|
+
// to the option the keys move to; this does both, nearest edge first, as
|
|
118
|
+
// Bits does in the popover. The viewport's scroll-padding keeps an option
|
|
119
|
+
// clear of a group heading stuck at the top.
|
|
120
|
+
function followHighlight(viewport: HTMLElement) {
|
|
121
|
+
const reveal = (option: Element | null) => {
|
|
122
|
+
if (!(option instanceof HTMLElement)) return;
|
|
123
|
+
const top = option.offsetTop - (parseFloat(getComputedStyle(viewport).scrollPaddingTop) || 0);
|
|
124
|
+
const bottom = option.offsetTop + option.offsetHeight;
|
|
125
|
+
if (top < viewport.scrollTop) viewport.scrollTop = top;
|
|
126
|
+
else if (bottom > viewport.scrollTop + viewport.clientHeight) {
|
|
127
|
+
viewport.scrollTop = bottom - viewport.clientHeight;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
reveal(viewport.querySelector('[data-selected]'));
|
|
131
|
+
const observer = new MutationObserver((records) => {
|
|
132
|
+
if (!keyboard) return;
|
|
133
|
+
for (const { target } of records) {
|
|
134
|
+
if (target instanceof Element && target.hasAttribute('data-highlighted')) reveal(target);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
observer.observe(viewport, { subtree: true, attributes: true, attributeFilter: ['data-highlighted'] });
|
|
138
|
+
return () => observer.disconnect();
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<div
|
|
143
|
+
class={['hp-select', className]}
|
|
144
|
+
data-state={open ? 'open' : 'closed'}
|
|
145
|
+
data-disabled={disabled || undefined}
|
|
146
|
+
data-placeholder={chosen ? undefined : ''}
|
|
147
|
+
data-label-beside={labelBeside === true ? 'always' : labelBeside || undefined}
|
|
148
|
+
onkeydowncapture={() => (keyboard = true)}
|
|
149
|
+
onpointerdowncapture={() => (keyboard = false)}
|
|
150
|
+
>
|
|
151
|
+
<label class="hp-select-label hp-label" id={labelId} for={id}>{label}</label>
|
|
152
|
+
|
|
153
|
+
<BitsSelect.Root type="single" bind:value bind:open items={grouped.ordered} {disabled} {name}>
|
|
154
|
+
<BitsSelect.Trigger bind:ref {id} aria-labelledby={labelId} {...rest}>
|
|
155
|
+
{#snippet child({ props })}
|
|
156
|
+
<span class="hp-select-field hp-well-chevron">
|
|
157
|
+
<button {...props} class="hp-select-trigger hp-well" data-placeholder={chosen ? undefined : ''}>
|
|
158
|
+
<span class="hp-select-value">{chosen?.label ?? placeholder}</span>
|
|
159
|
+
</button>
|
|
160
|
+
</span>
|
|
161
|
+
{/snippet}
|
|
162
|
+
</BitsSelect.Trigger>
|
|
163
|
+
|
|
164
|
+
<BitsSelect.Portal>
|
|
165
|
+
{#if wide.current}
|
|
166
|
+
<BitsSelect.Content
|
|
167
|
+
forceMount
|
|
168
|
+
side="bottom"
|
|
169
|
+
align="start"
|
|
170
|
+
sideOffset={8}
|
|
171
|
+
collisionPadding={8}
|
|
172
|
+
aria-labelledby={labelId}
|
|
173
|
+
>
|
|
174
|
+
{#snippet child({ wrapperProps, props, open: shown })}
|
|
175
|
+
{#if shown}
|
|
176
|
+
<div {...wrapperProps}>
|
|
177
|
+
<div
|
|
178
|
+
{...props}
|
|
179
|
+
class="hp-select-list"
|
|
180
|
+
data-form="popover"
|
|
181
|
+
data-keyboard={keyboard || undefined}
|
|
182
|
+
onpointermovecapture={() => (keyboard = false)}
|
|
183
|
+
transition:pop
|
|
184
|
+
>
|
|
185
|
+
{@render options(false)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
{/if}
|
|
189
|
+
{/snippet}
|
|
190
|
+
</BitsSelect.Content>
|
|
191
|
+
{:else}
|
|
192
|
+
<BitsSelect.ContentStatic forceMount preventScroll aria-labelledby={labelId} onInteractOutside={insideSheet}>
|
|
193
|
+
{#snippet child({ props, open: shown })}
|
|
194
|
+
<Sheet
|
|
195
|
+
open={shown}
|
|
196
|
+
layer="popover"
|
|
197
|
+
title={label}
|
|
198
|
+
onclose={close}
|
|
199
|
+
bind:ref={sheetNode}
|
|
200
|
+
onpointerdown={keepFocus}
|
|
201
|
+
>
|
|
202
|
+
<div
|
|
203
|
+
{...props}
|
|
204
|
+
class="hp-select-list"
|
|
205
|
+
data-form="sheet"
|
|
206
|
+
data-keyboard={keyboard || undefined}
|
|
207
|
+
onpointermovecapture={() => (keyboard = false)}
|
|
208
|
+
>
|
|
209
|
+
{@render options(true)}
|
|
210
|
+
</div>
|
|
211
|
+
</Sheet>
|
|
212
|
+
{/snippet}
|
|
213
|
+
</BitsSelect.ContentStatic>
|
|
214
|
+
{/if}
|
|
215
|
+
</BitsSelect.Portal>
|
|
216
|
+
</BitsSelect.Root>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{#snippet options(inSheet: boolean)}
|
|
220
|
+
<BitsSelect.Viewport>
|
|
221
|
+
{#snippet child({ props: viewportProps })}
|
|
222
|
+
<div
|
|
223
|
+
{...viewportProps}
|
|
224
|
+
class="hp-select-viewport"
|
|
225
|
+
data-grouped={grouped.groups.length ? '' : undefined}
|
|
226
|
+
{@attach inSheet && followHighlight}
|
|
227
|
+
>
|
|
228
|
+
{#each grouped.loose as entry (entry.value)}
|
|
229
|
+
{@render option(entry)}
|
|
230
|
+
{/each}
|
|
231
|
+
{#each grouped.groups as group (group.name)}
|
|
232
|
+
<!-- role="group", named by its heading, which is no option: keys and
|
|
233
|
+
typeahead skip it. -->
|
|
234
|
+
<BitsSelect.Group>
|
|
235
|
+
{#snippet child({ props: groupProps })}
|
|
236
|
+
<div {...groupProps} class="hp-select-group">
|
|
237
|
+
<BitsSelect.GroupHeading>
|
|
238
|
+
{#snippet child({ props: headingProps })}
|
|
239
|
+
<div {...headingProps} class="hp-select-group-heading">{group.name}</div>
|
|
240
|
+
{/snippet}
|
|
241
|
+
</BitsSelect.GroupHeading>
|
|
242
|
+
{#each group.items as entry (entry.value)}
|
|
243
|
+
{@render option(entry)}
|
|
244
|
+
{/each}
|
|
245
|
+
</div>
|
|
246
|
+
{/snippet}
|
|
247
|
+
</BitsSelect.Group>
|
|
248
|
+
{/each}
|
|
249
|
+
</div>
|
|
250
|
+
{/snippet}
|
|
251
|
+
</BitsSelect.Viewport>
|
|
252
|
+
{/snippet}
|
|
253
|
+
|
|
254
|
+
{#snippet option(entry: SelectItem)}
|
|
255
|
+
<BitsSelect.Item value={entry.value} label={entry.label} disabled={entry.disabled}>
|
|
256
|
+
{#snippet child({ props: optionProps, selected, highlighted })}
|
|
257
|
+
<div
|
|
258
|
+
{...optionProps}
|
|
259
|
+
class="hp-select-option"
|
|
260
|
+
data-described={entry.description ? '' : undefined}
|
|
261
|
+
aria-disabled={entry.disabled ? 'true' : undefined}
|
|
262
|
+
>
|
|
263
|
+
{#if itemContent}
|
|
264
|
+
{@render itemContent({ item: entry, selected, highlighted })}
|
|
265
|
+
{:else}
|
|
266
|
+
<span class="hp-select-option-label">{entry.label}</span>
|
|
267
|
+
{#if entry.description}
|
|
268
|
+
<span class="hp-select-option-description">{entry.description}</span>
|
|
269
|
+
{/if}
|
|
270
|
+
{/if}
|
|
271
|
+
</div>
|
|
272
|
+
{/snippet}
|
|
273
|
+
</BitsSelect.Item>
|
|
274
|
+
{/snippet}
|
|
275
|
+
|
|
276
|
+
<style>
|
|
277
|
+
/* ---- Local tokens --------------------------------------------------- */
|
|
278
|
+
|
|
279
|
+
/* The popover list renders in a portal at the end of <body>, outside
|
|
280
|
+
.hp-select, so it carries this itself. */
|
|
281
|
+
.hp-select-list {
|
|
282
|
+
/* The popover list's cap: eight and a half 36px rows, so a cut row
|
|
283
|
+
shows there is more. */
|
|
284
|
+
--select-list-height: 320px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ---- Field: caps label over a black well --------------------------- */
|
|
288
|
+
|
|
289
|
+
.hp-select {
|
|
290
|
+
/* Bits UI's hidden form input is absolutely placed in here. */
|
|
291
|
+
position: relative;
|
|
292
|
+
display: grid;
|
|
293
|
+
gap: var(--space-1);
|
|
294
|
+
min-width: 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.hp-select-label {
|
|
298
|
+
color: var(--muted);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* Label beside the trigger, as in a filter bar: always, or from a
|
|
302
|
+
breakpoint up. */
|
|
303
|
+
.hp-select[data-label-beside='always'] {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: var(--space-2);
|
|
307
|
+
}
|
|
308
|
+
@media (min-width: 600px) {
|
|
309
|
+
.hp-select[data-label-beside='medium'] {
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
gap: var(--space-2);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
@media (min-width: 900px) {
|
|
316
|
+
.hp-select[data-label-beside='expanded'] {
|
|
317
|
+
display: flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
gap: var(--space-2);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
@media (min-width: 1200px) {
|
|
323
|
+
.hp-select[data-label-beside='large'] {
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
gap: var(--space-2);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
.hp-select[data-label-beside] .hp-select-label {
|
|
330
|
+
flex: none;
|
|
331
|
+
white-space: nowrap;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* The trigger is base.css's well; its wrapper draws the chevron, like a
|
|
335
|
+
native select's. The wrapper is the grid or flex item, so it holds the
|
|
336
|
+
width and the minimum. */
|
|
337
|
+
.hp-select-field {
|
|
338
|
+
width: 100%;
|
|
339
|
+
min-width: 136px;
|
|
340
|
+
}
|
|
341
|
+
.hp-select-trigger {
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
width: 100%;
|
|
345
|
+
text-align: start;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
-webkit-tap-highlight-color: transparent;
|
|
348
|
+
}
|
|
349
|
+
.hp-select-trigger:disabled {
|
|
350
|
+
cursor: not-allowed;
|
|
351
|
+
}
|
|
352
|
+
.hp-select-trigger[data-placeholder] {
|
|
353
|
+
color: var(--muted);
|
|
354
|
+
}
|
|
355
|
+
.hp-select-value {
|
|
356
|
+
min-width: 0;
|
|
357
|
+
overflow: hidden;
|
|
358
|
+
text-overflow: ellipsis;
|
|
359
|
+
white-space: nowrap;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* ---- The list: a popover on medium and up, the shared sheet's body on
|
|
363
|
+
compact (internal/Sheet.svelte, on the popover layer) ---------------- */
|
|
364
|
+
|
|
365
|
+
.hp-select-list {
|
|
366
|
+
/* Option text lines up with the trigger's text (popover) or the
|
|
367
|
+
sheet's title (sheet). */
|
|
368
|
+
--option-inset: var(--space-3);
|
|
369
|
+
|
|
370
|
+
position: relative;
|
|
371
|
+
z-index: var(--layer-popover);
|
|
372
|
+
box-sizing: border-box;
|
|
373
|
+
background: var(--canvas);
|
|
374
|
+
color: var(--text);
|
|
375
|
+
font-family: var(--sans);
|
|
376
|
+
}
|
|
377
|
+
.hp-select-list[data-form='popover'] {
|
|
378
|
+
min-width: var(--bits-select-anchor-width);
|
|
379
|
+
max-width: max(var(--bits-select-anchor-width), min(var(--bits-select-content-available-width), 480px));
|
|
380
|
+
max-height: min(var(--select-list-height), var(--bits-select-content-available-height));
|
|
381
|
+
border: 1px solid var(--border-control);
|
|
382
|
+
}
|
|
383
|
+
.hp-select-list[data-form='sheet'] {
|
|
384
|
+
--option-inset: var(--gutter);
|
|
385
|
+
flex: 1 1 auto;
|
|
386
|
+
min-height: 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* Bits UI hides the viewport's scrollbar; a long list shows where it is. */
|
|
390
|
+
.hp-select-viewport[data-select-viewport] {
|
|
391
|
+
scrollbar-width: thin !important;
|
|
392
|
+
overscroll-behavior: contain;
|
|
393
|
+
padding-block: var(--space-1);
|
|
394
|
+
}
|
|
395
|
+
/* An option the keys or the chosen value scroll to stays clear of the
|
|
396
|
+
group heading stuck at the top: one heading line high. */
|
|
397
|
+
.hp-select-viewport[data-grouped] {
|
|
398
|
+
scroll-padding-top: calc(var(--space-2) + 16px + var(--space-1));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* ---- Group headings ------------------------------------------------- */
|
|
402
|
+
|
|
403
|
+
/* MenuGroup's heading: muted, on the options' text edge, 8px above and
|
|
404
|
+
4px below. The text stays as written, because a group name may be the
|
|
405
|
+
app's data (BRIEF, style rule 4). It stays at the top of the list while
|
|
406
|
+
its options scroll under it, until the next group pushes it up; that is
|
|
407
|
+
scrolling, not motion. It sticks to the viewport's edge, over its 4px
|
|
408
|
+
padding, so nothing shows above it. */
|
|
409
|
+
.hp-select-group-heading {
|
|
410
|
+
position: sticky;
|
|
411
|
+
top: calc(-1 * var(--space-1));
|
|
412
|
+
z-index: 1;
|
|
413
|
+
padding: var(--space-2) var(--option-inset) var(--space-1);
|
|
414
|
+
background: var(--canvas);
|
|
415
|
+
color: var(--muted);
|
|
416
|
+
font-size: var(--type-label);
|
|
417
|
+
font-weight: 500;
|
|
418
|
+
line-height: 16px;
|
|
419
|
+
overflow-wrap: anywhere;
|
|
420
|
+
user-select: none;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* ---- Options: Geist list items, sentence case ----------------------- */
|
|
424
|
+
|
|
425
|
+
.hp-select-option {
|
|
426
|
+
display: flex;
|
|
427
|
+
flex-direction: column;
|
|
428
|
+
justify-content: center;
|
|
429
|
+
box-sizing: border-box;
|
|
430
|
+
/* 36px rows on fine pointers, 44px on coarse. */
|
|
431
|
+
min-height: var(--control-height);
|
|
432
|
+
padding: var(--space-1) var(--option-inset);
|
|
433
|
+
font-size: var(--type-body);
|
|
434
|
+
line-height: 24px;
|
|
435
|
+
overflow-wrap: anywhere;
|
|
436
|
+
cursor: pointer;
|
|
437
|
+
user-select: none;
|
|
438
|
+
-webkit-tap-highlight-color: transparent;
|
|
439
|
+
}
|
|
440
|
+
.hp-select-option[data-described] {
|
|
441
|
+
padding-block: var(--space-2);
|
|
442
|
+
}
|
|
443
|
+
.hp-select-option-description {
|
|
444
|
+
color: var(--muted);
|
|
445
|
+
font-size: var(--type-caption);
|
|
446
|
+
line-height: 18px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* Highlight, by pointer or keys: the hover tint. Selection snaps, so
|
|
450
|
+
nothing here transitions (BRIEF, motion rule 3). */
|
|
451
|
+
.hp-select-option[data-highlighted],
|
|
452
|
+
.hp-select-option:active {
|
|
453
|
+
background: var(--surface-hover);
|
|
454
|
+
}
|
|
455
|
+
/* Selected: a 3px titanium edge and the tint, text unchanged. Hover and
|
|
456
|
+
press never darken it. */
|
|
457
|
+
.hp-select-option[data-selected] {
|
|
458
|
+
background: var(--surface-selected);
|
|
459
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
460
|
+
}
|
|
461
|
+
/* Keys move a virtual focus (focus stays on the trigger), so the
|
|
462
|
+
highlight they move also draws the focus contour, inside the row. */
|
|
463
|
+
.hp-select-list[data-keyboard] .hp-select-option[data-highlighted] {
|
|
464
|
+
outline: 2px solid var(--focus);
|
|
465
|
+
outline-offset: -2px;
|
|
466
|
+
}
|
|
467
|
+
.hp-select-option[data-disabled] {
|
|
468
|
+
color: var(--muted);
|
|
469
|
+
cursor: not-allowed;
|
|
470
|
+
}
|
|
471
|
+
</style>
|