@adia-ai/web-modules 0.8.12 → 0.8.14

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.
@@ -57,13 +57,39 @@ describe('theme-panel', () => {
57
57
  expect(customElements.get('theme-panel')).toBeDefined();
58
58
  });
59
59
 
60
- it('stamps internal DOM on connect (themes row)', async () => {
60
+ it('wraps its stamped content in <theme-provider scale="ui-md">, no [theme]', async () => {
61
61
  const tp = mount('<theme-panel></theme-panel>');
62
62
  await tick();
63
- const themes = tp.querySelector('[part="themes"]');
64
- expect(themes).toBeTruthy();
65
- // 8 default themes
66
- expect(themes.querySelectorAll('button-ui').length).toBe(8);
63
+ const provider = tp.querySelector(':scope > theme-provider');
64
+ expect(provider).toBeTruthy();
65
+ expect(provider.getAttribute('scale')).toBe('ui-md');
66
+ expect(provider.hasAttribute('theme')).toBe(false);
67
+ // Real content lives inside the provider, not as a direct theme-panel child.
68
+ expect(provider.querySelector('select-ui[part="themes"]')).toBeTruthy();
69
+ });
70
+
71
+ it('renders a Theme select with the 12 default identities by default', async () => {
72
+ const tp = mount('<theme-panel></theme-panel>');
73
+ await tick();
74
+ const select = tp.querySelector('select-ui[part="themes"]');
75
+ expect(select).toBeTruthy();
76
+ const values = [...select.querySelectorAll('option')].map((o) => o.value);
77
+ expect(values).toEqual(['', 'atlas', 'signal', 'terrarium', 'ledger', 'arcade', 'meridian', 'solstice', 'glacier', 'ember', 'botanica', 'vector', 'velour']);
78
+ });
79
+
80
+ it('[themes=""] suppresses the Theme row entirely', async () => {
81
+ const tp = mount('<theme-panel themes=""></theme-panel>');
82
+ await tick();
83
+ expect(tp.querySelector('[part="themes"]')).toBeNull();
84
+ });
85
+
86
+ it('[themes="…"] renders a Theme select with exactly the given slugs plus Default', async () => {
87
+ const tp = mount('<theme-panel themes="terrarium vector"></theme-panel>');
88
+ await tick();
89
+ const select = tp.querySelector('select-ui[part="themes"]');
90
+ expect(select).toBeTruthy();
91
+ const values = [...select.querySelectorAll('option')].map((o) => o.value);
92
+ expect(values).toEqual(['', 'terrarium', 'vector']);
67
93
  });
68
94
 
69
95
  it('renders only minimum surface by default (no sliders, presets, scheme)', async () => {
@@ -96,37 +122,41 @@ describe('theme-panel', () => {
96
122
  expect(tp.querySelector('[part="scheme"] segmented-ui')).toBeTruthy();
97
123
  });
98
124
 
99
- it('theme button click sets [theme] on the target (defaults to <html>)', async () => {
100
- const tp = mount('<theme-panel></theme-panel>');
125
+ it('selecting a theme sets [theme] on the target (defaults to <html>)', async () => {
126
+ const tp = mount('<theme-panel themes="atlas ledger terrarium"></theme-panel>');
101
127
  await tick();
102
- const oceanBtn = tp.querySelector('button-ui[data-theme-slug="ocean"]');
103
- oceanBtn.dispatchEvent(new Event('click', { bubbles: true }));
128
+ const select = tp.querySelector('select-ui[part="themes"]');
129
+ select.value = 'ledger';
130
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'ledger' } }));
104
131
  await tick();
105
- expect(document.documentElement.getAttribute('theme')).toBe('ocean');
106
- expect(tp.getAttribute('active-theme')).toBe('ocean');
132
+ expect(document.documentElement.getAttribute('theme')).toBe('ledger');
133
+ expect(tp.getAttribute('active-theme')).toBe('ledger');
107
134
  });
108
135
 
109
- it('theme button click for "default" clears [theme]', async () => {
110
- document.documentElement.setAttribute('theme', 'ocean');
111
- const tp = mount('<theme-panel></theme-panel>');
136
+ it('selecting the Default option clears [theme]', async () => {
137
+ document.documentElement.setAttribute('theme', 'ledger');
138
+ const tp = mount('<theme-panel themes="atlas ledger"></theme-panel>');
112
139
  await tick();
113
- const defaultBtn = tp.querySelector('button-ui[data-theme-slug="default"]');
114
- defaultBtn.dispatchEvent(new Event('click', { bubbles: true }));
140
+ const select = tp.querySelector('select-ui[part="themes"]');
141
+ select.value = '';
142
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '' } }));
115
143
  await tick();
116
144
  expect(document.documentElement.hasAttribute('theme')).toBe(false);
117
145
  expect(tp.getAttribute('active-theme')).toBe('');
118
146
  });
119
147
 
120
- it('fires theme-change event with source: "theme" on theme click', async () => {
121
- const tp = mount('<theme-panel></theme-panel>');
148
+ it('fires theme-change event with source: "theme" on selection', async () => {
149
+ const tp = mount('<theme-panel themes="terrarium"></theme-panel>');
122
150
  await tick();
123
151
  const onChange = vi.fn();
124
152
  tp.addEventListener('theme-change', onChange);
125
- tp.querySelector('button-ui[data-theme-slug="forest"]').dispatchEvent(new Event('click', { bubbles: true }));
153
+ const select = tp.querySelector('select-ui[part="themes"]');
154
+ select.value = 'terrarium';
155
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'terrarium' } }));
126
156
  await tick();
127
157
  expect(onChange).toHaveBeenCalled();
128
158
  const detail = onChange.mock.calls[0][0].detail;
129
- expect(detail.theme).toBe('forest');
159
+ expect(detail.theme).toBe('terrarium');
130
160
  expect(detail.source).toBe('theme');
131
161
  });
132
162
 
@@ -166,34 +196,55 @@ describe('theme-panel', () => {
166
196
  expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('1.25');
167
197
  });
168
198
 
169
- it('preset "default" (Reset) clears [theme]', async () => {
170
- document.documentElement.setAttribute('theme', 'ocean');
199
+ it('preset "default" only sets density/radius — it does NOT clear [theme]', async () => {
200
+ document.documentElement.setAttribute('theme', 'ledger');
171
201
  const tp = mount('<theme-panel parametric presets></theme-panel>');
172
202
  await tick();
173
- const reset = tp.querySelector('button-ui[data-preset="default"]');
174
- reset.dispatchEvent(new Event('click', { bubbles: true }));
203
+ const defaultBtn = tp.querySelector('button-ui[data-preset="default"]');
204
+ expect(defaultBtn.getAttribute('text')).toBe('Default');
205
+ defaultBtn.dispatchEvent(new Event('click', { bubbles: true }));
206
+ await tick();
207
+ expect(document.documentElement.getAttribute('theme')).toBe('ledger');
208
+ expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('1');
209
+ expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('1');
210
+ });
211
+
212
+ it('the Reset row is a dedicated bottom control, separate from the Preset row', async () => {
213
+ document.documentElement.setAttribute('theme', 'ledger');
214
+ document.documentElement.style.setProperty('--a-density', '1.2');
215
+ const tp = mount('<theme-panel presets></theme-panel>');
216
+ await tick();
217
+ const resetBtn = tp.querySelector('[part="reset-row"] button-ui[part="reset"]');
218
+ expect(resetBtn).toBeTruthy();
219
+ expect(resetBtn.getAttribute('text')).toBe('Reset');
220
+ resetBtn.dispatchEvent(new Event('click', { bubbles: true }));
175
221
  await tick();
176
222
  expect(document.documentElement.hasAttribute('theme')).toBe(false);
223
+ expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('');
177
224
  });
178
225
 
179
- it('[persist] writes localStorage keys on theme click', async () => {
180
- const tp = mount('<theme-panel persist></theme-panel>');
226
+ it('[persist] writes localStorage keys on theme selection', async () => {
227
+ const tp = mount('<theme-panel persist themes="atlas"></theme-panel>');
181
228
  await tick();
182
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
229
+ const select = tp.querySelector('select-ui[part="themes"]');
230
+ select.value = 'atlas';
231
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
183
232
  await tick();
184
- expect(localStorage.getItem('adia-theme-theme')).toBe('ocean');
233
+ expect(localStorage.getItem('adia-theme-theme')).toBe('atlas');
185
234
  });
186
235
 
187
236
  it('absence of [persist] does NOT write localStorage', async () => {
188
- const tp = mount('<theme-panel></theme-panel>');
237
+ const tp = mount('<theme-panel themes="atlas"></theme-panel>');
189
238
  await tick();
190
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
239
+ const select = tp.querySelector('select-ui[part="themes"]');
240
+ select.value = 'atlas';
241
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
191
242
  await tick();
192
243
  expect(localStorage.getItem('adia-theme-theme')).toBeNull();
193
244
  });
194
245
 
195
- it('reset() clears all state and emits source: "reset"', async () => {
196
- document.documentElement.setAttribute('theme', 'ocean');
246
+ it('reset() clears all state, resets the Theme select to Default, and emits source: "reset"', async () => {
247
+ document.documentElement.setAttribute('theme', 'ledger');
197
248
  document.documentElement.style.setProperty('--a-density', '1.2');
198
249
  const tp = mount('<theme-panel parametric persist></theme-panel>');
199
250
  await tick();
@@ -203,27 +254,31 @@ describe('theme-panel', () => {
203
254
  await tick();
204
255
  expect(document.documentElement.hasAttribute('theme')).toBe(false);
205
256
  expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('');
257
+ expect(tp.querySelector('select-ui[part="themes"]').value).toBe('');
206
258
  expect(onChange).toHaveBeenCalled();
207
259
  expect(onChange.mock.calls[0][0].detail.source).toBe('reset');
208
260
  });
209
261
 
210
262
  it('[storage-prefix] override changes LS key namespace', async () => {
211
- const tp = mount('<theme-panel persist storage-prefix="my-app-"></theme-panel>');
263
+ const tp = mount('<theme-panel persist storage-prefix="my-app-" themes="atlas"></theme-panel>');
212
264
  await tick();
213
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
265
+ const select = tp.querySelector('select-ui[part="themes"]');
266
+ select.value = 'atlas';
267
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
214
268
  await tick();
215
- expect(localStorage.getItem('my-app-theme')).toBe('ocean');
269
+ expect(localStorage.getItem('my-app-theme')).toBe('atlas');
216
270
  expect(localStorage.getItem('adia-theme-theme')).toBeNull();
217
271
  });
218
272
 
219
- it('apply({theme}) is equivalent to a theme click and emits source: "programmatic"', async () => {
273
+ it('apply({theme}) is equivalent to a theme selection, syncs the select, and emits source: "programmatic"', async () => {
220
274
  const tp = mount('<theme-panel></theme-panel>');
221
275
  await tick();
222
276
  const onChange = vi.fn();
223
277
  tp.addEventListener('theme-change', onChange);
224
- tp.apply({ theme: 'slate' });
278
+ tp.apply({ theme: 'velour' });
225
279
  await tick();
226
- expect(document.documentElement.getAttribute('theme')).toBe('slate');
280
+ expect(document.documentElement.getAttribute('theme')).toBe('velour');
281
+ expect(tp.querySelector('select-ui[part="themes"]').value).toBe('velour');
227
282
  expect(onChange.mock.calls[0][0].detail.source).toBe('programmatic');
228
283
  });
229
284
 
@@ -247,11 +302,13 @@ describe('theme-panel', () => {
247
302
  const preview = document.createElement('div');
248
303
  preview.id = 'preview';
249
304
  document.body.appendChild(preview);
250
- const tp = mount('<theme-panel target="#preview"></theme-panel>');
305
+ const tp = mount('<theme-panel target="#preview" themes="atlas"></theme-panel>');
251
306
  await tick();
252
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
307
+ const select = tp.querySelector('select-ui[part="themes"]');
308
+ select.value = 'atlas';
309
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
253
310
  await tick();
254
- expect(preview.getAttribute('theme')).toBe('ocean');
311
+ expect(preview.getAttribute('theme')).toBe('atlas');
255
312
  expect(document.documentElement.getAttribute('theme')).toBeNull();
256
313
  });
257
314
 
@@ -264,11 +321,111 @@ describe('theme-panel', () => {
264
321
  expect(mqlListeners.length).toBe(0);
265
322
  });
266
323
 
267
- it('restricted [themes] list renders only the listed slugs', async () => {
268
- const tp = mount('<theme-panel themes="default ocean"></theme-panel>');
324
+ it('tolerates unknown slugs renders an option and applies [theme] regardless', async () => {
325
+ const tp = mount('<theme-panel themes="brand-a"></theme-panel>');
326
+ await tick();
327
+ const select = tp.querySelector('select-ui[part="themes"]');
328
+ expect([...select.querySelectorAll('option')].map((o) => o.value)).toEqual(['', 'brand-a']);
329
+ select.value = 'brand-a';
330
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'brand-a' } }));
331
+ await tick();
332
+ expect(document.documentElement.getAttribute('theme')).toBe('brand-a');
333
+ });
334
+
335
+ it('[presets] labels its row "Preset", not "Density" — disambiguates from the Density slider row', async () => {
336
+ const tp = mount('<theme-panel parametric presets></theme-panel>');
337
+ await tick();
338
+ // The Density slider's label lives on slider-ui's own [label] attribute
339
+ // (its native header-row readout); the section labels (Parametric/
340
+ // Preset/Scale) are standalone text-ui rows.
341
+ expect(tp.querySelector('slider-ui[part="density"][label="Density"]')).toBeTruthy();
342
+ const sectionLabels = [...tp.querySelectorAll('text-ui[variant="label"]')].map((l) => l.textContent);
343
+ expect(sectionLabels).toContain('Preset');
344
+ expect(sectionLabels).not.toContain('Density');
345
+ });
346
+ });
347
+
348
+ describe('theme-panel — [register] Scale row (six [scale] tiers, supersedes verse/prose)', () => {
349
+ it('renders a select with the six canonical tiers plus a Default option', async () => {
350
+ const tp = mount('<theme-panel register></theme-panel>');
351
+ await tick();
352
+ const select = tp.querySelector('select-ui[part="scale"]');
353
+ expect(select).toBeTruthy();
354
+ const values = [...select.querySelectorAll('option')].map((o) => o.value);
355
+ expect(values).toEqual(['', 'ui-sm', 'ui-md', 'ui-lg', 'content-sm', 'content-md', 'content-lg']);
356
+ });
357
+
358
+ it('does not render the Scale row without [register]', async () => {
359
+ const tp = mount('<theme-panel presets></theme-panel>');
360
+ await tick();
361
+ expect(tp.querySelector('select-ui[part="scale"]')).toBeNull();
362
+ });
363
+
364
+ it('selecting a tier sets [scale="…"] on the target, never [verse]/[prose]', async () => {
365
+ const tp = mount('<theme-panel register></theme-panel>');
366
+ await tick();
367
+ const select = tp.querySelector('select-ui[part="scale"]');
368
+ select.value = 'content-md';
369
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'content-md' } }));
370
+ await tick();
371
+ expect(document.documentElement.getAttribute('scale')).toBe('content-md');
372
+ expect(document.documentElement.hasAttribute('verse')).toBe(false);
373
+ expect(document.documentElement.hasAttribute('prose')).toBe(false);
374
+ expect(tp.getAttribute('active-scale')).toBe('content-md');
375
+ });
376
+
377
+ it('selecting the Default option clears [scale] on the target', async () => {
378
+ const tp = mount('<theme-panel register></theme-panel>');
379
+ await tick();
380
+ const select = tp.querySelector('select-ui[part="scale"]');
381
+ select.value = 'ui-lg';
382
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'ui-lg' } }));
383
+ await tick();
384
+ expect(document.documentElement.getAttribute('scale')).toBe('ui-lg');
385
+
386
+ select.value = '';
387
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '' } }));
388
+ await tick();
389
+ expect(document.documentElement.hasAttribute('scale')).toBe(false);
390
+ expect(tp.getAttribute('active-scale')).toBe('');
391
+ });
392
+
393
+ it('apply({scale}) writes [scale] and emits source: "scale"', async () => {
394
+ const tp = mount('<theme-panel register></theme-panel>');
395
+ await tick();
396
+ const onChange = vi.fn();
397
+ tp.addEventListener('theme-change', onChange);
398
+ tp.apply({ scale: 'ui-sm' });
399
+ await tick();
400
+ expect(document.documentElement.getAttribute('scale')).toBe('ui-sm');
401
+ expect(onChange.mock.calls[0][0].detail.scale).toBe('ui-sm');
402
+ expect(onChange.mock.calls[0][0].detail.source).toBe('programmatic');
403
+ });
404
+
405
+ it('[persist] round-trips the scale tier through localStorage on reconnect', async () => {
406
+ const first = mount('<theme-panel register persist></theme-panel>');
407
+ await tick();
408
+ const select = first.querySelector('select-ui[part="scale"]');
409
+ select.value = 'content-lg';
410
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'content-lg' } }));
411
+ await tick();
412
+ expect(localStorage.getItem('adia-theme-scale')).toBe('content-lg');
413
+
414
+ first.remove();
415
+ document.documentElement.removeAttribute('scale');
416
+ const second = mount('<theme-panel register persist></theme-panel>');
417
+ await tick();
418
+ expect(document.documentElement.getAttribute('scale')).toBe('content-lg');
419
+ expect(second.getAttribute('active-scale')).toBe('content-lg');
420
+ });
421
+
422
+ it('reset() clears [scale] and resets the select to Default', async () => {
423
+ document.documentElement.setAttribute('scale', 'ui-md');
424
+ const tp = mount('<theme-panel register></theme-panel>');
425
+ await tick();
426
+ tp.reset();
269
427
  await tick();
270
- const buttons = [...tp.querySelectorAll('[part="themes"] button-ui')];
271
- expect(buttons.length).toBe(2);
272
- expect(buttons.map((b) => b.getAttribute('data-theme-slug'))).toEqual(['default', 'ocean']);
428
+ expect(document.documentElement.hasAttribute('scale')).toBe(false);
429
+ expect(tp.querySelector('select-ui[part="scale"]').value).toBe('');
273
430
  });
274
431
  });
@@ -14,9 +14,12 @@ description: |
14
14
  small attribute API.
15
15
 
16
16
  Drops into any consumer's <popover-ui slot="content"> (the canonical
17
- composition) or directly into a sidebar section. Eight named themes
18
- ship by default; section visibility flips on/off via boolean attributes
19
- ([parametric], [presets], [scheme-toggle]).
17
+ composition) or directly into a sidebar section. The 12 named identities
18
+ in styles/themes.css ship by default (a <select-ui> Theme row); pass
19
+ [themes="slug1 slug2"] to restrict, or "" to suppress the row entirely.
20
+ Section visibility flips on/off via boolean attributes ([parametric],
21
+ [presets], [register], [scheme-toggle]); a Reset row (own button, bottom
22
+ of the panel) always renders and clears everything via .reset().
20
23
 
21
24
  Promoted from the duplicated <div id="theme-panel"> block in site/ and
22
25
  playgrounds/admin-shell/ — see .claude/docs/specs/theme-panel-module.md.
@@ -26,20 +29,26 @@ description: |
26
29
  composes:
27
30
  - segmented-ui
28
31
  - segment-ui
32
+ - select-ui
29
33
  - text-ui
30
34
  - button-ui
31
35
  - divider-ui
32
- - field-ui
33
36
  - slider-ui
37
+ - theme-provider
34
38
  props:
35
39
  themes:
36
40
  description: |
37
- Space-separated list of theme slugs to render as buttons. Author
38
- may restrict ([themes="default ocean"]) or extend. Tolerant
39
- unknown slugs render a button; clicking applies [theme=slug]
40
- regardless of whether themes.css has a matching block.
41
+ Space-separated list of theme slugs to render in the Theme
42
+ <select-ui>. Defaults to the 12 named identities shipped in
43
+ styles/themes.css atlas, signal, terrarium, ledger, arcade,
44
+ meridian, solstice, glacier, ember, botanica, vector, velour
45
+ (each a real recolor + real Google/system font pairing, not a
46
+ decorative hue nudge — see that file's header). Pass "" to
47
+ suppress the Theme row entirely. Tolerant — unknown slugs still
48
+ render as an option; selecting one applies [theme=slug] regardless
49
+ of whether themes.css has a matching block.
41
50
  type: string
42
- default: "default ocean forest sunset lavender rose slate midnight"
51
+ default: "atlas signal terrarium ledger arcade meridian solstice glacier ember botanica vector velour"
43
52
  reflect: true
44
53
 
45
54
  parametric:
@@ -52,19 +61,24 @@ props:
52
61
 
53
62
  presets:
54
63
  description: |
55
- Renders the compact / reset / spacious preset row. Each preset
56
- applies a (density, radius) pair; "default" also clears the
57
- [theme] attribute.
64
+ Renders the compact / default / spacious preset row. Each preset
65
+ applies a (density, radius) pair only it does not touch [theme],
66
+ [scheme], or [scale]. Full state clearing lives in the panel's
67
+ always-rendered Reset row (bottom, own button, calls .reset()).
58
68
  type: boolean
59
69
  default: false
60
70
  reflect: true
61
71
 
62
72
  register:
63
73
  description: |
64
- Renders a "Scale Context" row (verse / regular / prose) a
65
- <segmented-ui> that toggles the [verse] / [prose] typographic register
66
- on the target element (regular clears both). Persisted like the other
67
- knobs when [persist] is set.
74
+ Renders a "Scale" row a <select-ui> offering the six ancestor
75
+ [scale] tiers (ui-sm | ui-md | ui-lg | content-sm | content-md |
76
+ content-lg, plus a Default option that clears the attribute)
77
+ writing [scale="<tier>"] directly on the target element, matching
78
+ theme-provider.class.js's own `scale` prop values exactly.
79
+ Supersedes the old verse/regular/prose register (deprecated
80
+ [verse]/[prose] aliases); this control never writes them.
81
+ Persisted like the other knobs when [persist] is set.
68
82
  type: boolean
69
83
  default: false
70
84
  reflect: true
@@ -90,9 +104,9 @@ props:
90
104
 
91
105
  storage-prefix:
92
106
  description: |
93
- Namespace for localStorage keys when [persist] is set. Four keys
107
+ Namespace for localStorage keys when [persist] is set. Five keys
94
108
  are written: {prefix}theme, {prefix}scheme, {prefix}density,
95
- {prefix}radius.
109
+ {prefix}radius, {prefix}scale.
96
110
  type: string
97
111
  default: "adia-theme-"
98
112
  reflect: true
@@ -155,14 +169,14 @@ props:
155
169
  reflect: true
156
170
  attribute: active-radius
157
171
 
158
- active-register:
172
+ active-scale:
159
173
  description: |
160
- Reflected — the active typographic register on the target
161
- (verse | prose); empty for regular.
174
+ Reflected — the active [scale] tier on the target element; empty
175
+ for the unscaled default (no [scale] attribute set).
162
176
  type: string
163
177
  default: ""
164
178
  reflect: true
165
- attribute: active-register
179
+ attribute: active-scale
166
180
 
167
181
  events:
168
182
  theme-change:
@@ -175,6 +189,7 @@ events:
175
189
  scheme: string
176
190
  density: number
177
191
  radius: number
192
+ scale: string
178
193
  source: string
179
194
 
180
195
  slots: {}
@@ -182,6 +197,21 @@ slots: {}
182
197
  states:
183
198
  - name: idle
184
199
  description: Default — panel rendered, awaiting input.
200
+ - name: fixed-scale
201
+ description: |
202
+ The panel's stamped content is wrapped in an internal
203
+ <theme-provider scale="ui-md"> (display:contents, adds no box) —
204
+ pins the panel's OWN chrome to the base size/scale register,
205
+ independent of whatever [scale]/--a-density the Scale select or
206
+ Density slider is driving on the target. Target defaults to :root,
207
+ a DOM ancestor of theme-panel, so those inherited custom properties
208
+ would otherwise cascade into the panel's own controls too. A visual
209
+ no-op under the unscaled default (ui-md is byte-identical to it,
210
+ scale.css's own invariant; --a-density/--a-radius-k pinned to the
211
+ "compact" preset's values, declared on the provider in
212
+ theme-panel.css) — only matters once an ancestor sets a different
213
+ tier. Deliberately does not set [theme] on the provider — the
214
+ panel's own accent color tracks whatever theme is active.
185
215
  - name: persisted
186
216
  description: |
187
217
  [persist] is set; selections write to localStorage under
@@ -207,8 +237,9 @@ a2ui:
207
237
  [persist] for playgrounds and embedded demos so state stays
208
238
  ephemeral. The default is ephemeral.
209
239
  - >-
210
- Add boolean attributes [parametric], [presets], [scheme-toggle]
211
- to enable sections; the minimum panel is the theme button grid.
240
+ Add boolean attributes [parametric], [presets], [register],
241
+ [scheme-toggle] to enable sections; the minimum panel is the theme
242
+ button grid (empty until [themes] is supplied — no default list).
212
243
 
213
244
  keywords:
214
245
  - theme
@@ -216,6 +247,7 @@ keywords:
216
247
  - palette
217
248
  - density
218
249
  - radius
250
+ - scale
219
251
  - scheme
220
252
  - dark-mode
221
253
  - light-mode