@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,1450 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ripple, tooltip } from '@delightstack/utilities';
|
|
3
|
+
import { getContext, type Snippet } from 'svelte';
|
|
4
|
+
import { fade, type TransitionConfig } from 'svelte/transition';
|
|
5
|
+
import { backOut, quartOut } from 'svelte/easing';
|
|
6
|
+
import Popover, { type PopoverPlacement, type PopoverStrategy } from './Popover.svelte';
|
|
7
|
+
import Progress from '../feedback/Progress.svelte';
|
|
8
|
+
import type { ButtonGroupContext } from './ButtonGroup.svelte';
|
|
9
|
+
|
|
10
|
+
const groupContext = getContext<ButtonGroupContext | undefined>('button-group');
|
|
11
|
+
|
|
12
|
+
// When a Button with `type="submit"` lives inside a <Form>, auto-wire its
|
|
13
|
+
// loading and disabled state to the form's submission lifecycle. Callers
|
|
14
|
+
// can still override by passing explicit `loading` or `disabled`.
|
|
15
|
+
type _FormSubmitContext = { is_submitting: boolean; disabled: boolean };
|
|
16
|
+
const formContext = getContext<_FormSubmitContext | undefined>('form');
|
|
17
|
+
|
|
18
|
+
const propId = $props.id();
|
|
19
|
+
let {
|
|
20
|
+
/** The size of the button - referencing the font size options in css vars*/
|
|
21
|
+
size = undefined as
|
|
22
|
+
| undefined
|
|
23
|
+
| '0000'
|
|
24
|
+
| '000'
|
|
25
|
+
| '00'
|
|
26
|
+
| '0'
|
|
27
|
+
| '1'
|
|
28
|
+
| '2'
|
|
29
|
+
| '3'
|
|
30
|
+
| '4'
|
|
31
|
+
| '5'
|
|
32
|
+
| '6',
|
|
33
|
+
|
|
34
|
+
/** Whether the button is an icon button only */
|
|
35
|
+
icon = false,
|
|
36
|
+
|
|
37
|
+
/** Whether the button is a pill shape (rounded corners) */
|
|
38
|
+
pill = false,
|
|
39
|
+
|
|
40
|
+
/** Whether the button has a transparent background */
|
|
41
|
+
transparent = false,
|
|
42
|
+
|
|
43
|
+
/** Whether the button has a semi-transparent background (takes on some of the color of the text color) */
|
|
44
|
+
translucent = false,
|
|
45
|
+
|
|
46
|
+
/** Whether the button has an outline style (transparent background with border) */
|
|
47
|
+
outline = false,
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether the button is part of a group of buttons.
|
|
51
|
+
* If so, the border radius will be removed on the sides that touch other buttons
|
|
52
|
+
* and the borders/margins will be merged
|
|
53
|
+
*/
|
|
54
|
+
grouped = false,
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether the will be styled to indicator an error (or danger).
|
|
58
|
+
* If 'transparent', this makes the text red instead of the background
|
|
59
|
+
* If not 'transparent', this makes the background red
|
|
60
|
+
*/
|
|
61
|
+
error = false,
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the will be styled to indicator sucess
|
|
65
|
+
* If 'transparent', this makes the text green instead of the background
|
|
66
|
+
* If not 'transparent', this makes the background green
|
|
67
|
+
*/
|
|
68
|
+
success = false,
|
|
69
|
+
|
|
70
|
+
/** Whether the button is an 'overlay' - blurs & darkens the background */
|
|
71
|
+
overlay = false,
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the background color should be the accent/primary/brand color.
|
|
75
|
+
* If 'transparent' is also true, this will change the color of the text instead
|
|
76
|
+
*/
|
|
77
|
+
accent = false,
|
|
78
|
+
|
|
79
|
+
/** Whether the button should be smaller (with less padding) */
|
|
80
|
+
dense = false,
|
|
81
|
+
|
|
82
|
+
/** Whether the button should be larger (with more padding) */
|
|
83
|
+
comfortable = false,
|
|
84
|
+
|
|
85
|
+
/** Whether the button should take up the full width of its container */
|
|
86
|
+
full_width = false,
|
|
87
|
+
|
|
88
|
+
/** Whether the button should take up the full height of its container */
|
|
89
|
+
full_height = false,
|
|
90
|
+
|
|
91
|
+
/** Whether there should not be a ripple animation on click */
|
|
92
|
+
disable_ripple = false,
|
|
93
|
+
|
|
94
|
+
/** The url to link to (turns the button into an anchor tag) */
|
|
95
|
+
href = undefined as string | undefined,
|
|
96
|
+
|
|
97
|
+
/** The target of the link (only used if href is provided) */
|
|
98
|
+
target = undefined as '_self' | '_blank' | '_parent' | '_top' | undefined,
|
|
99
|
+
|
|
100
|
+
/** The button type (ignored when `href` is set). @default 'button' */
|
|
101
|
+
type = 'button' as 'button' | 'submit' | 'reset',
|
|
102
|
+
|
|
103
|
+
/** The tooltip message to show on hover */
|
|
104
|
+
tooltip: tooltip_message = '',
|
|
105
|
+
|
|
106
|
+
/** Whether the button is disabled */
|
|
107
|
+
disabled = false,
|
|
108
|
+
|
|
109
|
+
/** Whether the button is in in the 'active' state (similar to how a toggle button would be 'selected') */
|
|
110
|
+
active = false,
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether a loading spinner should appear before the button text.
|
|
114
|
+
* Leave undefined to let a promise-returning `onclick` drive it
|
|
115
|
+
* automatically (see `onclick`).
|
|
116
|
+
*/
|
|
117
|
+
loading = undefined as boolean | undefined,
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* For the manual `loading` path only: when `loading` goes true -> false
|
|
121
|
+
* and this is true, a success checkmark briefly animates in to confirm the
|
|
122
|
+
* action, then animates away. (The promise-aware `onclick` path shows this
|
|
123
|
+
* checkmark automatically on resolve, so this prop isn't needed there.)
|
|
124
|
+
*/
|
|
125
|
+
loading_success = false,
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The text to show in a badge hovering over the top right corner of the button
|
|
129
|
+
* If "true", then a small dot will appear instead of a box with text
|
|
130
|
+
*/
|
|
131
|
+
badge = undefined as string | undefined | boolean,
|
|
132
|
+
|
|
133
|
+
/** The content to show in a dropdown menu when the button is clicked */
|
|
134
|
+
menu = undefined as undefined | Snippet<[{ close: () => void }]>,
|
|
135
|
+
|
|
136
|
+
/** Whether the button should have a chevron icon next to it (useful when used with the 'menu' optionk) */
|
|
137
|
+
show_chevron = false,
|
|
138
|
+
|
|
139
|
+
/** The content shown in a dropdown menu when the secondary dropdown button is clicked */
|
|
140
|
+
dropdown = undefined as undefined | Snippet<[{ close: () => void }]>,
|
|
141
|
+
|
|
142
|
+
/** The content shown when the button is in the loading state */
|
|
143
|
+
loading_content = undefined as undefined | Snippet<[{ close: () => void }]>,
|
|
144
|
+
|
|
145
|
+
/** Whether the dropdown menu should be disabled (and the secondary downdown button hidden) */
|
|
146
|
+
disable_dropdown = false,
|
|
147
|
+
|
|
148
|
+
/** Whether the dropdown menu should close when the user clicks a button like element inside of it */
|
|
149
|
+
popover_close_on_inside_click = false,
|
|
150
|
+
|
|
151
|
+
/** The placement of the popover (used when either "menu" or "dropdown" is provided) */
|
|
152
|
+
popover_placement = 'bottom-end' as PopoverPlacement,
|
|
153
|
+
|
|
154
|
+
/** The placement of the popover (used when either "menu" or "dropdown" is provided) */
|
|
155
|
+
popover_strategy = 'fixed' as PopoverStrategy,
|
|
156
|
+
|
|
157
|
+
/** Whether the intial focus should not be set automatically when opening the popover */
|
|
158
|
+
popover_disable_initial_focus = false,
|
|
159
|
+
|
|
160
|
+
/** The content shown in the button element */
|
|
161
|
+
children = undefined as
|
|
162
|
+
| undefined
|
|
163
|
+
| Snippet<[{ isLoading: boolean; isLoadingSuccess: boolean }]>,
|
|
164
|
+
|
|
165
|
+
/** The ID of the element. @defaults to a random ID */
|
|
166
|
+
id = propId,
|
|
167
|
+
|
|
168
|
+
/** A reference to the button element */
|
|
169
|
+
button_element = $bindable() as HTMLElement | undefined,
|
|
170
|
+
|
|
171
|
+
/** Specifies a custom class name for the container element */
|
|
172
|
+
class: class_name = '',
|
|
173
|
+
|
|
174
|
+
/** The css style string added to the component from the parent */
|
|
175
|
+
style = '',
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The function to call when the button is clicked.
|
|
179
|
+
* If it returns a promise, the button manages its own loading feedback:
|
|
180
|
+
* - A spinner appears only if the promise is still pending after ~100ms
|
|
181
|
+
* (faster resolves are treated as instant — no spinner flash).
|
|
182
|
+
* - Once shown, the spinner stays for at least ~1s so it can't blink away.
|
|
183
|
+
* - On resolve, a brief success checkmark confirms the action; on reject,
|
|
184
|
+
* no checkmark is shown.
|
|
185
|
+
*/
|
|
186
|
+
onclick = undefined as
|
|
187
|
+
| undefined
|
|
188
|
+
| ((e: MouseEvent) => void)
|
|
189
|
+
| ((e: MouseEvent) => Promise<void>),
|
|
190
|
+
|
|
191
|
+
...rest
|
|
192
|
+
} = $props();
|
|
193
|
+
|
|
194
|
+
// Merge ButtonGroup context with local props (local props take precedence when explicitly set)
|
|
195
|
+
const resolvedSize = $derived(size ?? groupContext?.size);
|
|
196
|
+
const resolvedOutline = $derived(outline || groupContext?.outline || false);
|
|
197
|
+
const resolvedTransparent = $derived(transparent || groupContext?.transparent || false);
|
|
198
|
+
const resolvedTranslucent = $derived(translucent || groupContext?.translucent || false);
|
|
199
|
+
const resolvedAccent = $derived(accent || groupContext?.accent || false);
|
|
200
|
+
const resolvedError = $derived(error || groupContext?.error || false);
|
|
201
|
+
const resolvedSuccess = $derived(success || groupContext?.success || false);
|
|
202
|
+
// A submit button inside a <Form> inherits the form's submitting/disabled
|
|
203
|
+
// state unless the caller explicitly set `loading` / `disabled`.
|
|
204
|
+
const isFormSubmit = $derived(type === 'submit' && !!formContext);
|
|
205
|
+
const resolvedDisabled = $derived(
|
|
206
|
+
disabled ||
|
|
207
|
+
groupContext?.disabled ||
|
|
208
|
+
(isFormSubmit && formContext!.disabled) ||
|
|
209
|
+
false,
|
|
210
|
+
);
|
|
211
|
+
const resolvedGrouped = $derived(grouped || !!groupContext);
|
|
212
|
+
|
|
213
|
+
let dropdownActive = $state(false);
|
|
214
|
+
let dropdownTrigger = $state(undefined as undefined | HTMLElement);
|
|
215
|
+
let menuActive = $state(false);
|
|
216
|
+
let mounted = $state(false);
|
|
217
|
+
$effect(() => {
|
|
218
|
+
mounted = true;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/* Promise-aware loading timing.
|
|
222
|
+
- SHOW_DELAY: a promise that settles faster than this never gets a
|
|
223
|
+
spinner — the action reads as "instant".
|
|
224
|
+
- MIN_VISIBLE: once the spinner *is* shown it stays at least this long, so
|
|
225
|
+
it can't flash on then immediately off.
|
|
226
|
+
- SPINNER_OUT: how long the spinner takes to collapse away. Kept in sync
|
|
227
|
+
with loadingTransition's "out" duration so the success check can slot in
|
|
228
|
+
right after the spinner clears.
|
|
229
|
+
- CHECK_HOLD: how long the success checkmark lingers before easing out. */
|
|
230
|
+
const SHOW_DELAY = 100;
|
|
231
|
+
const MIN_VISIBLE = 1000;
|
|
232
|
+
const SPINNER_OUT = 150;
|
|
233
|
+
const CHECK_HOLD = 1000;
|
|
234
|
+
|
|
235
|
+
let inFlight = $state(false); // a returned promise is running (covers the pre-spinner window)
|
|
236
|
+
let spinnerVisible = $state(false); // the spinner is actually rendered
|
|
237
|
+
let checkVisible = $state(false); // the success checkmark is rendered
|
|
238
|
+
|
|
239
|
+
let showTimer: ReturnType<typeof setTimeout> | undefined;
|
|
240
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
241
|
+
let checkTimer: ReturnType<typeof setTimeout> | undefined;
|
|
242
|
+
let spinnerShownAt = 0;
|
|
243
|
+
|
|
244
|
+
function clearTimers() {
|
|
245
|
+
clearTimeout(showTimer);
|
|
246
|
+
clearTimeout(hideTimer);
|
|
247
|
+
clearTimeout(checkTimer);
|
|
248
|
+
showTimer = hideTimer = checkTimer = undefined;
|
|
249
|
+
}
|
|
250
|
+
$effect(() => clearTimers); // tear down pending timers on destroy
|
|
251
|
+
|
|
252
|
+
// `loading` prop wins if provided; otherwise a submit button tracks the
|
|
253
|
+
// surrounding form's is_submitting flag.
|
|
254
|
+
const externalLoading = $derived(
|
|
255
|
+
loading ?? (isFormSubmit ? formContext!.is_submitting : undefined),
|
|
256
|
+
);
|
|
257
|
+
// "Busy" for a11y/styling: an external loading flag, or a returned promise
|
|
258
|
+
// that's in flight (including the brief pre-spinner window and the spinner's
|
|
259
|
+
// minimum-visible tail). `showSpinner` is what actually renders the spinner —
|
|
260
|
+
// it excludes the pre-spinner window so a sub-SHOW_DELAY promise never flashes
|
|
261
|
+
// one. `isLoadingSuccess` reflects the post-success checkmark.
|
|
262
|
+
const isLoading = $derived(!!externalLoading || inFlight);
|
|
263
|
+
const showSpinner = $derived(!!externalLoading || spinnerVisible);
|
|
264
|
+
const isLoadingSuccess = $derived(checkVisible);
|
|
265
|
+
|
|
266
|
+
// Manual loading path: when the caller drives `loading` true -> false and has
|
|
267
|
+
// opted in with `loading_success`, play the same confirming checkmark the
|
|
268
|
+
// promise path shows on success. Runs in `$effect.pre` so `checkVisible` is set
|
|
269
|
+
// in the same flush `loading` clears in — otherwise the icon slot would render
|
|
270
|
+
// one empty frame (spinner gone, check not yet set) and flash closed/open.
|
|
271
|
+
let wasExternalLoading = false;
|
|
272
|
+
$effect.pre(() => {
|
|
273
|
+
const now = !!externalLoading;
|
|
274
|
+
if (wasExternalLoading && !now && loading_success && !inFlight) flashCheck();
|
|
275
|
+
wasExternalLoading = now;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// The icon slot (the common parent of the spinner and the success check)
|
|
279
|
+
// grows/collapses with this width+opacity transition. Because it wraps both,
|
|
280
|
+
// the slot only animates open when one of them first appears and only
|
|
281
|
+
// collapses once both are gone — the spinner -> check handoff happens inside a
|
|
282
|
+
// stable, already-open slot (see `checkIn` for the check's own entrance).
|
|
283
|
+
//
|
|
284
|
+
// The slot's resting layout includes negative margins (they tuck the spinner
|
|
285
|
+
// in close to the label/edge) and the button's flex gap. Animating width
|
|
286
|
+
// alone would leave those at full strength, so at t=0 the slot's total
|
|
287
|
+
// layout contribution would be *negative* — the label gets pulled past its
|
|
288
|
+
// resting position and then snaps back the moment the node is removed. So
|
|
289
|
+
// the margins ride `t` too, with the gap folded into margin-right's final
|
|
290
|
+
// frame, making the total contribution hit exactly 0 at t=0: no snap on
|
|
291
|
+
// insert or removal.
|
|
292
|
+
function loadingTransition(
|
|
293
|
+
node: HTMLElement,
|
|
294
|
+
params?: { direction?: 'in' | 'out' },
|
|
295
|
+
): () => TransitionConfig {
|
|
296
|
+
return () => {
|
|
297
|
+
const style = getComputedStyle(node);
|
|
298
|
+
const width = parseFloat(style.width);
|
|
299
|
+
const marginLeft = parseFloat(style.marginLeft) || 0;
|
|
300
|
+
const marginRight = parseFloat(style.marginRight) || 0;
|
|
301
|
+
const gap = node.parentElement
|
|
302
|
+
? parseFloat(getComputedStyle(node.parentElement).columnGap) || 0
|
|
303
|
+
: 0;
|
|
304
|
+
const out = params?.direction === 'out';
|
|
305
|
+
return {
|
|
306
|
+
duration: out ? SPINNER_OUT : 320,
|
|
307
|
+
easing: out ? quartOut : backOut,
|
|
308
|
+
css: (t: number) =>
|
|
309
|
+
`width: ${t * width}px; ` +
|
|
310
|
+
`margin-left: ${t * marginLeft}px; ` +
|
|
311
|
+
`margin-right: ${t * marginRight - (1 - t) * gap}px; ` +
|
|
312
|
+
`opacity: ${t};`,
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// The checkmark's own entrance: a spring-scaled pop that simultaneously draws
|
|
318
|
+
// its stroke on (the dash offset rides `t`, so the tick paints itself in).
|
|
319
|
+
// Driving the draw from the transition — rather than a CSS @keyframes — keeps
|
|
320
|
+
// it reliable regardless of scoping. prefers-reduced-motion collapses it to a
|
|
321
|
+
// plain appear.
|
|
322
|
+
function checkIn(_node: Element): TransitionConfig {
|
|
323
|
+
const reduce =
|
|
324
|
+
typeof matchMedia !== 'undefined' &&
|
|
325
|
+
matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
326
|
+
return {
|
|
327
|
+
duration: reduce ? 0 : 440,
|
|
328
|
+
easing: backOut,
|
|
329
|
+
css: (t: number) =>
|
|
330
|
+
`transform: scale(${0.3 + 0.7 * t}); opacity: ${Math.min(1, t * 2)}; --check-draw: ${24 * (1 - t)};`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Pop the confirming checkmark, then retire it after CHECK_HOLD. spinnerVisible
|
|
335
|
+
// is cleared in the same tick by the caller, so the parent slot stays open and
|
|
336
|
+
// the spinner crossfades into the check rather than the slot reopening.
|
|
337
|
+
function flashCheck() {
|
|
338
|
+
clearTimeout(checkTimer);
|
|
339
|
+
checkVisible = true;
|
|
340
|
+
checkTimer = setTimeout(() => (checkVisible = false), CHECK_HOLD);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function handleClick(e: MouseEvent) {
|
|
344
|
+
if (inFlight || externalLoading) return;
|
|
345
|
+
if (!onclick) return;
|
|
346
|
+
const maybePromise = onclick(e);
|
|
347
|
+
if (!(maybePromise instanceof Promise)) return;
|
|
348
|
+
|
|
349
|
+
// A fresh action supersedes any checkmark still lingering from the last one.
|
|
350
|
+
clearTimers();
|
|
351
|
+
checkVisible = false;
|
|
352
|
+
|
|
353
|
+
inFlight = true;
|
|
354
|
+
// Hold off on the spinner — if the promise settles within SHOW_DELAY the
|
|
355
|
+
// action was effectively instant and never needs one.
|
|
356
|
+
showTimer = setTimeout(() => {
|
|
357
|
+
showTimer = undefined;
|
|
358
|
+
spinnerVisible = true;
|
|
359
|
+
spinnerShownAt = performance.now();
|
|
360
|
+
}, SHOW_DELAY);
|
|
361
|
+
|
|
362
|
+
maybePromise.then(
|
|
363
|
+
() => settle(true),
|
|
364
|
+
() => settle(false),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function settle(success: boolean) {
|
|
369
|
+
// Settled before the spinner ever appeared -> treat as instant: no
|
|
370
|
+
// spinner, no checkmark, just release.
|
|
371
|
+
if (showTimer) {
|
|
372
|
+
clearTimeout(showTimer);
|
|
373
|
+
showTimer = undefined;
|
|
374
|
+
inFlight = false;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// The spinner is up; keep it for the rest of its minimum-visible window so
|
|
378
|
+
// it doesn't blink away the instant the promise resolves (which otherwise
|
|
379
|
+
// reads as a glitch when the page updates a beat later).
|
|
380
|
+
const remaining = Math.max(0, MIN_VISIBLE - (performance.now() - spinnerShownAt));
|
|
381
|
+
clearTimeout(hideTimer);
|
|
382
|
+
hideTimer = setTimeout(() => {
|
|
383
|
+
spinnerVisible = false;
|
|
384
|
+
inFlight = false;
|
|
385
|
+
// On success, let the spinner collapse, then pop a brief checkmark.
|
|
386
|
+
if (success) flashCheck();
|
|
387
|
+
}, remaining);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function closeMenu() {
|
|
391
|
+
if (menuActive) menuActive = false;
|
|
392
|
+
if (dropdownActive) dropdownActive = false;
|
|
393
|
+
}
|
|
394
|
+
</script>
|
|
395
|
+
|
|
396
|
+
<div
|
|
397
|
+
{id}
|
|
398
|
+
class={['button', class_name].filter(Boolean).join(' ')}
|
|
399
|
+
class:has-dropdown-trigger={dropdown && !disable_dropdown}
|
|
400
|
+
class:icon
|
|
401
|
+
class:pill
|
|
402
|
+
class:dense
|
|
403
|
+
class:comfortable
|
|
404
|
+
class:grouped={resolvedGrouped}
|
|
405
|
+
class:group-h={resolvedGrouped &&
|
|
406
|
+
groupContext?.attached &&
|
|
407
|
+
groupContext?.orientation === 'horizontal'}
|
|
408
|
+
class:group-v={resolvedGrouped &&
|
|
409
|
+
groupContext?.attached &&
|
|
410
|
+
groupContext?.orientation === 'vertical'}
|
|
411
|
+
class:full-width={full_width}
|
|
412
|
+
class:full-height={full_height}
|
|
413
|
+
class:overlay
|
|
414
|
+
class:transparent={resolvedTransparent}
|
|
415
|
+
class:translucent={resolvedTranslucent}
|
|
416
|
+
class:outline={resolvedOutline}
|
|
417
|
+
class:success={resolvedSuccess}
|
|
418
|
+
class:accent={resolvedAccent}
|
|
419
|
+
class:active
|
|
420
|
+
class:error={resolvedError}
|
|
421
|
+
class:is-loading={isLoading}
|
|
422
|
+
{style}
|
|
423
|
+
style:font-size={resolvedSize === undefined ? null : `var(--font-size-${resolvedSize})`}
|
|
424
|
+
{@attach tooltip(tooltip_message)}>
|
|
425
|
+
{#if badge}
|
|
426
|
+
<div class="badge" class:dot={badge === true}>
|
|
427
|
+
{#if badge !== true}{badge}{/if}
|
|
428
|
+
</div>
|
|
429
|
+
{/if}
|
|
430
|
+
<svelte:element
|
|
431
|
+
this={href ? 'a' : 'button'}
|
|
432
|
+
type={href ? null : type}
|
|
433
|
+
role="button"
|
|
434
|
+
tabindex={resolvedDisabled || isLoading || (!mounted && !href) ? -1 : 0}
|
|
435
|
+
{...rest}
|
|
436
|
+
{target}
|
|
437
|
+
{href}
|
|
438
|
+
data-sveltekit-noscroll={href?.startsWith('?') ? true : null}
|
|
439
|
+
data-sveltekit-keepfocus={href?.startsWith('?') ? true : null}
|
|
440
|
+
{@attach ripple({
|
|
441
|
+
enabled: !disable_ripple && !resolvedDisabled && !isLoading,
|
|
442
|
+
zIndex: 1,
|
|
443
|
+
})}
|
|
444
|
+
disabled={resolvedDisabled || inFlight || (!mounted && !href)}
|
|
445
|
+
aria-busy={isLoading ? 'true' : null}
|
|
446
|
+
aria-haspopup={!!menu}
|
|
447
|
+
aria-expanded={menu ? menuActive : null}
|
|
448
|
+
bind:this={button_element}
|
|
449
|
+
onclick={handleClick}>
|
|
450
|
+
<!-- The transition divs must sit DIRECTLY inside the {#if} that toggles
|
|
451
|
+
with the loading state: transitions are local by default, so nesting
|
|
452
|
+
them one block deeper (e.g. an inner {#if icon}) means they never
|
|
453
|
+
play when this block mounts/unmounts. -->
|
|
454
|
+
{#if icon && (showSpinner || checkVisible)}
|
|
455
|
+
<!-- Icon mode has no label to sit beside, so the feedback can't use
|
|
456
|
+
the width-growing slot. Instead it overlays the square button
|
|
457
|
+
while the icon itself scales away beneath it (see &.icon CSS). -->
|
|
458
|
+
<div class="loading-icon" transition:fade={{ duration: 150 }}>
|
|
459
|
+
{@render loadingLayers()}
|
|
460
|
+
</div>
|
|
461
|
+
{:else if !icon && (showSpinner || checkVisible)}
|
|
462
|
+
<div
|
|
463
|
+
class="loading-icon"
|
|
464
|
+
in:loadingTransition={{ direction: 'in' }}
|
|
465
|
+
out:loadingTransition={{ direction: 'out' }}>
|
|
466
|
+
{@render loadingLayers()}
|
|
467
|
+
</div>
|
|
468
|
+
{/if}
|
|
469
|
+
{#if children}{@render children({ isLoading, isLoadingSuccess })}{/if}
|
|
470
|
+
{#if show_chevron && menu}
|
|
471
|
+
<svg
|
|
472
|
+
viewBox="0 0 24 24"
|
|
473
|
+
fill="currentColor"
|
|
474
|
+
style="pointer-events:none;"
|
|
475
|
+
class="chevron {menuActive ? 'active' : ''}"
|
|
476
|
+
aria-hidden="true">
|
|
477
|
+
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z" />
|
|
478
|
+
</svg>
|
|
479
|
+
{/if}
|
|
480
|
+
</svelte:element>
|
|
481
|
+
{#if dropdown && !disable_dropdown}
|
|
482
|
+
<button
|
|
483
|
+
class="dropdown-trigger"
|
|
484
|
+
type="button"
|
|
485
|
+
aria-haspopup="true"
|
|
486
|
+
aria-expanded={dropdownActive}
|
|
487
|
+
aria-label="Toggle dropdown"
|
|
488
|
+
title="Open for more actions"
|
|
489
|
+
{@attach ripple({
|
|
490
|
+
enabled: !disable_ripple && !resolvedDisabled && !isLoading,
|
|
491
|
+
zIndex: 1,
|
|
492
|
+
})}
|
|
493
|
+
bind:this={dropdownTrigger}>
|
|
494
|
+
<svg
|
|
495
|
+
viewBox="0 0 24 24"
|
|
496
|
+
fill="currentColor"
|
|
497
|
+
style="pointer-events:none"
|
|
498
|
+
class="chevron {dropdownActive ? 'active' : ''}"
|
|
499
|
+
aria-hidden="true">
|
|
500
|
+
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z" />
|
|
501
|
+
</svg>
|
|
502
|
+
</button>
|
|
503
|
+
{/if}
|
|
504
|
+
</div>
|
|
505
|
+
{#if menu}
|
|
506
|
+
<Popover
|
|
507
|
+
ref_element={button_element}
|
|
508
|
+
bind:opened={menuActive}
|
|
509
|
+
open_on_click
|
|
510
|
+
arrow={false}
|
|
511
|
+
strategy={popover_strategy}
|
|
512
|
+
close_on_inside_click={popover_close_on_inside_click}
|
|
513
|
+
disable_initial_focus={popover_disable_initial_focus}
|
|
514
|
+
placement={popover_placement}
|
|
515
|
+
radius="calc(var(--radius-lg, 10px) * 1.5)">
|
|
516
|
+
{@render menu({ close: closeMenu })}
|
|
517
|
+
</Popover>
|
|
518
|
+
{/if}
|
|
519
|
+
{#if dropdown && !disable_dropdown}
|
|
520
|
+
<Popover
|
|
521
|
+
ref_element={dropdownTrigger}
|
|
522
|
+
bind:opened={dropdownActive}
|
|
523
|
+
open_on_click
|
|
524
|
+
arrow={false}
|
|
525
|
+
strategy={popover_strategy}
|
|
526
|
+
close_on_inside_click={popover_close_on_inside_click}
|
|
527
|
+
disable_initial_focus={popover_disable_initial_focus}
|
|
528
|
+
placement={popover_placement}
|
|
529
|
+
radius="calc(var(--radius-lg, 10px) * 1.5)">
|
|
530
|
+
{@render dropdown({ close: closeMenu })}
|
|
531
|
+
</Popover>
|
|
532
|
+
{/if}
|
|
533
|
+
|
|
534
|
+
{#snippet loadingLayers()}
|
|
535
|
+
{#if showSpinner}
|
|
536
|
+
<div class="icon-layer" out:fade={{ duration: 120 }}>
|
|
537
|
+
<Progress size="00" color="currentColor" />
|
|
538
|
+
</div>
|
|
539
|
+
{:else}
|
|
540
|
+
<div class="icon-layer check-layer" in:checkIn>
|
|
541
|
+
<svg
|
|
542
|
+
class="check"
|
|
543
|
+
viewBox="2 2 20 20"
|
|
544
|
+
fill="none"
|
|
545
|
+
stroke="currentColor"
|
|
546
|
+
stroke-width="3"
|
|
547
|
+
stroke-linecap="round"
|
|
548
|
+
stroke-linejoin="round"
|
|
549
|
+
aria-hidden="true">
|
|
550
|
+
<path d="M5 12.5l4.5 4.5L19 7" />
|
|
551
|
+
</svg>
|
|
552
|
+
</div>
|
|
553
|
+
{/if}
|
|
554
|
+
{/snippet}
|
|
555
|
+
|
|
556
|
+
<style>
|
|
557
|
+
.button {
|
|
558
|
+
--_radius: var(--action-radius, var(--radius-lg));
|
|
559
|
+
/* Squircle (superellipse) corners where supported. corner-shape can't form a
|
|
560
|
+
pill, so .pill and the icon-circle opt back out (round + no doubling) below.
|
|
561
|
+
corner-shape and the radius doubling are applied per surface inside @supports,
|
|
562
|
+
so unsupported browsers keep the plain radius. */
|
|
563
|
+
--_corner-shape: squircle;
|
|
564
|
+
--_corner-scale: var(--squircle-ratio, 2);
|
|
565
|
+
--easing: var(--ease-spring);
|
|
566
|
+
/* Default font for an unsized button. Combined with the shared control
|
|
567
|
+
height below, a bare <Button> matches a default Input/Select height in
|
|
568
|
+
a row. An explicit `size` (inline font-size) or an in-field font
|
|
569
|
+
override (e.g. .input-icon-btn) wins over this. */
|
|
570
|
+
font-size: var(--control-font-1, 1rem);
|
|
571
|
+
display: inline-flex;
|
|
572
|
+
justify-content: center;
|
|
573
|
+
position: relative;
|
|
574
|
+
width: fit-content;
|
|
575
|
+
border-radius: var(--_radius);
|
|
576
|
+
@supports (corner-shape: squircle) {
|
|
577
|
+
corner-shape: var(--_corner-shape);
|
|
578
|
+
border-radius: calc(var(--_radius) * var(--_corner-scale));
|
|
579
|
+
}
|
|
580
|
+
perspective: 100px;
|
|
581
|
+
|
|
582
|
+
&.pill {
|
|
583
|
+
--_radius: var(--radius-full);
|
|
584
|
+
--_corner-shape: round;
|
|
585
|
+
--_corner-scale: 1;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
&:not(.transparent):not(.translucent) {
|
|
589
|
+
--color-bg: var(--color-action);
|
|
590
|
+
--color-bg-disabled: var(--color-action-disabled);
|
|
591
|
+
--color-bg-active: var(--color-action-active);
|
|
592
|
+
--color-text: var(--color-action-text);
|
|
593
|
+
--color-text-disabled: var(--color-action-text-disabled);
|
|
594
|
+
--color-text-active: var(--color-action-text-active);
|
|
595
|
+
--color-border: var(--button-border);
|
|
596
|
+
--color-border-disabled: var(--button-border-disabled);
|
|
597
|
+
--color-border-active: var(--button-border-active);
|
|
598
|
+
}
|
|
599
|
+
&.accent:not(.transparent):not(.translucent) {
|
|
600
|
+
--color-bg: var(--color-accent);
|
|
601
|
+
--color-bg-disabled: var(--color-accent-disabled);
|
|
602
|
+
--color-bg-active: var(--color-accent-active);
|
|
603
|
+
--color-text: var(--color-accent-text);
|
|
604
|
+
--color-text-active: var(--color-accent-text-active);
|
|
605
|
+
--color-text-disabled: var(--color-accent-text-disabled);
|
|
606
|
+
}
|
|
607
|
+
&.error:not(.transparent):not(.translucent) {
|
|
608
|
+
--color-bg: var(--color-error);
|
|
609
|
+
--color-bg-disabled: var(--color-error-disabled);
|
|
610
|
+
--color-bg-active: var(--color-error-active);
|
|
611
|
+
--color-text: var(--color-error-text);
|
|
612
|
+
--color-text-active: var(--color-error-text-active);
|
|
613
|
+
--color-text-disabled: var(--color-error-text-disabled);
|
|
614
|
+
}
|
|
615
|
+
&.success:not(.transparent):not(.translucent) {
|
|
616
|
+
--color-bg: var(--color-success);
|
|
617
|
+
--color-bg-disabled: var(--color-success-disabled);
|
|
618
|
+
--color-bg-active: var(--color-success-active);
|
|
619
|
+
--color-text: var(--color-success-text);
|
|
620
|
+
--color-text-active: var(--color-success-text-active);
|
|
621
|
+
--color-text-disabled: var(--color-success-text-disabled);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
&.outline:not(.transparent):not(.translucent) {
|
|
625
|
+
--color-bg: transparent;
|
|
626
|
+
--color-bg-disabled: transparent;
|
|
627
|
+
--color-bg-active: rgb(from var(--color-action) r g b / 0.08);
|
|
628
|
+
--button-border: 1px solid currentColor;
|
|
629
|
+
--button-border-disabled: 1px solid currentColor;
|
|
630
|
+
--button-border-active: 1px solid currentColor;
|
|
631
|
+
--color-text: light-dark(
|
|
632
|
+
oklch(from var(--color-action) min(l, 0.5) c h),
|
|
633
|
+
oklch(from var(--color-action) max(l, 0.65) c h)
|
|
634
|
+
);
|
|
635
|
+
--color-text-disabled: light-dark(
|
|
636
|
+
oklch(from var(--color-action) min(l + 0.1, 0.6) calc(c - 0.05) h),
|
|
637
|
+
oklch(from var(--color-action) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
638
|
+
);
|
|
639
|
+
--color-text-active: light-dark(
|
|
640
|
+
oklch(from var(--color-action) min(l - 0.2, 0.3) c h),
|
|
641
|
+
oklch(from var(--color-action) max(l + 0.1, 0.75) c h)
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
&.accent {
|
|
645
|
+
--color-bg-active: rgb(from var(--color-accent) r g b / 0.08);
|
|
646
|
+
--color-text: light-dark(
|
|
647
|
+
oklch(from var(--color-accent) min(l, 0.5) c h),
|
|
648
|
+
oklch(from var(--color-accent) max(l, 0.65) c h)
|
|
649
|
+
);
|
|
650
|
+
--color-text-disabled: light-dark(
|
|
651
|
+
oklch(from var(--color-accent) min(l + 0.1, 0.6) calc(c - 0.05) h),
|
|
652
|
+
oklch(from var(--color-accent) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
653
|
+
);
|
|
654
|
+
--color-text-active: light-dark(
|
|
655
|
+
oklch(from var(--color-accent) min(l - 0.1, 0.4) c h),
|
|
656
|
+
oklch(from var(--color-accent) max(l + 0.1, 0.75) c h)
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
&.error {
|
|
660
|
+
--color-bg-active: rgb(from var(--color-error) r g b / 0.08);
|
|
661
|
+
--color-text: light-dark(
|
|
662
|
+
oklch(from var(--color-error) min(l, 0.55) c h),
|
|
663
|
+
oklch(from var(--color-error) max(l, 0.65) c h)
|
|
664
|
+
);
|
|
665
|
+
--color-text-disabled: light-dark(
|
|
666
|
+
oklch(from var(--color-error) min(l + 0.1, 0.65) calc(c - 0.05) h),
|
|
667
|
+
oklch(from var(--color-error) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
668
|
+
);
|
|
669
|
+
--color-text-active: light-dark(
|
|
670
|
+
oklch(from var(--color-error) min(l - 0.1, 0.45) c h),
|
|
671
|
+
oklch(from var(--color-error) max(l + 0.1, 0.75) c h)
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
&.success {
|
|
675
|
+
--color-bg-active: rgb(from var(--color-success) r g b / 0.08);
|
|
676
|
+
--color-text: light-dark(
|
|
677
|
+
oklch(from var(--color-success) min(l, 0.5) c h),
|
|
678
|
+
oklch(from var(--color-success) max(l, 0.65) c h)
|
|
679
|
+
);
|
|
680
|
+
--color-text-disabled: light-dark(
|
|
681
|
+
oklch(from var(--color-success) min(l + 0.1, 0.6) calc(c - 0.05) h),
|
|
682
|
+
oklch(from var(--color-success) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
683
|
+
);
|
|
684
|
+
--color-text-active: light-dark(
|
|
685
|
+
oklch(from var(--color-success) min(l - 0.1, 0.4) c h),
|
|
686
|
+
oklch(from var(--color-success) max(l + 0.1, 0.75) c h)
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
&.transparent {
|
|
692
|
+
--color-bg: transparent;
|
|
693
|
+
--color-bg-disabled: transparent;
|
|
694
|
+
--color-bg-active: rgb(from var(--color-text) r g b / 0.06);
|
|
695
|
+
}
|
|
696
|
+
&.translucent {
|
|
697
|
+
backdrop-filter: blur(10px);
|
|
698
|
+
--color-bg: rgb(from var(--color-text) r g b / 0.09);
|
|
699
|
+
--color-bg-disabled: rgb(from var(--color-text) r g b / 0.04);
|
|
700
|
+
--color-bg-active: rgb(from var(--color-text) r g b / 0.15);
|
|
701
|
+
--button-border: none;
|
|
702
|
+
--button-border-disabled: none;
|
|
703
|
+
--button-border-active: none;
|
|
704
|
+
}
|
|
705
|
+
&.transparent,
|
|
706
|
+
&.translucent {
|
|
707
|
+
/* Plain (non-accent/error/success) transparent + translucent buttons
|
|
708
|
+
don't set their own --color-text, so the global --color-text-disabled
|
|
709
|
+
— a currentColor-relative dim — resolves against the *inherited* color.
|
|
710
|
+
Nested in a muted container (e.g. Breadcrumbs' trail) that compounds and
|
|
711
|
+
washes the disabled label into the background. Derive it from the
|
|
712
|
+
button's own full-contrast --color-text instead; the accent/error/success
|
|
713
|
+
variants below override with their own disabled tokens. */
|
|
714
|
+
--color-text: light-dark(
|
|
715
|
+
oklch(from var(--color-action) min(l, 0.5) c h),
|
|
716
|
+
oklch(from var(--color-action) max(l, 0.65) c h)
|
|
717
|
+
);
|
|
718
|
+
--color-text-disabled: light-dark(
|
|
719
|
+
oklch(from var(--color-action) min(l + 0.1, 0.6) calc(c - 0.05) h),
|
|
720
|
+
oklch(from var(--color-action) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
721
|
+
);
|
|
722
|
+
--color-text-active: light-dark(
|
|
723
|
+
oklch(from var(--color-action) min(l - 0.2, 0.3) c h),
|
|
724
|
+
oklch(from var(--color-action) max(l + 0.1, 0.75) c h)
|
|
725
|
+
);
|
|
726
|
+
&.accent {
|
|
727
|
+
--color-text: light-dark(
|
|
728
|
+
oklch(from var(--color-accent) min(l, 0.5) c h),
|
|
729
|
+
oklch(from var(--color-accent) max(l, 0.65) c h)
|
|
730
|
+
);
|
|
731
|
+
--color-text-disabled: light-dark(
|
|
732
|
+
oklch(from var(--color-accent) min(l + 0.1, 0.6) calc(c - 0.05) h),
|
|
733
|
+
oklch(from var(--color-accent) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
734
|
+
);
|
|
735
|
+
--color-text-active: light-dark(
|
|
736
|
+
oklch(from var(--color-accent) min(l - 0.1, 0.4) c h),
|
|
737
|
+
oklch(from var(--color-accent) max(l + 0.1, 0.76) c h)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
&.error {
|
|
741
|
+
--color-text: light-dark(
|
|
742
|
+
oklch(from var(--color-error) min(l, 0.55) c h),
|
|
743
|
+
oklch(from var(--color-error) max(l, 0.65) c h)
|
|
744
|
+
);
|
|
745
|
+
--color-text-disabled: light-dark(
|
|
746
|
+
oklch(from var(--color-error) min(l + 0.1, 0.65) calc(c - 0.05) h),
|
|
747
|
+
oklch(from var(--color-error) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
748
|
+
);
|
|
749
|
+
--color-text-active: light-dark(
|
|
750
|
+
oklch(from var(--color-error) min(l - 0.1, 0.45) c h),
|
|
751
|
+
oklch(from var(--color-error) max(l + 0.1, 0.75) c h)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
&.success {
|
|
755
|
+
--color-text: light-dark(
|
|
756
|
+
oklch(from var(--color-success) min(l, 0.5) c h),
|
|
757
|
+
oklch(from var(--color-success) max(l, 0.65) c h)
|
|
758
|
+
);
|
|
759
|
+
--color-text-disabled: light-dark(
|
|
760
|
+
oklch(from var(--color-success) min(l + 0.1, 0.6) calc(c - 0.05) h),
|
|
761
|
+
oklch(from var(--color-success) max(l - 0.1, 0.55) calc(c - 0.05) h)
|
|
762
|
+
);
|
|
763
|
+
--color-text-active: light-dark(
|
|
764
|
+
oklch(from var(--color-success) min(l - 0.1, 0.4) c h),
|
|
765
|
+
oklch(from var(--color-success) max(l + 0.1, 0.75) c h)
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
&.is-loading {
|
|
770
|
+
cursor: not-allowed;
|
|
771
|
+
a,
|
|
772
|
+
button {
|
|
773
|
+
pointer-events: none;
|
|
774
|
+
/* A loading button is "busy", not "disabled". It still carries the
|
|
775
|
+
disabled attribute (to block clicks/keyboard activation), but it
|
|
776
|
+
must stay fully legible — so restore the resting colors instead
|
|
777
|
+
of the muted disabled treatment. --color-text-disabled is a
|
|
778
|
+
currentColor-relative dim; when a transparent button inherits an
|
|
779
|
+
already-muted color (e.g. Breadcrumbs' muted trail) that dim
|
|
780
|
+
compounds and washes the label almost into the background. */
|
|
781
|
+
&:disabled,
|
|
782
|
+
&[aria-disabled='true'] {
|
|
783
|
+
background-color: var(--color-bg);
|
|
784
|
+
color: var(--color-text);
|
|
785
|
+
border: var(--button-border);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
&.active {
|
|
790
|
+
--color-bg: var(--color-bg-active) !important;
|
|
791
|
+
--color-text: var(--color-text-active) !important;
|
|
792
|
+
}
|
|
793
|
+
&.full-width {
|
|
794
|
+
width: 100%;
|
|
795
|
+
a,
|
|
796
|
+
button:not(.dropdown-trigger) {
|
|
797
|
+
width: 100%;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
&.full-height {
|
|
801
|
+
height: 100%;
|
|
802
|
+
a,
|
|
803
|
+
button {
|
|
804
|
+
height: 100%;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.badge {
|
|
809
|
+
position: absolute;
|
|
810
|
+
top: -0.2em;
|
|
811
|
+
right: -0.2em;
|
|
812
|
+
display: flex;
|
|
813
|
+
align-items: center;
|
|
814
|
+
justify-content: center;
|
|
815
|
+
background-color: var(--color-bg, var(--color-action));
|
|
816
|
+
color: var(--color-text, var(--color-action-text));
|
|
817
|
+
border-radius: var(--radius-full);
|
|
818
|
+
font-size: 0.8em;
|
|
819
|
+
line-height: 0.8em;
|
|
820
|
+
padding: 0.1em 0.5em;
|
|
821
|
+
min-width: 1.5em;
|
|
822
|
+
min-height: 1.5em;
|
|
823
|
+
pointer-events: none;
|
|
824
|
+
z-index: 1;
|
|
825
|
+
&.dot {
|
|
826
|
+
width: 0.75rem;
|
|
827
|
+
height: 0.75rem;
|
|
828
|
+
min-width: 0.75rem;
|
|
829
|
+
min-height: 0.75rem;
|
|
830
|
+
top: -0.1em;
|
|
831
|
+
right: -0.1em;
|
|
832
|
+
padding: 0;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
&.transparent,
|
|
836
|
+
&.translucent {
|
|
837
|
+
.badge {
|
|
838
|
+
background-color: var(--color-action);
|
|
839
|
+
color: var(--color-action-text);
|
|
840
|
+
}
|
|
841
|
+
&.accent {
|
|
842
|
+
.badge {
|
|
843
|
+
background-color: var(--color-accent);
|
|
844
|
+
color: var(--color-accent-text);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
&.error {
|
|
848
|
+
.badge {
|
|
849
|
+
background-color: var(--color-error);
|
|
850
|
+
color: var(--color-error-text);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
&.success {
|
|
854
|
+
.badge {
|
|
855
|
+
background-color: var(--color-success);
|
|
856
|
+
color: var(--color-success-text);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
&.icon {
|
|
861
|
+
.badge {
|
|
862
|
+
/* Scale the number badge down relative to the (small) icon
|
|
863
|
+
button and pull it in to hug the circle's top-right edge,
|
|
864
|
+
instead of floating off the bounding-box corner. */
|
|
865
|
+
font-size: 0.8em;
|
|
866
|
+
top: -0.25em;
|
|
867
|
+
right: -0.5em;
|
|
868
|
+
&.dot {
|
|
869
|
+
top: 0;
|
|
870
|
+
right: 0;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
&.transparent,
|
|
874
|
+
&.translucent {
|
|
875
|
+
.badge {
|
|
876
|
+
top: 0.5em;
|
|
877
|
+
right: 0.5em;
|
|
878
|
+
|
|
879
|
+
&.dot {
|
|
880
|
+
top: 0.75em;
|
|
881
|
+
right: 0.75em;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/* Badge cutout: mask the inner button so the badge sits in a clean gap */
|
|
888
|
+
&:not(.icon):has(> .badge:not(.dot)) {
|
|
889
|
+
button,
|
|
890
|
+
a {
|
|
891
|
+
mask-image: radial-gradient(
|
|
892
|
+
circle at calc(100% - 0.5em) 0.4em,
|
|
893
|
+
transparent calc(0.65em + 3px),
|
|
894
|
+
black calc(0.65em + 3.5px)
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
&:not(.icon):has(> .badge.dot) {
|
|
899
|
+
button,
|
|
900
|
+
a {
|
|
901
|
+
mask-image: radial-gradient(
|
|
902
|
+
circle at calc(100% - 0.3rem) 0.3rem,
|
|
903
|
+
transparent calc(0.375rem + 3px),
|
|
904
|
+
black calc(0.375rem + 3.55px)
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/* Icon button badge cutouts */
|
|
909
|
+
&.icon:has(> .badge:not(.dot)) {
|
|
910
|
+
button,
|
|
911
|
+
a {
|
|
912
|
+
mask-image: radial-gradient(
|
|
913
|
+
circle at calc(100% - 0.3em) 0.4em,
|
|
914
|
+
transparent 0.75em,
|
|
915
|
+
black calc(0.75em + 0.5px)
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
&.icon:has(> .badge.dot) {
|
|
920
|
+
button,
|
|
921
|
+
a {
|
|
922
|
+
mask-image: radial-gradient(
|
|
923
|
+
circle at calc(100% - 0.375rem) 0.375rem,
|
|
924
|
+
transparent calc(0.375rem + 3px),
|
|
925
|
+
black calc(0.375rem + 3.5px)
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
&.icon.transparent:has(> .badge:not(.dot)),
|
|
930
|
+
&.icon.translucent:has(> .badge:not(.dot)) {
|
|
931
|
+
button,
|
|
932
|
+
a {
|
|
933
|
+
mask-image: radial-gradient(
|
|
934
|
+
circle at calc(100% - 0.75em) 0.75em,
|
|
935
|
+
transparent 0.5em,
|
|
936
|
+
black calc(0.5em + 0.5px)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
&.icon.transparent:has(> .badge.dot),
|
|
941
|
+
&.icon.translucent:has(> .badge.dot) {
|
|
942
|
+
button,
|
|
943
|
+
a {
|
|
944
|
+
mask-image: radial-gradient(
|
|
945
|
+
circle at calc(100% - 0.75em - 0.375rem) calc(0.75em + 0.375rem),
|
|
946
|
+
transparent calc(0.375rem + 3px),
|
|
947
|
+
black calc(0.375rem + 3.5px)
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
button,
|
|
953
|
+
a {
|
|
954
|
+
display: flex;
|
|
955
|
+
align-items: center;
|
|
956
|
+
justify-content: center;
|
|
957
|
+
cursor: pointer;
|
|
958
|
+
position: relative;
|
|
959
|
+
overflow: hidden;
|
|
960
|
+
outline: none;
|
|
961
|
+
border: var(--button-border);
|
|
962
|
+
text-align: center;
|
|
963
|
+
text-decoration: none;
|
|
964
|
+
width: fit-content;
|
|
965
|
+
border-radius: var(--_radius);
|
|
966
|
+
@supports (corner-shape: squircle) {
|
|
967
|
+
corner-shape: var(--_corner-shape);
|
|
968
|
+
border-radius: calc(var(--_radius) * var(--_corner-scale));
|
|
969
|
+
}
|
|
970
|
+
background-color: var(--color-bg);
|
|
971
|
+
color: var(--color-text);
|
|
972
|
+
cursor: pointer;
|
|
973
|
+
padding: 0.75em 1.5em;
|
|
974
|
+
/* Set an explicit line-height so the label centers symmetrically
|
|
975
|
+
* regardless of the host page's inherited line-height. Inheriting a
|
|
976
|
+
* loose prose line-height (e.g. 1.75) leaves fractional half-leading
|
|
977
|
+
* that rounds unevenly at small font sizes, pushing the text upward. */
|
|
978
|
+
line-height: normal;
|
|
979
|
+
transition:
|
|
980
|
+
background-color 300ms,
|
|
981
|
+
color 300ms,
|
|
982
|
+
box-shadow 300ms ease,
|
|
983
|
+
translate 200ms ease;
|
|
984
|
+
box-shadow: inset 0px 0px 0px 0px var(--color-text);
|
|
985
|
+
gap: 0.5em;
|
|
986
|
+
|
|
987
|
+
&:focus-visible:not(:disabled):not([aria-disabled='true']) {
|
|
988
|
+
box-shadow: inset 0px 0px 0px 2px var(--color-text);
|
|
989
|
+
outline: solid 2px var(--color-bg);
|
|
990
|
+
}
|
|
991
|
+
&:disabled,
|
|
992
|
+
&[aria-disabled='true'] {
|
|
993
|
+
background-color: var(--color-bg-disabled);
|
|
994
|
+
color: var(--color-text-disabled);
|
|
995
|
+
cursor: not-allowed;
|
|
996
|
+
border: var(--button-border-disabled);
|
|
997
|
+
}
|
|
998
|
+
/* While loading the button is "busy" and shouldn't react to pointer
|
|
999
|
+
interaction — gate :hover/:active on :not([aria-busy='true']) so it
|
|
1000
|
+
behaves like a disabled control (aria-busy is set whenever isLoading,
|
|
1001
|
+
covering anchors and prop/form-driven loading that aren't :disabled). */
|
|
1002
|
+
&:hover:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']) {
|
|
1003
|
+
background-color: var(--color-bg-active);
|
|
1004
|
+
color: var(--color-text-active);
|
|
1005
|
+
border: var(--button-border-active);
|
|
1006
|
+
text-decoration: none;
|
|
1007
|
+
transition: translate 200ms ease;
|
|
1008
|
+
}
|
|
1009
|
+
&:active:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']) {
|
|
1010
|
+
translate: 0px 1px clamp(-10px, calc(0.2em - 12px), -2px);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/* Shared control height: a standalone (non-icon) button snaps to the
|
|
1015
|
+
same height as Input/Select for a given size (see --control-height-*
|
|
1016
|
+
in tokens.css), so a Button lines up in a form row. The floor is
|
|
1017
|
+
em-based, so an explicitly sized button scales up too. The
|
|
1018
|
+
dropdown-trigger is excluded — it stretches to match its sibling.
|
|
1019
|
+
Icon buttons keep their 4em square (and so do the font-scaled
|
|
1020
|
+
buttons embedded inside Input/Select). */
|
|
1021
|
+
&:not(.icon) {
|
|
1022
|
+
button:not(.dropdown-trigger),
|
|
1023
|
+
a {
|
|
1024
|
+
box-sizing: border-box;
|
|
1025
|
+
min-height: calc(1em * var(--control-height-ratio, 3));
|
|
1026
|
+
}
|
|
1027
|
+
&.dense {
|
|
1028
|
+
button:not(.dropdown-trigger),
|
|
1029
|
+
a {
|
|
1030
|
+
min-height: calc(1em * var(--control-height-ratio-dense, 2.5));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
&.comfortable {
|
|
1034
|
+
button:not(.dropdown-trigger),
|
|
1035
|
+
a {
|
|
1036
|
+
min-height: calc(1em * var(--control-height-ratio-comfortable, 3.5));
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.loading-icon {
|
|
1042
|
+
position: relative;
|
|
1043
|
+
display: flex;
|
|
1044
|
+
justify-content: center;
|
|
1045
|
+
align-items: center;
|
|
1046
|
+
width: 1.5em;
|
|
1047
|
+
margin-left: -0.75em;
|
|
1048
|
+
margin-right: -0.25em;
|
|
1049
|
+
height: 100%;
|
|
1050
|
+
flex-shrink: 0;
|
|
1051
|
+
flex-grow: 0;
|
|
1052
|
+
:global(.logo) {
|
|
1053
|
+
display: block;
|
|
1054
|
+
width: 100%;
|
|
1055
|
+
height: auto;
|
|
1056
|
+
aspect-ratio: 1;
|
|
1057
|
+
flex-shrink: 0;
|
|
1058
|
+
flex-grow: 0;
|
|
1059
|
+
}
|
|
1060
|
+
:global(circle.track) {
|
|
1061
|
+
stroke: rgb(from currentColor r g b / 0.2);
|
|
1062
|
+
}
|
|
1063
|
+
/* Spinner and success check occupy the same spot so they can
|
|
1064
|
+
crossfade during the handoff without nudging the label. */
|
|
1065
|
+
.icon-layer {
|
|
1066
|
+
position: absolute;
|
|
1067
|
+
inset: 0;
|
|
1068
|
+
display: flex;
|
|
1069
|
+
align-items: center;
|
|
1070
|
+
justify-content: center;
|
|
1071
|
+
}
|
|
1072
|
+
.check {
|
|
1073
|
+
display: block;
|
|
1074
|
+
/* The tick only spans the middle of its viewBox, so at 1rem it read
|
|
1075
|
+
much smaller than the spinner ring. Fill the slot (paired with the
|
|
1076
|
+
tightened viewBox above) so it's sized like the spinner. The slot
|
|
1077
|
+
width is fixed and the layer is absolutely positioned, so this
|
|
1078
|
+
never shifts layout. */
|
|
1079
|
+
width: 1.25em;
|
|
1080
|
+
height: 1.25em;
|
|
1081
|
+
}
|
|
1082
|
+
.check path {
|
|
1083
|
+
/* Dash length >= the tick's path length; checkIn() rides
|
|
1084
|
+
--check-draw from 24 (hidden) down to 0 (fully drawn). The 0
|
|
1085
|
+
fallback keeps it drawn once the transition's inline style is
|
|
1086
|
+
gone. */
|
|
1087
|
+
stroke-dasharray: 24;
|
|
1088
|
+
stroke-dashoffset: var(--check-draw, 0);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
:global(.chevron) {
|
|
1093
|
+
display: flex;
|
|
1094
|
+
align-items: center;
|
|
1095
|
+
justify-content: center;
|
|
1096
|
+
pointer-events: none;
|
|
1097
|
+
transform: rotate(0);
|
|
1098
|
+
transition: transform 300ms var(--easing);
|
|
1099
|
+
}
|
|
1100
|
+
:global(.chevron.active) {
|
|
1101
|
+
transform: rotate(180deg);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
&.has-dropdown-trigger {
|
|
1105
|
+
> button:not(.dropdown-trigger),
|
|
1106
|
+
> a {
|
|
1107
|
+
border-top-right-radius: 0;
|
|
1108
|
+
border-bottom-right-radius: 0;
|
|
1109
|
+
padding-right: 0.75em;
|
|
1110
|
+
}
|
|
1111
|
+
> button,
|
|
1112
|
+
> a {
|
|
1113
|
+
&:first-child {
|
|
1114
|
+
border-right: none !important;
|
|
1115
|
+
}
|
|
1116
|
+
&:last-child {
|
|
1117
|
+
border-left: none !important;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
.dropdown-trigger {
|
|
1122
|
+
border-top-left-radius: 0;
|
|
1123
|
+
border-bottom-left-radius: 0;
|
|
1124
|
+
border-top-right-radius: var(--_radius);
|
|
1125
|
+
border-bottom-right-radius: var(--_radius);
|
|
1126
|
+
@supports (corner-shape: squircle) {
|
|
1127
|
+
corner-shape: var(--_corner-shape);
|
|
1128
|
+
border-top-right-radius: calc(var(--_radius) * var(--_corner-scale));
|
|
1129
|
+
border-bottom-right-radius: calc(var(--_radius) * var(--_corner-scale));
|
|
1130
|
+
}
|
|
1131
|
+
display: flex;
|
|
1132
|
+
align-items: center;
|
|
1133
|
+
padding: 0 0.5em 0 0.5em;
|
|
1134
|
+
&::before {
|
|
1135
|
+
content: '';
|
|
1136
|
+
height: 1em;
|
|
1137
|
+
margin: 0 -0.25em 0 -0.5em;
|
|
1138
|
+
padding: 0;
|
|
1139
|
+
background-color: var(--color-text);
|
|
1140
|
+
width: 1px;
|
|
1141
|
+
opacity: 0.2;
|
|
1142
|
+
}
|
|
1143
|
+
:global(svg) {
|
|
1144
|
+
width: 1.5em;
|
|
1145
|
+
height: 1.5em;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
&.dense {
|
|
1150
|
+
button:not(.dropdown-trigger),
|
|
1151
|
+
a {
|
|
1152
|
+
line-height: 1em;
|
|
1153
|
+
padding: 0.5em 1em;
|
|
1154
|
+
gap: 0.3em;
|
|
1155
|
+
}
|
|
1156
|
+
&.has-dropdown-trigger {
|
|
1157
|
+
> button:not(.dropdown-trigger),
|
|
1158
|
+
> a {
|
|
1159
|
+
padding: 0.5em 1em 0.5em 1.1em;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
:global(.chevron) {
|
|
1163
|
+
margin: 0 -0.2em;
|
|
1164
|
+
}
|
|
1165
|
+
.loading-icon {
|
|
1166
|
+
margin-left: -0.5em;
|
|
1167
|
+
margin-right: -0.15em;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
&.comfortable {
|
|
1172
|
+
button:not(.dropdown-trigger),
|
|
1173
|
+
a {
|
|
1174
|
+
padding: 1em 2em;
|
|
1175
|
+
gap: 0.65em;
|
|
1176
|
+
}
|
|
1177
|
+
&.has-dropdown-trigger {
|
|
1178
|
+
> button:not(.dropdown-trigger),
|
|
1179
|
+
> a {
|
|
1180
|
+
padding: 1em 2em 1em 1.5em;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
&.icon {
|
|
1186
|
+
/* A standalone icon button is a control-height square so it lines up
|
|
1187
|
+
in a row with text controls (Input/Select/Button). It scales with
|
|
1188
|
+
the button's font, so a sized icon button grows too. Icon buttons
|
|
1189
|
+
embedded in a field (.input-icon-btn / .input-pill-btn) pin their
|
|
1190
|
+
own size and are unaffected. */
|
|
1191
|
+
--_icon-size: calc(1em * var(--control-height-ratio, 3));
|
|
1192
|
+
height: var(--_icon-size);
|
|
1193
|
+
width: var(--_icon-size);
|
|
1194
|
+
aspect-ratio: 1 / 1;
|
|
1195
|
+
/* Make the icon button a real circle by setting --_radius on the
|
|
1196
|
+
inner button/a (which owns the background, border, ripple AND the
|
|
1197
|
+
:active translate). Don't clip the circle from this wrapper with
|
|
1198
|
+
overflow:hidden — that fakes the shape (so the square inner radius
|
|
1199
|
+
shows through on :active and the outline border gets clipped) and
|
|
1200
|
+
crops the badge that hangs off the corner. The inner element's own
|
|
1201
|
+
overflow:hidden still clips the ripple to the circle. */
|
|
1202
|
+
--_radius: var(--radius-full);
|
|
1203
|
+
--_corner-shape: round;
|
|
1204
|
+
--_corner-scale: 1;
|
|
1205
|
+
&.dense {
|
|
1206
|
+
--_icon-size: calc(1em * var(--control-height-ratio-dense, 2.5));
|
|
1207
|
+
--_feedback-size: 60%;
|
|
1208
|
+
button,
|
|
1209
|
+
a {
|
|
1210
|
+
padding: 0;
|
|
1211
|
+
:global(> svg),
|
|
1212
|
+
:global(> img) {
|
|
1213
|
+
width: 60%;
|
|
1214
|
+
height: 60%;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
&.comfortable {
|
|
1219
|
+
--_icon-size: calc(1em * var(--control-height-ratio-comfortable, 3.5));
|
|
1220
|
+
}
|
|
1221
|
+
button,
|
|
1222
|
+
a {
|
|
1223
|
+
align-items: center;
|
|
1224
|
+
justify-content: center;
|
|
1225
|
+
aspect-ratio: 1 / 1;
|
|
1226
|
+
padding: 0;
|
|
1227
|
+
width: 100%;
|
|
1228
|
+
height: 100%;
|
|
1229
|
+
:global(svg) {
|
|
1230
|
+
width: 50%;
|
|
1231
|
+
height: 50%;
|
|
1232
|
+
}
|
|
1233
|
+
/* The icon eases away/back as the loading/success feedback overlay
|
|
1234
|
+
crossfades over it. */
|
|
1235
|
+
:global(> svg),
|
|
1236
|
+
:global(> img) {
|
|
1237
|
+
transition:
|
|
1238
|
+
opacity 150ms ease,
|
|
1239
|
+
scale 200ms var(--ease-spring, ease);
|
|
1240
|
+
}
|
|
1241
|
+
.loading-icon ~ :global(svg),
|
|
1242
|
+
.loading-icon ~ :global(img) {
|
|
1243
|
+
opacity: 0;
|
|
1244
|
+
scale: 0.5;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
/* Loading/success feedback covers the whole square button instead of
|
|
1248
|
+
the width-growing slot used beside a label. Sized to match the icon
|
|
1249
|
+
it replaces (50%, 60% when dense). */
|
|
1250
|
+
--_feedback-size: 50%;
|
|
1251
|
+
.loading-icon {
|
|
1252
|
+
position: absolute;
|
|
1253
|
+
inset: 0;
|
|
1254
|
+
width: auto;
|
|
1255
|
+
margin: 0;
|
|
1256
|
+
:global(.progress) {
|
|
1257
|
+
width: var(--_feedback-size);
|
|
1258
|
+
height: var(--_feedback-size);
|
|
1259
|
+
}
|
|
1260
|
+
:global(.progress svg) {
|
|
1261
|
+
width: 100%;
|
|
1262
|
+
height: 100%;
|
|
1263
|
+
}
|
|
1264
|
+
.check {
|
|
1265
|
+
width: var(--_feedback-size);
|
|
1266
|
+
height: var(--_feedback-size);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/* Grouped attached: border-radius adjustments and border merging */
|
|
1271
|
+
&.group-h,
|
|
1272
|
+
&.group-v {
|
|
1273
|
+
&:hover,
|
|
1274
|
+
&:focus-within {
|
|
1275
|
+
z-index: 1;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
&.group-h {
|
|
1279
|
+
&:not(:first-child):not(:last-child) {
|
|
1280
|
+
border-radius: 0;
|
|
1281
|
+
button,
|
|
1282
|
+
a {
|
|
1283
|
+
border-radius: 0;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
&:first-child:not(:last-child) {
|
|
1287
|
+
border-top-right-radius: 0;
|
|
1288
|
+
border-bottom-right-radius: 0;
|
|
1289
|
+
button,
|
|
1290
|
+
a {
|
|
1291
|
+
border-top-right-radius: 0;
|
|
1292
|
+
border-bottom-right-radius: 0;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
&:last-child:not(:first-child) {
|
|
1296
|
+
border-top-left-radius: 0;
|
|
1297
|
+
border-bottom-left-radius: 0;
|
|
1298
|
+
button,
|
|
1299
|
+
a {
|
|
1300
|
+
border-top-left-radius: 0;
|
|
1301
|
+
border-bottom-left-radius: 0;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/* Remove right border on non-last so outline buttons share a single border */
|
|
1305
|
+
&:not(:last-child) {
|
|
1306
|
+
button,
|
|
1307
|
+
a {
|
|
1308
|
+
border-right: none;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
/* Icon-only ends: the pill curve crowds a centered icon at the
|
|
1312
|
+
rounded outer end. Widen the end button and pad its rounded side
|
|
1313
|
+
so the icon keeps its size and its spacing from the flat (inner)
|
|
1314
|
+
edge while gaining a little breathing room from the curve. */
|
|
1315
|
+
&.icon {
|
|
1316
|
+
--_group-end-pad: 0.5em;
|
|
1317
|
+
&:first-child:not(:last-child) {
|
|
1318
|
+
width: calc(var(--_icon-size) + var(--_group-end-pad));
|
|
1319
|
+
button,
|
|
1320
|
+
a {
|
|
1321
|
+
box-sizing: border-box;
|
|
1322
|
+
padding-left: var(--_group-end-pad);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
&:last-child:not(:first-child) {
|
|
1326
|
+
width: calc(var(--_icon-size) + var(--_group-end-pad));
|
|
1327
|
+
button,
|
|
1328
|
+
a {
|
|
1329
|
+
box-sizing: border-box;
|
|
1330
|
+
padding-right: var(--_group-end-pad);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
& + :global(.button) {
|
|
1335
|
+
margin-left: -1px;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
&.group-v {
|
|
1339
|
+
width: auto;
|
|
1340
|
+
button,
|
|
1341
|
+
a {
|
|
1342
|
+
width: 100%;
|
|
1343
|
+
}
|
|
1344
|
+
&:not(:first-child):not(:last-child) {
|
|
1345
|
+
border-radius: 0;
|
|
1346
|
+
button,
|
|
1347
|
+
a {
|
|
1348
|
+
border-radius: 0;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
&:first-child:not(:last-child) {
|
|
1352
|
+
border-bottom-left-radius: 0;
|
|
1353
|
+
border-bottom-right-radius: 0;
|
|
1354
|
+
button,
|
|
1355
|
+
a {
|
|
1356
|
+
border-bottom-left-radius: 0;
|
|
1357
|
+
border-bottom-right-radius: 0;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
&:last-child:not(:first-child) {
|
|
1361
|
+
border-top-left-radius: 0;
|
|
1362
|
+
border-top-right-radius: 0;
|
|
1363
|
+
button,
|
|
1364
|
+
a {
|
|
1365
|
+
border-top-left-radius: 0;
|
|
1366
|
+
border-top-right-radius: 0;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/* Remove bottom border on non-last so outline buttons share a single border */
|
|
1370
|
+
&:not(:last-child) {
|
|
1371
|
+
button,
|
|
1372
|
+
a {
|
|
1373
|
+
border-bottom: none;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/* Icon-only ends: same breathing room as horizontal, but the rounded
|
|
1377
|
+
ends are top/bottom here, so pad the block axis. */
|
|
1378
|
+
&.icon {
|
|
1379
|
+
--_group-end-pad: 0.5em;
|
|
1380
|
+
&:first-child:not(:last-child) {
|
|
1381
|
+
height: calc(var(--_icon-size) + var(--_group-end-pad));
|
|
1382
|
+
button,
|
|
1383
|
+
a {
|
|
1384
|
+
box-sizing: border-box;
|
|
1385
|
+
padding-top: var(--_group-end-pad);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
&:last-child:not(:first-child) {
|
|
1389
|
+
height: calc(var(--_icon-size) + var(--_group-end-pad));
|
|
1390
|
+
button,
|
|
1391
|
+
a {
|
|
1392
|
+
box-sizing: border-box;
|
|
1393
|
+
padding-bottom: var(--_group-end-pad);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
& + :global(.button) {
|
|
1398
|
+
margin-top: -1px;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
&.overlay {
|
|
1403
|
+
--button-border: none;
|
|
1404
|
+
--button-border-disabled: none;
|
|
1405
|
+
--button-border-active: none;
|
|
1406
|
+
&.active {
|
|
1407
|
+
button,
|
|
1408
|
+
a {
|
|
1409
|
+
color: rgba(255, 255, 255, 1);
|
|
1410
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
1411
|
+
@supports (backdrop-filter: blur(10px)) {
|
|
1412
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
button,
|
|
1417
|
+
a {
|
|
1418
|
+
color: rgba(255, 255, 255, 0.85);
|
|
1419
|
+
backdrop-filter: blur(10px);
|
|
1420
|
+
background-color: rgba(0, 0, 0, 0.85);
|
|
1421
|
+
@supports (backdrop-filter: blur(10px)) {
|
|
1422
|
+
background-color: rgba(0, 0, 0, 0.65);
|
|
1423
|
+
}
|
|
1424
|
+
&:disabled,
|
|
1425
|
+
&[aria-disabled='true'] {
|
|
1426
|
+
color: rgba(255, 255, 255, 0.65);
|
|
1427
|
+
}
|
|
1428
|
+
&:focus-visible:not(:disabled):not([aria-disabled='true']) {
|
|
1429
|
+
box-shadow: none;
|
|
1430
|
+
outline: solid 2px white;
|
|
1431
|
+
outline-offset: 1px;
|
|
1432
|
+
}
|
|
1433
|
+
&:hover:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']) {
|
|
1434
|
+
transition: none;
|
|
1435
|
+
}
|
|
1436
|
+
&:hover:not(:disabled):not([aria-disabled='true']):not([aria-busy='true']),
|
|
1437
|
+
&:focus-visible:not(:disabled):not([aria-disabled='true']) {
|
|
1438
|
+
color: rgba(255, 255, 255, 1);
|
|
1439
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
1440
|
+
@supports (backdrop-filter: blur(10px)) {
|
|
1441
|
+
background-color: rgba(0, 0, 0, 0.75);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
&::before {
|
|
1445
|
+
background-color: rgba(0, 0, 0, 1);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
</style>
|