@threadlabs/looma 0.37.5 → 0.38.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/components/ui-button/ui-button.html +20 -5
- package/components/ui-combobox/ui-combobox.html +24 -3
- package/components/ui-combobox/ui-combobox.js +18 -3
- package/components/ui-icon-button/ui-icon-button.html +17 -8
- package/components/ui-menu-item/ui-menu-item.html +6 -1
- package/components/ui-select/ui-select.html +12 -0
- package/components/ui-skeleton/ui-skeleton.html +91 -0
- package/dist/index.js +1 -1
- package/docs/README.md +23 -10
- package/docs/catalog.json +246 -31
- package/docs/customizer-render.mjs +36 -20
- package/docs/customizer.js +76 -25
- package/docs/documentation.css +2 -0
- package/docs/palette.mjs +3 -3
- package/docs/site.mjs +1 -1
- package/docs/theme-session.mjs +1 -0
- package/docs/theme.mjs +11 -10
- package/package.json +2 -2
- package/styles/ui-button.css +19 -9
- package/styles/ui-combobox.css +40 -2
- package/styles/ui-icon-button.css +12 -2
- package/styles/ui-menu-item.css +6 -1
- package/styles/ui-select.css +9 -0
- package/styles/ui-skeleton.css +91 -0
- package/theme-system.css +7 -2
- package/vanilla/UiButton.js +1 -1
- package/vanilla/UiCombobox.d.ts +1 -0
- package/vanilla/UiCombobox.js +38 -37
- package/vanilla/UiIconButton.d.ts +1 -1
- package/vanilla/UiIconButton.js +1 -1
- package/vanilla/UiSelect.d.ts +1 -0
- package/vanilla/UiSelect.js +7 -7
- package/vanilla/UiSkeleton.d.ts +26 -0
- package/vanilla/UiSkeleton.js +42 -0
- package/vanilla/index.js +1 -0
- package/vue/UiButton.vue +30 -12
- package/vue/UiCombobox.d.ts +6 -0
- package/vue/UiCombobox.vue +70 -6
- package/vue/UiIconButton.d.ts +2 -2
- package/vue/UiIconButton.vue +23 -18
- package/vue/UiMenuItem.vue +6 -1
- package/vue/UiSelect.d.ts +6 -0
- package/vue/UiSelect.vue +30 -1
- package/vue/UiSkeleton.d.ts +37 -0
- package/vue/UiSkeleton.js +2 -0
- package/vue/UiSkeleton.vue +235 -0
- package/vue/chunks/UiButton.js +4 -4
- package/vue/chunks/UiCombobox.js +72 -46
- package/vue/chunks/UiIconButton.js +6 -7
- package/vue/chunks/UiMenuItem.js +1 -1
- package/vue/chunks/UiSelect.js +27 -2
- package/vue/chunks/UiSkeleton.js +151 -0
- package/vue/components.css +197 -25
- package/vue/editor/index.js +2 -3
- package/vue/index.d.ts +1 -0
- package/vue/index.js +2 -1
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
keep their normal layout.</prop>
|
|
25
25
|
<prop name="shape" type="keyword" values="rounded, pill" default="rounded"
|
|
26
26
|
>rounded uses the theme's action radius; pill rounds the ends fully, for a compact choice such as a status or a person.</prop>
|
|
27
|
-
<prop name="tone" type="keyword" values="accent, neutral, danger, success, warning, info" default="
|
|
27
|
+
<prop name="tone" type="keyword" values="accent, neutral, danger, success, warning, info" default="neutral"
|
|
28
28
|
>Which colour the button speaks in. It applies to every variant, so an outline, a ghost, and a solid in the same tone are the same action at three volumes:
|
|
29
29
|
tone="danger" variant="outline" is a destructive secondary action. neutral is the quiet one, for a button that should not claim a colour. The field
|
|
30
30
|
variant always uses neutral field colors.</prop>
|
|
@@ -375,7 +375,6 @@
|
|
|
375
375
|
background-color: var(--ui-button-hover-surface, oklch(from var(--_tone) l c h / 12%));
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
/* A neutral card encloses content quietly; ordinary outlines keep their action-tone edge. */
|
|
379
378
|
:host[data-appearance="card"][data-tone="neutral"] {
|
|
380
379
|
border-color: var(--ui-button-border, var(--ui-border));
|
|
381
380
|
}
|
|
@@ -384,6 +383,12 @@
|
|
|
384
383
|
border-color: var(--ui-button-hover-border, var(--ui-border-strong));
|
|
385
384
|
}
|
|
386
385
|
|
|
386
|
+
:host[data-appearance="card"][aria-pressed="true"] {
|
|
387
|
+
background-color: var(--ui-selection-surface);
|
|
388
|
+
border-color: var(--ui-selection-border);
|
|
389
|
+
color: var(--ui-selection-text);
|
|
390
|
+
}
|
|
391
|
+
|
|
387
392
|
/* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
|
|
388
393
|
:host:active:not(:disabled, [aria-disabled="true"]) {
|
|
389
394
|
scale: var(--ui-press-scale, 1);
|
|
@@ -431,6 +436,18 @@
|
|
|
431
436
|
background-color: var(--ui-button-surface, var(--ui-neutral-surface));
|
|
432
437
|
color: var(--ui-button-text, var(--ui-text));
|
|
433
438
|
}
|
|
439
|
+
:host[data-tone="neutral"][data-appearance="outline"] {
|
|
440
|
+
border-color: var(--ui-button-border, var(--ui-control-border));
|
|
441
|
+
background-color: var(--ui-button-surface, transparent);
|
|
442
|
+
}
|
|
443
|
+
:host[data-tone="neutral"][data-appearance="outline"]:hover:not(:disabled, [aria-disabled="true"]) {
|
|
444
|
+
border-color: var(--ui-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border)));
|
|
445
|
+
background-color: var(--ui-button-hover-surface, var(--ui-neutral-hover));
|
|
446
|
+
}
|
|
447
|
+
:host[data-tone="neutral"][data-appearance="outline"]:active:not(:disabled, [aria-disabled="true"]) {
|
|
448
|
+
border-color: var(--ui-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border)));
|
|
449
|
+
background-color: var(--ui-button-active-surface, var(--ui-neutral-active));
|
|
450
|
+
}
|
|
434
451
|
:host[data-tone="neutral"][data-appearance="solid"]:hover:not(:disabled, [aria-disabled="true"]) {
|
|
435
452
|
background-color: var(--ui-button-hover-surface, var(--ui-neutral-hover));
|
|
436
453
|
}
|
|
@@ -518,10 +535,8 @@
|
|
|
518
535
|
border-color: transparent;
|
|
519
536
|
}
|
|
520
537
|
|
|
521
|
-
/* Pressed, or holding its menu open, a ghost button sinks slightly: a light surface with a faint
|
|
522
|
-
edge and a soft inner shadow. Lighter than hover, so an open menu's trigger never reads as heavy. */
|
|
523
538
|
:host[data-appearance="ghost"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
|
|
524
|
-
background-color: var(--ui-button-ghost-active-surface,
|
|
539
|
+
background-color: var(--ui-button-ghost-active-surface, oklch(from var(--_tone) l c h / 18%));
|
|
525
540
|
border-color: transparent;
|
|
526
541
|
box-shadow: inset 0 0 0 1px oklch(from var(--_tone) l c h / 10%), inset 0 1px 2px oklch(from var(--_tone) l c h / 8%);
|
|
527
542
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
<prop name="label" type="string" default=""
|
|
6
6
|
>The field's label, tied to its input and shown above it; required adds an asterisk. It also names the suggestion list. Pair it with
|
|
7
7
|
labelVisibility="sr-only" when the context already labels the field visibly.</prop>
|
|
8
|
+
<prop name="muted" type="boolean" default="false"
|
|
9
|
+
>Shows the selected field value with supporting-text emphasis, for an inherited or default choice. Suggestions keep normal emphasis and the field stays interactive.</prop>
|
|
8
10
|
<prop name="placeholder" type="string" default=""
|
|
9
11
|
>Hint text shown in the empty input, as on a native input. It disappears as the user types, so it never replaces label.</prop>
|
|
10
12
|
<prop name="name" type="string" default=""
|
|
@@ -79,7 +81,7 @@
|
|
|
79
81
|
when the field is left empty. In multiple mode a chosen item satisfies it, whatever is left in the input.</prop>
|
|
80
82
|
<prop name="invalid" type="boolean" default="false"
|
|
81
83
|
>Exposes an externally validated error on the input and field. It remains until the consumer clears it, independently of native validation.</prop>
|
|
82
|
-
<prop name="openOnFocus" type="boolean" default="false">Opens suggestions when the input receives focus
|
|
84
|
+
<prop name="openOnFocus" type="boolean" default="false">Opens suggestions when the input receives focus or is clicked. A committed selection shows all choices until typing filters them; an explicitly supplied query keeps its filtering.</prop>
|
|
83
85
|
<prop name="loading" type="boolean" default="false"
|
|
84
86
|
>Marks the suggestion list busy while the consumer loads options. It announces loading and suppresses the empty state and create offer until cleared.</prop>
|
|
85
87
|
<prop name="size" type="keyword" values="sm, md, lg" default="md"
|
|
@@ -423,7 +425,7 @@
|
|
|
423
425
|
<div class="control">
|
|
424
426
|
<div class="field" $ref="field">
|
|
425
427
|
<div class="entry">
|
|
426
|
-
<slot name="start"></slot>
|
|
428
|
+
<span class="start"><slot name="start"></slot></span>
|
|
427
429
|
<template $if="$multiple">
|
|
428
430
|
<button
|
|
429
431
|
$each="item, index of $internalItems"
|
|
@@ -698,6 +700,21 @@
|
|
|
698
700
|
flex: 1 1 auto;
|
|
699
701
|
min-inline-size: 0;
|
|
700
702
|
}
|
|
703
|
+
.start {
|
|
704
|
+
display: flex !important;
|
|
705
|
+
align-items: center !important;
|
|
706
|
+
flex: none;
|
|
707
|
+
padding-inline-start: var(--ui-space-3) !important;
|
|
708
|
+
}
|
|
709
|
+
.start:empty {
|
|
710
|
+
display: none !important;
|
|
711
|
+
}
|
|
712
|
+
:host([size="sm"]) .start {
|
|
713
|
+
padding-inline-start: var(--ui-space-2) !important;
|
|
714
|
+
}
|
|
715
|
+
:host([size="lg"]) .start {
|
|
716
|
+
padding-inline-start: var(--ui-space-4) !important;
|
|
717
|
+
}
|
|
701
718
|
/* A ghost icon button: the affordance is the mark, not a box around it. */
|
|
702
719
|
.help {
|
|
703
720
|
flex: none;
|
|
@@ -736,6 +753,10 @@
|
|
|
736
753
|
block-size: var(--ui-icon-size-sm);
|
|
737
754
|
}
|
|
738
755
|
|
|
756
|
+
:host([muted]) input:not(:disabled) {
|
|
757
|
+
color: var(--ui-text-secondary);
|
|
758
|
+
}
|
|
759
|
+
|
|
739
760
|
input::selection {
|
|
740
761
|
background: color-mix(in srgb, var(--ui-accent-solid) 20%, transparent);
|
|
741
762
|
color: inherit;
|
|
@@ -820,7 +841,7 @@
|
|
|
820
841
|
padding: var(--ui-space-1) !important;
|
|
821
842
|
border: var(--ui-border-width) solid var(--_ui-combobox-popup-border, var(--ui-overlay-border, var(--ui-border-strong)));
|
|
822
843
|
border-radius: var(--_ui-combobox-popup-radius, var(--ui-overlay-radius, var(--ui-radius-md)));
|
|
823
|
-
background: var(--_ui-combobox-popup-surface, var(--ui-
|
|
844
|
+
background: var(--_ui-combobox-popup-surface, var(--ui-material-content, var(--ui-surface-elevated)));
|
|
824
845
|
color: var(--ui-text-primary);
|
|
825
846
|
box-shadow: var(--_ui-combobox-popup-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
|
|
826
847
|
}
|
|
@@ -35,6 +35,7 @@ export function connect(host) {
|
|
|
35
35
|
let composing = false;
|
|
36
36
|
let alive = true;
|
|
37
37
|
let fullSet = false;
|
|
38
|
+
let suppressFocusOpening = false;
|
|
38
39
|
let initialRaw = "";
|
|
39
40
|
let awaitingLabel = null;
|
|
40
41
|
let proposedChange;
|
|
@@ -461,14 +462,28 @@ export function connect(host) {
|
|
|
461
462
|
else if (config().allowFreeText) { event.preventDefault(); commit(null, host.state.raw, null, "free-entry", "keyboard"); close(); }
|
|
462
463
|
}
|
|
463
464
|
};
|
|
465
|
+
const openFromField = () => {
|
|
466
|
+
const selected = config().options?.find(option => option.value === host.state.selected) ?? knownOptions.get(host.state.selected);
|
|
467
|
+
// A committed label is a selection, not a search draft. Typing still filters.
|
|
468
|
+
open(host.props.query.value == null && selected?.label === host.state.raw ? "disclosure" : "input");
|
|
469
|
+
};
|
|
464
470
|
const onClick = (event) => {
|
|
465
471
|
const option = event.target.closest?.('[role="option"][data-index]');
|
|
466
472
|
if (option) { choose(Number(option.dataset.index), "pointer"); return; }
|
|
467
473
|
const action = event.target.closest?.("[data-combobox-action]")?.dataset.comboboxAction;
|
|
468
474
|
if (action && (host.props.disabled.value || host.props.readonly.value)) return;
|
|
469
475
|
if (action === "clear") { lastSelection = null; commit(null, "", null, "clear", "pointer"); close(); input.focus(); }
|
|
470
|
-
else if (action === "disclosure") {
|
|
471
|
-
|
|
476
|
+
else if (action === "disclosure") {
|
|
477
|
+
const expanded = host.state.expanded;
|
|
478
|
+
// Returning focus must not undo an explicit request to close the list.
|
|
479
|
+
suppressFocusOpening = true;
|
|
480
|
+
try { input.focus(); } finally { suppressFocusOpening = false; }
|
|
481
|
+
expanded ? close() : open("disclosure");
|
|
482
|
+
}
|
|
483
|
+
else if (field.contains(event.target) && !event.target.closest?.("button")) {
|
|
484
|
+
if (host.props.openOnFocus.value && !host.state.expanded) openFromField();
|
|
485
|
+
input.focus();
|
|
486
|
+
}
|
|
472
487
|
};
|
|
473
488
|
const onPointerdown = (event) => { if (event.target.closest?.('[role="option"]')) event.preventDefault(); };
|
|
474
489
|
const onFocusout = (event) => {
|
|
@@ -481,7 +496,7 @@ export function connect(host) {
|
|
|
481
496
|
void validateCurrent();
|
|
482
497
|
};
|
|
483
498
|
const onFocusin = (event) => {
|
|
484
|
-
if (event.target === input && host.props.openOnFocus.value && !host.state.expanded)
|
|
499
|
+
if (event.target === input && host.props.openOnFocus.value && !host.state.expanded && !suppressFocusOpening) openFromField();
|
|
485
500
|
};
|
|
486
501
|
const onCompositionstart = (event) => { if (event.target === input) composing = true; };
|
|
487
502
|
const onCompositionend = (event) => { if (event.target === input) { composing = false; onInput(new InputEvent("input")); } };
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
<prop name="preset" type="keyword" values="none, primary, standard, quiet, destructive" default="none"
|
|
5
5
|
>The same shortcuts as Button: primary is solid accent, standard is solid neutral, quiet is ghost neutral, and destructive is solid danger. A named preset
|
|
6
6
|
wins over variant and tone; none preserves independent choices.</prop>
|
|
7
|
-
<!-- Computed maps read every member in HTML Next. auto keeps an omitted tone concrete so the preset map can resolve, while retaining the existing variant-dependent appearance. -->
|
|
8
7
|
<computed name="appearance" from="{ none: $variant, primary: 'solid', standard: 'solid', quiet: 'ghost', destructive: 'solid' }[$preset]"></computed>
|
|
9
8
|
<computed name="colorTone" from="{ none: $tone, primary: 'accent', standard: 'neutral', quiet: 'neutral', destructive: 'danger' }[$preset]"></computed>
|
|
10
9
|
<prop name="disabled" type="boolean" default="false"
|
|
@@ -21,12 +20,12 @@
|
|
|
21
20
|
>Matches a boxed Button of the same size when the two share a flex row. Uses the control-size theme tokens and stretches to the row's height, on mouse and
|
|
22
21
|
touch alike. Leave it off for compact toolbar and inline icon buttons.</prop>
|
|
23
22
|
<prop name="variant" type="keyword" values="ghost, outline, solid" default="ghost"
|
|
24
|
-
>Emphasis. ghost shows only the icon until it is hovered, for toolbars and dense rows; outline adds a border on the default surface; solid fills with
|
|
25
|
-
|
|
26
|
-
<prop name="tone" type="keyword" values="
|
|
27
|
-
>Colour for every variant.
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
>Emphasis. ghost shows only the icon until it is hovered, for toolbars and dense rows; outline adds a border on the default surface; solid fills with its
|
|
24
|
+
chosen tone.</prop>
|
|
25
|
+
<prop name="tone" type="keyword" values="accent, neutral, danger, success, warning, info" default="neutral"
|
|
26
|
+
>Colour for every variant. The default neutral shares the quiet control family with fields and text buttons. Choose accent explicitly for a coloured action,
|
|
27
|
+
or primary for solid accent. danger, success, warning and info share Button's intent colors. Native aria-pressed="true" keeps an outline toggle visibly
|
|
28
|
+
pressed; the consumer owns that state.</prop>
|
|
30
29
|
<prop name="anticipatory" type="boolean" default="false"
|
|
31
30
|
>Keeps the button out of sight until it is wanted: at rest a fine pointer sees only a small guide dot, and the button appears on hover, on focus, or when
|
|
32
31
|
the pointer comes near inside a ui-affordance-scope. Touch devices and the solid variant always show it.</prop>
|
|
@@ -217,10 +216,14 @@
|
|
|
217
216
|
color: var(--_icon-button-text);
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
/*
|
|
219
|
+
/* Neutral actions share the same quiet surface and border roles as fields. */
|
|
221
220
|
:host[data-tone="neutral"] {
|
|
222
221
|
--_tone-highlight: var(--ui-neutral-highlight, none);
|
|
223
222
|
--_tone-glow: var(--ui-neutral-glow, 0 0 transparent);
|
|
223
|
+
--_icon-button-outline-surface: transparent;
|
|
224
|
+
--_icon-button-border: var(--ui-control-border);
|
|
225
|
+
--_icon-button-hover-border: var(--ui-control-border-hover, var(--ui-control-border));
|
|
226
|
+
--_icon-button-text: var(--ui-text);
|
|
224
227
|
--_icon-button-solid-surface: var(--ui-neutral-surface);
|
|
225
228
|
--_icon-button-solid-hover-surface: var(--ui-neutral-hover);
|
|
226
229
|
--_icon-button-solid-text: var(--ui-text);
|
|
@@ -408,6 +411,12 @@
|
|
|
408
411
|
:host[data-appearance="outline"]:hover:enabled:not([aria-disabled="true"]) {
|
|
409
412
|
border-color: var(--ui-icon-button-hover-border, var(--_icon-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border))));
|
|
410
413
|
}
|
|
414
|
+
:host[data-tone="neutral"][data-appearance="outline"]:hover:enabled:not([aria-disabled="true"]) {
|
|
415
|
+
background-color: var(--ui-icon-button-hover-surface, var(--ui-neutral-hover));
|
|
416
|
+
}
|
|
417
|
+
:host[data-tone="neutral"][data-appearance="outline"]:active:enabled:not([aria-disabled="true"]) {
|
|
418
|
+
background-color: var(--ui-icon-button-active-surface, var(--ui-neutral-active));
|
|
419
|
+
}
|
|
411
420
|
|
|
412
421
|
:host[data-appearance="solid"]:hover:enabled:not([aria-disabled="true"]) {
|
|
413
422
|
background-color: var(--ui-icon-button-solid-hover-surface, var(--_icon-button-solid-hover-surface, var(--ui-accent-hover)));
|
|
@@ -181,7 +181,12 @@
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
:host:is(:hover, :focus-visible):not([aria-disabled="true"]) {
|
|
184
|
-
background: var(--ui-menu-item-hover-surface, var(--ui-
|
|
184
|
+
background: var(--ui-menu-item-hover-surface, var(--ui-accent-subtle));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
:host[aria-checked="true"]:not([aria-disabled="true"]) {
|
|
188
|
+
background: var(--ui-selection-surface);
|
|
189
|
+
color: var(--ui-selection-text);
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
:host:focus-visible {
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
<prop name="disabled" type="boolean" default="false">Prevents selection and form submission.</prop>
|
|
7
7
|
<prop name="invalid" type="boolean" default="false">Exposes invalid state to assistive technology and styling.</prop>
|
|
8
8
|
<prop name="required" type="boolean" default="false">Requires a non-empty option for form validation.</prop>
|
|
9
|
+
<prop name="muted" type="boolean" default="false"
|
|
10
|
+
>Shows the selected value with supporting-text emphasis, for an inherited or default choice. The select remains interactive and its options retain normal
|
|
11
|
+
emphasis.</prop>
|
|
9
12
|
<prop name="size" type="keyword" values="sm, md, lg" default="md"
|
|
10
13
|
>Height, padding, chevron, and type size, matching Button's sizes so a field and a button of one size share a row. sm is the small control height (2rem by
|
|
11
14
|
default) with smaller text and a smaller chevron, for compact rows such as a table row or a filter bar; md is the standard control height; lg is taller
|
|
@@ -95,6 +98,15 @@
|
|
|
95
98
|
background-size: 1.125rem 1.125rem, 100% 100%;
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
:host([muted]) {
|
|
102
|
+
color: var(--ui-text-secondary);
|
|
103
|
+
}
|
|
104
|
+
:host([muted]) :slotted(option),
|
|
105
|
+
:host([muted]) :slotted(optgroup) {
|
|
106
|
+
color: var(--ui-text-primary);
|
|
107
|
+
background-color: var(--ui-control-surface);
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
:host:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(:user-invalid) {
|
|
99
111
|
border-color: var(--ui-select-border-hover, var(--ui-control-border-hover, var(--ui-text-secondary)));
|
|
100
112
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<template component="ui-skeleton">
|
|
2
|
+
<defs>
|
|
3
|
+
<prop name="shape" type="keyword" values="text, circle, rectangle" default="text"
|
|
4
|
+
>The content's silhouette. text reserves one inherited line box with a shorter bar inside it; circle matches a medium Avatar; rectangle reserves a media
|
|
5
|
+
area.</prop>
|
|
6
|
+
<prop name="width" type="string" default=""
|
|
7
|
+
>Inline size as a CSS length or percentage, constrained to the container. Empty fills the available width for text and rectangles, or matches a medium
|
|
8
|
+
Avatar's outer size for circles.</prop>
|
|
9
|
+
<prop name="height" type="string" default=""
|
|
10
|
+
>Block size as a CSS length. Empty reserves one line for text, a square for circles, or 8rem for rectangles. Set only width to keep a circle square.</prop>
|
|
11
|
+
<prop name="animated" type="boolean" default="false"
|
|
12
|
+
>Gently pulses the placeholder. Off by default, and always still when the reader requests reduced motion.</prop>
|
|
13
|
+
</defs>
|
|
14
|
+
<span aria-hidden="true" style:--ui-skeleton-width="$width" style:--ui-skeleton-height="$height"></span>
|
|
15
|
+
<style>
|
|
16
|
+
/* Dimensions belong to this placeholder, never a nested or neighbouring one. */
|
|
17
|
+
@property --ui-skeleton-width {
|
|
18
|
+
syntax: "*";
|
|
19
|
+
inherits: false;
|
|
20
|
+
}
|
|
21
|
+
@property --ui-skeleton-height {
|
|
22
|
+
syntax: "*";
|
|
23
|
+
inherits: false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host {
|
|
27
|
+
display: block !important;
|
|
28
|
+
position: relative;
|
|
29
|
+
box-sizing: border-box !important;
|
|
30
|
+
flex: none;
|
|
31
|
+
min-inline-size: 0;
|
|
32
|
+
max-inline-size: 100%;
|
|
33
|
+
inline-size: var(--ui-skeleton-width, 100%);
|
|
34
|
+
block-size: var(--ui-skeleton-height, 1lh);
|
|
35
|
+
margin: 0 !important;
|
|
36
|
+
font: inherit;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:host::before {
|
|
40
|
+
content: "";
|
|
41
|
+
position: absolute;
|
|
42
|
+
inset: 0;
|
|
43
|
+
border-radius: var(--ui-radius-sm);
|
|
44
|
+
background: color-mix(in srgb, var(--ui-text-primary) 16%, transparent);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:host([shape="text"])::before {
|
|
48
|
+
inset-block: 22%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:host([shape="circle"]) {
|
|
52
|
+
inline-size: var(--ui-skeleton-width, calc(2.5rem + 2 * var(--ui-border-width)));
|
|
53
|
+
block-size: var(--ui-skeleton-height, auto);
|
|
54
|
+
aspect-ratio: 1;
|
|
55
|
+
}
|
|
56
|
+
:host([shape="circle"])::before {
|
|
57
|
+
border-radius: var(--ui-radius-round);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:host([shape="rectangle"]) {
|
|
61
|
+
block-size: var(--ui-skeleton-height, 8rem);
|
|
62
|
+
}
|
|
63
|
+
:host([shape="rectangle"])::before {
|
|
64
|
+
border-radius: var(--ui-radius-md);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:host([animated])::before {
|
|
68
|
+
animation: ui-skeleton-pulse 1.6s ease-in-out infinite;
|
|
69
|
+
}
|
|
70
|
+
@keyframes ui-skeleton-pulse {
|
|
71
|
+
50% {
|
|
72
|
+
opacity: 0.45;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
@media (prefers-reduced-motion: reduce) {
|
|
76
|
+
:host([animated])::before {
|
|
77
|
+
animation: none;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
@media (forced-colors: active) {
|
|
81
|
+
:host::before {
|
|
82
|
+
background: CanvasText;
|
|
83
|
+
opacity: 0.2;
|
|
84
|
+
animation: none;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
:host[hidden] {
|
|
88
|
+
display: none !important;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
91
|
+
</template>
|