@lyeve-labs/ui-kit 0.23.3 → 0.25.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 +42 -0
- package/dist/components/AccordionItem.svelte +3 -5
- package/dist/components/AccountMenu.svelte +4 -3
- package/dist/components/Alert.svelte +2 -1
- package/dist/components/AppShell.svelte +166 -54
- package/dist/components/AppShell.svelte.d.ts +41 -6
- package/dist/components/Autocomplete.svelte +3 -2
- package/dist/components/Badge.svelte +10 -5
- package/dist/components/Banner.svelte +2 -1
- package/dist/components/Breadcrumb.svelte +7 -2
- package/dist/components/Button.svelte +15 -2
- package/dist/components/Card.svelte +1 -1
- package/dist/components/Collapsible.svelte +5 -4
- package/dist/components/CopyButton.svelte +2 -1
- package/dist/components/DatePicker.svelte +77 -62
- package/dist/components/Drawer.svelte +6 -5
- package/dist/components/Dropdown.svelte +5 -2
- package/dist/components/FileInput.svelte +1 -1
- package/dist/components/Modal.svelte +6 -3
- package/dist/components/MultiSelect.svelte +8 -6
- package/dist/components/NumberInput.svelte +2 -2
- package/dist/components/PageHeader.svelte +13 -1
- package/dist/components/PageShell.svelte +62 -7
- package/dist/components/PageShell.svelte.d.ts +27 -1
- package/dist/components/Pagination.svelte +7 -2
- package/dist/components/PasswordInput.svelte +2 -1
- package/dist/components/Progress.svelte +1 -1
- package/dist/components/SearchInput.svelte +3 -2
- package/dist/components/SectionHeading.svelte +10 -1
- package/dist/components/SegmentedControl.svelte +101 -27
- package/dist/components/SegmentedControl.svelte.d.ts +9 -0
- package/dist/components/Select.svelte +4 -3
- package/dist/components/SidebarNav.svelte +26 -17
- package/dist/components/StepIndicator.svelte +7 -9
- package/dist/components/Table.svelte +39 -29
- package/dist/components/Tabs.svelte +128 -24
- package/dist/components/Tag.svelte +2 -1
- package/dist/components/ThemeToggle.svelte +2 -1
- package/dist/components/Toaster.svelte +6 -2
- package/dist/components/Toggle.svelte +23 -5
- package/dist/components/Tooltip.svelte +10 -5
- package/dist/components/TreeView.svelte +2 -2
- package/dist/components/dialog/Dialog.svelte +15 -32
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -1
- package/dist/internal/choice.js +10 -3
- package/dist/internal/field.js +3 -3
- package/dist/internal/layout.d.ts +10 -1
- package/dist/internal/layout.js +10 -1
- package/dist/internal/panel.d.ts +13 -3
- package/dist/internal/panel.js +16 -5
- package/dist/internal/scroll-edges.d.ts +33 -0
- package/dist/internal/scroll-edges.js +33 -0
- package/dist/internal/touch.d.ts +28 -0
- package/dist/internal/touch.js +28 -0
- package/dist/motion.d.ts +63 -0
- package/dist/motion.js +165 -0
- package/dist/styles/theme.css +131 -63
- package/package.json +1 -1
- package/src/lib/styles/theme.css +131 -63
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* items is open, and an item asks it; this decides nothing beyond itself.
|
|
14
14
|
*/
|
|
15
15
|
import type { Component, Snippet } from 'svelte';
|
|
16
|
+
import { TOUCH_GROW } from '../internal/touch.js';
|
|
16
17
|
|
|
17
18
|
interface Props {
|
|
18
19
|
open?: boolean;
|
|
@@ -70,8 +71,8 @@
|
|
|
70
71
|
aria-expanded={open}
|
|
71
72
|
aria-controls={panelId}
|
|
72
73
|
onclick={toggle}
|
|
73
|
-
class="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-start text-sm font-medium
|
|
74
|
-
text-fg transition-colors
|
|
74
|
+
class="{TOUCH_GROW} flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-start text-sm font-medium
|
|
75
|
+
text-fg transition-colors outline-none focus-visible:ring-2
|
|
75
76
|
focus-visible:ring-inset focus-visible:ring-brand disabled:cursor-not-allowed
|
|
76
77
|
disabled:opacity-50 {disabled ? '' : 'hover:bg-surface-2'}"
|
|
77
78
|
>
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
stroke-width="2"
|
|
87
88
|
stroke-linecap="round"
|
|
88
89
|
stroke-linejoin="round"
|
|
89
|
-
class="shrink-0 text-faint transition-transform duration-
|
|
90
|
+
class="shrink-0 text-faint transition-transform duration-base {open
|
|
90
91
|
? ''
|
|
91
92
|
: '-rotate-90 rtl:rotate-90'}"
|
|
92
93
|
aria-hidden="true"
|
|
@@ -116,7 +117,7 @@
|
|
|
116
117
|
clipped rather than spilling while the row grows.
|
|
117
118
|
-->
|
|
118
119
|
<div
|
|
119
|
-
class="grid transition-[grid-template-rows] duration-
|
|
120
|
+
class="grid transition-[grid-template-rows] duration-base {open
|
|
120
121
|
? 'grid-rows-[1fr]'
|
|
121
122
|
: 'grid-rows-[0fr]'}"
|
|
122
123
|
>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Check, Copy } from '@lucide/svelte';
|
|
3
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
/** The text written to the clipboard. */
|
|
@@ -105,7 +106,7 @@
|
|
|
105
106
|
type="button"
|
|
106
107
|
aria-label={label}
|
|
107
108
|
onclick={copy}
|
|
108
|
-
class="inline-flex items-center justify-center rounded-md p-1 outline-none transition-colors
|
|
109
|
+
class="{HIT_AREA} inline-flex items-center justify-center rounded-md p-1 outline-none transition-colors active:bg-surface-2 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand {status ===
|
|
109
110
|
'copied'
|
|
110
111
|
? 'text-success'
|
|
111
112
|
: 'text-faint hover:text-fg'}"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
2
3
|
import {
|
|
3
4
|
CONTROL_BASE,
|
|
4
5
|
FIELD_ERROR,
|
|
@@ -8,6 +9,8 @@
|
|
|
8
9
|
controlBorder,
|
|
9
10
|
describedBy,
|
|
10
11
|
} from '../internal/field.js';
|
|
12
|
+
import { PANEL_LIST_UNCAPPED, PANEL_SURFACE, placePanel } from '../internal/panel.js';
|
|
13
|
+
import * as motion from '../motion.js';
|
|
11
14
|
// DatePicker: a text trigger + calendar popover. Binds `value` to an ISO date
|
|
12
15
|
// string ("YYYY-MM-DD"). Pure local-date math (no timezone surprises).
|
|
13
16
|
let {
|
|
@@ -237,80 +240,92 @@
|
|
|
237
240
|
dialog with no name is announced as "dialog" and nothing else. The name
|
|
238
241
|
is fixed rather than built from the field label, so a reader hears what
|
|
239
242
|
the popup does instead of the label they just heard.
|
|
243
|
+
|
|
244
|
+
The surface is the shared panel, placed by placePanel: the calendar sat
|
|
245
|
+
under the trigger unconditionally, so inside a modal body it opened into
|
|
246
|
+
the part of the scroll region nobody could reach and was cut below the
|
|
247
|
+
weekday row. The width stays here, sized to seven columns. The region
|
|
248
|
+
inside is capped by the room and not by the list token, which is shorter
|
|
249
|
+
than six weeks of days, so a short modal scrolls the calendar rather
|
|
250
|
+
than cutting it.
|
|
240
251
|
-->
|
|
241
252
|
<div
|
|
242
253
|
id={calendarId}
|
|
243
254
|
role="dialog"
|
|
244
255
|
aria-label="Choose date"
|
|
245
|
-
|
|
256
|
+
use:placePanel
|
|
257
|
+
transition:motion.popover
|
|
258
|
+
class="{PANEL_SURFACE} w-68"
|
|
246
259
|
>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<svg width="14" height="14" viewBox="0 0 12 12" fill="none"
|
|
256
|
-
><path
|
|
257
|
-
d="M7.5 2L4 6l3.5 4"
|
|
258
|
-
stroke="currentColor"
|
|
259
|
-
stroke-width="1.5"
|
|
260
|
-
stroke-linecap="round"
|
|
261
|
-
stroke-linejoin="round"
|
|
262
|
-
/></svg
|
|
260
|
+
<div class="overflow-y-auto overscroll-contain p-3" data-panel-list={PANEL_LIST_UNCAPPED}>
|
|
261
|
+
<!-- Header -->
|
|
262
|
+
<div class="flex items-center justify-between mb-2">
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
aria-label="Previous month"
|
|
266
|
+
onclick={prevMonth}
|
|
267
|
+
class="{HIT_AREA} p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors"
|
|
263
268
|
>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
269
|
+
<svg width="14" height="14" viewBox="0 0 12 12" fill="none"
|
|
270
|
+
><path
|
|
271
|
+
d="M7.5 2L4 6l3.5 4"
|
|
272
|
+
stroke="currentColor"
|
|
273
|
+
stroke-width="1.5"
|
|
274
|
+
stroke-linecap="round"
|
|
275
|
+
stroke-linejoin="round"
|
|
276
|
+
/></svg
|
|
277
|
+
>
|
|
278
|
+
</button>
|
|
279
|
+
<span class="text-sm font-medium text-fg">{MONTHS[viewMonth - 1]} {viewYear}</span>
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
aria-label="Next month"
|
|
283
|
+
onclick={nextMonth}
|
|
284
|
+
class="{HIT_AREA} p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors"
|
|
280
285
|
>
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
<svg width="14" height="14" viewBox="0 0 12 12" fill="none"
|
|
287
|
+
><path
|
|
288
|
+
d="M4.5 2L8 6l-3.5 4"
|
|
289
|
+
stroke="currentColor"
|
|
290
|
+
stroke-width="1.5"
|
|
291
|
+
stroke-linecap="round"
|
|
292
|
+
stroke-linejoin="round"
|
|
293
|
+
/></svg
|
|
294
|
+
>
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
283
297
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
<!-- Weekday labels -->
|
|
299
|
+
<div class="grid grid-cols-7 mb-1">
|
|
300
|
+
{#each WEEKDAYS as wd}
|
|
301
|
+
<span class="text-center text-[0.65rem] font-medium text-faint py-1">{wd}</span>
|
|
302
|
+
{/each}
|
|
303
|
+
</div>
|
|
290
304
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
<!-- Day grid -->
|
|
306
|
+
<div class="grid grid-cols-7 gap-0.5">
|
|
307
|
+
{#each cells as d}
|
|
308
|
+
{#if d === null}
|
|
309
|
+
<span></span>
|
|
310
|
+
{:else}
|
|
311
|
+
{@const iso = toISO(viewYear, viewMonth, d)}
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
disabled={isDisabled(d)}
|
|
315
|
+
onclick={() => pick(d)}
|
|
316
|
+
aria-current={iso === todayISO ? 'date' : undefined}
|
|
317
|
+
aria-label={iso}
|
|
318
|
+
class="{HIT_AREA} h-8 w-8 mx-auto flex items-center justify-center rounded-md text-sm transition-colors
|
|
305
319
|
disabled:opacity-30 disabled:cursor-not-allowed
|
|
306
320
|
{iso === value
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
321
|
+
? 'bg-brand text-ink font-medium'
|
|
322
|
+
: iso === todayISO
|
|
323
|
+
? 'text-brand font-medium hover:bg-surface-2'
|
|
324
|
+
: 'text-fg hover:bg-surface-2'}">{d}</button
|
|
325
|
+
>
|
|
326
|
+
{/if}
|
|
327
|
+
{/each}
|
|
328
|
+
</div>
|
|
314
329
|
</div>
|
|
315
330
|
</div>
|
|
316
331
|
{/if}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
3
4
|
import { overlay } from '../internal/overlay.js';
|
|
5
|
+
import * as motion from '../motion.js';
|
|
4
6
|
|
|
5
7
|
type Side = 'left' | 'right';
|
|
6
8
|
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -63,15 +65,14 @@
|
|
|
63
65
|
aria-hidden="true"
|
|
64
66
|
class="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-default"
|
|
65
67
|
onclick={close}
|
|
68
|
+
transition:motion.scrim
|
|
66
69
|
></button>
|
|
67
70
|
|
|
68
71
|
<div
|
|
69
72
|
use:overlay
|
|
73
|
+
transition:motion.drawer={{ side }}
|
|
70
74
|
class="relative flex h-full max-w-full flex-col {widths[size]} bg-surface shadow-2xl
|
|
71
|
-
{side === 'right' ? 'border-s' : 'border-e'} border-line
|
|
72
|
-
{side === 'right'
|
|
73
|
-
? 'animate-[drawer-in-right_var(--duration-drawer-in)_ease-out]'
|
|
74
|
-
: 'animate-[drawer-in-left_var(--duration-drawer-in)_ease-out]'}"
|
|
75
|
+
{side === 'right' ? 'border-s' : 'border-e'} border-line"
|
|
75
76
|
role="dialog"
|
|
76
77
|
aria-modal="true"
|
|
77
78
|
aria-labelledby={title ? headingId : undefined}
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
<button
|
|
90
91
|
type="button"
|
|
91
92
|
onclick={close}
|
|
92
|
-
class="-mt-0.5 text-xl leading-none text-faint transition-colors
|
|
93
|
+
class="{HIT_AREA} -mt-0.5 text-xl leading-none text-faint transition-colors hover:text-fg"
|
|
93
94
|
aria-label="Close"
|
|
94
95
|
>
|
|
95
96
|
<svg
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* and Escape and Tab both close and hand focus back to the trigger.
|
|
13
13
|
*/
|
|
14
14
|
import type { Component, Snippet } from 'svelte';
|
|
15
|
+
import { TOUCH_GROW } from '../internal/touch.js';
|
|
15
16
|
import { PANEL_LIST, PANEL_SURFACE, placePanel } from '../internal/panel.js';
|
|
17
|
+
import * as motion from '../motion.js';
|
|
16
18
|
|
|
17
19
|
interface DropdownItem {
|
|
18
20
|
label: string;
|
|
@@ -254,6 +256,7 @@
|
|
|
254
256
|
bind:this={menuEl}
|
|
255
257
|
role="menu"
|
|
256
258
|
use:placePanel
|
|
259
|
+
transition:motion.popover
|
|
257
260
|
class="{PANEL_SURFACE} min-w-36 {align === 'right' ? 'end-0' : 'start-0'}"
|
|
258
261
|
>
|
|
259
262
|
<div class={PANEL_LIST} data-panel-list>
|
|
@@ -264,8 +267,8 @@
|
|
|
264
267
|
disabled={item.disabled}
|
|
265
268
|
tabindex={index === active ? 0 : -1}
|
|
266
269
|
onclick={() => handleItemClick(item)}
|
|
267
|
-
class="flex w-full items-center gap-2.5 rounded-lg px-2 py-2 text-sm transition-colors
|
|
268
|
-
|
|
270
|
+
class="{TOUCH_GROW} flex w-full items-center gap-2.5 rounded-lg px-2 py-2 text-sm transition-colors
|
|
271
|
+
outline-none focus-visible:ring-2 focus-visible:ring-inset
|
|
269
272
|
focus-visible:ring-brand disabled:cursor-not-allowed disabled:opacity-40
|
|
270
273
|
{item.variant === 'danger' ? 'text-danger hover:bg-danger/10' : 'hover:bg-surface-2'}"
|
|
271
274
|
>
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
half alpha reads as a weaker affordance for no reason a user can infer. -->
|
|
58
58
|
<label
|
|
59
59
|
class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-xl
|
|
60
|
-
border-2 border-dashed px-4 py-8 text-center transition-colors
|
|
60
|
+
border-2 border-dashed px-4 py-8 text-center transition-colors
|
|
61
61
|
{error
|
|
62
62
|
? 'border-danger bg-danger/5 hover:bg-danger/8'
|
|
63
63
|
: dragOver
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
3
4
|
import { overlay } from '../internal/overlay.js';
|
|
5
|
+
import * as motion from '../motion.js';
|
|
4
6
|
|
|
5
7
|
type Size = 'sm' | 'md' | 'lg';
|
|
6
8
|
|
|
@@ -58,13 +60,14 @@
|
|
|
58
60
|
aria-hidden="true"
|
|
59
61
|
class="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-default"
|
|
60
62
|
onclick={close}
|
|
63
|
+
transition:motion.scrim
|
|
61
64
|
></button>
|
|
62
65
|
|
|
63
66
|
<div
|
|
64
67
|
use:overlay
|
|
68
|
+
transition:motion.dialog
|
|
65
69
|
class="relative flex max-h-[calc(100dvh-2rem)] w-full {widths[size]} flex-col
|
|
66
|
-
overflow-hidden rounded-xl border border-line bg-surface shadow-2xl
|
|
67
|
-
animate-[modal-in_var(--duration-modal-in)_ease-out]"
|
|
70
|
+
overflow-hidden rounded-xl border border-line bg-surface shadow-2xl"
|
|
68
71
|
role="dialog"
|
|
69
72
|
aria-modal="true"
|
|
70
73
|
aria-labelledby={title ? headingId : undefined}
|
|
@@ -82,7 +85,7 @@
|
|
|
82
85
|
<button
|
|
83
86
|
type="button"
|
|
84
87
|
onclick={close}
|
|
85
|
-
class="shrink-0 rounded text-faint transition-colors
|
|
88
|
+
class="{HIT_AREA} shrink-0 rounded text-faint transition-colors hover:text-fg
|
|
86
89
|
outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
|
|
87
90
|
aria-label="Close"
|
|
88
91
|
>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* reason the factory reports a selection rather than closing on one.
|
|
10
10
|
*/
|
|
11
11
|
import { applyFilter, type FilterInput } from '../internal/filter.js';
|
|
12
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
12
13
|
import {
|
|
13
14
|
FIELD_ERROR,
|
|
14
15
|
FIELD_HINT,
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
panelOption,
|
|
26
27
|
placePanel,
|
|
27
28
|
} from '../internal/panel.js';
|
|
29
|
+
import * as motion from '../motion.js';
|
|
28
30
|
import type { ListOption } from './Autocomplete.svelte';
|
|
29
31
|
|
|
30
32
|
interface Props {
|
|
@@ -187,7 +189,7 @@
|
|
|
187
189
|
onkeydown={onTriggerKeydown}
|
|
188
190
|
{...box.triggerAttrs}
|
|
189
191
|
class="flex min-h-control w-full cursor-pointer flex-wrap items-center gap-1.5 rounded-lg
|
|
190
|
-
border bg-surface-2 px-2.5 py-1.5 text-start text-sm transition-colors
|
|
192
|
+
border bg-surface-2 px-2.5 py-1.5 text-start text-sm transition-colors
|
|
191
193
|
outline-none {disabled ? 'cursor-not-allowed opacity-50' : ''} {controlBorder(!!error)}"
|
|
192
194
|
>
|
|
193
195
|
{#if selected.length === 0}
|
|
@@ -205,7 +207,7 @@
|
|
|
205
207
|
event.stopPropagation();
|
|
206
208
|
remove(option.value);
|
|
207
209
|
}}
|
|
208
|
-
class="-m-1.5 p-1.5 transition-colors
|
|
210
|
+
class="{HIT_AREA} -m-1.5 p-1.5 transition-colors hover:text-brand-light"
|
|
209
211
|
>
|
|
210
212
|
<!--
|
|
211
213
|
12px inside 6px of padding is a 24px target, which is the floor
|
|
@@ -238,7 +240,7 @@
|
|
|
238
240
|
height="12"
|
|
239
241
|
viewBox="0 0 12 12"
|
|
240
242
|
fill="none"
|
|
241
|
-
class="transition-transform duration-
|
|
243
|
+
class="transition-transform duration-base {box.open ? 'rotate-180' : ''}"
|
|
242
244
|
>
|
|
243
245
|
<path
|
|
244
246
|
d="M2 4l4 4 4-4"
|
|
@@ -252,7 +254,7 @@
|
|
|
252
254
|
</div>
|
|
253
255
|
|
|
254
256
|
{#if box.open}
|
|
255
|
-
<div use:placePanel class="{PANEL_SURFACE} w-full">
|
|
257
|
+
<div use:placePanel transition:motion.popover class="{PANEL_SURFACE} w-full">
|
|
256
258
|
{#if searchable}
|
|
257
259
|
<div class="border-b border-line p-2">
|
|
258
260
|
<input
|
|
@@ -268,7 +270,7 @@
|
|
|
268
270
|
aria-label={label ? `Search ${label}` : 'Search options'}
|
|
269
271
|
{...box.triggerAttrs}
|
|
270
272
|
class="w-full rounded-md border border-line-strong bg-surface-2 px-2.5 py-1.5
|
|
271
|
-
text-sm text-fg outline-none transition-colors
|
|
273
|
+
text-sm text-fg outline-none transition-colors
|
|
272
274
|
placeholder:text-faint focus:border-brand"
|
|
273
275
|
/>
|
|
274
276
|
</div>
|
|
@@ -310,7 +312,7 @@
|
|
|
310
312
|
>
|
|
311
313
|
<span
|
|
312
314
|
class="flex h-4 w-4 shrink-0 items-center justify-center rounded border
|
|
313
|
-
transition-colors
|
|
315
|
+
transition-colors
|
|
314
316
|
{isSelected
|
|
315
317
|
? 'border-brand bg-brand text-ink'
|
|
316
318
|
: 'border-line-strong bg-surface-2'}"
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
// the part a low-vision user could not find.
|
|
73
73
|
const step_ =
|
|
74
74
|
'flex w-control shrink-0 items-center justify-center border-line-strong bg-surface-2 text-muted ' +
|
|
75
|
-
'transition-colors
|
|
75
|
+
'transition-colors hover:bg-line hover:text-fg ' +
|
|
76
76
|
'disabled:opacity-50 disabled:cursor-not-allowed';
|
|
77
77
|
</script>
|
|
78
78
|
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
aria-invalid={error ? 'true' : undefined}
|
|
120
120
|
aria-describedby={describedBy(fieldId, error, hint)}
|
|
121
121
|
class="min-w-0 flex-1 border-y bg-surface-2 text-center text-sm text-fg
|
|
122
|
-
outline-none transition-colors
|
|
122
|
+
outline-none transition-colors
|
|
123
123
|
disabled:cursor-not-allowed disabled:opacity-50
|
|
124
124
|
[appearance:textfield]
|
|
125
125
|
[&::-webkit-inner-spin-button]:appearance-none
|
|
@@ -73,7 +73,19 @@
|
|
|
73
73
|
</h1>
|
|
74
74
|
{#if description && !compact}<p class="mt-1 text-sm text-muted">{description}</p>{/if}
|
|
75
75
|
</div>
|
|
76
|
+
<!-- The slot wraps in two steps. It sits beside the title while both fit
|
|
77
|
+
and drops under it, at the end edge, when they do not; wider than the
|
|
78
|
+
row on its own, it wraps its controls inside itself, each line ending at
|
|
79
|
+
the end edge, in the order they were given. It used to be `shrink-0`,
|
|
80
|
+
which sizes a flex item to its content and so cannot wrap even when the
|
|
81
|
+
page puts a wrapping row inside it: four actions pushed the primary one
|
|
82
|
+
83px past a 400px screen. -->
|
|
76
83
|
{#if actions}
|
|
77
|
-
<div
|
|
84
|
+
<div
|
|
85
|
+
data-testid="page-actions"
|
|
86
|
+
class="ms-auto flex min-w-0 flex-wrap items-center justify-end gap-2"
|
|
87
|
+
>
|
|
88
|
+
{@render actions()}
|
|
89
|
+
</div>
|
|
78
90
|
{/if}
|
|
79
91
|
</header>
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* matches for every assistive technology reading the page.
|
|
16
16
|
*/
|
|
17
17
|
import type { Snippet } from 'svelte';
|
|
18
|
+
import { ArrowLeft } from '@lucide/svelte';
|
|
19
|
+
import { safeHref } from '../internal/href.js';
|
|
18
20
|
import { PAGE_PAD, PAGE_STACK, PAGE_WIDTH, type PageWidth } from '../internal/layout.js';
|
|
21
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
19
22
|
import PageHeader from './PageHeader.svelte';
|
|
20
23
|
|
|
21
24
|
interface Props {
|
|
@@ -41,7 +44,30 @@
|
|
|
41
44
|
* asks for it.
|
|
42
45
|
*/
|
|
43
46
|
compact?: boolean;
|
|
44
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* A link back to the page this one sits under, rendered above the title
|
|
49
|
+
* with an arrow and the parent's label ("Flows", "Settings").
|
|
50
|
+
*
|
|
51
|
+
* A page two levels down had only the sidebar and the browser to get back
|
|
52
|
+
* with. A third of the nested pages in one app built a breadcrumb of their
|
|
53
|
+
* own, half of those drew an arrow by hand, and the rest offered nothing.
|
|
54
|
+
* The affordance lives in the shell so every page gets it the same way.
|
|
55
|
+
* It is a plain anchor, so the keyboard needs nothing extra.
|
|
56
|
+
*/
|
|
57
|
+
back?: { href: string; label: string };
|
|
58
|
+
/**
|
|
59
|
+
* Keep the h1 for assistive technology and render no title row at all: no
|
|
60
|
+
* back link, no breadcrumb, no description, no actions.
|
|
61
|
+
*
|
|
62
|
+
* For a page whose toolbar is its header. An editor that owns the viewport
|
|
63
|
+
* spent two rows before its canvas, the title row and then its own toolbar
|
|
64
|
+
* carrying the same name, with the shell's gap between them. The page
|
|
65
|
+
* still has exactly one h1, so a heading query and a screen reader see
|
|
66
|
+
* what every other page gives them; it is `sr-only`, not absent. A page
|
|
67
|
+
* that hides the row takes on the way back: `back` renders nothing here.
|
|
68
|
+
*/
|
|
69
|
+
titleHidden?: boolean;
|
|
70
|
+
/** Rendered above the title at one fixed distance, after `back` when both are set. */
|
|
45
71
|
breadcrumb?: Snippet;
|
|
46
72
|
/** Right-aligned controls in the title row. */
|
|
47
73
|
actions?: Snippet;
|
|
@@ -55,6 +81,8 @@
|
|
|
55
81
|
width = 'default',
|
|
56
82
|
fill = false,
|
|
57
83
|
compact = false,
|
|
84
|
+
back = undefined,
|
|
85
|
+
titleHidden = false,
|
|
58
86
|
breadcrumb,
|
|
59
87
|
actions,
|
|
60
88
|
class: klass = '',
|
|
@@ -91,17 +119,44 @@
|
|
|
91
119
|
|
|
92
120
|
/** The content stack. On a fill page it also takes the leftover height. */
|
|
93
121
|
const content = $derived(fill ? `${PAGE_STACK} min-h-0 flex-1` : PAGE_STACK);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The same rest, hover and focus treatment as a Breadcrumb link, so the two
|
|
125
|
+
* read as one row. The ring replaces the global outline rather than adding
|
|
126
|
+
* to it, and the padding gives the ring a shape to sit on: on bare text the
|
|
127
|
+
* ring hugs the glyphs and clips the descenders.
|
|
128
|
+
*/
|
|
129
|
+
const BACK =
|
|
130
|
+
`${HIT_AREA} -ms-1 inline-flex items-center gap-1 rounded-md px-1 py-0.5 text-xs text-muted ` +
|
|
131
|
+
'outline-none transition-colors hover:text-fg ' +
|
|
132
|
+
'focus-visible:ring-2 focus-visible:ring-brand';
|
|
94
133
|
</script>
|
|
95
134
|
|
|
96
135
|
<div class="{frame} {klass}">
|
|
97
|
-
|
|
136
|
+
{#if titleHidden}
|
|
137
|
+
<h1 data-testid="page-title" class="sr-only">{title}</h1>
|
|
138
|
+
{:else}
|
|
139
|
+
<!-- The breadcrumb and the title are one group, so the distance between them
|
|
98
140
|
is fixed here and does not change with whether a description is set. -->
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
141
|
+
<div class="flex flex-col gap-2 {headerPad}">
|
|
142
|
+
<!-- The back link leads and the breadcrumb follows on one row, so a page
|
|
143
|
+
that has both does not stack two lines of navigation over its title. -->
|
|
144
|
+
{#if back || breadcrumb}
|
|
145
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
146
|
+
{#if back}
|
|
147
|
+
<a href={safeHref(back.href)} data-testid="page-back" class={BACK}>
|
|
148
|
+
<ArrowLeft size={14} aria-hidden="true" class="shrink-0 rtl:rotate-180" />
|
|
149
|
+
{back.label}
|
|
150
|
+
</a>
|
|
151
|
+
{/if}
|
|
152
|
+
{#if breadcrumb}{@render breadcrumb()}{/if}
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
<!-- flush: the shell's own section stack supplies the gap below the title,
|
|
102
156
|
so the header must not add a second one. -->
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
<PageHeader {title} {description} {actions} {compact} flush />
|
|
158
|
+
</div>
|
|
159
|
+
{/if}
|
|
105
160
|
|
|
106
161
|
<div class={content}>
|
|
107
162
|
{@render children()}
|
|
@@ -38,7 +38,33 @@ interface Props {
|
|
|
38
38
|
* asks for it.
|
|
39
39
|
*/
|
|
40
40
|
compact?: boolean;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A link back to the page this one sits under, rendered above the title
|
|
43
|
+
* with an arrow and the parent's label ("Flows", "Settings").
|
|
44
|
+
*
|
|
45
|
+
* A page two levels down had only the sidebar and the browser to get back
|
|
46
|
+
* with. A third of the nested pages in one app built a breadcrumb of their
|
|
47
|
+
* own, half of those drew an arrow by hand, and the rest offered nothing.
|
|
48
|
+
* The affordance lives in the shell so every page gets it the same way.
|
|
49
|
+
* It is a plain anchor, so the keyboard needs nothing extra.
|
|
50
|
+
*/
|
|
51
|
+
back?: {
|
|
52
|
+
href: string;
|
|
53
|
+
label: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Keep the h1 for assistive technology and render no title row at all: no
|
|
57
|
+
* back link, no breadcrumb, no description, no actions.
|
|
58
|
+
*
|
|
59
|
+
* For a page whose toolbar is its header. An editor that owns the viewport
|
|
60
|
+
* spent two rows before its canvas, the title row and then its own toolbar
|
|
61
|
+
* carrying the same name, with the shell's gap between them. The page
|
|
62
|
+
* still has exactly one h1, so a heading query and a screen reader see
|
|
63
|
+
* what every other page gives them; it is `sr-only`, not absent. A page
|
|
64
|
+
* that hides the row takes on the way back: `back` renders nothing here.
|
|
65
|
+
*/
|
|
66
|
+
titleHidden?: boolean;
|
|
67
|
+
/** Rendered above the title at one fixed distance, after `back` when both are set. */
|
|
42
68
|
breadcrumb?: Snippet;
|
|
43
69
|
/** Right-aligned controls in the title row. */
|
|
44
70
|
actions?: Snippet;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { safeHref } from '../internal/href.js';
|
|
3
3
|
import { formatCount } from '../internal/number.js';
|
|
4
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
4
5
|
|
|
5
6
|
interface Base {
|
|
6
7
|
page: number;
|
|
@@ -179,8 +180,12 @@
|
|
|
179
180
|
return href && enabled ? safeHref(href(target)) : undefined;
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
/**
|
|
184
|
+
* 28px drawn, 44px under a finger. The steps sit 2px apart, so growing the
|
|
185
|
+
* visual would put a seven-page pager past a phone's width; the hit box
|
|
186
|
+
* grows instead and the pager keeps its shape.
|
|
187
|
+
*/
|
|
188
|
+
const btnBase = `${HIT_AREA} inline-flex items-center justify-center w-7 h-7 rounded text-xs font-medium transition-colors`;
|
|
184
189
|
|
|
185
190
|
/**
|
|
186
191
|
* A page control that is not the page you are on, in every state a pointer
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Eye, EyeOff } from '@lucide/svelte';
|
|
3
|
+
import { HIT_AREA_POSITIONED } from '../internal/touch.js';
|
|
3
4
|
import {
|
|
4
5
|
CONTROL_BASE,
|
|
5
6
|
FIELD_ERROR,
|
|
@@ -120,7 +121,7 @@
|
|
|
120
121
|
aria-pressed={revealed}
|
|
121
122
|
aria-controls={fieldId}
|
|
122
123
|
aria-label={revealed ? 'Hide password' : 'Show password'}
|
|
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
|
|
124
|
+
class="{HIT_AREA_POSITIONED} 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 hover:text-fg focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
|
|
124
125
|
>
|
|
125
126
|
{#if revealed}
|
|
126
127
|
<EyeOff size={15} aria-hidden="true" />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { CONTROL_BASE, FIELD_LABEL, FIELD_WRAP, controlBorder } from '../internal/field.js';
|
|
3
|
+
import { HIT_AREA_POSITIONED } from '../internal/touch.js';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
value?: string;
|
|
@@ -84,8 +85,8 @@
|
|
|
84
85
|
type="button"
|
|
85
86
|
onclick={clear}
|
|
86
87
|
aria-label="Clear search"
|
|
87
|
-
class="absolute end-2.5 top-1/2 -mx-1.5 -translate-y-1/2 p-1.5 text-faint
|
|
88
|
-
transition-colors
|
|
88
|
+
class="{HIT_AREA_POSITIONED} absolute end-2.5 top-1/2 -mx-1.5 -translate-y-1/2 p-1.5 text-faint
|
|
89
|
+
transition-colors hover:text-fg"
|
|
89
90
|
>
|
|
90
91
|
<svg
|
|
91
92
|
width="14"
|