@adia-ai/web-components 0.6.32 → 0.6.34
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/CHANGELOG.md +44 -0
- package/components/accordion/accordion.css +2 -2
- package/components/action-list/action-list.css +2 -2
- package/components/agent-artifact/agent-artifact.css +31 -31
- package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
- package/components/agent-questions/agent-questions.css +57 -57
- package/components/agent-reasoning/agent-reasoning.css +62 -62
- package/components/agent-suggestions/agent-suggestions.css +4 -4
- package/components/agent-trace/agent-trace.css +53 -53
- package/components/alert/alert.css +41 -41
- package/components/avatar/avatar.css +27 -27
- package/components/badge/badge.css +27 -27
- package/components/block/block.css +16 -16
- package/components/breadcrumb/breadcrumb.css +23 -23
- package/components/button/button.css +101 -91
- package/components/calendar-grid/calendar-grid.a2ui.json +136 -0
- package/components/calendar-grid/calendar-grid.css +226 -0
- package/components/calendar-grid/calendar-grid.d.ts +37 -0
- package/components/calendar-grid/calendar-grid.js +17 -0
- package/components/calendar-grid/calendar-grid.yaml +116 -0
- package/components/calendar-grid/class.js +300 -0
- package/components/calendar-picker/calendar-picker.css +139 -139
- package/components/canvas/canvas.css +12 -12
- package/components/card/card.css +83 -83
- package/components/chart/chart.css +224 -224
- package/components/chart-legend/chart-legend.css +26 -26
- package/components/check/check.css +40 -40
- package/components/code/code.css +125 -125
- package/components/col/col.css +15 -15
- package/components/color-picker/color-picker.css +55 -55
- package/components/combobox/class.js +861 -0
- package/components/combobox/combobox.a2ui.json +363 -0
- package/components/combobox/combobox.css +244 -0
- package/components/combobox/combobox.d.ts +113 -0
- package/components/combobox/combobox.examples.md +59 -0
- package/components/combobox/combobox.js +17 -0
- package/components/combobox/combobox.test.js +181 -0
- package/components/combobox/combobox.yaml +369 -0
- package/components/command/command.css +90 -90
- package/components/date-range-picker/class.js +775 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
- package/components/date-range-picker/date-range-picker.css +178 -0
- package/components/date-range-picker/date-range-picker.d.ts +82 -0
- package/components/date-range-picker/date-range-picker.examples.md +37 -0
- package/components/date-range-picker/date-range-picker.js +17 -0
- package/components/date-range-picker/date-range-picker.test.js +387 -0
- package/components/date-range-picker/date-range-picker.yaml +285 -0
- package/components/datetime-picker/class.js +706 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
- package/components/datetime-picker/datetime-picker.css +150 -0
- package/components/datetime-picker/datetime-picker.d.ts +86 -0
- package/components/datetime-picker/datetime-picker.examples.md +46 -0
- package/components/datetime-picker/datetime-picker.js +17 -0
- package/components/datetime-picker/datetime-picker.test.js +454 -0
- package/components/datetime-picker/datetime-picker.yaml +332 -0
- package/components/demo-toggle/demo-toggle.css +27 -27
- package/components/description-list/description-list.css +18 -18
- package/components/divider/divider.css +24 -24
- package/components/embed/embed.css +6 -6
- package/components/empty-state/empty-state.css +27 -27
- package/components/feed/feed.css +12 -12
- package/components/field/field.css +37 -28
- package/components/field/field.test.js +32 -0
- package/components/fields/fields.css +5 -5
- package/components/grid/grid.css +5 -5
- package/components/heatmap/heatmap.css +63 -63
- package/components/icon/icon.css +12 -12
- package/components/image/image.css +14 -14
- package/components/index.js +8 -0
- package/components/input/input.css +66 -66
- package/components/inspector/inspector.css +6 -6
- package/components/integration-card/class.js +410 -0
- package/components/integration-card/integration-card.a2ui.json +268 -0
- package/components/integration-card/integration-card.css +169 -0
- package/components/integration-card/integration-card.d.ts +63 -0
- package/components/integration-card/integration-card.examples.md +41 -0
- package/components/integration-card/integration-card.js +17 -0
- package/components/integration-card/integration-card.test.js +306 -0
- package/components/integration-card/integration-card.yaml +280 -0
- package/components/kbd/kbd.css +32 -32
- package/components/link/link.css +12 -12
- package/components/list/list.css +8 -8
- package/components/list-window/class.js +688 -0
- package/components/list-window/list-window.a2ui.json +277 -0
- package/components/list-window/list-window.css +124 -0
- package/components/list-window/list-window.d.ts +84 -0
- package/components/list-window/list-window.examples.md +73 -0
- package/components/list-window/list-window.js +17 -0
- package/components/list-window/list-window.test.js +303 -0
- package/components/list-window/list-window.yaml +270 -0
- package/components/menu/menu.css +8 -8
- package/components/modal/modal.css +43 -43
- package/components/nav/nav.css +40 -40
- package/components/nav-group/nav-group.css +52 -52
- package/components/nav-item/nav-item.css +44 -44
- package/components/noodles/noodles.css +31 -31
- package/components/option-card/option-card.css +69 -69
- package/components/otp-input/otp-input.css +30 -30
- package/components/page/page.css +18 -18
- package/components/pagination/pagination.css +61 -61
- package/components/pane/pane.css +57 -57
- package/components/pipeline-status/pipeline-status.css +65 -65
- package/components/popover/popover.css +17 -17
- package/components/progress/progress.css +23 -23
- package/components/progress-row/progress-row.css +17 -17
- package/components/radio/radio.css +39 -39
- package/components/range/range.css +55 -55
- package/components/rating/rating.css +28 -28
- package/components/richtext/richtext.css +133 -133
- package/components/row/row.css +19 -19
- package/components/search/search.css +5 -5
- package/components/segment/segment.css +24 -24
- package/components/segmented/segmented.css +25 -25
- package/components/select/select.css +84 -84
- package/components/skeleton/skeleton.css +14 -14
- package/components/slider/slider.css +46 -46
- package/components/spinner/class.js +69 -0
- package/components/spinner/spinner.a2ui.json +197 -0
- package/components/spinner/spinner.css +165 -0
- package/components/spinner/spinner.d.ts +26 -0
- package/components/spinner/spinner.examples.md +26 -0
- package/components/spinner/spinner.js +17 -0
- package/components/spinner/spinner.test.js +234 -0
- package/components/spinner/spinner.yaml +230 -0
- package/components/stack/stack.css +11 -11
- package/components/stat/stat.css +25 -25
- package/components/step-progress/step-progress.css +20 -20
- package/components/stepper/stepper.css +29 -29
- package/components/stream/stream.css +12 -12
- package/components/swatch/swatch.css +68 -68
- package/components/swiper/swiper.css +57 -57
- package/components/switch/switch.css +52 -52
- package/components/table/class.js +9 -0
- package/components/table/table.a2ui.json +1 -1
- package/components/table/table.css +162 -162
- package/components/table/table.d.ts +1 -1
- package/components/table/table.test.js +53 -0
- package/components/table/table.yaml +13 -1
- package/components/table-toolbar/table-toolbar.css +32 -32
- package/components/tabs/tabs.css +51 -51
- package/components/tag/tag.css +48 -48
- package/components/text/text.css +44 -44
- package/components/textarea/textarea.css +46 -46
- package/components/time-picker/class.js +693 -0
- package/components/time-picker/time-picker.a2ui.json +267 -0
- package/components/time-picker/time-picker.css +122 -0
- package/components/time-picker/time-picker.d.ts +75 -0
- package/components/time-picker/time-picker.examples.md +35 -0
- package/components/time-picker/time-picker.js +17 -0
- package/components/time-picker/time-picker.test.js +287 -0
- package/components/time-picker/time-picker.yaml +256 -0
- package/components/timeline/timeline.css +50 -50
- package/components/toast/toast.css +58 -58
- package/components/toggle-group/toggle-group.css +6 -6
- package/components/toggle-scheme/toggle-scheme.css +2 -2
- package/components/toolbar/toolbar.css +17 -17
- package/components/tooltip/tooltip.css +2 -2
- package/components/tree/tree.css +37 -37
- package/components/upload/upload.css +49 -49
- package/dist/icons-manifest.js +3 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +121 -83
- package/package.json +1 -1
- package/styles/components.css +8 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# spinner — Examples
|
|
2
|
+
|
|
3
|
+
## Default
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<spinner-ui></spinner-ui>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Loading button
|
|
10
|
+
|
|
11
|
+
Canonical composition. Disable the button while the operation is in
|
|
12
|
+
progress; `tone="current"` keeps the spinner matched to the button's
|
|
13
|
+
label color.
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<button-ui variant="primary" disabled>
|
|
17
|
+
<spinner-ui size="sm" tone="current" label="Saving"></spinner-ui>
|
|
18
|
+
Saving
|
|
19
|
+
</button-ui>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Typing indicator
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<spinner-ui variant="dots" tone="subtle" label="Assistant is typing"></spinner-ui>
|
|
26
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<spinner-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
6
|
+
*
|
|
7
|
+
* import { UISpinner } from '@adia-ai/web-components/components/spinner/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UISpinner } from './class.js';
|
|
14
|
+
|
|
15
|
+
defineIfFree('spinner-ui', UISpinner);
|
|
16
|
+
|
|
17
|
+
export { UISpinner };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spinner-ui tests — covers SPEC-001 § Verification.
|
|
3
|
+
*
|
|
4
|
+
* The component is a CSS-only animation (no stamped DOM, no JS timers).
|
|
5
|
+
* Tests fall in two categories:
|
|
6
|
+
*
|
|
7
|
+
* 1. Behavioral — props reflect to host attributes, defaults apply,
|
|
8
|
+
* ARIA wiring is in place, element is not a focus target.
|
|
9
|
+
* 2. CSS source — happy-dom doesn't resolve @scope rules through
|
|
10
|
+
* getComputedStyle(), so the size-ladder, tone-mapping, and
|
|
11
|
+
* reduced-motion fallback are validated against the CSS source
|
|
12
|
+
* directly (same recipe as tag-ui / description-list / text).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { dirname, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const SPINNER_CSS = readFileSync(resolve(HERE, 'spinner.css'), 'utf8');
|
|
22
|
+
|
|
23
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await import('./spinner.js');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function mount(html) {
|
|
30
|
+
const wrap = document.createElement('div');
|
|
31
|
+
wrap.innerHTML = html;
|
|
32
|
+
document.body.appendChild(wrap);
|
|
33
|
+
return wrap.firstElementChild;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── 1. Defaults + property reflection ──────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe('spinner-ui — defaults', () => {
|
|
39
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
40
|
+
|
|
41
|
+
it('renders without props using documented defaults', async () => {
|
|
42
|
+
const s = mount('<spinner-ui></spinner-ui>');
|
|
43
|
+
await tick();
|
|
44
|
+
expect(s.size).toBe('md');
|
|
45
|
+
expect(s.variant).toBe('arc');
|
|
46
|
+
expect(s.tone).toBe('current');
|
|
47
|
+
expect(s.paused).toBe(false);
|
|
48
|
+
expect(s.label).toBe('Loading');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('sets role="progressbar" on the host', async () => {
|
|
52
|
+
const s = mount('<spinner-ui></spinner-ui>');
|
|
53
|
+
await tick();
|
|
54
|
+
expect(s.getAttribute('role')).toBe('progressbar');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('sets aria-busy="true" on the host', async () => {
|
|
58
|
+
const s = mount('<spinner-ui></spinner-ui>');
|
|
59
|
+
await tick();
|
|
60
|
+
expect(s.getAttribute('aria-busy')).toBe('true');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('sets aria-valuetext to the default label', async () => {
|
|
64
|
+
const s = mount('<spinner-ui></spinner-ui>');
|
|
65
|
+
await tick();
|
|
66
|
+
expect(s.getAttribute('aria-valuetext')).toBe('Loading');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── 2. Reflected props ─────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe('spinner-ui — reflected attributes', () => {
|
|
73
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
74
|
+
|
|
75
|
+
it('reflects size="sm" / "lg" to the host attribute', async () => {
|
|
76
|
+
const sm = mount('<spinner-ui size="sm"></spinner-ui>');
|
|
77
|
+
await tick();
|
|
78
|
+
expect(sm.getAttribute('size')).toBe('sm');
|
|
79
|
+
|
|
80
|
+
const lg = mount('<spinner-ui size="lg"></spinner-ui>');
|
|
81
|
+
await tick();
|
|
82
|
+
expect(lg.getAttribute('size')).toBe('lg');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('reflects variant="ring" / "dots" to the host attribute', async () => {
|
|
86
|
+
const ring = mount('<spinner-ui variant="ring"></spinner-ui>');
|
|
87
|
+
await tick();
|
|
88
|
+
expect(ring.getAttribute('variant')).toBe('ring');
|
|
89
|
+
|
|
90
|
+
const dots = mount('<spinner-ui variant="dots"></spinner-ui>');
|
|
91
|
+
await tick();
|
|
92
|
+
expect(dots.getAttribute('variant')).toBe('dots');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('reflects tone="subtle" / "accent" / "inverse" to the host attribute', async () => {
|
|
96
|
+
const subtle = mount('<spinner-ui tone="subtle"></spinner-ui>');
|
|
97
|
+
const accent = mount('<spinner-ui tone="accent"></spinner-ui>');
|
|
98
|
+
const inverse = mount('<spinner-ui tone="inverse"></spinner-ui>');
|
|
99
|
+
await tick();
|
|
100
|
+
expect(subtle.getAttribute('tone')).toBe('subtle');
|
|
101
|
+
expect(accent.getAttribute('tone')).toBe('accent');
|
|
102
|
+
expect(inverse.getAttribute('tone')).toBe('inverse');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('reflects [paused] as a boolean attribute', async () => {
|
|
106
|
+
const s = mount('<spinner-ui paused></spinner-ui>');
|
|
107
|
+
await tick();
|
|
108
|
+
expect(s.hasAttribute('paused')).toBe(true);
|
|
109
|
+
expect(s.paused).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── 3. Accessibility wiring ────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
describe('spinner-ui — accessibility', () => {
|
|
116
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
117
|
+
|
|
118
|
+
it('mirrors a custom [label] to aria-valuetext', async () => {
|
|
119
|
+
const s = mount('<spinner-ui label="Saving"></spinner-ui>');
|
|
120
|
+
await tick();
|
|
121
|
+
expect(s.getAttribute('aria-valuetext')).toBe('Saving');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('updates aria-valuetext when label is set programmatically', async () => {
|
|
125
|
+
const s = mount('<spinner-ui></spinner-ui>');
|
|
126
|
+
await tick();
|
|
127
|
+
s.label = 'Uploading';
|
|
128
|
+
await tick();
|
|
129
|
+
expect(s.getAttribute('aria-valuetext')).toBe('Uploading');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('is NOT a focus target (no tabindex set, body tabIndex chain skips it)', async () => {
|
|
133
|
+
const s = mount('<spinner-ui></spinner-ui>');
|
|
134
|
+
await tick();
|
|
135
|
+
expect(s.hasAttribute('tabindex')).toBe(false);
|
|
136
|
+
// tabIndex defaults to -1 for non-focusable elements in happy-dom
|
|
137
|
+
expect(s.tabIndex).toBeLessThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('does not stamp any internal DOM (static template = null)', async () => {
|
|
141
|
+
const s = mount('<spinner-ui></spinner-ui>');
|
|
142
|
+
await tick();
|
|
143
|
+
// The visual is entirely pseudo-element-driven; the host has no
|
|
144
|
+
// element children of its own.
|
|
145
|
+
expect(s.children.length).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('preserves user-supplied role / aria-busy when set in markup', async () => {
|
|
149
|
+
const s = mount('<spinner-ui role="status" aria-busy="false"></spinner-ui>');
|
|
150
|
+
await tick();
|
|
151
|
+
// connected() only sets the default ARIA when absent
|
|
152
|
+
expect(s.getAttribute('role')).toBe('status');
|
|
153
|
+
expect(s.getAttribute('aria-busy')).toBe('false');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── 4. CSS source contract ─────────────────────────────────────────────
|
|
158
|
+
// happy-dom doesn't resolve @scope rules through getComputedStyle, so we
|
|
159
|
+
// validate the CSS source shape directly. Same recipe as tag-ui.
|
|
160
|
+
|
|
161
|
+
describe('spinner-ui — CSS source contract', () => {
|
|
162
|
+
it('opens with the canonical @scope (spinner-ui) block', () => {
|
|
163
|
+
expect(SPINNER_CSS).toMatch(/^@scope \(spinner-ui\)/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('declares both the :where(:scope) token block and a :scope base block', () => {
|
|
167
|
+
expect(SPINNER_CSS).toMatch(/:where\(:scope\)\s*\{/);
|
|
168
|
+
expect(SPINNER_CSS).toMatch(/^\s*:scope\s*\{/m);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('declares all 7 component tokens', () => {
|
|
172
|
+
for (const tok of [
|
|
173
|
+
'--spinner-size',
|
|
174
|
+
'--spinner-color',
|
|
175
|
+
'--spinner-stroke',
|
|
176
|
+
'--spinner-duration',
|
|
177
|
+
'--spinner-track-opacity',
|
|
178
|
+
'--spinner-dot-size',
|
|
179
|
+
'--spinner-dot-gap',
|
|
180
|
+
]) {
|
|
181
|
+
expect(SPINNER_CSS).toContain(tok);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('size="sm" / "md" / "lg" all override --spinner-size', () => {
|
|
186
|
+
expect(SPINNER_CSS).toMatch(/:scope\[size="sm"\][^}]*--spinner-size:\s*0\.875rem/);
|
|
187
|
+
expect(SPINNER_CSS).toMatch(/:scope\[size="md"\][^}]*--spinner-size:\s*1rem/);
|
|
188
|
+
expect(SPINNER_CSS).toMatch(/:scope\[size="lg"\][^}]*--spinner-size:\s*1\.25rem/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('tone="subtle" / "accent" / "inverse" override --spinner-color via semantic tokens', () => {
|
|
192
|
+
expect(SPINNER_CSS).toMatch(/:scope\[tone="subtle"\][^}]*--spinner-color:\s*var\(--a-fg-subtle\)/);
|
|
193
|
+
expect(SPINNER_CSS).toMatch(/:scope\[tone="accent"\][^}]*--spinner-color:\s*var\(--a-accent-strong\)/);
|
|
194
|
+
expect(SPINNER_CSS).toMatch(/:scope\[tone="inverse"\][^}]*--spinner-color:\s*var\(--a-chrome-light\)/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('arc variant uses a rotating quarter-circle border on ::before', () => {
|
|
198
|
+
expect(SPINNER_CSS).toMatch(/:scope\[variant="arc"\]::before/);
|
|
199
|
+
// The arc colors three borders transparent + the top currentColor
|
|
200
|
+
expect(SPINNER_CSS).toMatch(/border-color:\s*currentColor\s+transparent\s+transparent\s+transparent/);
|
|
201
|
+
expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-rotate/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('ring variant uses a full ring with one rotating colored segment', () => {
|
|
205
|
+
expect(SPINNER_CSS).toMatch(/:scope\[variant="ring"\]::before/);
|
|
206
|
+
expect(SPINNER_CSS).toMatch(/border-top-color:\s*currentColor/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('dots variant uses ::before + ::after with bounce keyframes', () => {
|
|
210
|
+
expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]::before/);
|
|
211
|
+
expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]::after/);
|
|
212
|
+
expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-bounce/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('paused state freezes the animation via animation-play-state', () => {
|
|
216
|
+
expect(SPINNER_CSS).toMatch(/:scope\[paused\][^}]*animation-play-state:\s*paused/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('reduced-motion media query replaces the animation with a static ellipsis', () => {
|
|
220
|
+
expect(SPINNER_CSS).toMatch(/@media\s*\(prefers-reduced-motion:\s*reduce\)/);
|
|
221
|
+
expect(SPINNER_CSS).toMatch(/content:\s*"…"/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('uses semantic --a-* tokens for color tones; no raw hex / rgb / oklch in the file', () => {
|
|
225
|
+
// Lints adjacent to the token contract: no raw colors in component CSS.
|
|
226
|
+
expect(SPINNER_CSS).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
|
|
227
|
+
expect(SPINNER_CSS).not.toMatch(/\brgb\s*\(/);
|
|
228
|
+
expect(SPINNER_CSS).not.toMatch(/\boklch\s*\(/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('declares a linear rotation timing function (load-bearing for smooth spin)', () => {
|
|
232
|
+
expect(SPINNER_CSS).toMatch(/spinner-ui-rotate[^;]*linear/);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Hand-authored per SPEC-001 (docs/specs/implementation-ready/SPEC-001-spinner-loader.md).
|
|
2
|
+
# Edit this file; run `npm run build:components` to regenerate spinner.a2ui.json.
|
|
3
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
4
|
+
name: UISpinner
|
|
5
|
+
tag: spinner-ui
|
|
6
|
+
status: stable
|
|
7
|
+
component: Spinner
|
|
8
|
+
category: feedback
|
|
9
|
+
version: 1
|
|
10
|
+
description: >-
|
|
11
|
+
Circular animated indicator for indeterminate loading. Renders a rotating
|
|
12
|
+
arc, full ring, or three bouncing dots inside a sized box; the animation
|
|
13
|
+
runs while the element is in the DOM and `[paused]` is unset. Fills the
|
|
14
|
+
circular-spinner gap left by <skeleton-ui> (rectangular placeholder) and
|
|
15
|
+
<progress-ui> (linear determinate bar) — use <spinner-ui> when the wait
|
|
16
|
+
duration is unknown and the shape of the eventual content is irregular
|
|
17
|
+
or the region is too small for a placeholder block.
|
|
18
|
+
props:
|
|
19
|
+
size:
|
|
20
|
+
description: Diameter — matches icon-ui's ladder (sm 14px, md 16px, lg 20px).
|
|
21
|
+
type: string
|
|
22
|
+
default: md
|
|
23
|
+
enum:
|
|
24
|
+
- sm
|
|
25
|
+
- md
|
|
26
|
+
- lg
|
|
27
|
+
reflect: true
|
|
28
|
+
variant:
|
|
29
|
+
description: Visual flavor — arc (rotating quarter-circle), ring (full ring with one colored segment), dots (three bouncing dots).
|
|
30
|
+
type: string
|
|
31
|
+
default: arc
|
|
32
|
+
enum:
|
|
33
|
+
- arc
|
|
34
|
+
- ring
|
|
35
|
+
- dots
|
|
36
|
+
reflect: true
|
|
37
|
+
tone:
|
|
38
|
+
description: Color tone — `current` inherits parent text color (matches button label), `accent` uses brand accent, `subtle` is muted, `inverse` flips for on-accent surfaces.
|
|
39
|
+
type: string
|
|
40
|
+
default: current
|
|
41
|
+
enum:
|
|
42
|
+
- current
|
|
43
|
+
- accent
|
|
44
|
+
- subtle
|
|
45
|
+
- inverse
|
|
46
|
+
reflect: true
|
|
47
|
+
paused:
|
|
48
|
+
description: Pause the animation in-place. Useful for screenshot tests and explicit-control flows.
|
|
49
|
+
type: boolean
|
|
50
|
+
default: false
|
|
51
|
+
reflect: true
|
|
52
|
+
label:
|
|
53
|
+
description: Accessible operation name surfaced via `aria-valuetext`. Override for context-specific labels ("Saving", "Uploading").
|
|
54
|
+
type: string
|
|
55
|
+
default: Loading
|
|
56
|
+
events: {}
|
|
57
|
+
slots: {}
|
|
58
|
+
states:
|
|
59
|
+
- name: running
|
|
60
|
+
description: Default; animation active.
|
|
61
|
+
- name: paused
|
|
62
|
+
description: Animation frozen at the current frame.
|
|
63
|
+
attribute: paused
|
|
64
|
+
- name: reduced
|
|
65
|
+
description: Triggered by prefers-reduced-motion. Animation replaced with a static ellipsis. Detected via CSS, not JS.
|
|
66
|
+
traits: []
|
|
67
|
+
tokens:
|
|
68
|
+
--spinner-size:
|
|
69
|
+
description: Diameter of the spinner box.
|
|
70
|
+
default: 1rem
|
|
71
|
+
--spinner-color:
|
|
72
|
+
description: Color of the active arc / ring / dots. Defaults to currentColor so a tone-driven cascade resolves naturally.
|
|
73
|
+
default: currentColor
|
|
74
|
+
--spinner-stroke:
|
|
75
|
+
description: Border thickness for the arc / ring variants.
|
|
76
|
+
default: 2px
|
|
77
|
+
--spinner-duration:
|
|
78
|
+
description: One full rotation duration.
|
|
79
|
+
default: var(--a-duration-slow)
|
|
80
|
+
--spinner-track-opacity:
|
|
81
|
+
description: Opacity for the non-rotating ring track (variant=ring only).
|
|
82
|
+
default: '0.25'
|
|
83
|
+
a2ui:
|
|
84
|
+
rules:
|
|
85
|
+
- >-
|
|
86
|
+
Use <Spinner> for INDETERMINATE loading where the duration is
|
|
87
|
+
unknown. For determinate progress (a known fraction complete), use
|
|
88
|
+
<Progress> (linear) instead. For known-shape placeholder loading,
|
|
89
|
+
use <Skeleton>.
|
|
90
|
+
- >-
|
|
91
|
+
When a Spinner is inside a Button, set tone="current" so it
|
|
92
|
+
matches the button label color, and disable the button while the
|
|
93
|
+
operation is in progress.
|
|
94
|
+
- >-
|
|
95
|
+
When overriding [label], use a present-progressive verb form
|
|
96
|
+
("Loading", "Saving", "Uploading"). Never use "Spin" or "Wait" —
|
|
97
|
+
they describe the visual, not the operation.
|
|
98
|
+
- >-
|
|
99
|
+
Do not nest <Spinner> inside <Skeleton>; they are siblings (two
|
|
100
|
+
different loading idioms), not parent/child.
|
|
101
|
+
- >-
|
|
102
|
+
Do not stack multiple sibling <Spinner>s in the same viewport
|
|
103
|
+
region. Use one parent-level Spinner instead — multiple spinners
|
|
104
|
+
add visual noise without extra information.
|
|
105
|
+
anti_patterns:
|
|
106
|
+
- wrong: |
|
|
107
|
+
{"component": "Spinner", "label": "Spin", "value": 0.42}
|
|
108
|
+
why: |
|
|
109
|
+
Spinner is INDETERMINATE only. `value` and any quantitative
|
|
110
|
+
progress field belongs on Progress, not Spinner. Also "Spin" is
|
|
111
|
+
not a valid operation label.
|
|
112
|
+
fix: |
|
|
113
|
+
{"component": "Progress", "value": 42, "max": 100}
|
|
114
|
+
- wrong: |
|
|
115
|
+
{"component": "Skeleton", "variant": "circle", "animation": "rotate"}
|
|
116
|
+
why: |
|
|
117
|
+
Skeleton is a placeholder; rotation is not part of its contract.
|
|
118
|
+
A rotating circle is a Spinner.
|
|
119
|
+
fix: |
|
|
120
|
+
{"component": "Spinner", "size": "md"}
|
|
121
|
+
- wrong: |
|
|
122
|
+
{"component": "Card", "children": [
|
|
123
|
+
{"component": "Spinner"},
|
|
124
|
+
{"component": "Spinner"},
|
|
125
|
+
{"component": "Spinner"}
|
|
126
|
+
]}
|
|
127
|
+
why: |
|
|
128
|
+
Multiple sibling spinners in one region produce visual noise
|
|
129
|
+
without extra information. Use one parent-level Spinner.
|
|
130
|
+
fix: |
|
|
131
|
+
{"component": "Card", "children": [
|
|
132
|
+
{"component": "Spinner", "size": "lg"}
|
|
133
|
+
]}
|
|
134
|
+
examples:
|
|
135
|
+
- name: button-saving
|
|
136
|
+
description: Loading button — primary action with a saving spinner. The button is disabled while the operation is in progress; the spinner matches the button label color via tone="current".
|
|
137
|
+
a2ui: >-
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
"id": "btn-save",
|
|
141
|
+
"component": "Button",
|
|
142
|
+
"text": "Saving",
|
|
143
|
+
"variant": "primary",
|
|
144
|
+
"disabled": true,
|
|
145
|
+
"children": ["sp-1"]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "sp-1",
|
|
149
|
+
"component": "Spinner",
|
|
150
|
+
"size": "sm",
|
|
151
|
+
"tone": "current",
|
|
152
|
+
"label": "Saving"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
- name: centered-card-loading
|
|
156
|
+
description: Standalone centered spinner inside a card while body content fetches.
|
|
157
|
+
a2ui: >-
|
|
158
|
+
[
|
|
159
|
+
{
|
|
160
|
+
"id": "card",
|
|
161
|
+
"component": "Card",
|
|
162
|
+
"children": ["row"]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "row",
|
|
166
|
+
"component": "Row",
|
|
167
|
+
"justify": "center",
|
|
168
|
+
"align": "center",
|
|
169
|
+
"children": ["sp"]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "sp",
|
|
173
|
+
"component": "Spinner",
|
|
174
|
+
"size": "lg",
|
|
175
|
+
"tone": "subtle",
|
|
176
|
+
"label": "Loading dashboard"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
- name: typing-indicator
|
|
180
|
+
description: Three bouncing dots — good for chat / typing indicators.
|
|
181
|
+
a2ui: >-
|
|
182
|
+
[
|
|
183
|
+
{
|
|
184
|
+
"id": "sp",
|
|
185
|
+
"component": "Spinner",
|
|
186
|
+
"variant": "dots",
|
|
187
|
+
"tone": "subtle",
|
|
188
|
+
"label": "Assistant is typing"
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
keywords:
|
|
192
|
+
- spinner
|
|
193
|
+
- loader
|
|
194
|
+
- loading
|
|
195
|
+
- indeterminate
|
|
196
|
+
- progress
|
|
197
|
+
- busy
|
|
198
|
+
- feedback
|
|
199
|
+
- circular
|
|
200
|
+
synonyms:
|
|
201
|
+
spinner:
|
|
202
|
+
- spinner
|
|
203
|
+
- loader
|
|
204
|
+
- progress
|
|
205
|
+
loader:
|
|
206
|
+
- spinner
|
|
207
|
+
- loading
|
|
208
|
+
- progress
|
|
209
|
+
loading:
|
|
210
|
+
- spinner
|
|
211
|
+
- loading
|
|
212
|
+
- progress
|
|
213
|
+
- skeleton
|
|
214
|
+
busy:
|
|
215
|
+
- spinner
|
|
216
|
+
- loading
|
|
217
|
+
indeterminate:
|
|
218
|
+
- spinner
|
|
219
|
+
- progress
|
|
220
|
+
saving:
|
|
221
|
+
- spinner
|
|
222
|
+
- loading
|
|
223
|
+
uploading:
|
|
224
|
+
- spinner
|
|
225
|
+
- loading
|
|
226
|
+
related:
|
|
227
|
+
- Progress
|
|
228
|
+
- Skeleton
|
|
229
|
+
- Button
|
|
230
|
+
- EmptyState
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
@scope (stack-ui) {
|
|
2
2
|
:where(:scope) {
|
|
3
|
-
--stack-align: center;
|
|
3
|
+
--stack-align-default: center;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
:scope {
|
|
7
7
|
box-sizing: border-box;
|
|
8
8
|
display: grid;
|
|
9
|
-
place-items: var(--stack-align);
|
|
9
|
+
place-items: var(--stack-align, var(--stack-align-default));
|
|
10
10
|
/* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
|
|
11
11
|
padding: var(--a-padding, 0);
|
|
12
12
|
margin: var(--a-margin, 0);
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/* Alignment */
|
|
22
|
-
:scope[align="center"] { --stack-align: center; }
|
|
23
|
-
:scope[align="top-left"] { --stack-align: start start; }
|
|
24
|
-
:scope[align="top-right"] { --stack-align: start end; }
|
|
25
|
-
:scope[align="bottom-left"] { --stack-align: end start; }
|
|
26
|
-
:scope[align="bottom-right"] { --stack-align: end end; }
|
|
27
|
-
:scope[align="top"] { --stack-align: start center; }
|
|
28
|
-
:scope[align="bottom"] { --stack-align: end center; }
|
|
29
|
-
:scope[align="left"] { --stack-align: center start; }
|
|
30
|
-
:scope[align="right"] { --stack-align: center end; }
|
|
22
|
+
:scope[align="center"] { --stack-align-default: center; }
|
|
23
|
+
:scope[align="top-left"] { --stack-align-default: start start; }
|
|
24
|
+
:scope[align="top-right"] { --stack-align-default: start end; }
|
|
25
|
+
:scope[align="bottom-left"] { --stack-align-default: end start; }
|
|
26
|
+
:scope[align="bottom-right"] { --stack-align-default: end end; }
|
|
27
|
+
:scope[align="top"] { --stack-align-default: start center; }
|
|
28
|
+
:scope[align="bottom"] { --stack-align-default: end center; }
|
|
29
|
+
:scope[align="left"] { --stack-align-default: center start; }
|
|
30
|
+
:scope[align="right"] { --stack-align-default: center end; }
|
|
31
31
|
}
|
package/components/stat/stat.css
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
:where(:scope) {
|
|
3
3
|
/* ── Tokens ──
|
|
4
4
|
Use size-responsive tokens so stat shrinks when parent card has size="sm". */
|
|
5
|
-
--stat-value-size: var(--a-title-size);
|
|
6
|
-
--stat-value-weight: var(--a-weight-bold);
|
|
7
|
-
--stat-value-fg: var(--a-fg-strong);
|
|
8
|
-
--stat-label-size: var(--a-ui-size);
|
|
9
|
-
--stat-label-fg: var(--a-fg);
|
|
10
|
-
--stat-change-size: var(--a-ui-size);
|
|
11
|
-
--stat-up-fg: var(--a-success-bg);
|
|
12
|
-
--stat-down-fg: var(--a-danger-bg);
|
|
13
|
-
--stat-icon-fg: var(--a-fg-muted);
|
|
5
|
+
--stat-value-size-default: var(--a-title-size);
|
|
6
|
+
--stat-value-weight-default: var(--a-weight-bold);
|
|
7
|
+
--stat-value-fg-default: var(--a-fg-strong);
|
|
8
|
+
--stat-label-size-default: var(--a-ui-size);
|
|
9
|
+
--stat-label-fg-default: var(--a-fg);
|
|
10
|
+
--stat-change-size-default: var(--a-ui-size);
|
|
11
|
+
--stat-up-fg-default: var(--a-success-bg);
|
|
12
|
+
--stat-down-fg-default: var(--a-danger-bg);
|
|
13
|
+
--stat-icon-fg-default: var(--a-fg-muted);
|
|
14
14
|
|
|
15
15
|
/* ── Spacing ── */
|
|
16
|
-
--stat-column-gap: var(--a-gap);
|
|
17
|
-
--stat-row-gap: var(--a-gap-sm);
|
|
16
|
+
--stat-column-gap-default: var(--a-gap);
|
|
17
|
+
--stat-row-gap-default: var(--a-gap-sm);
|
|
18
18
|
text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"label icon"
|
|
32
32
|
"value value"
|
|
33
33
|
"change change";
|
|
34
|
-
column-gap: var(--stat-column-gap);
|
|
35
|
-
row-gap: var(--stat-row-gap);
|
|
34
|
+
column-gap: var(--stat-column-gap, var(--stat-column-gap-default));
|
|
35
|
+
row-gap: var(--stat-row-gap, var(--stat-row-gap-default));
|
|
36
36
|
align-items: baseline;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
/* ── Label (eyebrow) ── */
|
|
72
72
|
[slot="label"] {
|
|
73
73
|
grid-area: label;
|
|
74
|
-
font-size: var(--stat-label-size);
|
|
75
|
-
color: var(--stat-label-fg);
|
|
74
|
+
font-size: var(--stat-label-size, var(--stat-label-size-default));
|
|
75
|
+
color: var(--stat-label-fg, var(--stat-label-fg-default));
|
|
76
76
|
line-height: 1.4;
|
|
77
77
|
min-width: 0;
|
|
78
78
|
overflow: hidden;
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
display: inline-flex;
|
|
87
87
|
align-items: center;
|
|
88
88
|
gap: 0.25em;
|
|
89
|
-
font-size: var(--stat-change-size);
|
|
89
|
+
font-size: var(--stat-change-size, var(--stat-change-size-default));
|
|
90
90
|
line-height: 1;
|
|
91
91
|
justify-self: start;
|
|
92
92
|
}
|
|
@@ -94,9 +94,9 @@
|
|
|
94
94
|
/* ── Value ── */
|
|
95
95
|
[slot="value"] {
|
|
96
96
|
grid-column: 1 / -1;
|
|
97
|
-
font-size: var(--stat-value-size);
|
|
98
|
-
font-weight: var(--stat-value-weight);
|
|
99
|
-
color: var(--stat-value-fg);
|
|
97
|
+
font-size: var(--stat-value-size, var(--stat-value-size-default));
|
|
98
|
+
font-weight: var(--stat-value-weight, var(--stat-value-weight-default));
|
|
99
|
+
color: var(--stat-value-fg, var(--stat-value-fg-default));
|
|
100
100
|
line-height: 1.2;
|
|
101
101
|
min-width: 0;
|
|
102
102
|
overflow: hidden;
|
|
@@ -107,29 +107,29 @@
|
|
|
107
107
|
/* Trend arrows via ::before */
|
|
108
108
|
:scope[trend="up"] [slot="change"]::before {
|
|
109
109
|
content: "\25B2";
|
|
110
|
-
color: var(--stat-up-fg);
|
|
110
|
+
color: var(--stat-up-fg, var(--stat-up-fg-default));
|
|
111
111
|
}
|
|
112
112
|
:scope[trend="up"] [slot="change"] {
|
|
113
|
-
color: var(--stat-up-fg);
|
|
113
|
+
color: var(--stat-up-fg, var(--stat-up-fg-default));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
:scope[trend="down"] [slot="change"]::before {
|
|
117
117
|
content: "\25BC";
|
|
118
|
-
color: var(--stat-down-fg);
|
|
118
|
+
color: var(--stat-down-fg, var(--stat-down-fg-default));
|
|
119
119
|
}
|
|
120
120
|
:scope[trend="down"] [slot="change"] {
|
|
121
|
-
color: var(--stat-down-fg);
|
|
121
|
+
color: var(--stat-down-fg, var(--stat-down-fg-default));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
:scope[trend="neutral"] [slot="change"] {
|
|
125
|
-
color: var(--stat-label-fg);
|
|
125
|
+
color: var(--stat-label-fg, var(--stat-label-fg-default));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/* ── Icon ── */
|
|
129
129
|
[slot="icon"] {
|
|
130
130
|
grid-area: icon;
|
|
131
131
|
align-self: start;
|
|
132
|
-
color: var(--stat-icon-fg);
|
|
132
|
+
color: var(--stat-icon-fg, var(--stat-icon-fg-default));
|
|
133
133
|
--a-icon-size: 1.25rem;
|
|
134
134
|
}
|
|
135
135
|
|