@mystravil/hardpoint 0.1.0 → 0.2.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/CHANGELOG.md +20 -0
- package/README.md +5 -5
- package/dist/base.css +279 -0
- package/dist/components/Checkbox.svelte +46 -9
- package/dist/components/Checkbox.svelte.d.ts +9 -2
- package/dist/components/CheckboxGroup.svelte +131 -23
- package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
- package/dist/components/Combobox.svelte +104 -27
- package/dist/components/Combobox.svelte.d.ts +10 -1
- package/dist/components/Dialog.svelte +4 -1
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +135 -119
- 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 +6 -6
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +89 -23
- package/dist/components/Select.svelte.d.ts +6 -0
- package/dist/components/Spinner.svelte +3 -2
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/Toaster.svelte +6 -6
- package/dist/components/Tooltip.svelte +5 -5
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/tokens.css +4 -0
- package/package.json +1 -1
|
@@ -8,58 +8,119 @@
|
|
|
8
8
|
label: string;
|
|
9
9
|
/** A second line in `--muted` under the label. */
|
|
10
10
|
hint?: string;
|
|
11
|
+
/** A Tag after the label, such as "Recommended": a short fixed string, set in caps. */
|
|
12
|
+
tag?: string;
|
|
11
13
|
disabled?: boolean;
|
|
12
14
|
};
|
|
13
15
|
|
|
16
|
+
/** The group's name: a fixed caps `label`, or a `legend` set as written. Exactly one. */
|
|
17
|
+
type CheckboxGroupName =
|
|
18
|
+
| {
|
|
19
|
+
/** The caps label that names the group: a fixed string the app defines. */
|
|
20
|
+
label: string;
|
|
21
|
+
legend?: never;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
label?: never;
|
|
25
|
+
/**
|
|
26
|
+
* The group's name as content, set as written in 16px at weight 500:
|
|
27
|
+
* a question, or a name with a counter before it.
|
|
28
|
+
*/
|
|
29
|
+
legend: Snippet;
|
|
30
|
+
};
|
|
31
|
+
|
|
14
32
|
/** Attributes and handlers not listed here go to the group element. */
|
|
15
|
-
export type CheckboxGroupProps = Omit<CheckboxPrimitive.GroupProps, 'child' | 'children' | 'ref'> &
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
export type CheckboxGroupProps = Omit<CheckboxPrimitive.GroupProps, 'child' | 'children' | 'ref'> &
|
|
34
|
+
CheckboxGroupName & {
|
|
35
|
+
/** A line in `--muted` under the name, read out as the group's description. */
|
|
36
|
+
hint?: string;
|
|
37
|
+
/**
|
|
38
|
+
* The error to show under the boxes, with an icon, linked by
|
|
39
|
+
* `aria-describedby`; it sets `aria-invalid` on the group. Say how to
|
|
40
|
+
* fix the problem. The app validates; the group shows what it is given.
|
|
41
|
+
*/
|
|
42
|
+
error?: string;
|
|
43
|
+
/** The choices. Without it, pass `Checkbox` components with a `value` as children. */
|
|
44
|
+
items?: CheckboxGroupItem[];
|
|
45
|
+
children?: Snippet;
|
|
46
|
+
/** The group element. */
|
|
47
|
+
ref?: HTMLElement | null;
|
|
48
|
+
};
|
|
26
49
|
</script>
|
|
27
50
|
|
|
28
51
|
<script lang="ts">
|
|
52
|
+
import { untrack } from 'svelte';
|
|
29
53
|
import { Checkbox as BitsCheckbox } from 'bits-ui';
|
|
30
54
|
import Checkbox from './Checkbox.svelte';
|
|
55
|
+
import { fadeIn } from './internal/fade-in.js';
|
|
31
56
|
|
|
32
57
|
const uid = $props.id();
|
|
33
58
|
|
|
34
59
|
let {
|
|
35
60
|
value = $bindable([]),
|
|
36
61
|
label,
|
|
62
|
+
legend,
|
|
37
63
|
hint,
|
|
64
|
+
error,
|
|
38
65
|
items,
|
|
39
66
|
id = uid,
|
|
40
67
|
ref = $bindable(null),
|
|
41
68
|
class: className,
|
|
42
69
|
'aria-describedby': describedBy,
|
|
70
|
+
'aria-invalid': ariaInvalid,
|
|
43
71
|
children,
|
|
44
72
|
...rest
|
|
45
73
|
}: CheckboxGroupProps = $props();
|
|
46
74
|
|
|
47
75
|
const hintId = $derived(`${id}-hint`);
|
|
48
|
-
const
|
|
76
|
+
const errorId = $derived(`${id}-error`);
|
|
77
|
+
// The error first, so a screen reader reads it before the hint.
|
|
78
|
+
const describedByIds = $derived(
|
|
79
|
+
[error ? errorId : undefined, hint ? hintId : undefined, describedBy].filter(Boolean).join(' ') ||
|
|
80
|
+
undefined
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// ---- Short motion: the error (BRIEF, --dur-message) ---------------------
|
|
84
|
+
|
|
85
|
+
// As in TextField: an error that appears after the first render fades in
|
|
86
|
+
// from 0 over --dur-message; one that clears goes at once; a group that
|
|
87
|
+
// renders with an error shows it at once.
|
|
88
|
+
let errorShown = untrack(() => !!error);
|
|
89
|
+
$effect(() => {
|
|
90
|
+
const shown = !!error;
|
|
91
|
+
untrack(() => {
|
|
92
|
+
const appeared = shown && !errorShown;
|
|
93
|
+
errorShown = shown;
|
|
94
|
+
const message = appeared ? ref?.querySelector(':scope > .error') : null;
|
|
95
|
+
if (message) fadeIn(message, '--dur-message');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
49
98
|
</script>
|
|
50
99
|
|
|
51
100
|
<!--
|
|
52
|
-
Several independent choices under one
|
|
53
|
-
|
|
54
|
-
the checked boxes. `disabled`,
|
|
55
|
-
|
|
101
|
+
Several independent choices under one name: a Bits UI Checkbox.Group, a
|
|
102
|
+
role="group" element named by its caps `label` or its `legend`.
|
|
103
|
+
`bind:value` holds the values of the checked boxes. `disabled`,
|
|
104
|
+
`required`, `readonly` and `name` apply to every box. Attributes, handlers
|
|
105
|
+
and `class` go to the group element.
|
|
56
106
|
-->
|
|
57
|
-
<BitsCheckbox.Group
|
|
107
|
+
<BitsCheckbox.Group
|
|
108
|
+
bind:value
|
|
109
|
+
bind:ref
|
|
110
|
+
{id}
|
|
111
|
+
aria-describedby={describedByIds}
|
|
112
|
+
aria-invalid={error ? true : ariaInvalid}
|
|
113
|
+
{...rest}
|
|
114
|
+
>
|
|
58
115
|
{#snippet child({ props })}
|
|
59
|
-
<div {...props} class={['hp-checkbox-group', className]}>
|
|
116
|
+
<div {...props} class={['hp-checkbox-group', className]} data-invalid={error ? '' : undefined}>
|
|
60
117
|
<BitsCheckbox.GroupLabel>
|
|
61
118
|
{#snippet child({ props: labelProps })}
|
|
62
|
-
|
|
119
|
+
{#if legend}
|
|
120
|
+
<div {...labelProps} class="legend">{@render legend()}</div>
|
|
121
|
+
{:else}
|
|
122
|
+
<span {...labelProps} class="hp-label group-label">{label}</span>
|
|
123
|
+
{/if}
|
|
63
124
|
{/snippet}
|
|
64
125
|
</BitsCheckbox.GroupLabel>
|
|
65
126
|
{#if hint}
|
|
@@ -68,20 +129,37 @@
|
|
|
68
129
|
<div class="items">
|
|
69
130
|
{#if items}
|
|
70
131
|
{#each items as item (item.value)}
|
|
71
|
-
<Checkbox
|
|
132
|
+
<Checkbox
|
|
133
|
+
value={item.value}
|
|
134
|
+
label={item.label}
|
|
135
|
+
tag={item.tag}
|
|
136
|
+
hint={item.hint}
|
|
137
|
+
disabled={item.disabled}
|
|
138
|
+
/>
|
|
72
139
|
{/each}
|
|
73
140
|
{:else}
|
|
74
141
|
{@render children?.()}
|
|
75
142
|
{/if}
|
|
76
143
|
</div>
|
|
144
|
+
{#if error}
|
|
145
|
+
<p class="error hp-caption" id={errorId}>
|
|
146
|
+
<span class="icon" aria-hidden="true">
|
|
147
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
|
|
148
|
+
<path d="M1.5 1.5h13v13h-13z" />
|
|
149
|
+
<path d="M8 4v4M8 10v2" stroke-width="2" />
|
|
150
|
+
</svg>
|
|
151
|
+
</span>
|
|
152
|
+
<span>{error}</span>
|
|
153
|
+
</p>
|
|
154
|
+
{/if}
|
|
77
155
|
</div>
|
|
78
156
|
{/snippet}
|
|
79
157
|
</BitsCheckbox.Group>
|
|
80
158
|
|
|
81
159
|
<style>
|
|
82
|
-
/* The
|
|
83
|
-
|
|
84
|
-
the group taller. */
|
|
160
|
+
/* The name, its hint, then the boxes, 8px apart so neighbouring targets
|
|
161
|
+
never touch, then the error. Rows keep their own height when a layout
|
|
162
|
+
makes the group taller. */
|
|
85
163
|
.hp-checkbox-group {
|
|
86
164
|
display: grid;
|
|
87
165
|
align-content: start;
|
|
@@ -92,8 +170,38 @@
|
|
|
92
170
|
.hint {
|
|
93
171
|
color: var(--muted);
|
|
94
172
|
}
|
|
173
|
+
/* A name set as written: a question, or a name with a counter. */
|
|
174
|
+
.legend {
|
|
175
|
+
font-size: var(--type-body);
|
|
176
|
+
font-weight: 500;
|
|
177
|
+
line-height: 24px;
|
|
178
|
+
}
|
|
179
|
+
[data-disabled] > .legend {
|
|
180
|
+
color: var(--muted);
|
|
181
|
+
}
|
|
95
182
|
.items {
|
|
96
183
|
display: grid;
|
|
97
184
|
gap: var(--space-2);
|
|
98
185
|
}
|
|
186
|
+
|
|
187
|
+
/* ---- The error: TextField's ------------------------------------------- */
|
|
188
|
+
|
|
189
|
+
/* It starts 8px under the last box's row, clear of its focus contour. */
|
|
190
|
+
.error {
|
|
191
|
+
display: flex;
|
|
192
|
+
gap: var(--space-2);
|
|
193
|
+
margin-top: var(--space-1);
|
|
194
|
+
color: var(--danger);
|
|
195
|
+
}
|
|
196
|
+
/* One line box high, so the icon centres on the first line of the
|
|
197
|
+
message however it wraps. */
|
|
198
|
+
.icon {
|
|
199
|
+
display: flex;
|
|
200
|
+
flex: none;
|
|
201
|
+
align-items: center;
|
|
202
|
+
height: 1lh;
|
|
203
|
+
}
|
|
204
|
+
.icon svg {
|
|
205
|
+
display: block;
|
|
206
|
+
}
|
|
99
207
|
</style>
|
|
@@ -6,14 +6,33 @@ export type CheckboxGroupItem = {
|
|
|
6
6
|
label: string;
|
|
7
7
|
/** A second line in `--muted` under the label. */
|
|
8
8
|
hint?: string;
|
|
9
|
+
/** A Tag after the label, such as "Recommended": a short fixed string, set in caps. */
|
|
10
|
+
tag?: string;
|
|
9
11
|
disabled?: boolean;
|
|
10
12
|
};
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
/** The caps label that names the group. */
|
|
13
|
+
/** The group's name: a fixed caps `label`, or a `legend` set as written. Exactly one. */
|
|
14
|
+
type CheckboxGroupName = {
|
|
15
|
+
/** The caps label that names the group: a fixed string the app defines. */
|
|
14
16
|
label: string;
|
|
15
|
-
|
|
17
|
+
legend?: never;
|
|
18
|
+
} | {
|
|
19
|
+
label?: never;
|
|
20
|
+
/**
|
|
21
|
+
* The group's name as content, set as written in 16px at weight 500:
|
|
22
|
+
* a question, or a name with a counter before it.
|
|
23
|
+
*/
|
|
24
|
+
legend: Snippet;
|
|
25
|
+
};
|
|
26
|
+
/** Attributes and handlers not listed here go to the group element. */
|
|
27
|
+
export type CheckboxGroupProps = Omit<CheckboxPrimitive.GroupProps, 'child' | 'children' | 'ref'> & CheckboxGroupName & {
|
|
28
|
+
/** A line in `--muted` under the name, read out as the group's description. */
|
|
16
29
|
hint?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The error to show under the boxes, with an icon, linked by
|
|
32
|
+
* `aria-describedby`; it sets `aria-invalid` on the group. Say how to
|
|
33
|
+
* fix the problem. The app validates; the group shows what it is given.
|
|
34
|
+
*/
|
|
35
|
+
error?: string;
|
|
17
36
|
/** The choices. Without it, pass `Checkbox` components with a `value` as children. */
|
|
18
37
|
items?: CheckboxGroupItem[];
|
|
19
38
|
children?: Snippet;
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
label: string;
|
|
9
9
|
/** A second line in `--muted`. */
|
|
10
10
|
description?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The heading the option sits under, as written; the default filter
|
|
13
|
+
* matches it too. Options with the same group share one heading, in the
|
|
14
|
+
* order each group first appears; options without one come first.
|
|
15
|
+
*/
|
|
16
|
+
group?: string;
|
|
11
17
|
disabled?: boolean;
|
|
12
18
|
};
|
|
13
19
|
|
|
@@ -51,7 +57,10 @@
|
|
|
51
57
|
* The hint and the error stay under the field.
|
|
52
58
|
*/
|
|
53
59
|
labelBeside?: ComboboxLabelBeside;
|
|
54
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Replaces the default filter: the label or the group contains the text,
|
|
62
|
+
* ignoring case and accents.
|
|
63
|
+
*/
|
|
55
64
|
filter?: ComboboxFilter;
|
|
56
65
|
/** Shown in place of the options when nothing matches. */
|
|
57
66
|
emptyText?: string;
|
|
@@ -72,6 +81,7 @@
|
|
|
72
81
|
import { MediaQuery } from 'svelte/reactivity';
|
|
73
82
|
import { pop } from '../motion/index.js';
|
|
74
83
|
import { fadeIn } from './internal/fade-in.js';
|
|
84
|
+
import { groupOptions } from './internal/groups.js';
|
|
75
85
|
import Sheet from './internal/Sheet.svelte';
|
|
76
86
|
|
|
77
87
|
const uid = $props.id();
|
|
@@ -150,13 +160,19 @@
|
|
|
150
160
|
let query = $state('');
|
|
151
161
|
|
|
152
162
|
const fold = (s: string) => s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
|
|
153
|
-
const
|
|
163
|
+
const byLabelOrGroup: ComboboxFilter = (entry, q) => {
|
|
164
|
+
const needle = fold(q);
|
|
165
|
+
return fold(entry.label).includes(needle) || (!!entry.group && fold(entry.group).includes(needle));
|
|
166
|
+
};
|
|
154
167
|
const matches = $derived.by(() => {
|
|
155
168
|
const q = query.trim();
|
|
156
169
|
if (!q) return items;
|
|
157
|
-
const keep = filter ??
|
|
170
|
+
const keep = filter ?? byLabelOrGroup;
|
|
158
171
|
return items.filter((entry) => keep(entry, q));
|
|
159
172
|
});
|
|
173
|
+
// Options without a group, then each group that still has a match; a
|
|
174
|
+
// group whose options all filter out drops its heading too.
|
|
175
|
+
const grouped = $derived(groupOptions(matches));
|
|
160
176
|
|
|
161
177
|
// The highlight moved by keys gets the focus contour; a pointer's gets the
|
|
162
178
|
// hover tint alone. Focus itself stays in the input.
|
|
@@ -212,6 +228,11 @@
|
|
|
212
228
|
oninputProp?.(e);
|
|
213
229
|
query = e.currentTarget.value;
|
|
214
230
|
text = query;
|
|
231
|
+
// The matches show from the top, so Bits UI's highlight, which goes to
|
|
232
|
+
// the first option in view, lands on the first match, and no match
|
|
233
|
+
// waits above the fold or under a group heading.
|
|
234
|
+
const viewport = document.getElementById(listId)?.querySelector('[data-combobox-viewport]');
|
|
235
|
+
if (viewport) viewport.scrollTop = 0;
|
|
215
236
|
}
|
|
216
237
|
|
|
217
238
|
// A press on the field opens the list (openChanged selects its text).
|
|
@@ -271,12 +292,13 @@
|
|
|
271
292
|
// ContentStatic with forceMount reports placement once, at mount, while
|
|
272
293
|
// still closed. So Bits neither opens it at the chosen option nor scrolls
|
|
273
294
|
// to the option the keys move to; this does both, nearest edge first, as
|
|
274
|
-
// Bits does in the popover (Select does the same).
|
|
295
|
+
// Bits does in the popover (Select does the same). The viewport's
|
|
296
|
+
// scroll-padding keeps an option clear of a group heading stuck at the top.
|
|
275
297
|
function followHighlight(viewport: HTMLElement) {
|
|
276
298
|
const reveal = (option: Element | null) => {
|
|
277
299
|
if (!(option instanceof HTMLElement)) return;
|
|
278
|
-
const top = option.offsetTop;
|
|
279
|
-
const bottom =
|
|
300
|
+
const top = option.offsetTop - (parseFloat(getComputedStyle(viewport).scrollPaddingTop) || 0);
|
|
301
|
+
const bottom = option.offsetTop + option.offsetHeight;
|
|
280
302
|
if (top < viewport.scrollTop) viewport.scrollTop = top;
|
|
281
303
|
else if (bottom > viewport.scrollTop + viewport.clientHeight) {
|
|
282
304
|
viewport.scrollTop = bottom - viewport.clientHeight;
|
|
@@ -484,35 +506,63 @@
|
|
|
484
506
|
{#snippet options(inSheet: boolean)}
|
|
485
507
|
<BitsCombobox.Viewport>
|
|
486
508
|
{#snippet child({ props: viewportProps })}
|
|
487
|
-
<div
|
|
488
|
-
{
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
{
|
|
504
|
-
|
|
509
|
+
<div
|
|
510
|
+
{...viewportProps}
|
|
511
|
+
class="hp-combobox-viewport"
|
|
512
|
+
data-grouped={grouped.groups.length ? '' : undefined}
|
|
513
|
+
{@attach inSheet && followHighlight}
|
|
514
|
+
>
|
|
515
|
+
{#each grouped.loose as entry (entry.value)}
|
|
516
|
+
{@render option(entry)}
|
|
517
|
+
{/each}
|
|
518
|
+
{#each grouped.groups as group (group.name)}
|
|
519
|
+
<!-- role="group", named by its heading, which is no option: keys
|
|
520
|
+
skip it. -->
|
|
521
|
+
<BitsCombobox.Group>
|
|
522
|
+
{#snippet child({ props: groupProps })}
|
|
523
|
+
<div {...groupProps} class="hp-combobox-group">
|
|
524
|
+
<BitsCombobox.GroupHeading>
|
|
525
|
+
{#snippet child({ props: headingProps })}
|
|
526
|
+
<div {...headingProps} class="hp-combobox-group-heading">{group.name}</div>
|
|
527
|
+
{/snippet}
|
|
528
|
+
</BitsCombobox.GroupHeading>
|
|
529
|
+
{#each group.items as entry (entry.value)}
|
|
530
|
+
{@render option(entry)}
|
|
531
|
+
{/each}
|
|
505
532
|
</div>
|
|
506
533
|
{/snippet}
|
|
507
|
-
</BitsCombobox.
|
|
534
|
+
</BitsCombobox.Group>
|
|
508
535
|
{/each}
|
|
509
536
|
</div>
|
|
510
537
|
{/snippet}
|
|
511
538
|
</BitsCombobox.Viewport>
|
|
512
539
|
{/snippet}
|
|
513
540
|
|
|
514
|
-
|
|
515
|
-
|
|
541
|
+
{#snippet option(entry: ComboboxItem)}
|
|
542
|
+
<BitsCombobox.Item value={entry.value} label={entry.label} disabled={entry.disabled}>
|
|
543
|
+
{#snippet child({ props: optionProps, selected, highlighted })}
|
|
544
|
+
<div
|
|
545
|
+
{...optionProps}
|
|
546
|
+
class="hp-combobox-option"
|
|
547
|
+
data-described={entry.description ? '' : undefined}
|
|
548
|
+
aria-disabled={entry.disabled ? 'true' : undefined}
|
|
549
|
+
>
|
|
550
|
+
{#if itemContent}
|
|
551
|
+
{@render itemContent({ item: entry, selected, highlighted })}
|
|
552
|
+
{:else}
|
|
553
|
+
<span class="hp-combobox-option-label">{entry.label}</span>
|
|
554
|
+
{#if entry.description}
|
|
555
|
+
<span class="hp-combobox-option-description">{entry.description}</span>
|
|
556
|
+
{/if}
|
|
557
|
+
{/if}
|
|
558
|
+
</div>
|
|
559
|
+
{/snippet}
|
|
560
|
+
</BitsCombobox.Item>
|
|
561
|
+
{/snippet}
|
|
562
|
+
|
|
563
|
+
<!-- Outside the listbox, which may hold options and groups only. Mounted
|
|
564
|
+
while the list is open, so the text it gains when nothing matches is
|
|
565
|
+
announced. -->
|
|
516
566
|
{#snippet empty(inSheet: boolean)}
|
|
517
567
|
<p class="hp-combobox-empty" data-form={inSheet ? 'sheet' : 'popover'} role="status">
|
|
518
568
|
{#if !matches.length}{emptyText}{/if}
|
|
@@ -746,6 +796,33 @@
|
|
|
746
796
|
.hp-combobox-list[data-empty] .hp-combobox-viewport[data-combobox-viewport] {
|
|
747
797
|
padding-block: 0;
|
|
748
798
|
}
|
|
799
|
+
/* An option the keys or the chosen value scroll to stays clear of the
|
|
800
|
+
group heading stuck at the top: one heading line high. */
|
|
801
|
+
.hp-combobox-viewport[data-grouped] {
|
|
802
|
+
scroll-padding-top: calc(var(--space-2) + 16px + var(--space-1));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/* ---- Group headings: as Select's ------------------------------------ */
|
|
806
|
+
|
|
807
|
+
/* MenuGroup's heading: muted, on the options' text edge, 8px above and
|
|
808
|
+
4px below. The text stays as written, because a group name may be the
|
|
809
|
+
app's data (BRIEF, style rule 4). It stays at the top of the list while
|
|
810
|
+
its options scroll under it, until the next group pushes it up; that is
|
|
811
|
+
scrolling, not motion. It sticks to the viewport's edge, over its 4px
|
|
812
|
+
padding, so nothing shows above it. */
|
|
813
|
+
.hp-combobox-group-heading {
|
|
814
|
+
position: sticky;
|
|
815
|
+
top: calc(-1 * var(--space-1));
|
|
816
|
+
z-index: 1;
|
|
817
|
+
padding: var(--space-2) var(--option-inset) var(--space-1);
|
|
818
|
+
background: var(--canvas);
|
|
819
|
+
color: var(--muted);
|
|
820
|
+
font-size: var(--type-label);
|
|
821
|
+
font-weight: 500;
|
|
822
|
+
line-height: 16px;
|
|
823
|
+
overflow-wrap: anywhere;
|
|
824
|
+
user-select: none;
|
|
825
|
+
}
|
|
749
826
|
|
|
750
827
|
/* ---- Options: Geist list items, sentence case ----------------------- */
|
|
751
828
|
|
|
@@ -6,6 +6,12 @@ export type ComboboxItem = {
|
|
|
6
6
|
label: string;
|
|
7
7
|
/** A second line in `--muted`. */
|
|
8
8
|
description?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The heading the option sits under, as written; the default filter
|
|
11
|
+
* matches it too. Options with the same group share one heading, in the
|
|
12
|
+
* order each group first appears; options without one come first.
|
|
13
|
+
*/
|
|
14
|
+
group?: string;
|
|
9
15
|
disabled?: boolean;
|
|
10
16
|
};
|
|
11
17
|
export type ComboboxItemSnippetProps = {
|
|
@@ -45,7 +51,10 @@ export type ComboboxProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'id'>
|
|
|
45
51
|
* The hint and the error stay under the field.
|
|
46
52
|
*/
|
|
47
53
|
labelBeside?: ComboboxLabelBeside;
|
|
48
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Replaces the default filter: the label or the group contains the text,
|
|
56
|
+
* ignoring case and accents.
|
|
57
|
+
*/
|
|
49
58
|
filter?: ComboboxFilter;
|
|
50
59
|
/** Shown in place of the options when nothing matches. */
|
|
51
60
|
emptyText?: string;
|
|
@@ -309,13 +309,16 @@
|
|
|
309
309
|
/* ---- Body and actions, in every form -------------------------------- */
|
|
310
310
|
|
|
311
311
|
/* The top padding keeps a first field's focus contour (4px out, 2px
|
|
312
|
-
wide) inside the scrolling box.
|
|
312
|
+
wide) inside the scrolling box. The scroll padding keeps a focused
|
|
313
|
+
field's caret 8px clear of the action row when the browser reveals it,
|
|
314
|
+
as when a TextArea grows while someone types. */
|
|
313
315
|
.hp-dialog-body {
|
|
314
316
|
display: grid;
|
|
315
317
|
align-content: start;
|
|
316
318
|
gap: var(--space-4);
|
|
317
319
|
min-width: 0;
|
|
318
320
|
padding: var(--space-2) var(--gutter);
|
|
321
|
+
scroll-padding-block: var(--space-2);
|
|
319
322
|
font-size: var(--type-body);
|
|
320
323
|
line-height: 24px;
|
|
321
324
|
}
|