@delightstack/components 0.1.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/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ripple } from '@delightstack/utilities';
|
|
3
|
+
import { getContext, type Snippet } from 'svelte';
|
|
4
|
+
import { type ListContext } from './List.svelte';
|
|
5
|
+
import { fade, type TransitionConfig } from 'svelte/transition';
|
|
6
|
+
import { backOut, quartOut } from 'svelte/easing';
|
|
7
|
+
import type { PopoverPlacement } from './../actions/Popover.svelte';
|
|
8
|
+
import Button from './../actions/Button.svelte';
|
|
9
|
+
import Progress from '../feedback/Progress.svelte';
|
|
10
|
+
import Checkbox from '../form/Checkbox.svelte';
|
|
11
|
+
import Radio from '../form/Radio.svelte';
|
|
12
|
+
import Toggle from '../form/Toggle.svelte';
|
|
13
|
+
import ListContextReset from './ListContextReset.svelte';
|
|
14
|
+
|
|
15
|
+
const propId = $props.id();
|
|
16
|
+
let {
|
|
17
|
+
/** Whether this button/checkbox/radio should be disabled */
|
|
18
|
+
disabled = false,
|
|
19
|
+
|
|
20
|
+
/** The target of the link (only used if href is provided) */
|
|
21
|
+
target = undefined as '_self' | '_blank' | '_parent' | '_top' | undefined,
|
|
22
|
+
|
|
23
|
+
/** The link the user should be navigated to (uses an 'a' tag instead of the button) */
|
|
24
|
+
href = undefined as string | undefined,
|
|
25
|
+
|
|
26
|
+
/** Whether the list item is active (like when used as a button selection list) */
|
|
27
|
+
active = false,
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether a loading spinner should appear on the action row (`button`
|
|
31
|
+
* type). Leave undefined to let a promise-returning `onclick` drive it
|
|
32
|
+
* automatically (see `onclick`).
|
|
33
|
+
*/
|
|
34
|
+
loading = undefined as boolean | undefined,
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* For the manual `loading` path only: when `loading` goes true -> false and
|
|
38
|
+
* this is true, a success checkmark briefly animates in to confirm the
|
|
39
|
+
* action, then animates away. (The promise-aware `onclick` path shows this
|
|
40
|
+
* checkmark automatically on resolve, so this prop isn't needed there.)
|
|
41
|
+
*/
|
|
42
|
+
loading_success = false,
|
|
43
|
+
|
|
44
|
+
/** The content to show in a dropdown menu when the button is clicked */
|
|
45
|
+
menu = undefined as undefined | Snippet,
|
|
46
|
+
|
|
47
|
+
/** Whether the dropdown menu should close when the user clicks a button like element inside of it */
|
|
48
|
+
popover_close_on_inside_click = false,
|
|
49
|
+
|
|
50
|
+
/** The placement of the popover (used when either "menu" or "dropdown" is provided) */
|
|
51
|
+
popover_placement = 'bottom-end' as PopoverPlacement,
|
|
52
|
+
|
|
53
|
+
/** The css style string added to the component from the parent */
|
|
54
|
+
style = '',
|
|
55
|
+
|
|
56
|
+
/** The ID of the select element. @defaults to a random ID */
|
|
57
|
+
id = propId,
|
|
58
|
+
|
|
59
|
+
/** Specifies a custom class name for the container element */
|
|
60
|
+
class: class_name = '',
|
|
61
|
+
|
|
62
|
+
/** The child elements to display inside the component */
|
|
63
|
+
children = undefined as undefined | Snippet,
|
|
64
|
+
|
|
65
|
+
/** Emits when the list item is selected/deselected */
|
|
66
|
+
onchange = undefined as ((value: boolean) => void) | undefined,
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The function to call when the list item is clicked.
|
|
70
|
+
* If it returns a promise, the row manages its own loading feedback:
|
|
71
|
+
* - A spinner appears only if the promise is still pending after ~100ms
|
|
72
|
+
* (faster resolves are treated as instant — no spinner flash).
|
|
73
|
+
* - Once shown, the spinner stays for at least ~1s so it can't blink away.
|
|
74
|
+
* - On resolve, a brief success checkmark confirms the action; on reject,
|
|
75
|
+
* no checkmark is shown.
|
|
76
|
+
*/
|
|
77
|
+
onclick = undefined as
|
|
78
|
+
| undefined
|
|
79
|
+
| ((e: MouseEvent) => void)
|
|
80
|
+
| ((e: MouseEvent) => Promise<void>),
|
|
81
|
+
} = $props();
|
|
82
|
+
|
|
83
|
+
let element = $state<HTMLElement | undefined>(undefined);
|
|
84
|
+
let checked = $state(false);
|
|
85
|
+
const context = getContext<ListContext | undefined>('list');
|
|
86
|
+
|
|
87
|
+
$effect(() => {
|
|
88
|
+
if (!context?.value) return;
|
|
89
|
+
if (!element?.parentElement?.children) return;
|
|
90
|
+
const index = Array.from(element.parentElement.children).indexOf(element);
|
|
91
|
+
checked = context.value.includes(index);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/* Promise-aware loading timing (mirrors Button).
|
|
95
|
+
- SHOW_DELAY: a promise that settles faster than this never gets a spinner;
|
|
96
|
+
the action reads as "instant".
|
|
97
|
+
- MIN_VISIBLE: once shown, the spinner stays at least this long so it can't
|
|
98
|
+
flash on then immediately off.
|
|
99
|
+
- SPINNER_OUT: spinner collapse duration (kept in sync with the "out"
|
|
100
|
+
transition) so the success check can slot in right after it clears.
|
|
101
|
+
- CHECK_HOLD: how long the success checkmark lingers before easing out. */
|
|
102
|
+
const SHOW_DELAY = 100;
|
|
103
|
+
const MIN_VISIBLE = 1000;
|
|
104
|
+
const SPINNER_OUT = 150;
|
|
105
|
+
const CHECK_HOLD = 1000;
|
|
106
|
+
|
|
107
|
+
let inFlight = $state(false); // a returned promise is running (covers the pre-spinner window)
|
|
108
|
+
let spinnerVisible = $state(false); // the spinner is actually rendered
|
|
109
|
+
let checkVisible = $state(false); // the success checkmark is rendered
|
|
110
|
+
|
|
111
|
+
let showTimer: ReturnType<typeof setTimeout> | undefined;
|
|
112
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
113
|
+
let checkTimer: ReturnType<typeof setTimeout> | undefined;
|
|
114
|
+
let spinnerShownAt = 0;
|
|
115
|
+
|
|
116
|
+
function clearTimers() {
|
|
117
|
+
clearTimeout(showTimer);
|
|
118
|
+
clearTimeout(hideTimer);
|
|
119
|
+
clearTimeout(checkTimer);
|
|
120
|
+
showTimer = hideTimer = checkTimer = undefined;
|
|
121
|
+
}
|
|
122
|
+
$effect(() => clearTimers); // tear down pending timers on destroy
|
|
123
|
+
|
|
124
|
+
// The external `loading` prop drives the spinner directly; a returned promise
|
|
125
|
+
// drives `inFlight`/`spinnerVisible`. "Busy" (a11y/pointer-gating) is either of
|
|
126
|
+
// those; `showSpinner` excludes the brief pre-spinner window so a sub-SHOW_DELAY
|
|
127
|
+
// promise never flashes one. The checkmark renders straight off `checkVisible`.
|
|
128
|
+
const externalLoading = $derived(loading);
|
|
129
|
+
const isLoading = $derived(!!externalLoading || inFlight);
|
|
130
|
+
const showSpinner = $derived(!!externalLoading || spinnerVisible);
|
|
131
|
+
|
|
132
|
+
// Manual loading path: when the caller drives `loading` true -> false and has
|
|
133
|
+
// opted in with `loading_success`, play the same confirming checkmark. Runs in
|
|
134
|
+
// `$effect.pre` so `checkVisible` is set in the same flush `loading` clears in,
|
|
135
|
+
// otherwise the icon slot would render one empty frame and flash closed/open.
|
|
136
|
+
let wasExternalLoading = false;
|
|
137
|
+
$effect.pre(() => {
|
|
138
|
+
const now = !!externalLoading;
|
|
139
|
+
if (wasExternalLoading && !now && loading_success && !inFlight) flashCheck();
|
|
140
|
+
wasExternalLoading = now;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// The icon slot (the common parent of the spinner and the success check)
|
|
144
|
+
// grows/collapses with this width+opacity transition. Wrapping both means the
|
|
145
|
+
// slot only opens when one first appears and only collapses once both are gone
|
|
146
|
+
// — the spinner -> check handoff happens inside a stable, already-open slot.
|
|
147
|
+
//
|
|
148
|
+
// The margins (and any parent flex gap) ride `t` along with the width so the
|
|
149
|
+
// slot's total layout contribution hits exactly 0 at t=0 — animating width
|
|
150
|
+
// alone leaves them at full strength, which over/under-shoots the label's
|
|
151
|
+
// resting position and snaps it when the node is finally removed (see the
|
|
152
|
+
// matching note in Button.svelte).
|
|
153
|
+
function loadingTransition(
|
|
154
|
+
node: HTMLElement,
|
|
155
|
+
params?: { direction?: 'in' | 'out' },
|
|
156
|
+
): () => TransitionConfig {
|
|
157
|
+
return () => {
|
|
158
|
+
const style = getComputedStyle(node);
|
|
159
|
+
const width = parseFloat(style.width);
|
|
160
|
+
const marginLeft = parseFloat(style.marginLeft) || 0;
|
|
161
|
+
const marginRight = parseFloat(style.marginRight) || 0;
|
|
162
|
+
const gap = node.parentElement
|
|
163
|
+
? parseFloat(getComputedStyle(node.parentElement).columnGap) || 0
|
|
164
|
+
: 0;
|
|
165
|
+
const out = params?.direction === 'out';
|
|
166
|
+
return {
|
|
167
|
+
duration: out ? SPINNER_OUT : 320,
|
|
168
|
+
easing: out ? quartOut : backOut,
|
|
169
|
+
css: (t: number) =>
|
|
170
|
+
`width: ${t * width}px; ` +
|
|
171
|
+
`margin-left: ${t * marginLeft}px; ` +
|
|
172
|
+
`margin-right: ${t * marginRight - (1 - t) * gap}px; ` +
|
|
173
|
+
`opacity: ${t};`,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The checkmark's own entrance: a spring-scaled pop that simultaneously draws
|
|
179
|
+
// its stroke on (the dash offset rides `t`). Driving the draw from the
|
|
180
|
+
// transition — rather than a CSS @keyframes — keeps it reliable regardless of
|
|
181
|
+
// scoping. prefers-reduced-motion collapses it to a plain appear.
|
|
182
|
+
function checkIn(_node: Element): TransitionConfig {
|
|
183
|
+
const reduce =
|
|
184
|
+
typeof matchMedia !== 'undefined' &&
|
|
185
|
+
matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
186
|
+
return {
|
|
187
|
+
duration: reduce ? 0 : 440,
|
|
188
|
+
easing: backOut,
|
|
189
|
+
css: (t: number) =>
|
|
190
|
+
`transform: scale(${0.3 + 0.7 * t}); opacity: ${Math.min(1, t * 2)}; --check-draw: ${24 * (1 - t)};`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Pop the confirming checkmark, then retire it after CHECK_HOLD. spinnerVisible
|
|
195
|
+
// is cleared in the same tick by the caller, so the slot stays open and the
|
|
196
|
+
// spinner crossfades into the check rather than the slot reopening.
|
|
197
|
+
function flashCheck() {
|
|
198
|
+
clearTimeout(checkTimer);
|
|
199
|
+
checkVisible = true;
|
|
200
|
+
checkTimer = setTimeout(() => (checkVisible = false), CHECK_HOLD);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleClick(e: MouseEvent) {
|
|
204
|
+
if (inFlight || externalLoading) return;
|
|
205
|
+
if (!onclick) return;
|
|
206
|
+
const maybePromise = onclick(e);
|
|
207
|
+
if (!(maybePromise instanceof Promise)) return;
|
|
208
|
+
|
|
209
|
+
// A fresh action supersedes any checkmark still lingering from the last one.
|
|
210
|
+
clearTimers();
|
|
211
|
+
checkVisible = false;
|
|
212
|
+
|
|
213
|
+
inFlight = true;
|
|
214
|
+
// Hold off on the spinner — if the promise settles within SHOW_DELAY the
|
|
215
|
+
// action was effectively instant and never needs one.
|
|
216
|
+
showTimer = setTimeout(() => {
|
|
217
|
+
showTimer = undefined;
|
|
218
|
+
spinnerVisible = true;
|
|
219
|
+
spinnerShownAt = performance.now();
|
|
220
|
+
}, SHOW_DELAY);
|
|
221
|
+
|
|
222
|
+
maybePromise.then(
|
|
223
|
+
() => settle(true),
|
|
224
|
+
() => settle(false),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function settle(success: boolean) {
|
|
229
|
+
// Settled before the spinner ever appeared -> treat as instant: no spinner,
|
|
230
|
+
// no checkmark, just release.
|
|
231
|
+
if (showTimer) {
|
|
232
|
+
clearTimeout(showTimer);
|
|
233
|
+
showTimer = undefined;
|
|
234
|
+
inFlight = false;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// The spinner is up; keep it for the rest of its minimum-visible window so
|
|
238
|
+
// it doesn't blink away the instant the promise resolves.
|
|
239
|
+
const remaining = Math.max(0, MIN_VISIBLE - (performance.now() - spinnerShownAt));
|
|
240
|
+
clearTimeout(hideTimer);
|
|
241
|
+
hideTimer = setTimeout(() => {
|
|
242
|
+
spinnerVisible = false;
|
|
243
|
+
inFlight = false;
|
|
244
|
+
// On success, let the spinner collapse, then pop a brief checkmark.
|
|
245
|
+
if (success) flashCheck();
|
|
246
|
+
}, remaining);
|
|
247
|
+
}
|
|
248
|
+
</script>
|
|
249
|
+
|
|
250
|
+
{#if context}
|
|
251
|
+
<li
|
|
252
|
+
class={['list-item', context.type, class_name].filter(Boolean).join(' ')}
|
|
253
|
+
class:disabled={context.disabled || disabled}
|
|
254
|
+
class:is-loading={isLoading}
|
|
255
|
+
class:dense={context.dense}
|
|
256
|
+
class:comfortable={context.comfortable}
|
|
257
|
+
{style}
|
|
258
|
+
{id}
|
|
259
|
+
bind:this={element}
|
|
260
|
+
class:active={checked || active}
|
|
261
|
+
style:--level={context.level}
|
|
262
|
+
{@attach ripple({
|
|
263
|
+
zIndex: 1,
|
|
264
|
+
enabled: !context.disabled && !disabled && context.type !== 'text' && !isLoading,
|
|
265
|
+
})}>
|
|
266
|
+
{#if context.type === 'checkbox'}
|
|
267
|
+
<label for="checkbox-{id}">
|
|
268
|
+
{#if children}{@render children()}{/if}
|
|
269
|
+
<div class="spacer"></div>
|
|
270
|
+
<input
|
|
271
|
+
type="checkbox"
|
|
272
|
+
id="checkbox-{id}"
|
|
273
|
+
name={id}
|
|
274
|
+
disabled={context.disabled || disabled}
|
|
275
|
+
{checked}
|
|
276
|
+
onchange={() => onchange?.(checked)} />
|
|
277
|
+
<!-- Presentational only: the hidden native input above owns
|
|
278
|
+
interaction, focus and a11y. `inert` keeps the Checkbox from
|
|
279
|
+
becoming a second focusable/clickable control. -->
|
|
280
|
+
<span class="control" inert>
|
|
281
|
+
<Checkbox
|
|
282
|
+
{checked}
|
|
283
|
+
disabled={context.disabled || disabled}
|
|
284
|
+
size={context.dense ? '0' : '1'} />
|
|
285
|
+
</span>
|
|
286
|
+
</label>
|
|
287
|
+
{:else if context.type === 'toggle'}
|
|
288
|
+
<label for="toggle-{id}">
|
|
289
|
+
{#if children}{@render children()}{/if}
|
|
290
|
+
<div class="spacer"></div>
|
|
291
|
+
<input
|
|
292
|
+
type="checkbox"
|
|
293
|
+
id="toggle-{id}"
|
|
294
|
+
name={id}
|
|
295
|
+
disabled={context.disabled || disabled}
|
|
296
|
+
{checked}
|
|
297
|
+
onchange={() => onchange?.(checked)} />
|
|
298
|
+
<!-- Presentational only: the hidden native input above owns
|
|
299
|
+
interaction, focus and a11y (List's change delegation treats it
|
|
300
|
+
like a checkbox). `inert` keeps the Toggle from becoming a
|
|
301
|
+
second focusable/clickable control. -->
|
|
302
|
+
<span class="control" inert>
|
|
303
|
+
<Toggle
|
|
304
|
+
{checked}
|
|
305
|
+
disabled={context.disabled || disabled}
|
|
306
|
+
size={context.dense ? '0' : '1'} />
|
|
307
|
+
</span>
|
|
308
|
+
</label>
|
|
309
|
+
{:else if context.type === 'radio'}
|
|
310
|
+
<label for="radio-{id}">
|
|
311
|
+
{#if children}{@render children()}{/if}
|
|
312
|
+
<div class="spacer"></div>
|
|
313
|
+
<input
|
|
314
|
+
type="radio"
|
|
315
|
+
disabled={context.disabled || disabled}
|
|
316
|
+
id="radio-{id}"
|
|
317
|
+
name={context.id}
|
|
318
|
+
onchange={() => onchange?.(checked)}
|
|
319
|
+
{checked} />
|
|
320
|
+
<span class="control" inert>
|
|
321
|
+
<Radio
|
|
322
|
+
{checked}
|
|
323
|
+
disabled={context.disabled || disabled}
|
|
324
|
+
size={context.dense ? '0' : '1'} />
|
|
325
|
+
</span>
|
|
326
|
+
</label>
|
|
327
|
+
{:else if context.type === 'button'}
|
|
328
|
+
{#if href}
|
|
329
|
+
<a aria-disabled={context.disabled || disabled} {href} {target}>
|
|
330
|
+
{#if children}{@render children()}{/if}
|
|
331
|
+
</a>
|
|
332
|
+
{:else}
|
|
333
|
+
<button
|
|
334
|
+
type="button"
|
|
335
|
+
disabled={context.disabled || disabled}
|
|
336
|
+
aria-busy={isLoading ? 'true' : null}
|
|
337
|
+
onclick={handleClick}>
|
|
338
|
+
{#if showSpinner || checkVisible}
|
|
339
|
+
<div
|
|
340
|
+
class="loading-icon"
|
|
341
|
+
in:loadingTransition={{ direction: 'in' }}
|
|
342
|
+
out:loadingTransition={{ direction: 'out' }}>
|
|
343
|
+
{#if showSpinner}
|
|
344
|
+
<div class="icon-layer" out:fade={{ duration: 120 }}>
|
|
345
|
+
<Progress size="00" color="currentColor" />
|
|
346
|
+
</div>
|
|
347
|
+
{:else}
|
|
348
|
+
<div class="icon-layer check-layer" in:checkIn>
|
|
349
|
+
<svg
|
|
350
|
+
class="check"
|
|
351
|
+
viewBox="2 2 20 20"
|
|
352
|
+
fill="none"
|
|
353
|
+
stroke="currentColor"
|
|
354
|
+
stroke-width="3"
|
|
355
|
+
stroke-linecap="round"
|
|
356
|
+
stroke-linejoin="round"
|
|
357
|
+
aria-hidden="true">
|
|
358
|
+
<path d="M5 12.5l4.5 4.5L19 7" />
|
|
359
|
+
</svg>
|
|
360
|
+
</div>
|
|
361
|
+
{/if}
|
|
362
|
+
</div>
|
|
363
|
+
{/if}
|
|
364
|
+
{#if children}{@render children()}{/if}
|
|
365
|
+
</button>
|
|
366
|
+
{/if}
|
|
367
|
+
{:else if context.type === 'text'}
|
|
368
|
+
<span class="text-content">
|
|
369
|
+
{#if children}{@render children()}{/if}
|
|
370
|
+
</span>
|
|
371
|
+
{/if}
|
|
372
|
+
{#if menu}
|
|
373
|
+
<Button
|
|
374
|
+
icon
|
|
375
|
+
transparent
|
|
376
|
+
size="0"
|
|
377
|
+
class="action"
|
|
378
|
+
{popover_close_on_inside_click}
|
|
379
|
+
{popover_placement}
|
|
380
|
+
menu={resetMenu}>
|
|
381
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
382
|
+
<path
|
|
383
|
+
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
|
384
|
+
</svg>
|
|
385
|
+
</Button>
|
|
386
|
+
{/if}
|
|
387
|
+
</li>
|
|
388
|
+
{/if}
|
|
389
|
+
|
|
390
|
+
{#snippet resetMenu()}
|
|
391
|
+
<ListContextReset>
|
|
392
|
+
{#if menu}{@render menu()}{/if}
|
|
393
|
+
</ListContextReset>
|
|
394
|
+
{/snippet}
|
|
395
|
+
|
|
396
|
+
<style>
|
|
397
|
+
li {
|
|
398
|
+
min-height: 3rem;
|
|
399
|
+
padding: 0;
|
|
400
|
+
margin: 0;
|
|
401
|
+
position: relative;
|
|
402
|
+
overflow: hidden;
|
|
403
|
+
list-style: none;
|
|
404
|
+
display: flex;
|
|
405
|
+
align-items: center;
|
|
406
|
+
perspective: 100px;
|
|
407
|
+
--_radius: calc(var(--radius-lg) * 1.5);
|
|
408
|
+
:global(> .ripple) {
|
|
409
|
+
inset: 1px var(--border-inset) 1px
|
|
410
|
+
calc(var(--border-inset) + ((var(--level) - 1) * 1rem)) !important;
|
|
411
|
+
border-radius: calc(var(--_radius) - var(--border-inset)) !important;
|
|
412
|
+
@supports (corner-shape: squircle) {
|
|
413
|
+
corner-shape: squircle;
|
|
414
|
+
border-radius: calc(
|
|
415
|
+
(var(--_radius) - var(--border-inset)) * var(--squircle-ratio, 2)
|
|
416
|
+
) !important;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
&.active {
|
|
420
|
+
a,
|
|
421
|
+
button,
|
|
422
|
+
label {
|
|
423
|
+
&::before {
|
|
424
|
+
opacity: 0.06;
|
|
425
|
+
/* Snap the active highlight in instantly (so keyboard navigation —
|
|
426
|
+
e.g. arrowing an autocomplete list — feels immediate). The base
|
|
427
|
+
::before rule still eases it out when the item is deselected. */
|
|
428
|
+
transition:
|
|
429
|
+
opacity 0ms ease,
|
|
430
|
+
border-radius 150ms ease;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
.text-content::before {
|
|
434
|
+
opacity: 0.06;
|
|
435
|
+
transition:
|
|
436
|
+
opacity 0ms ease,
|
|
437
|
+
border-radius 150ms ease;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
&.disabled .text-content {
|
|
441
|
+
color: var(--color-text-disabled);
|
|
442
|
+
}
|
|
443
|
+
&::after {
|
|
444
|
+
content: '';
|
|
445
|
+
position: absolute;
|
|
446
|
+
top: 0px;
|
|
447
|
+
right: 1rem;
|
|
448
|
+
left: 1rem;
|
|
449
|
+
border-top: solid 1px color-mix(in oklch, transparent, var(--color-text) 6%);
|
|
450
|
+
}
|
|
451
|
+
&:first-child {
|
|
452
|
+
&::after {
|
|
453
|
+
content: none;
|
|
454
|
+
}
|
|
455
|
+
&::before {
|
|
456
|
+
top: var(--border-inset);
|
|
457
|
+
}
|
|
458
|
+
:global(> .ripple) {
|
|
459
|
+
top: var(--border-inset) !important;
|
|
460
|
+
}
|
|
461
|
+
a,
|
|
462
|
+
button,
|
|
463
|
+
label {
|
|
464
|
+
padding-top: calc(var(--border-inset, 0px) + 0.25rem);
|
|
465
|
+
&::before,
|
|
466
|
+
&::after {
|
|
467
|
+
top: var(--border-inset);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
&:last-child {
|
|
472
|
+
&::before {
|
|
473
|
+
bottom: var(--border-inset);
|
|
474
|
+
}
|
|
475
|
+
:global(> .ripple) {
|
|
476
|
+
bottom: var(--border-inset) !important;
|
|
477
|
+
}
|
|
478
|
+
a,
|
|
479
|
+
button,
|
|
480
|
+
label {
|
|
481
|
+
padding-bottom: calc(var(--border-inset, 0px) + 0.25rem);
|
|
482
|
+
&::before,
|
|
483
|
+
&::after {
|
|
484
|
+
bottom: var(--border-inset);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
&.dense {
|
|
489
|
+
--_radius: var(--radius-lg);
|
|
490
|
+
min-height: 2.5rem;
|
|
491
|
+
a,
|
|
492
|
+
button,
|
|
493
|
+
label {
|
|
494
|
+
padding-top: 0;
|
|
495
|
+
padding-bottom: 0;
|
|
496
|
+
padding-right: 1rem;
|
|
497
|
+
padding-left: calc(1rem + ((var(--level) - 1) * 1rem));
|
|
498
|
+
}
|
|
499
|
+
&:first-child {
|
|
500
|
+
a,
|
|
501
|
+
button,
|
|
502
|
+
label {
|
|
503
|
+
padding-top: calc(var(--border-inset, 0px));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
&:last-child {
|
|
507
|
+
a,
|
|
508
|
+
button,
|
|
509
|
+
label {
|
|
510
|
+
padding-bottom: calc(var(--border-inset, 0px));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
&.comfortable {
|
|
515
|
+
--_radius: var(--radius-xl);
|
|
516
|
+
min-height: 3.5rem;
|
|
517
|
+
a,
|
|
518
|
+
button,
|
|
519
|
+
label {
|
|
520
|
+
padding-top: 0.5rem;
|
|
521
|
+
padding-bottom: 0.5rem;
|
|
522
|
+
padding-left: calc(2rem + var(--list-pad-x, 0px));
|
|
523
|
+
padding-right: calc(
|
|
524
|
+
2rem + var(--list-pad-x, 0px) + ((var(--level) - 1) * 1.5rem)
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
&:first-child {
|
|
528
|
+
a,
|
|
529
|
+
button,
|
|
530
|
+
label {
|
|
531
|
+
padding-top: calc(var(--border-inset, 0px) + 0.5rem);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
&:last-child {
|
|
535
|
+
a,
|
|
536
|
+
button,
|
|
537
|
+
label {
|
|
538
|
+
padding-bottom: calc(var(--border-inset, 0px) + 0.5rem);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
&.disabled {
|
|
543
|
+
cursor: not-allowed;
|
|
544
|
+
a,
|
|
545
|
+
button,
|
|
546
|
+
label {
|
|
547
|
+
cursor: not-allowed;
|
|
548
|
+
color: var(--color-text-disabled);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
&.is-loading {
|
|
552
|
+
/* Busy while the onclick promise resolves — the inner button stops
|
|
553
|
+
taking pointer events (see &[aria-busy] below), so set the row cursor
|
|
554
|
+
here so the whole row reads as not-interactive. */
|
|
555
|
+
cursor: not-allowed;
|
|
556
|
+
}
|
|
557
|
+
&:not(.disabled) {
|
|
558
|
+
a,
|
|
559
|
+
button,
|
|
560
|
+
label {
|
|
561
|
+
&:hover:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']) {
|
|
562
|
+
&::before {
|
|
563
|
+
opacity: 0.06;
|
|
564
|
+
transition:
|
|
565
|
+
opacity 0ms ease,
|
|
566
|
+
border-radius 150ms ease;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/* --- Adjacent highlighted rows merge into one block --- */
|
|
573
|
+
/* When two neighbouring rows are both "highlighted" — active, or hovered
|
|
574
|
+
while enabled and interactive — square off the corners where they meet so
|
|
575
|
+
the pair reads as one continuous selection instead of two rounded pills.
|
|
576
|
+
The border-radius transition on the highlight ::before animates it.
|
|
577
|
+
|
|
578
|
+
The sibling/:has parts are wrapped in :global() because ListItem renders a
|
|
579
|
+
single <li>; without it Svelte prunes these as "unused" (it can't see a
|
|
580
|
+
sibling .list-item in this component's own template). The `.text` guard
|
|
581
|
+
skips non-interactive text rows in the hover case (no highlight to merge). */
|
|
582
|
+
:global(.list-item.active:has(+ .list-item.active))
|
|
583
|
+
:is(a, button, label, .text-content)::before,
|
|
584
|
+
:global(
|
|
585
|
+
.list-item.active:has(+ .list-item:hover:not(.disabled):not(.text):not(.is-loading))
|
|
586
|
+
)
|
|
587
|
+
:is(a, button, label, .text-content)::before,
|
|
588
|
+
:global(
|
|
589
|
+
.list-item:hover:not(.disabled):not(.text):not(.is-loading):has(+ .list-item.active)
|
|
590
|
+
)
|
|
591
|
+
:is(a, button, label, .text-content)::before {
|
|
592
|
+
border-bottom-left-radius: 0;
|
|
593
|
+
border-bottom-right-radius: 0;
|
|
594
|
+
}
|
|
595
|
+
:global(.list-item.active + .list-item.active)
|
|
596
|
+
:is(a, button, label, .text-content)::before,
|
|
597
|
+
:global(.list-item:hover:not(.disabled):not(.text):not(.is-loading) + .list-item.active)
|
|
598
|
+
:is(a, button, label, .text-content)::before,
|
|
599
|
+
:global(.list-item.active + .list-item:hover:not(.disabled):not(.text):not(.is-loading))
|
|
600
|
+
:is(a, button, label, .text-content)::before {
|
|
601
|
+
border-top-left-radius: 0;
|
|
602
|
+
border-top-right-radius: 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.spacer {
|
|
606
|
+
flex: 1;
|
|
607
|
+
min-width: 1.5rem;
|
|
608
|
+
}
|
|
609
|
+
/* The native inputs are visually hidden but remain the real, focusable
|
|
610
|
+
* controls (the <label> toggles them, List delegates off their change
|
|
611
|
+
* event). The adjacent <Checkbox>/<Radio> render the visual state. */
|
|
612
|
+
input[type='radio'],
|
|
613
|
+
input[type='checkbox'] {
|
|
614
|
+
opacity: 0;
|
|
615
|
+
position: absolute;
|
|
616
|
+
width: 1px;
|
|
617
|
+
height: 1px;
|
|
618
|
+
margin: 0;
|
|
619
|
+
pointer-events: none;
|
|
620
|
+
}
|
|
621
|
+
.control {
|
|
622
|
+
display: inline-flex;
|
|
623
|
+
align-items: center;
|
|
624
|
+
flex-shrink: 0;
|
|
625
|
+
pointer-events: none;
|
|
626
|
+
}
|
|
627
|
+
/* Keyboard focus ring, driven by the hidden native input's focus state */
|
|
628
|
+
label:has(input:focus-visible) .control :global(.indicator-wrapper) {
|
|
629
|
+
box-shadow: 0 0 0 2px var(--color-border-active);
|
|
630
|
+
border-radius: 50%;
|
|
631
|
+
}
|
|
632
|
+
/* Same ring for toggle mode, following the Toggle's pill-shaped track */
|
|
633
|
+
label:has(input:focus-visible) .control :global(.toggle .track) {
|
|
634
|
+
box-shadow: 0 0 0 2px var(--color-border-active);
|
|
635
|
+
}
|
|
636
|
+
.text-content {
|
|
637
|
+
position: relative;
|
|
638
|
+
flex: 1;
|
|
639
|
+
/* Match the row's full height so the active background fills it (see the
|
|
640
|
+
* align-self note on a/button/label). */
|
|
641
|
+
align-self: stretch;
|
|
642
|
+
padding-top: 0.25rem;
|
|
643
|
+
padding-bottom: 0.25rem;
|
|
644
|
+
padding-right: calc(1.5rem + var(--list-pad-x, 0px));
|
|
645
|
+
padding-left: calc(1.5rem + var(--list-pad-x, 0px) + ((var(--level) - 1) * 1rem));
|
|
646
|
+
display: flex;
|
|
647
|
+
align-items: center;
|
|
648
|
+
color: var(--color-text);
|
|
649
|
+
|
|
650
|
+
&::before {
|
|
651
|
+
content: '';
|
|
652
|
+
opacity: 0;
|
|
653
|
+
position: absolute;
|
|
654
|
+
top: 1px;
|
|
655
|
+
right: var(--border-inset);
|
|
656
|
+
bottom: 1px;
|
|
657
|
+
left: calc(var(--border-inset) + ((var(--level) - 1) * 1rem));
|
|
658
|
+
border-radius: calc(var(--_radius) - var(--border-inset));
|
|
659
|
+
@supports (corner-shape: squircle) {
|
|
660
|
+
corner-shape: squircle;
|
|
661
|
+
border-radius: calc(
|
|
662
|
+
(var(--_radius) - var(--border-inset)) * var(--squircle-ratio, 2)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
background-color: var(--color-text);
|
|
666
|
+
transition:
|
|
667
|
+
opacity 300ms ease,
|
|
668
|
+
border-radius 150ms ease;
|
|
669
|
+
z-index: 0;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
li.dense & {
|
|
673
|
+
padding-top: 0;
|
|
674
|
+
padding-bottom: 0;
|
|
675
|
+
padding-right: 1rem;
|
|
676
|
+
padding-left: calc(1rem + ((var(--level) - 1) * 1rem));
|
|
677
|
+
}
|
|
678
|
+
li:first-child & {
|
|
679
|
+
padding-top: calc(var(--border-inset, 0px) + 0.25rem);
|
|
680
|
+
}
|
|
681
|
+
li:last-child & {
|
|
682
|
+
padding-bottom: calc(var(--border-inset, 0px) + 0.25rem);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
a,
|
|
687
|
+
button,
|
|
688
|
+
label {
|
|
689
|
+
flex: 1;
|
|
690
|
+
padding-top: 0.25rem;
|
|
691
|
+
padding-bottom: 0.25rem;
|
|
692
|
+
padding-right: calc(1.5rem + var(--list-pad-x, 0px));
|
|
693
|
+
padding-left: calc(1.5rem + var(--list-pad-x, 0px) + ((var(--level) - 1) * 1rem));
|
|
694
|
+
margin: 0;
|
|
695
|
+
border: none;
|
|
696
|
+
/* Establish a containing block at rest so the ::before background is
|
|
697
|
+
* always positioned against this element. The :active press applies a
|
|
698
|
+
* translateZ, and a transformed element becomes the containing block for
|
|
699
|
+
* its absolutely-positioned descendants — without this, the ::before's
|
|
700
|
+
* containing block would switch from the <li> to here only while pressed,
|
|
701
|
+
* snapping its size on mousedown/up. Being relative up front keeps it
|
|
702
|
+
* stable, so the press just smoothly scales the background with the
|
|
703
|
+
* content via the list's `perspective`. */
|
|
704
|
+
position: relative;
|
|
705
|
+
display: flex;
|
|
706
|
+
align-items: center;
|
|
707
|
+
/* Fill the full list-item width so the hover/active background spans
|
|
708
|
+
* the row uniformly with the ripple — previously `max-content` made
|
|
709
|
+
* the hover-bg only as wide as the text, producing a tight inner
|
|
710
|
+
* highlight that fought the wider ripple on click. */
|
|
711
|
+
width: 100%;
|
|
712
|
+
/* Fill the row's full height. `height: 100%` can't be used here: the
|
|
713
|
+
* <li>'s height comes from `min-height` (e.g. dense mode), which is
|
|
714
|
+
* indefinite for percentage resolution, so the percentage collapses to
|
|
715
|
+
* the element's intrinsic height — shorter than the row whenever padding
|
|
716
|
+
* is small (dense). align-self stretches to the flex line's cross size,
|
|
717
|
+
* which does honour min-height, so the ::before background (and the press
|
|
718
|
+
* scale that rides on it) always matches the full row. */
|
|
719
|
+
align-self: stretch;
|
|
720
|
+
cursor: pointer;
|
|
721
|
+
color: var(--color-text);
|
|
722
|
+
background-color: transparent;
|
|
723
|
+
text-decoration: none;
|
|
724
|
+
box-shadow: none;
|
|
725
|
+
transition: translate 200ms ease;
|
|
726
|
+
|
|
727
|
+
&:active:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']) {
|
|
728
|
+
translate: 0px 1px clamp(-10px, calc(0.2em - 12px), -2px);
|
|
729
|
+
}
|
|
730
|
+
/* Busy while the onclick promise is in flight: block pointer interaction
|
|
731
|
+
so a re-click can't re-press (:active) — the ripple is gated by its
|
|
732
|
+
`enabled` flag, and hover by the :not([aria-busy]) guards above. */
|
|
733
|
+
&[aria-busy='true'] {
|
|
734
|
+
pointer-events: none;
|
|
735
|
+
}
|
|
736
|
+
&::before {
|
|
737
|
+
content: '';
|
|
738
|
+
opacity: 0;
|
|
739
|
+
position: absolute;
|
|
740
|
+
top: 1px;
|
|
741
|
+
right: var(--border-inset);
|
|
742
|
+
bottom: 1px;
|
|
743
|
+
left: calc(var(--border-inset) + ((var(--level) - 1) * 1rem));
|
|
744
|
+
border-radius: calc(var(--_radius) - var(--border-inset));
|
|
745
|
+
@supports (corner-shape: squircle) {
|
|
746
|
+
corner-shape: squircle;
|
|
747
|
+
border-radius: calc(
|
|
748
|
+
(var(--_radius) - var(--border-inset)) * var(--squircle-ratio, 2)
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
background-color: var(--color-text);
|
|
752
|
+
transition:
|
|
753
|
+
opacity 300ms ease,
|
|
754
|
+
border-radius 150ms ease;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
a[aria-disabled='true'] {
|
|
758
|
+
color: var(--color-text-disabled);
|
|
759
|
+
cursor: auto;
|
|
760
|
+
}
|
|
761
|
+
button,
|
|
762
|
+
label {
|
|
763
|
+
&:disabled {
|
|
764
|
+
color: var(--color-text-disabled);
|
|
765
|
+
cursor: auto;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
button {
|
|
769
|
+
&:focus-visible {
|
|
770
|
+
&::after {
|
|
771
|
+
content: '';
|
|
772
|
+
position: absolute;
|
|
773
|
+
top: 1px;
|
|
774
|
+
right: var(--border-inset);
|
|
775
|
+
bottom: 1px;
|
|
776
|
+
left: calc(var(--border-inset) + ((var(--level) - 1) * 1rem));
|
|
777
|
+
border-radius: calc(var(--_radius) - var(--border-inset));
|
|
778
|
+
@supports (corner-shape: squircle) {
|
|
779
|
+
corner-shape: squircle;
|
|
780
|
+
border-radius: calc(
|
|
781
|
+
(var(--_radius) - var(--border-inset)) * var(--squircle-ratio, 2)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
border: solid 1px var(--color-border-active);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.loading-icon {
|
|
790
|
+
position: relative;
|
|
791
|
+
display: flex;
|
|
792
|
+
justify-content: center;
|
|
793
|
+
align-items: center;
|
|
794
|
+
width: 1.5em;
|
|
795
|
+
margin-left: -0.25em;
|
|
796
|
+
margin-right: 0.5em;
|
|
797
|
+
height: 100%;
|
|
798
|
+
flex-shrink: 0;
|
|
799
|
+
flex-grow: 0;
|
|
800
|
+
:global(.logo) {
|
|
801
|
+
display: block;
|
|
802
|
+
width: 100%;
|
|
803
|
+
height: auto;
|
|
804
|
+
aspect-ratio: 1;
|
|
805
|
+
flex-shrink: 0;
|
|
806
|
+
flex-grow: 0;
|
|
807
|
+
}
|
|
808
|
+
/* Spinner and success check occupy the same spot so they can crossfade
|
|
809
|
+
during the handoff without nudging the label. */
|
|
810
|
+
.icon-layer {
|
|
811
|
+
position: absolute;
|
|
812
|
+
inset: 0;
|
|
813
|
+
display: flex;
|
|
814
|
+
align-items: center;
|
|
815
|
+
justify-content: center;
|
|
816
|
+
}
|
|
817
|
+
.check {
|
|
818
|
+
display: block;
|
|
819
|
+
/* The tick only spans the middle of its viewBox, so fill the slot
|
|
820
|
+
(paired with the tightened viewBox) to size it like the spinner. The
|
|
821
|
+
slot width is fixed and the layer is absolutely positioned, so this
|
|
822
|
+
never shifts layout. */
|
|
823
|
+
width: 1.25em;
|
|
824
|
+
height: 1.25em;
|
|
825
|
+
}
|
|
826
|
+
.check path {
|
|
827
|
+
/* Dash length >= the tick's path length; checkIn() rides --check-draw
|
|
828
|
+
from 24 (hidden) down to 0 (drawn). The 0 fallback keeps it drawn
|
|
829
|
+
once the transition's inline style is gone. */
|
|
830
|
+
stroke-dasharray: 24;
|
|
831
|
+
stroke-dashoffset: var(--check-draw, 0);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
</style>
|