@kwtv/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.
Files changed (57) hide show
  1. package/README.md +77 -0
  2. package/package.json +32 -0
  3. package/src/astro/Accordion.astro +25 -0
  4. package/src/astro/AccordionItem.astro +77 -0
  5. package/src/astro/ActiveFilterTags.astro +65 -0
  6. package/src/astro/Alert.astro +93 -0
  7. package/src/astro/Badge.astro +138 -0
  8. package/src/astro/Band.astro +83 -0
  9. package/src/astro/Breadcrumb.astro +74 -0
  10. package/src/astro/Button.astro +217 -0
  11. package/src/astro/Card.astro +115 -0
  12. package/src/astro/CartLine.astro +112 -0
  13. package/src/astro/Checkbox.astro +89 -0
  14. package/src/astro/Chip.astro +130 -0
  15. package/src/astro/ChipRail.astro +24 -0
  16. package/src/astro/DotGrid.astro +62 -0
  17. package/src/astro/EmptyState.astro +53 -0
  18. package/src/astro/FilterGroup.astro +58 -0
  19. package/src/astro/Filters.astro +84 -0
  20. package/src/astro/Footer.astro +237 -0
  21. package/src/astro/FooterColumn.astro +64 -0
  22. package/src/astro/Header.astro +116 -0
  23. package/src/astro/Icon.astro +72 -0
  24. package/src/astro/IconButton.astro +117 -0
  25. package/src/astro/Marquee.astro +72 -0
  26. package/src/astro/NavDropdown.astro +164 -0
  27. package/src/astro/NavLink.astro +94 -0
  28. package/src/astro/Newsletter.astro +118 -0
  29. package/src/astro/Pager.astro +195 -0
  30. package/src/astro/PickCard.astro +142 -0
  31. package/src/astro/ProductCard.astro +195 -0
  32. package/src/astro/Radio.astro +89 -0
  33. package/src/astro/Skeleton.astro +80 -0
  34. package/src/astro/Social.astro +55 -0
  35. package/src/astro/StepIndicator.astro +104 -0
  36. package/src/astro/Sticker.astro +80 -0
  37. package/src/astro/Swatch.astro +100 -0
  38. package/src/astro/SwipeCarousel.astro +14 -0
  39. package/src/astro/Table.astro +117 -0
  40. package/src/astro/TableCard.astro +63 -0
  41. package/src/astro/TextField.astro +231 -0
  42. package/src/astro/Totals.astro +92 -0
  43. package/src/react/Drawer.css +67 -0
  44. package/src/react/Drawer.tsx +115 -0
  45. package/src/react/Modal.css +116 -0
  46. package/src/react/Modal.tsx +152 -0
  47. package/src/react/NewsCarousel.css +86 -0
  48. package/src/react/NewsCarousel.tsx +97 -0
  49. package/src/react/QuantityStepper.css +93 -0
  50. package/src/react/QuantityStepper.tsx +143 -0
  51. package/src/react/Select.css +183 -0
  52. package/src/react/Select.tsx +389 -0
  53. package/src/react/Toast.css +140 -0
  54. package/src/react/Toast.tsx +157 -0
  55. package/src/styles/base.css +39 -0
  56. package/src/styles/controls.css +211 -0
  57. package/src/styles/motion.css +238 -0
@@ -0,0 +1,183 @@
1
+ /* Select. Trigger plus listbox. All values from the design-system tokens. */
2
+
3
+ .ui-field {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: var(--space-1-5);
7
+ }
8
+
9
+ .ui-field__label {
10
+ font-size: var(--text-sm);
11
+ font-weight: var(--weight-medium);
12
+ color: var(--text);
13
+ }
14
+
15
+ .ui-field__req {
16
+ color: var(--danger);
17
+ margin-inline-start: var(--space-0-5);
18
+ }
19
+
20
+ .ui-field__hint {
21
+ font-size: var(--text-xs);
22
+ line-height: var(--leading-xs);
23
+ color: var(--text-muted);
24
+ }
25
+
26
+ .ui-field__error {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ gap: var(--space-1-5);
30
+ font-size: var(--text-xs);
31
+ line-height: var(--leading-xs);
32
+ color: var(--danger);
33
+ }
34
+
35
+ .ui-select {
36
+ position: relative;
37
+ display: block;
38
+ }
39
+
40
+ .ui-select__btn {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ gap: var(--space-2);
45
+ width: 100%;
46
+ min-height: 40px;
47
+ padding: 0 var(--space-3);
48
+ background: var(--surface);
49
+ border: 1px solid var(--border);
50
+ border-radius: var(--radius-md);
51
+ color: var(--text);
52
+ font-family: var(--font-sans);
53
+ font-size: var(--text-sm);
54
+ text-align: start;
55
+ cursor: pointer;
56
+ transition: border-color var(--duration-fast) var(--ease-standard);
57
+ }
58
+
59
+ .ui-select__btn:hover,
60
+ .ui-select__btn.is-hover {
61
+ border-color: var(--border-strong);
62
+ }
63
+
64
+ .ui-select__btn:focus-visible,
65
+ .ui-select__btn.is-focus {
66
+ outline: 2px solid var(--focus);
67
+ outline-offset: 2px;
68
+ box-shadow: 0 0 0 4px var(--focus-outer);
69
+ }
70
+
71
+ .ui-select__btn:disabled {
72
+ opacity: 0.5;
73
+ cursor: not-allowed;
74
+ }
75
+
76
+ .ui-select__value {
77
+ overflow: hidden;
78
+ text-overflow: ellipsis;
79
+ white-space: nowrap;
80
+ }
81
+
82
+ .ui-select__value--empty {
83
+ color: var(--text-muted);
84
+ }
85
+
86
+ .ui-select__chev {
87
+ display: inline-flex;
88
+ flex: none;
89
+ color: var(--text-muted);
90
+ }
91
+
92
+ /* Invalid: 2 px danger border. */
93
+ .ui-select--invalid .ui-select__btn {
94
+ border-width: 2px;
95
+ border-color: var(--danger);
96
+ padding-inline: calc(var(--space-3) - 1px);
97
+ }
98
+
99
+ /* The list. 6 px offset, 272 px max height, 36 px rows. */
100
+ .ui-select__list {
101
+ position: absolute;
102
+ top: calc(100% + var(--space-1-5));
103
+ left: 0;
104
+ right: 0;
105
+ z-index: var(--z-dropdown);
106
+ max-height: 272px;
107
+ overflow-y: auto;
108
+ padding: var(--space-1);
109
+ background: var(--surface);
110
+ border: 1px solid var(--border);
111
+ border-radius: var(--radius-md);
112
+ box-shadow: var(--shadow-lg);
113
+ }
114
+
115
+ .ui-select__opt {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: var(--space-2);
119
+ width: 100%;
120
+ min-height: 36px;
121
+ padding: 0 var(--space-2);
122
+ background: transparent;
123
+ border: 0;
124
+ border-radius: var(--radius-sm);
125
+ color: var(--text);
126
+ font-family: var(--font-sans);
127
+ font-size: var(--text-sm);
128
+ text-align: start;
129
+ cursor: pointer;
130
+ }
131
+
132
+ .ui-select__opt:hover,
133
+ .ui-select__opt.is-hover,
134
+ .ui-select__opt.is-focus {
135
+ background: var(--surface-hover);
136
+ }
137
+
138
+ /* Selection marked by the tick and a weight change, not the fill alone. */
139
+ .ui-select__opt[aria-selected="true"] {
140
+ font-weight: var(--weight-semibold);
141
+ }
142
+
143
+ .ui-select__tick {
144
+ display: inline-flex;
145
+ flex: none;
146
+ width: 13px;
147
+ color: var(--primary);
148
+ visibility: hidden;
149
+ }
150
+
151
+ .ui-select__opt[aria-selected="true"] .ui-select__tick {
152
+ visibility: visible;
153
+ }
154
+
155
+ .ui-select__opt.is-disabled,
156
+ .ui-select__opt[aria-disabled="true"] {
157
+ opacity: 0.5;
158
+ cursor: not-allowed;
159
+ }
160
+
161
+ /* Group headings. Not selectable. */
162
+ .ui-select__grouplabel {
163
+ display: block;
164
+ padding: var(--space-2) var(--space-2) var(--space-1);
165
+ font-family: var(--font-mono);
166
+ font-size: var(--text-2xs);
167
+ font-weight: var(--weight-semibold);
168
+ letter-spacing: var(--tracking-wider);
169
+ text-transform: uppercase;
170
+ color: var(--text-muted);
171
+ }
172
+
173
+ @media (max-width: 700px), (pointer: coarse) {
174
+ .ui-select__opt {
175
+ min-height: 40px;
176
+ }
177
+ }
178
+
179
+ @media (prefers-reduced-motion: reduce) {
180
+ .ui-select__btn {
181
+ transition: none;
182
+ }
183
+ }
@@ -0,0 +1,389 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useRef,
6
+ useState,
7
+ type KeyboardEvent,
8
+ } from "react";
9
+ import "./Select.css";
10
+
11
+ /**
12
+ * Select. A custom combobox built as a trigger plus a listbox, because the
13
+ * native popup cannot be styled and breaks the brand at the one moment the
14
+ * reader is choosing.
15
+ *
16
+ * Keyboard behaviour stays native: Enter or Space opens, arrows move the active
17
+ * option, Enter picks, Escape closes and returns focus to the trigger.
18
+ *
19
+ * Selection is marked by a tick and a weight change, never by the sage fill
20
+ * alone, so it reads without colour.
21
+ *
22
+ * Grouping: pass each option a `group` string. Options that share a group are
23
+ * rendered under one non-selectable `.ui-select__grouplabel`. Ungrouped options
24
+ * render first, in order. Disabled options stay in the list so the reader
25
+ * learns the group exists.
26
+ */
27
+
28
+ export interface SelectOption {
29
+ value: string;
30
+ label: string;
31
+ disabled?: boolean;
32
+ /** Optional group heading. Options sharing a group render under one label. */
33
+ group?: string;
34
+ }
35
+
36
+ export interface SelectProps {
37
+ /** Visible label text. Rendered above the field and wired by aria-labelledby. */
38
+ label?: string;
39
+ /** Accessible name when there is no visible label. */
40
+ ariaLabel?: string;
41
+ options: SelectOption[];
42
+ value?: string;
43
+ onChange?: (value: string) => void;
44
+ placeholder?: string;
45
+ invalid?: boolean;
46
+ /** Error text, shown under the field and wired by aria-describedby. */
47
+ error?: string;
48
+ disabled?: boolean;
49
+ required?: boolean;
50
+ /** Hint text under the field, when not invalid. */
51
+ hint?: string;
52
+ /** Start with the listbox open. Useful for a persistent first view or specimen. */
53
+ defaultOpen?: boolean;
54
+ className?: string;
55
+ }
56
+
57
+ interface FlatRow {
58
+ kind: "group" | "option";
59
+ label: string;
60
+ option?: SelectOption;
61
+ /** Index into the list of selectable options, for active tracking. */
62
+ optionIndex?: number;
63
+ }
64
+
65
+ function buildRows(options: SelectOption[]): {
66
+ rows: FlatRow[];
67
+ selectable: SelectOption[];
68
+ } {
69
+ const ungrouped = options.filter((o) => !o.group);
70
+ const groups: string[] = [];
71
+ for (const o of options) {
72
+ if (o.group && !groups.includes(o.group)) groups.push(o.group);
73
+ }
74
+
75
+ const rows: FlatRow[] = [];
76
+ const selectable: SelectOption[] = [];
77
+
78
+ const pushOption = (o: SelectOption) => {
79
+ const optionIndex = selectable.length;
80
+ selectable.push(o);
81
+ rows.push({ kind: "option", label: o.label, option: o, optionIndex });
82
+ };
83
+
84
+ ungrouped.forEach(pushOption);
85
+ for (const g of groups) {
86
+ rows.push({ kind: "group", label: g });
87
+ options.filter((o) => o.group === g).forEach(pushOption);
88
+ }
89
+
90
+ return { rows, selectable };
91
+ }
92
+
93
+ export default function Select({
94
+ label,
95
+ ariaLabel,
96
+ options,
97
+ value,
98
+ onChange,
99
+ placeholder = "Kies een optie",
100
+ invalid = false,
101
+ error,
102
+ disabled = false,
103
+ required = false,
104
+ hint,
105
+ defaultOpen = false,
106
+ className,
107
+ }: SelectProps) {
108
+ const [open, setOpen] = useState(defaultOpen);
109
+ const [activeIndex, setActiveIndex] = useState<number>(-1);
110
+
111
+ const rootRef = useRef<HTMLDivElement>(null);
112
+ const btnRef = useRef<HTMLButtonElement>(null);
113
+ const listRef = useRef<HTMLDivElement>(null);
114
+
115
+ const reactId = useId();
116
+ const labelId = label ? `${reactId}-label` : undefined;
117
+ const errorId = error ? `${reactId}-error` : undefined;
118
+ const hintId = hint && !error ? `${reactId}-hint` : undefined;
119
+
120
+ const { rows, selectable } = buildRows(options);
121
+ const selected = selectable.find((o) => o.value === value);
122
+
123
+ const close = useCallback((returnFocus = true) => {
124
+ setOpen(false);
125
+ setActiveIndex(-1);
126
+ if (returnFocus) btnRef.current?.focus();
127
+ }, []);
128
+
129
+ const openList = useCallback(() => {
130
+ if (disabled) return;
131
+ setOpen(true);
132
+ const current = selectable.findIndex((o) => o.value === value && !o.disabled);
133
+ const firstEnabled = selectable.findIndex((o) => !o.disabled);
134
+ setActiveIndex(current >= 0 ? current : firstEnabled);
135
+ }, [disabled, selectable, value]);
136
+
137
+ const pick = useCallback(
138
+ (option: SelectOption) => {
139
+ if (option.disabled) return;
140
+ onChange?.(option.value);
141
+ close();
142
+ },
143
+ [onChange, close],
144
+ );
145
+
146
+ // Close on outside click.
147
+ useEffect(() => {
148
+ if (!open) return;
149
+ const onDown = (e: MouseEvent) => {
150
+ if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
151
+ setOpen(false);
152
+ setActiveIndex(-1);
153
+ }
154
+ };
155
+ document.addEventListener("mousedown", onDown);
156
+ return () => document.removeEventListener("mousedown", onDown);
157
+ }, [open]);
158
+
159
+ // Keep the active option scrolled into view.
160
+ useEffect(() => {
161
+ if (!open || activeIndex < 0 || !listRef.current) return;
162
+ const el = listRef.current.querySelector<HTMLElement>(
163
+ `[data-opt-index="${activeIndex}"]`,
164
+ );
165
+ el?.scrollIntoView({ block: "nearest" });
166
+ }, [open, activeIndex]);
167
+
168
+ const moveActive = (dir: 1 | -1) => {
169
+ if (selectable.length === 0) return;
170
+ let i = activeIndex;
171
+ for (let step = 0; step < selectable.length; step++) {
172
+ i = (i + dir + selectable.length) % selectable.length;
173
+ if (!selectable[i].disabled) {
174
+ setActiveIndex(i);
175
+ return;
176
+ }
177
+ }
178
+ };
179
+
180
+ const onKeyDown = (e: KeyboardEvent) => {
181
+ switch (e.key) {
182
+ case "Enter":
183
+ case " ":
184
+ e.preventDefault();
185
+ if (!open) {
186
+ openList();
187
+ } else if (activeIndex >= 0) {
188
+ pick(selectable[activeIndex]);
189
+ }
190
+ break;
191
+ case "ArrowDown":
192
+ e.preventDefault();
193
+ if (!open) openList();
194
+ else moveActive(1);
195
+ break;
196
+ case "ArrowUp":
197
+ e.preventDefault();
198
+ if (!open) openList();
199
+ else moveActive(-1);
200
+ break;
201
+ case "Escape":
202
+ if (open) {
203
+ e.preventDefault();
204
+ close();
205
+ }
206
+ break;
207
+ case "Tab":
208
+ if (open) {
209
+ setOpen(false);
210
+ setActiveIndex(-1);
211
+ }
212
+ break;
213
+ case "Home":
214
+ if (open) {
215
+ e.preventDefault();
216
+ const first = selectable.findIndex((o) => !o.disabled);
217
+ if (first >= 0) setActiveIndex(first);
218
+ }
219
+ break;
220
+ case "End":
221
+ if (open) {
222
+ e.preventDefault();
223
+ for (let i = selectable.length - 1; i >= 0; i--) {
224
+ if (!selectable[i].disabled) {
225
+ setActiveIndex(i);
226
+ break;
227
+ }
228
+ }
229
+ }
230
+ break;
231
+ }
232
+ };
233
+
234
+ const activeId =
235
+ open && activeIndex >= 0 ? `${reactId}-opt-${activeIndex}` : undefined;
236
+
237
+ const describedBy = [errorId, hintId].filter(Boolean).join(" ") || undefined;
238
+
239
+ const fieldClasses = ["ui-select", invalid && "ui-select--invalid", open && "is-open"]
240
+ .filter(Boolean)
241
+ .join(" ");
242
+
243
+ const selectControl = (
244
+ <div className={fieldClasses} ref={rootRef}>
245
+ <button
246
+ ref={btnRef}
247
+ type="button"
248
+ className="ui-select__btn"
249
+ role="combobox"
250
+ aria-haspopup="listbox"
251
+ aria-expanded={open}
252
+ aria-controls={`${reactId}-list`}
253
+ aria-activedescendant={activeId}
254
+ aria-labelledby={labelId}
255
+ aria-label={!labelId ? ariaLabel : undefined}
256
+ aria-invalid={invalid || undefined}
257
+ aria-describedby={describedBy}
258
+ aria-required={required || undefined}
259
+ disabled={disabled}
260
+ onClick={() => (open ? close(false) : openList())}
261
+ onKeyDown={onKeyDown}
262
+ >
263
+ <span
264
+ className={`ui-select__value${selected ? "" : " ui-select__value--empty"}`}
265
+ >
266
+ {selected ? selected.label : placeholder}
267
+ </span>
268
+ <span className="ui-select__chev" aria-hidden="true">
269
+ <svg
270
+ width="14"
271
+ height="14"
272
+ viewBox="0 0 24 24"
273
+ fill="none"
274
+ stroke="currentColor"
275
+ strokeWidth="2.2"
276
+ strokeLinecap="round"
277
+ strokeLinejoin="round"
278
+ >
279
+ <path d="M6 9l6 6 6-6" />
280
+ </svg>
281
+ </span>
282
+ </button>
283
+ {open && (
284
+ <div
285
+ ref={listRef}
286
+ id={`${reactId}-list`}
287
+ className="ui-select__list"
288
+ role="listbox"
289
+ aria-labelledby={labelId}
290
+ aria-label={!labelId ? ariaLabel : undefined}
291
+ tabIndex={-1}
292
+ >
293
+ {rows.map((row, i) =>
294
+ row.kind === "group" ? (
295
+ <span
296
+ key={`g-${i}`}
297
+ className="ui-select__grouplabel"
298
+ role="presentation"
299
+ >
300
+ {row.label}
301
+ </span>
302
+ ) : (
303
+ <button
304
+ key={`o-${row.option!.value}`}
305
+ id={`${reactId}-opt-${row.optionIndex}`}
306
+ data-opt-index={row.optionIndex}
307
+ type="button"
308
+ className={[
309
+ "ui-select__opt",
310
+ row.optionIndex === activeIndex && "is-focus",
311
+ row.option!.disabled && "is-disabled",
312
+ ]
313
+ .filter(Boolean)
314
+ .join(" ")}
315
+ role="option"
316
+ aria-selected={row.option!.value === value}
317
+ aria-disabled={row.option!.disabled || undefined}
318
+ onClick={() => pick(row.option!)}
319
+ onMouseEnter={() =>
320
+ !row.option!.disabled && setActiveIndex(row.optionIndex!)
321
+ }
322
+ tabIndex={-1}
323
+ >
324
+ <span className="ui-select__tick" aria-hidden="true">
325
+ <svg
326
+ width="13"
327
+ height="13"
328
+ viewBox="0 0 24 24"
329
+ fill="none"
330
+ stroke="currentColor"
331
+ strokeWidth="2.6"
332
+ strokeLinecap="round"
333
+ strokeLinejoin="round"
334
+ >
335
+ <path d="M5 13l4 4L19 7" />
336
+ </svg>
337
+ </span>
338
+ {row.label}
339
+ </button>
340
+ ),
341
+ )}
342
+ </div>
343
+ )}
344
+ </div>
345
+ );
346
+
347
+ if (!label && !error && !hint) {
348
+ return <div className={className}>{selectControl}</div>;
349
+ }
350
+
351
+ return (
352
+ <label className={["ui-field", className].filter(Boolean).join(" ")}>
353
+ {label && (
354
+ <span className="ui-field__label" id={labelId}>
355
+ {label}
356
+ {required && (
357
+ <span className="ui-field__req" aria-hidden="true">
358
+ *
359
+ </span>
360
+ )}
361
+ </span>
362
+ )}
363
+ {selectControl}
364
+ {error ? (
365
+ <span className="ui-field__error" id={errorId}>
366
+ <span aria-hidden="true">
367
+ <svg
368
+ width="12"
369
+ height="12"
370
+ viewBox="0 0 24 24"
371
+ fill="none"
372
+ stroke="currentColor"
373
+ strokeWidth="2.2"
374
+ strokeLinecap="round"
375
+ >
376
+ <circle cx="12" cy="12" r="9" />
377
+ <path d="M12 8v4.5M12 16v.5" />
378
+ </svg>
379
+ </span>
380
+ {error}
381
+ </span>
382
+ ) : hint ? (
383
+ <span className="ui-field__hint" id={hintId}>
384
+ {hint}
385
+ </span>
386
+ ) : null}
387
+ </label>
388
+ );
389
+ }
@@ -0,0 +1,140 @@
1
+ /* Toast. All values from the design-system tokens. */
2
+
3
+ .ui-toast-region {
4
+ position: fixed;
5
+ z-index: var(--z-toast);
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--space-2);
9
+ padding: var(--space-4);
10
+ /* Shrink-wrap to the toasts instead of covering a whole corner, so the empty
11
+ area never overlays or blocks clicks on the page behind it. */
12
+ width: max-content;
13
+ max-width: 100vw;
14
+ }
15
+
16
+ .ui-toast-region--bottom-right {
17
+ right: 0;
18
+ bottom: 0;
19
+ align-items: flex-end;
20
+ }
21
+
22
+ .ui-toast-region--bottom-center {
23
+ left: 50%;
24
+ bottom: 0;
25
+ transform: translateX(-50%);
26
+ align-items: center;
27
+ }
28
+
29
+ /* A light card lifted off the page by its shadow. Dark text keeps every word
30
+ at full contrast; the tone shows in a coloured icon, not the surface. */
31
+ .ui-toast {
32
+ display: flex;
33
+ align-items: flex-start;
34
+ gap: var(--space-3);
35
+ width: 100%;
36
+ max-width: 380px;
37
+ padding: var(--space-4);
38
+ /* ivory-light, not ivory-base: ivory-base (#C9C2B1) is dark enough that the
39
+ undo link drops to 3.79 on it, the very contrast problem this redesign
40
+ fixes. ivory-light keeps a warm card distinct from the page and clears AA
41
+ for the link (5.65) and everything on it. */
42
+ background: var(--surface-hover);
43
+ border: 1px solid var(--border);
44
+ border-radius: var(--radius-lg);
45
+ box-shadow: var(--shadow-xl);
46
+ font-size: var(--text-sm);
47
+ line-height: var(--leading-sm);
48
+ color: var(--text);
49
+ pointer-events: auto;
50
+ animation: ui-toast-in var(--duration-base) var(--ease-enter);
51
+ }
52
+
53
+ .ui-toast__icon {
54
+ display: inline-flex;
55
+ flex: none;
56
+ margin-top: 1px;
57
+ }
58
+
59
+ /* One coloured icon per tone. The status roles already clear 4.5:1 on the
60
+ light surface, so the glyph carries the meaning without tinting the card. */
61
+ .ui-toast--success .ui-toast__icon {
62
+ color: var(--success);
63
+ }
64
+ .ui-toast--danger .ui-toast__icon {
65
+ color: var(--danger);
66
+ }
67
+ .ui-toast--warning .ui-toast__icon {
68
+ color: var(--warning);
69
+ }
70
+ .ui-toast--info .ui-toast__icon {
71
+ color: var(--primary);
72
+ }
73
+
74
+ .ui-toast__msg {
75
+ flex: 1;
76
+ }
77
+
78
+ .ui-toast__action {
79
+ display: inline-block;
80
+ margin-top: var(--space-1);
81
+ background: none;
82
+ border: 0;
83
+ padding: 0;
84
+ font: inherit;
85
+ font-weight: var(--weight-semibold);
86
+ color: var(--link);
87
+ text-decoration: underline;
88
+ cursor: pointer;
89
+ }
90
+ .ui-toast__action:hover {
91
+ color: var(--link-hover);
92
+ }
93
+ .ui-toast__action:focus-visible {
94
+ outline: 2px solid var(--focus);
95
+ outline-offset: 2px;
96
+ border-radius: var(--radius-sm);
97
+ }
98
+
99
+ .ui-toast__x {
100
+ display: inline-flex;
101
+ flex: none;
102
+ align-items: center;
103
+ justify-content: center;
104
+ width: 24px;
105
+ height: 24px;
106
+ margin: -2px -2px 0 0;
107
+ background: none;
108
+ border: 0;
109
+ border-radius: var(--radius-sm);
110
+ color: var(--text-muted);
111
+ cursor: pointer;
112
+ }
113
+ .ui-toast__x:hover {
114
+ color: var(--text);
115
+ background: var(--surface-hover);
116
+ }
117
+ .ui-toast__x:focus-visible {
118
+ outline: 2px solid var(--focus);
119
+ outline-offset: 2px;
120
+ }
121
+
122
+ /* The SVG in the close button is decorative. Let clicks and the cursor fall
123
+ through to the button, so its pointer cursor always shows. */
124
+ .ui-toast__x svg,
125
+ .ui-toast__action svg {
126
+ pointer-events: none;
127
+ }
128
+
129
+ @keyframes ui-toast-in {
130
+ from {
131
+ opacity: 0;
132
+ transform: translateY(8px);
133
+ }
134
+ }
135
+
136
+ @media (prefers-reduced-motion: reduce) {
137
+ .ui-toast {
138
+ animation: none;
139
+ }
140
+ }