@lyeve-labs/ui-kit 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -17
- package/dist/components/AccordionItem.svelte +2 -2
- package/dist/components/AccountMenu.svelte +1 -1
- package/dist/components/AppShell.svelte +22 -8
- package/dist/components/Autocomplete.svelte +13 -4
- package/dist/components/AvatarGroup.svelte +2 -2
- package/dist/components/Breadcrumb.svelte +1 -1
- package/dist/components/Button.svelte +34 -9
- package/dist/components/ButtonGroup.svelte +3 -3
- package/dist/components/Card.svelte +2 -1
- package/dist/components/Checkbox.svelte +1 -1
- package/dist/components/CheckboxGroup.svelte +1 -1
- package/dist/components/Collapsible.svelte +4 -4
- package/dist/components/CopyButton.svelte +1 -1
- package/dist/components/DatePicker.svelte +3 -3
- package/dist/components/DateTimePicker.svelte +1 -1
- package/dist/components/Drawer.svelte +7 -27
- package/dist/components/Dropdown.svelte +197 -11
- package/dist/components/Dropdown.svelte.d.ts +12 -0
- package/dist/components/Field.svelte +1 -1
- package/dist/components/Input.svelte +1 -1
- package/dist/components/Label.svelte +1 -1
- package/dist/components/Logo.svelte +77 -0
- package/dist/components/Logo.svelte.d.ts +35 -0
- package/dist/components/Modal.svelte +2 -15
- package/dist/components/MultiSelect.svelte +16 -7
- package/dist/components/NumberInput.svelte +3 -3
- package/dist/components/PageHeader.svelte +23 -1
- package/dist/components/Pagination.svelte +164 -65
- package/dist/components/Pagination.svelte.d.ts +39 -2
- package/dist/components/Panel.svelte +1 -1
- package/dist/components/PasswordInput.svelte +4 -4
- package/dist/components/Radio.svelte +1 -1
- package/dist/components/RadioGroup.svelte +1 -1
- package/dist/components/SearchInput.svelte +13 -4
- package/dist/components/SegmentedControl.svelte +3 -1
- package/dist/components/Select.svelte +6 -6
- package/dist/components/SidebarNav.svelte +20 -5
- package/dist/components/Table.svelte +256 -10
- package/dist/components/Table.svelte.d.ts +44 -0
- package/dist/components/Tabs.svelte +1 -1
- package/dist/components/Tag.svelte +1 -1
- package/dist/components/Textarea.svelte +1 -1
- package/dist/components/ThemeToggle.svelte +76 -13
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/TimePicker.svelte +2 -2
- package/dist/components/Toaster.svelte +3 -15
- package/dist/components/Toggle.svelte +10 -4
- package/dist/components/Tooltip.svelte +1 -1
- package/dist/components/TreeView.svelte +4 -4
- package/dist/components/dialog/Dialog.svelte +10 -5
- package/dist/components/dialog/dialog-manager.svelte.d.ts +20 -1
- package/dist/components/dialog/dialog-manager.svelte.js +13 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -2
- package/dist/internal/href.d.ts +13 -0
- package/dist/internal/href.js +27 -0
- package/dist/internal/layout.js +11 -2
- package/dist/internal/panel.d.ts +1 -1
- package/dist/internal/panel.js +2 -2
- package/dist/styles/theme.css +288 -0
- package/dist/utils/theme.d.ts +68 -3
- package/dist/utils/theme.js +118 -3
- package/package.json +20 -22
- package/src/lib/styles/theme.css +288 -0
|
@@ -1,24 +1,73 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { safeHref } from '../internal/href.js';
|
|
3
|
+
|
|
4
|
+
interface Base {
|
|
5
|
+
page: number;
|
|
6
|
+
/**
|
|
7
|
+
* Collection size, when the endpoint states one.
|
|
8
|
+
*
|
|
9
|
+
* Leave it out for a list whose endpoint reports no count and say `hasNext`
|
|
10
|
+
* instead. Absent is not zero: a caller who does state a total, even a
|
|
11
|
+
* nonsense one, gets the counted pager with that total clamped.
|
|
12
|
+
*/
|
|
13
|
+
total?: number;
|
|
14
|
+
perPage?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a page after this one exists.
|
|
17
|
+
*
|
|
18
|
+
* Read only when `total` is absent, because a counted list already knows
|
|
19
|
+
* where it ends. It is the answer an endpoint can give without counting the
|
|
20
|
+
* collection: ask for one row more than the page holds and see if it comes
|
|
21
|
+
* back.
|
|
22
|
+
*/
|
|
23
|
+
hasNext?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How the pager moves, and why it takes both spellings.
|
|
29
|
+
*
|
|
30
|
+
* `onchange` cannot run before the page hydrates, so a pager that has only a
|
|
31
|
+
* callback is inert on the first paint and an early click lands on nothing.
|
|
32
|
+
* That is not a rare state: it is every server-rendered document, for as long
|
|
33
|
+
* as the bundle takes to arrive. A caller that has to page from that document
|
|
34
|
+
* supplies `href` instead and gets links, which the browser follows with no
|
|
35
|
+
* script at all.
|
|
36
|
+
*
|
|
37
|
+
* Exclusive rather than both together: an anchor whose click also ran the
|
|
38
|
+
* callback would page twice, once through the handler and again through the
|
|
39
|
+
* navigation it did not cancel.
|
|
40
|
+
*/
|
|
41
|
+
type Nav =
|
|
42
|
+
| { onchange: (page: number) => void; href?: undefined }
|
|
43
|
+
| { href: (page: number) => string; onchange?: undefined };
|
|
44
|
+
|
|
2
45
|
let {
|
|
3
46
|
page,
|
|
4
|
-
total,
|
|
47
|
+
total = undefined,
|
|
5
48
|
perPage = 20,
|
|
49
|
+
hasNext = false,
|
|
6
50
|
class: cls = '',
|
|
7
51
|
onchange,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
total: number;
|
|
11
|
-
perPage?: number;
|
|
12
|
-
class?: string;
|
|
13
|
-
onchange: (page: number) => void;
|
|
14
|
-
} = $props();
|
|
52
|
+
href,
|
|
53
|
+
}: Base & Nav = $props();
|
|
15
54
|
|
|
16
|
-
|
|
55
|
+
/**
|
|
56
|
+
* A stated total is what makes the numbered pages knowable. Without one the
|
|
57
|
+
* pager can offer the step either side of where it stands and nothing more,
|
|
58
|
+
* because the last page is not a number anybody here has.
|
|
59
|
+
*/
|
|
60
|
+
let counted = $derived(total !== undefined);
|
|
61
|
+
let safeTotal = $derived(typeof total === 'number' && isFinite(total) && total >= 0 ? total : 0);
|
|
17
62
|
let safePage = $derived(isFinite(page) && page >= 1 ? page : 1);
|
|
18
63
|
let totalPages = $derived(Math.max(1, Math.ceil(safeTotal / perPage)));
|
|
19
64
|
let from = $derived(Math.min((safePage - 1) * perPage + 1, safeTotal));
|
|
20
65
|
let to = $derived(Math.min(safePage * perPage, safeTotal));
|
|
21
66
|
|
|
67
|
+
let canPrev = $derived(safePage > 1);
|
|
68
|
+
let canNext = $derived(counted ? safePage < totalPages : hasNext);
|
|
69
|
+
let hasControls = $derived(counted ? totalPages > 1 : canPrev || canNext);
|
|
70
|
+
|
|
22
71
|
/**
|
|
23
72
|
* The elided run is the sentinel string 'gap', never the horizontal-ellipsis
|
|
24
73
|
* character.
|
|
@@ -51,46 +100,112 @@
|
|
|
51
100
|
return slot === 'gap' ? `gap-${index}` : `page-${slot}`;
|
|
52
101
|
}
|
|
53
102
|
|
|
54
|
-
let nums = $derived(pageNumbers(safePage, totalPages));
|
|
103
|
+
let nums = $derived(counted ? pageNumbers(safePage, totalPages) : []);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The count, when there is one to state.
|
|
107
|
+
*
|
|
108
|
+
* An uncounted list says which page it is on and nothing else. It cannot say
|
|
109
|
+
* "1 to 20 of 400" without the total, and it cannot say "1 to 20" either: the
|
|
110
|
+
* last page is short and this component is never told how short.
|
|
111
|
+
*/
|
|
112
|
+
let summary = $derived(
|
|
113
|
+
counted
|
|
114
|
+
? safeTotal === 0
|
|
115
|
+
? 'No results'
|
|
116
|
+
: `${from} to ${to} of ${safeTotal}`
|
|
117
|
+
: `Page ${safePage}`,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
/** The target of a control, sanitized, or nothing when the step is unavailable. */
|
|
121
|
+
function linkTo(target: number, enabled: boolean): string | undefined {
|
|
122
|
+
return href && enabled ? safeHref(href(target)) : undefined;
|
|
123
|
+
}
|
|
55
124
|
|
|
56
125
|
const btnBase =
|
|
57
126
|
'inline-flex items-center justify-center w-7 h-7 rounded text-xs font-medium transition-colors duration-150';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A page control that is not the page you are on, in every state a pointer
|
|
130
|
+
* can put it in.
|
|
131
|
+
*
|
|
132
|
+
* Held as well as hovered, because `hover:` never matches on a touch screen
|
|
133
|
+
* and paging is a thumb gesture: without the pressed step a tap on a phone
|
|
134
|
+
* changed the page with nothing on the control to say it had been hit.
|
|
135
|
+
*
|
|
136
|
+
* Stated once rather than spelled out on the previous button, the next button
|
|
137
|
+
* and every numbered one. It was written three times, and a state added to
|
|
138
|
+
* one copy is a state missing from two.
|
|
139
|
+
*/
|
|
140
|
+
const btnRest = 'text-muted hover:text-fg hover:bg-surface-2 active:bg-line active:text-fg';
|
|
141
|
+
|
|
142
|
+
const btnCurrent = 'bg-brand text-ink';
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A link with nowhere to go.
|
|
146
|
+
*
|
|
147
|
+
* An anchor has no `disabled`, and `disabled:` never matches one, so the two
|
|
148
|
+
* states have to be painted rather than declared. Dropping the href is what
|
|
149
|
+
* takes it out of the tab order and stops activation; the class is only the
|
|
150
|
+
* part a reader can see, at the same 30% the disabled buttons use.
|
|
151
|
+
*/
|
|
152
|
+
const linkInert = 'pointer-events-none opacity-30';
|
|
58
153
|
</script>
|
|
59
154
|
|
|
155
|
+
{#snippet arrow(d: string)}
|
|
156
|
+
<svg
|
|
157
|
+
width="14"
|
|
158
|
+
height="14"
|
|
159
|
+
viewBox="0 0 24 24"
|
|
160
|
+
fill="none"
|
|
161
|
+
stroke="currentColor"
|
|
162
|
+
stroke-width="2"
|
|
163
|
+
stroke-linecap="round"
|
|
164
|
+
stroke-linejoin="round"
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
class="rtl:rotate-180"
|
|
167
|
+
>
|
|
168
|
+
<path {d} />
|
|
169
|
+
</svg>
|
|
170
|
+
{/snippet}
|
|
171
|
+
|
|
172
|
+
<!-- Previous and next, as a link when the caller builds hrefs and as a button
|
|
173
|
+
when it hands over a callback. The two branches carry the same classes and
|
|
174
|
+
the same label, so the pager reads and sounds the same either way. -->
|
|
175
|
+
{#snippet step(target: number, label: string, enabled: boolean, d: string)}
|
|
176
|
+
{#if href}
|
|
177
|
+
<a
|
|
178
|
+
href={linkTo(target, enabled)}
|
|
179
|
+
aria-label={label}
|
|
180
|
+
aria-disabled={enabled ? undefined : 'true'}
|
|
181
|
+
class="{btnBase} {btnRest} {enabled ? '' : linkInert}"
|
|
182
|
+
>
|
|
183
|
+
{@render arrow(d)}
|
|
184
|
+
</a>
|
|
185
|
+
{:else}
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
disabled={!enabled}
|
|
189
|
+
onclick={() => onchange?.(target)}
|
|
190
|
+
aria-label={label}
|
|
191
|
+
class="{btnBase} {btnRest} disabled:cursor-not-allowed disabled:opacity-30"
|
|
192
|
+
>
|
|
193
|
+
{@render arrow(d)}
|
|
194
|
+
</button>
|
|
195
|
+
{/if}
|
|
196
|
+
{/snippet}
|
|
197
|
+
|
|
60
198
|
<!-- The summary renders whenever there is a count to state, and the page
|
|
61
199
|
buttons only when there is more than one page. The whole component used to
|
|
62
200
|
be behind `totalPages > 1`, and an empty list has one page, so the 'No
|
|
63
201
|
results' line below could never appear and a single page of results showed
|
|
64
202
|
no count at all. -->
|
|
65
203
|
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 {cls}">
|
|
66
|
-
<span class="text-xs text-faint
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<div class="flex flex-wrap items-center gap-0.5 ml-auto">
|
|
72
|
-
<button
|
|
73
|
-
type="button"
|
|
74
|
-
disabled={safePage <= 1}
|
|
75
|
-
onclick={() => onchange(safePage - 1)}
|
|
76
|
-
aria-label="Previous page"
|
|
77
|
-
class="{btnBase} text-muted hover:text-fg hover:bg-surface-2
|
|
78
|
-
disabled:opacity-30 disabled:cursor-not-allowed"
|
|
79
|
-
>
|
|
80
|
-
<svg
|
|
81
|
-
width="14"
|
|
82
|
-
height="14"
|
|
83
|
-
viewBox="0 0 24 24"
|
|
84
|
-
fill="none"
|
|
85
|
-
stroke="currentColor"
|
|
86
|
-
stroke-width="2"
|
|
87
|
-
stroke-linecap="round"
|
|
88
|
-
stroke-linejoin="round"
|
|
89
|
-
aria-hidden="true"
|
|
90
|
-
>
|
|
91
|
-
<path d="M15 18l-6-6 6-6" />
|
|
92
|
-
</svg>
|
|
93
|
-
</button>
|
|
204
|
+
<span class="shrink-0 text-xs text-faint">{summary}</span>
|
|
205
|
+
|
|
206
|
+
{#if hasControls}
|
|
207
|
+
<div class="ms-auto flex flex-wrap items-center gap-0.5">
|
|
208
|
+
{@render step(safePage - 1, 'Previous page', canPrev, 'M15 18l-6-6 6-6')}
|
|
94
209
|
|
|
95
210
|
{#each nums as n, i (slotKey(n, i))}
|
|
96
211
|
{#if n === 'gap'}
|
|
@@ -111,43 +226,27 @@
|
|
|
111
226
|
<path d="M5 12h.01M12 12h.01M19 12h.01" />
|
|
112
227
|
</svg>
|
|
113
228
|
</span>
|
|
229
|
+
{:else if href}
|
|
230
|
+
<a
|
|
231
|
+
href={linkTo(n, true)}
|
|
232
|
+
aria-current={safePage === n ? 'page' : undefined}
|
|
233
|
+
class="{btnBase} {safePage === n ? btnCurrent : btnRest}"
|
|
234
|
+
>
|
|
235
|
+
{n}
|
|
236
|
+
</a>
|
|
114
237
|
{:else}
|
|
115
238
|
<button
|
|
116
239
|
type="button"
|
|
117
|
-
onclick={() => onchange(n as number)}
|
|
240
|
+
onclick={() => onchange?.(n as number)}
|
|
118
241
|
aria-current={safePage === n ? 'page' : undefined}
|
|
119
|
-
class="{btnBase}
|
|
120
|
-
{safePage === n
|
|
121
|
-
? 'bg-brand text-ink'
|
|
122
|
-
: 'text-muted hover:text-fg hover:bg-surface-2'}"
|
|
242
|
+
class="{btnBase} {safePage === n ? btnCurrent : btnRest}"
|
|
123
243
|
>
|
|
124
244
|
{n}
|
|
125
245
|
</button>
|
|
126
246
|
{/if}
|
|
127
247
|
{/each}
|
|
128
248
|
|
|
129
|
-
|
|
130
|
-
type="button"
|
|
131
|
-
disabled={safePage >= totalPages}
|
|
132
|
-
onclick={() => onchange(safePage + 1)}
|
|
133
|
-
aria-label="Next page"
|
|
134
|
-
class="{btnBase} text-muted hover:text-fg hover:bg-surface-2
|
|
135
|
-
disabled:opacity-30 disabled:cursor-not-allowed"
|
|
136
|
-
>
|
|
137
|
-
<svg
|
|
138
|
-
width="14"
|
|
139
|
-
height="14"
|
|
140
|
-
viewBox="0 0 24 24"
|
|
141
|
-
fill="none"
|
|
142
|
-
stroke="currentColor"
|
|
143
|
-
stroke-width="2"
|
|
144
|
-
stroke-linecap="round"
|
|
145
|
-
stroke-linejoin="round"
|
|
146
|
-
aria-hidden="true"
|
|
147
|
-
>
|
|
148
|
-
<path d="M9 18l6-6-6-6" />
|
|
149
|
-
</svg>
|
|
150
|
-
</button>
|
|
249
|
+
{@render step(safePage + 1, 'Next page', canNext, 'M9 18l6-6-6-6')}
|
|
151
250
|
</div>
|
|
152
251
|
{/if}
|
|
153
252
|
</div>
|
|
@@ -1,10 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
interface Base {
|
|
2
2
|
page: number;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Collection size, when the endpoint states one.
|
|
5
|
+
*
|
|
6
|
+
* Leave it out for a list whose endpoint reports no count and say `hasNext`
|
|
7
|
+
* instead. Absent is not zero: a caller who does state a total, even a
|
|
8
|
+
* nonsense one, gets the counted pager with that total clamped.
|
|
9
|
+
*/
|
|
10
|
+
total?: number;
|
|
4
11
|
perPage?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a page after this one exists.
|
|
14
|
+
*
|
|
15
|
+
* Read only when `total` is absent, because a counted list already knows
|
|
16
|
+
* where it ends. It is the answer an endpoint can give without counting the
|
|
17
|
+
* collection: ask for one row more than the page holds and see if it comes
|
|
18
|
+
* back.
|
|
19
|
+
*/
|
|
20
|
+
hasNext?: boolean;
|
|
5
21
|
class?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* How the pager moves, and why it takes both spellings.
|
|
25
|
+
*
|
|
26
|
+
* `onchange` cannot run before the page hydrates, so a pager that has only a
|
|
27
|
+
* callback is inert on the first paint and an early click lands on nothing.
|
|
28
|
+
* That is not a rare state: it is every server-rendered document, for as long
|
|
29
|
+
* as the bundle takes to arrive. A caller that has to page from that document
|
|
30
|
+
* supplies `href` instead and gets links, which the browser follows with no
|
|
31
|
+
* script at all.
|
|
32
|
+
*
|
|
33
|
+
* Exclusive rather than both together: an anchor whose click also ran the
|
|
34
|
+
* callback would page twice, once through the handler and again through the
|
|
35
|
+
* navigation it did not cancel.
|
|
36
|
+
*/
|
|
37
|
+
type Nav = {
|
|
6
38
|
onchange: (page: number) => void;
|
|
39
|
+
href?: undefined;
|
|
40
|
+
} | {
|
|
41
|
+
href: (page: number) => string;
|
|
42
|
+
onchange?: undefined;
|
|
7
43
|
};
|
|
44
|
+
type $$ComponentProps = Base & Nav;
|
|
8
45
|
declare const Pagination: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
46
|
type Pagination = ReturnType<typeof Pagination>;
|
|
10
47
|
export default Pagination;
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
const surface = $derived(`${SURFACE[variant]} ${CARD_PAD[pad]}`.trim());
|
|
69
69
|
</script>
|
|
70
70
|
|
|
71
|
-
<div class="flex flex-col gap-stack {surface} {klass}">
|
|
71
|
+
<div data-print="keep" class="flex flex-col gap-stack {surface} {klass}">
|
|
72
72
|
{#snippet headingText()}
|
|
73
73
|
{#if icon}
|
|
74
74
|
{@const Icon = icon}
|
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
<div class="{FIELD_WRAP} {klass}">
|
|
86
86
|
{#if label}
|
|
87
87
|
<label for={fieldId} class="{FIELD_LABEL} {labelHidden ? 'sr-only' : ''}">
|
|
88
|
-
{label}{#if required}<span class="
|
|
88
|
+
{label}{#if required}<span class="ms-0.5 text-danger" aria-hidden="true">*</span>{/if}
|
|
89
89
|
</label>
|
|
90
90
|
{/if}
|
|
91
91
|
|
|
92
92
|
<div class="relative">
|
|
93
|
-
<!--
|
|
93
|
+
<!-- pe-10 overrides the px-3 CONTROL_BASE states, because `pe` is emitted
|
|
94
94
|
after `px` and so wins. Without it a long key ran under the button and
|
|
95
95
|
the operator could not read the end of what they had pasted. -->
|
|
96
96
|
<input
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
{onchange}
|
|
108
108
|
aria-invalid={error ? 'true' : undefined}
|
|
109
109
|
aria-describedby={describedBy(fieldId, error, hint)}
|
|
110
|
-
class="{CONTROL_BASE} {controlBorder(!!error)} {showToggle ? '
|
|
110
|
+
class="{CONTROL_BASE} {controlBorder(!!error)} {showToggle ? 'pe-10' : ''} {controlClass}"
|
|
111
111
|
/>
|
|
112
112
|
|
|
113
113
|
{#if showToggle}
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
aria-pressed={revealed}
|
|
121
121
|
aria-controls={fieldId}
|
|
122
122
|
aria-label={revealed ? 'Hide password' : 'Show password'}
|
|
123
|
-
class="absolute top-1/2
|
|
123
|
+
class="absolute top-1/2 end-1 flex -translate-y-1/2 items-center justify-center rounded-md p-1.5 text-faint outline-none transition-colors duration-150 hover:text-fg focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
|
|
124
124
|
>
|
|
125
125
|
{#if revealed}
|
|
126
126
|
<EyeOff size={15} aria-hidden="true" />
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
own text it became part of the name, and the reader heard the
|
|
146
146
|
whole paragraph before it reached the selected state. -->
|
|
147
147
|
<span id="{fieldId}-label" class="{choiceLabel(size)} {labelHidden ? 'sr-only' : ''}">
|
|
148
|
-
{label}{#if required}<span class="
|
|
148
|
+
{label}{#if required}<span class="ms-0.5 text-danger" aria-hidden="true">*</span>{/if}
|
|
149
149
|
</span>
|
|
150
150
|
{/if}
|
|
151
151
|
</span>
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
ahead of them saying what the set is for.
|
|
110
110
|
-->
|
|
111
111
|
<legend class="{FIELD_LABEL} {labelHidden ? 'sr-only' : ''}">
|
|
112
|
-
{label}{#if required}<span class="text-danger
|
|
112
|
+
{label}{#if required}<span class="text-danger ms-0.5" aria-hidden="true">*</span>{/if}
|
|
113
113
|
</legend>
|
|
114
114
|
|
|
115
115
|
<div class={choiceGroupList(orientation)}>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
stroke-width="2"
|
|
52
52
|
stroke-linecap="round"
|
|
53
53
|
stroke-linejoin="round"
|
|
54
|
-
class="pointer-events-none absolute
|
|
54
|
+
class="pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-faint"
|
|
55
55
|
aria-hidden="true"
|
|
56
56
|
>
|
|
57
57
|
<circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" />
|
|
@@ -65,17 +65,26 @@
|
|
|
65
65
|
{disabled}
|
|
66
66
|
{value}
|
|
67
67
|
oninput={handleInput}
|
|
68
|
-
class="{CONTROL_BASE} {controlBorder(false)}
|
|
68
|
+
class="{CONTROL_BASE} {controlBorder(false)} ps-9
|
|
69
69
|
[&::-webkit-search-cancel-button]:appearance-none
|
|
70
|
-
{value ? '
|
|
70
|
+
{value ? 'pe-8' : 'pe-3'}"
|
|
71
71
|
/>
|
|
72
72
|
|
|
73
73
|
{#if value}
|
|
74
|
+
<!--
|
|
75
|
+
14px inside 6px of padding is a 26px target, past the 24px floor SC
|
|
76
|
+
2.5.8 puts on a control with no spacing exception to claim, and this one
|
|
77
|
+
sits inside the field. The padding is taken back off the horizontal axis
|
|
78
|
+
only: the box is positioned from its right edge, so a negative right
|
|
79
|
+
margin holds the glyph exactly where it was, while a negative top margin
|
|
80
|
+
would fight the translate that centres it and lift it 6px off the middle
|
|
81
|
+
of the field.
|
|
82
|
+
-->
|
|
74
83
|
<button
|
|
75
84
|
type="button"
|
|
76
85
|
onclick={clear}
|
|
77
86
|
aria-label="Clear search"
|
|
78
|
-
class="absolute
|
|
87
|
+
class="absolute end-2.5 top-1/2 -mx-1.5 -translate-y-1/2 p-1.5 text-faint
|
|
79
88
|
transition-colors duration-150 hover:text-fg"
|
|
80
89
|
>
|
|
81
90
|
<svg
|
|
@@ -164,7 +164,9 @@
|
|
|
164
164
|
onkeydown={(e) => onSegmentKeydown(e, i)}
|
|
165
165
|
class="inline-flex items-center justify-center rounded-md outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand disabled:cursor-not-allowed {SIZES[
|
|
166
166
|
size
|
|
167
|
-
].segment} {selected
|
|
167
|
+
].segment} {selected
|
|
168
|
+
? 'bg-surface text-fg shadow-sm'
|
|
169
|
+
: 'text-muted hover:text-fg active:bg-surface active:text-fg'}"
|
|
168
170
|
>
|
|
169
171
|
{#if Icon}
|
|
170
172
|
<Icon size={SIZES[size].icon} class="shrink-0" />
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
|
|
275
275
|
{#snippet chevron(open: boolean)}
|
|
276
276
|
<span
|
|
277
|
-
class="pointer-events-none absolute
|
|
277
|
+
class="pointer-events-none absolute end-2.5 top-1/2 -translate-y-1/2 text-faint"
|
|
278
278
|
aria-hidden="true"
|
|
279
279
|
>
|
|
280
280
|
<svg
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
height="12"
|
|
329
329
|
viewBox="0 0 12 12"
|
|
330
330
|
fill="none"
|
|
331
|
-
class="
|
|
331
|
+
class="ms-auto shrink-0"
|
|
332
332
|
aria-hidden="true"
|
|
333
333
|
>
|
|
334
334
|
<path
|
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
<div class="{FIELD_WRAP} {cls}">
|
|
347
347
|
{#if label}
|
|
348
348
|
<label for={fieldId} class={FIELD_LABEL}>
|
|
349
|
-
{label}{#if required}<span class="text-danger
|
|
349
|
+
{label}{#if required}<span class="text-danger ms-0.5" aria-hidden="true">*</span>{/if}
|
|
350
350
|
</label>
|
|
351
351
|
{/if}
|
|
352
352
|
|
|
@@ -381,8 +381,8 @@
|
|
|
381
381
|
aria-required={required ? 'true' : undefined}
|
|
382
382
|
aria-describedby={describedBy(fieldId, error, hint)}
|
|
383
383
|
{...box.triggerAttrs}
|
|
384
|
-
class="{CONTROL_BASE} {controlBorder(!!error)} flex cursor-pointer items-center gap-2
|
|
385
|
-
text-
|
|
384
|
+
class="{CONTROL_BASE} {controlBorder(!!error)} flex cursor-pointer items-center gap-2 pe-8
|
|
385
|
+
text-start"
|
|
386
386
|
>
|
|
387
387
|
{#if trigger}
|
|
388
388
|
{@render trigger({ selected, open: box.open })}
|
|
@@ -472,7 +472,7 @@
|
|
|
472
472
|
onchange={nativeChange}
|
|
473
473
|
aria-invalid={error ? 'true' : undefined}
|
|
474
474
|
aria-describedby={describedBy(fieldId, error, hint)}
|
|
475
|
-
class="{CONTROL_BASE} {controlBorder(!!error)} cursor-pointer appearance-none
|
|
475
|
+
class="{CONTROL_BASE} {controlBorder(!!error)} cursor-pointer appearance-none pe-8"
|
|
476
476
|
>
|
|
477
477
|
{#if children}
|
|
478
478
|
{@render children()}
|
|
@@ -69,11 +69,25 @@
|
|
|
69
69
|
'transition-colors duration-150 outline-none ' +
|
|
70
70
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
71
71
|
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Exactly one border colour, one background and one text colour per state.
|
|
74
|
+
*
|
|
75
|
+
* Every state answers the pointer, including the current one. It did not: the
|
|
76
|
+
* current row was the only paint with no hover step, and the current row is
|
|
77
|
+
* the tail of the active trail, so on any nav deep enough to have sections it
|
|
78
|
+
* is a nested entry. Beside its own siblings, which all lit up, the row for
|
|
79
|
+
* the page you were on read as the one dead entry in the list.
|
|
80
|
+
*
|
|
81
|
+
* The held step is separate from the hovered one because `hover:` compiles
|
|
82
|
+
* inside `@media (hover: hover)` and never matches a finger. A sidebar is
|
|
83
|
+
* mostly used on a phone as a drawer, where the tap that closed it was the
|
|
84
|
+
* one gesture in the component with nothing on screen behind it.
|
|
85
|
+
*/
|
|
73
86
|
function rowPaint(state: 'current' | 'ancestor' | 'rest'): string {
|
|
74
|
-
if (state === 'current')
|
|
75
|
-
|
|
76
|
-
return 'border-
|
|
87
|
+
if (state === 'current')
|
|
88
|
+
return 'border-brand bg-surface-2 font-medium text-brand hover:bg-line active:bg-line';
|
|
89
|
+
if (state === 'ancestor') return 'border-brand text-fg hover:bg-surface-2 active:bg-line';
|
|
90
|
+
return 'border-transparent text-muted hover:bg-surface-2 hover:text-fg active:bg-line active:text-fg';
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
const ROW_DISABLED = 'cursor-not-allowed border-transparent text-muted opacity-50';
|
|
@@ -87,6 +101,7 @@
|
|
|
87
101
|
const DISCLOSURE =
|
|
88
102
|
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-muted ' +
|
|
89
103
|
'transition-colors duration-150 outline-none hover:bg-surface-2 hover:text-fg ' +
|
|
104
|
+
'active:bg-line active:text-fg ' +
|
|
90
105
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
91
106
|
|
|
92
107
|
const BADGE_TONE: Record<AccentTone, string> = {
|
|
@@ -257,7 +272,7 @@
|
|
|
257
272
|
stroke-linecap="round"
|
|
258
273
|
stroke-linejoin="round"
|
|
259
274
|
aria-hidden="true"
|
|
260
|
-
class="shrink-0 transition-transform duration-150 {open ? 'rotate-90' : ''}"
|
|
275
|
+
class="shrink-0 transition-transform duration-150 {open ? 'rotate-90' : 'rtl:rotate-180'}"
|
|
261
276
|
>
|
|
262
277
|
<path d="M9 18l6-6-6-6" />
|
|
263
278
|
</svg>
|