@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/components/accordion/accordion.css +2 -2
  3. package/components/action-list/action-list.css +2 -2
  4. package/components/agent-artifact/agent-artifact.css +31 -31
  5. package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
  6. package/components/agent-questions/agent-questions.css +57 -57
  7. package/components/agent-reasoning/agent-reasoning.css +62 -62
  8. package/components/agent-suggestions/agent-suggestions.css +4 -4
  9. package/components/agent-trace/agent-trace.css +53 -53
  10. package/components/alert/alert.css +41 -41
  11. package/components/avatar/avatar.css +27 -27
  12. package/components/badge/badge.css +27 -27
  13. package/components/block/block.css +16 -16
  14. package/components/breadcrumb/breadcrumb.css +23 -23
  15. package/components/button/button.css +101 -91
  16. package/components/calendar-grid/calendar-grid.a2ui.json +136 -0
  17. package/components/calendar-grid/calendar-grid.css +226 -0
  18. package/components/calendar-grid/calendar-grid.d.ts +37 -0
  19. package/components/calendar-grid/calendar-grid.js +17 -0
  20. package/components/calendar-grid/calendar-grid.yaml +116 -0
  21. package/components/calendar-grid/class.js +300 -0
  22. package/components/calendar-picker/calendar-picker.css +139 -139
  23. package/components/canvas/canvas.css +12 -12
  24. package/components/card/card.css +83 -83
  25. package/components/chart/chart.css +224 -224
  26. package/components/chart-legend/chart-legend.css +26 -26
  27. package/components/check/check.css +40 -40
  28. package/components/code/code.css +125 -125
  29. package/components/col/col.css +15 -15
  30. package/components/color-picker/color-picker.css +55 -55
  31. package/components/combobox/class.js +861 -0
  32. package/components/combobox/combobox.a2ui.json +363 -0
  33. package/components/combobox/combobox.css +244 -0
  34. package/components/combobox/combobox.d.ts +113 -0
  35. package/components/combobox/combobox.examples.md +59 -0
  36. package/components/combobox/combobox.js +17 -0
  37. package/components/combobox/combobox.test.js +181 -0
  38. package/components/combobox/combobox.yaml +369 -0
  39. package/components/command/command.css +90 -90
  40. package/components/date-range-picker/class.js +775 -0
  41. package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
  42. package/components/date-range-picker/date-range-picker.css +178 -0
  43. package/components/date-range-picker/date-range-picker.d.ts +82 -0
  44. package/components/date-range-picker/date-range-picker.examples.md +37 -0
  45. package/components/date-range-picker/date-range-picker.js +17 -0
  46. package/components/date-range-picker/date-range-picker.test.js +387 -0
  47. package/components/date-range-picker/date-range-picker.yaml +285 -0
  48. package/components/datetime-picker/class.js +706 -0
  49. package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
  50. package/components/datetime-picker/datetime-picker.css +150 -0
  51. package/components/datetime-picker/datetime-picker.d.ts +86 -0
  52. package/components/datetime-picker/datetime-picker.examples.md +46 -0
  53. package/components/datetime-picker/datetime-picker.js +17 -0
  54. package/components/datetime-picker/datetime-picker.test.js +454 -0
  55. package/components/datetime-picker/datetime-picker.yaml +332 -0
  56. package/components/demo-toggle/demo-toggle.css +27 -27
  57. package/components/description-list/description-list.css +18 -18
  58. package/components/divider/divider.css +24 -24
  59. package/components/embed/embed.css +6 -6
  60. package/components/empty-state/empty-state.css +27 -27
  61. package/components/feed/feed.css +12 -12
  62. package/components/field/field.css +37 -28
  63. package/components/field/field.test.js +32 -0
  64. package/components/fields/fields.css +5 -5
  65. package/components/grid/grid.css +5 -5
  66. package/components/heatmap/heatmap.css +63 -63
  67. package/components/icon/icon.css +12 -12
  68. package/components/image/image.css +14 -14
  69. package/components/index.js +8 -0
  70. package/components/input/input.css +66 -66
  71. package/components/inspector/inspector.css +6 -6
  72. package/components/integration-card/class.js +410 -0
  73. package/components/integration-card/integration-card.a2ui.json +268 -0
  74. package/components/integration-card/integration-card.css +169 -0
  75. package/components/integration-card/integration-card.d.ts +63 -0
  76. package/components/integration-card/integration-card.examples.md +41 -0
  77. package/components/integration-card/integration-card.js +17 -0
  78. package/components/integration-card/integration-card.test.js +306 -0
  79. package/components/integration-card/integration-card.yaml +280 -0
  80. package/components/kbd/kbd.css +32 -32
  81. package/components/link/link.css +12 -12
  82. package/components/list/list.css +8 -8
  83. package/components/list-window/class.js +688 -0
  84. package/components/list-window/list-window.a2ui.json +277 -0
  85. package/components/list-window/list-window.css +124 -0
  86. package/components/list-window/list-window.d.ts +84 -0
  87. package/components/list-window/list-window.examples.md +73 -0
  88. package/components/list-window/list-window.js +17 -0
  89. package/components/list-window/list-window.test.js +303 -0
  90. package/components/list-window/list-window.yaml +270 -0
  91. package/components/menu/menu.css +8 -8
  92. package/components/modal/modal.css +43 -43
  93. package/components/nav/nav.css +40 -40
  94. package/components/nav-group/nav-group.css +52 -52
  95. package/components/nav-item/nav-item.css +44 -44
  96. package/components/noodles/noodles.css +31 -31
  97. package/components/option-card/option-card.css +69 -69
  98. package/components/otp-input/otp-input.css +30 -30
  99. package/components/page/page.css +18 -18
  100. package/components/pagination/pagination.css +61 -61
  101. package/components/pane/pane.css +57 -57
  102. package/components/pipeline-status/pipeline-status.css +65 -65
  103. package/components/popover/popover.css +17 -17
  104. package/components/progress/progress.css +23 -23
  105. package/components/progress-row/progress-row.css +17 -17
  106. package/components/radio/radio.css +39 -39
  107. package/components/range/range.css +55 -55
  108. package/components/rating/rating.css +28 -28
  109. package/components/richtext/richtext.css +133 -133
  110. package/components/row/row.css +19 -19
  111. package/components/search/search.css +5 -5
  112. package/components/segment/segment.css +24 -24
  113. package/components/segmented/segmented.css +25 -25
  114. package/components/select/select.css +84 -84
  115. package/components/skeleton/skeleton.css +14 -14
  116. package/components/slider/slider.css +46 -46
  117. package/components/spinner/class.js +69 -0
  118. package/components/spinner/spinner.a2ui.json +197 -0
  119. package/components/spinner/spinner.css +165 -0
  120. package/components/spinner/spinner.d.ts +26 -0
  121. package/components/spinner/spinner.examples.md +26 -0
  122. package/components/spinner/spinner.js +17 -0
  123. package/components/spinner/spinner.test.js +234 -0
  124. package/components/spinner/spinner.yaml +230 -0
  125. package/components/stack/stack.css +11 -11
  126. package/components/stat/stat.css +25 -25
  127. package/components/step-progress/step-progress.css +20 -20
  128. package/components/stepper/stepper.css +29 -29
  129. package/components/stream/stream.css +12 -12
  130. package/components/swatch/swatch.css +68 -68
  131. package/components/swiper/swiper.css +57 -57
  132. package/components/switch/switch.css +52 -52
  133. package/components/table/class.js +9 -0
  134. package/components/table/table.a2ui.json +1 -1
  135. package/components/table/table.css +162 -162
  136. package/components/table/table.d.ts +1 -1
  137. package/components/table/table.test.js +53 -0
  138. package/components/table/table.yaml +13 -1
  139. package/components/table-toolbar/table-toolbar.css +32 -32
  140. package/components/tabs/tabs.css +51 -51
  141. package/components/tag/tag.css +48 -48
  142. package/components/text/text.css +44 -44
  143. package/components/textarea/textarea.css +46 -46
  144. package/components/time-picker/class.js +693 -0
  145. package/components/time-picker/time-picker.a2ui.json +267 -0
  146. package/components/time-picker/time-picker.css +122 -0
  147. package/components/time-picker/time-picker.d.ts +75 -0
  148. package/components/time-picker/time-picker.examples.md +35 -0
  149. package/components/time-picker/time-picker.js +17 -0
  150. package/components/time-picker/time-picker.test.js +287 -0
  151. package/components/time-picker/time-picker.yaml +256 -0
  152. package/components/timeline/timeline.css +50 -50
  153. package/components/toast/toast.css +58 -58
  154. package/components/toggle-group/toggle-group.css +6 -6
  155. package/components/toggle-scheme/toggle-scheme.css +2 -2
  156. package/components/toolbar/toolbar.css +17 -17
  157. package/components/tooltip/tooltip.css +2 -2
  158. package/components/tree/tree.css +37 -37
  159. package/components/upload/upload.css +49 -49
  160. package/dist/icons-manifest.js +3 -3
  161. package/dist/web-components.min.css +1 -1
  162. package/dist/web-components.min.js +121 -83
  163. package/package.json +1 -1
  164. 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
  }
@@ -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