@adia-ai/web-modules 0.8.12 → 0.8.13

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,28 @@ 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('renders a Theme select with the 12 default identities by default', 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 select = tp.querySelector('select-ui[part="themes"]');
64
+ expect(select).toBeTruthy();
65
+ const values = [...select.querySelectorAll('option')].map((o) => o.value);
66
+ expect(values).toEqual(['', 'atlas', 'signal', 'terrarium', 'ledger', 'arcade', 'meridian', 'solstice', 'glacier', 'ember', 'botanica', 'vector', 'velour']);
67
+ });
68
+
69
+ it('[themes=""] suppresses the Theme row entirely', async () => {
70
+ const tp = mount('<theme-panel themes=""></theme-panel>');
71
+ await tick();
72
+ expect(tp.querySelector('[part="themes"]')).toBeNull();
73
+ });
74
+
75
+ it('[themes="…"] renders a Theme select with exactly the given slugs plus Default', async () => {
76
+ const tp = mount('<theme-panel themes="terrarium vector"></theme-panel>');
77
+ await tick();
78
+ const select = tp.querySelector('select-ui[part="themes"]');
79
+ expect(select).toBeTruthy();
80
+ const values = [...select.querySelectorAll('option')].map((o) => o.value);
81
+ expect(values).toEqual(['', 'terrarium', 'vector']);
67
82
  });
68
83
 
69
84
  it('renders only minimum surface by default (no sliders, presets, scheme)', async () => {
@@ -96,37 +111,41 @@ describe('theme-panel', () => {
96
111
  expect(tp.querySelector('[part="scheme"] segmented-ui')).toBeTruthy();
97
112
  });
98
113
 
99
- it('theme button click sets [theme] on the target (defaults to <html>)', async () => {
100
- const tp = mount('<theme-panel></theme-panel>');
114
+ it('selecting a theme sets [theme] on the target (defaults to <html>)', async () => {
115
+ const tp = mount('<theme-panel themes="atlas ledger terrarium"></theme-panel>');
101
116
  await tick();
102
- const oceanBtn = tp.querySelector('button-ui[data-theme-slug="ocean"]');
103
- oceanBtn.dispatchEvent(new Event('click', { bubbles: true }));
117
+ const select = tp.querySelector('select-ui[part="themes"]');
118
+ select.value = 'ledger';
119
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'ledger' } }));
104
120
  await tick();
105
- expect(document.documentElement.getAttribute('theme')).toBe('ocean');
106
- expect(tp.getAttribute('active-theme')).toBe('ocean');
121
+ expect(document.documentElement.getAttribute('theme')).toBe('ledger');
122
+ expect(tp.getAttribute('active-theme')).toBe('ledger');
107
123
  });
108
124
 
109
- it('theme button click for "default" clears [theme]', async () => {
110
- document.documentElement.setAttribute('theme', 'ocean');
111
- const tp = mount('<theme-panel></theme-panel>');
125
+ it('selecting the Default option clears [theme]', async () => {
126
+ document.documentElement.setAttribute('theme', 'ledger');
127
+ const tp = mount('<theme-panel themes="atlas ledger"></theme-panel>');
112
128
  await tick();
113
- const defaultBtn = tp.querySelector('button-ui[data-theme-slug="default"]');
114
- defaultBtn.dispatchEvent(new Event('click', { bubbles: true }));
129
+ const select = tp.querySelector('select-ui[part="themes"]');
130
+ select.value = '';
131
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '' } }));
115
132
  await tick();
116
133
  expect(document.documentElement.hasAttribute('theme')).toBe(false);
117
134
  expect(tp.getAttribute('active-theme')).toBe('');
118
135
  });
119
136
 
120
- it('fires theme-change event with source: "theme" on theme click', async () => {
121
- const tp = mount('<theme-panel></theme-panel>');
137
+ it('fires theme-change event with source: "theme" on selection', async () => {
138
+ const tp = mount('<theme-panel themes="terrarium"></theme-panel>');
122
139
  await tick();
123
140
  const onChange = vi.fn();
124
141
  tp.addEventListener('theme-change', onChange);
125
- tp.querySelector('button-ui[data-theme-slug="forest"]').dispatchEvent(new Event('click', { bubbles: true }));
142
+ const select = tp.querySelector('select-ui[part="themes"]');
143
+ select.value = 'terrarium';
144
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'terrarium' } }));
126
145
  await tick();
127
146
  expect(onChange).toHaveBeenCalled();
128
147
  const detail = onChange.mock.calls[0][0].detail;
129
- expect(detail.theme).toBe('forest');
148
+ expect(detail.theme).toBe('terrarium');
130
149
  expect(detail.source).toBe('theme');
131
150
  });
132
151
 
@@ -166,34 +185,55 @@ describe('theme-panel', () => {
166
185
  expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('1.25');
167
186
  });
168
187
 
169
- it('preset "default" (Reset) clears [theme]', async () => {
170
- document.documentElement.setAttribute('theme', 'ocean');
188
+ it('preset "default" only sets density/radius — it does NOT clear [theme]', async () => {
189
+ document.documentElement.setAttribute('theme', 'ledger');
171
190
  const tp = mount('<theme-panel parametric presets></theme-panel>');
172
191
  await tick();
173
- const reset = tp.querySelector('button-ui[data-preset="default"]');
174
- reset.dispatchEvent(new Event('click', { bubbles: true }));
192
+ const defaultBtn = tp.querySelector('button-ui[data-preset="default"]');
193
+ expect(defaultBtn.getAttribute('text')).toBe('Default');
194
+ defaultBtn.dispatchEvent(new Event('click', { bubbles: true }));
195
+ await tick();
196
+ expect(document.documentElement.getAttribute('theme')).toBe('ledger');
197
+ expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('1');
198
+ expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('1');
199
+ });
200
+
201
+ it('the Reset row is a dedicated bottom control, separate from the Preset row', async () => {
202
+ document.documentElement.setAttribute('theme', 'ledger');
203
+ document.documentElement.style.setProperty('--a-density', '1.2');
204
+ const tp = mount('<theme-panel presets></theme-panel>');
205
+ await tick();
206
+ const resetBtn = tp.querySelector('[part="reset-row"] button-ui[part="reset"]');
207
+ expect(resetBtn).toBeTruthy();
208
+ expect(resetBtn.getAttribute('text')).toBe('Reset');
209
+ resetBtn.dispatchEvent(new Event('click', { bubbles: true }));
175
210
  await tick();
176
211
  expect(document.documentElement.hasAttribute('theme')).toBe(false);
212
+ expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('');
177
213
  });
178
214
 
179
- it('[persist] writes localStorage keys on theme click', async () => {
180
- const tp = mount('<theme-panel persist></theme-panel>');
215
+ it('[persist] writes localStorage keys on theme selection', async () => {
216
+ const tp = mount('<theme-panel persist themes="atlas"></theme-panel>');
181
217
  await tick();
182
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
218
+ const select = tp.querySelector('select-ui[part="themes"]');
219
+ select.value = 'atlas';
220
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
183
221
  await tick();
184
- expect(localStorage.getItem('adia-theme-theme')).toBe('ocean');
222
+ expect(localStorage.getItem('adia-theme-theme')).toBe('atlas');
185
223
  });
186
224
 
187
225
  it('absence of [persist] does NOT write localStorage', async () => {
188
- const tp = mount('<theme-panel></theme-panel>');
226
+ const tp = mount('<theme-panel themes="atlas"></theme-panel>');
189
227
  await tick();
190
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
228
+ const select = tp.querySelector('select-ui[part="themes"]');
229
+ select.value = 'atlas';
230
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
191
231
  await tick();
192
232
  expect(localStorage.getItem('adia-theme-theme')).toBeNull();
193
233
  });
194
234
 
195
- it('reset() clears all state and emits source: "reset"', async () => {
196
- document.documentElement.setAttribute('theme', 'ocean');
235
+ it('reset() clears all state, resets the Theme select to Default, and emits source: "reset"', async () => {
236
+ document.documentElement.setAttribute('theme', 'ledger');
197
237
  document.documentElement.style.setProperty('--a-density', '1.2');
198
238
  const tp = mount('<theme-panel parametric persist></theme-panel>');
199
239
  await tick();
@@ -203,27 +243,31 @@ describe('theme-panel', () => {
203
243
  await tick();
204
244
  expect(document.documentElement.hasAttribute('theme')).toBe(false);
205
245
  expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('');
246
+ expect(tp.querySelector('select-ui[part="themes"]').value).toBe('');
206
247
  expect(onChange).toHaveBeenCalled();
207
248
  expect(onChange.mock.calls[0][0].detail.source).toBe('reset');
208
249
  });
209
250
 
210
251
  it('[storage-prefix] override changes LS key namespace', async () => {
211
- const tp = mount('<theme-panel persist storage-prefix="my-app-"></theme-panel>');
252
+ const tp = mount('<theme-panel persist storage-prefix="my-app-" themes="atlas"></theme-panel>');
212
253
  await tick();
213
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
254
+ const select = tp.querySelector('select-ui[part="themes"]');
255
+ select.value = 'atlas';
256
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
214
257
  await tick();
215
- expect(localStorage.getItem('my-app-theme')).toBe('ocean');
258
+ expect(localStorage.getItem('my-app-theme')).toBe('atlas');
216
259
  expect(localStorage.getItem('adia-theme-theme')).toBeNull();
217
260
  });
218
261
 
219
- it('apply({theme}) is equivalent to a theme click and emits source: "programmatic"', async () => {
262
+ it('apply({theme}) is equivalent to a theme selection, syncs the select, and emits source: "programmatic"', async () => {
220
263
  const tp = mount('<theme-panel></theme-panel>');
221
264
  await tick();
222
265
  const onChange = vi.fn();
223
266
  tp.addEventListener('theme-change', onChange);
224
- tp.apply({ theme: 'slate' });
267
+ tp.apply({ theme: 'velour' });
225
268
  await tick();
226
- expect(document.documentElement.getAttribute('theme')).toBe('slate');
269
+ expect(document.documentElement.getAttribute('theme')).toBe('velour');
270
+ expect(tp.querySelector('select-ui[part="themes"]').value).toBe('velour');
227
271
  expect(onChange.mock.calls[0][0].detail.source).toBe('programmatic');
228
272
  });
229
273
 
@@ -247,11 +291,13 @@ describe('theme-panel', () => {
247
291
  const preview = document.createElement('div');
248
292
  preview.id = 'preview';
249
293
  document.body.appendChild(preview);
250
- const tp = mount('<theme-panel target="#preview"></theme-panel>');
294
+ const tp = mount('<theme-panel target="#preview" themes="atlas"></theme-panel>');
251
295
  await tick();
252
- tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
296
+ const select = tp.querySelector('select-ui[part="themes"]');
297
+ select.value = 'atlas';
298
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'atlas' } }));
253
299
  await tick();
254
- expect(preview.getAttribute('theme')).toBe('ocean');
300
+ expect(preview.getAttribute('theme')).toBe('atlas');
255
301
  expect(document.documentElement.getAttribute('theme')).toBeNull();
256
302
  });
257
303
 
@@ -264,11 +310,110 @@ describe('theme-panel', () => {
264
310
  expect(mqlListeners.length).toBe(0);
265
311
  });
266
312
 
267
- it('restricted [themes] list renders only the listed slugs', async () => {
268
- const tp = mount('<theme-panel themes="default ocean"></theme-panel>');
313
+ it('tolerates unknown slugs renders an option and applies [theme] regardless', async () => {
314
+ const tp = mount('<theme-panel themes="brand-a"></theme-panel>');
315
+ await tick();
316
+ const select = tp.querySelector('select-ui[part="themes"]');
317
+ expect([...select.querySelectorAll('option')].map((o) => o.value)).toEqual(['', 'brand-a']);
318
+ select.value = 'brand-a';
319
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'brand-a' } }));
320
+ await tick();
321
+ expect(document.documentElement.getAttribute('theme')).toBe('brand-a');
322
+ });
323
+
324
+ it('[presets] labels its row "Preset", not "Density" — disambiguates from the Density slider row', async () => {
325
+ const tp = mount('<theme-panel parametric presets></theme-panel>');
326
+ await tick();
327
+ // The Density slider's label lives on <field-ui label="Density">; the
328
+ // section labels (Parametric/Preset/Scale) are standalone text-ui rows.
329
+ expect(tp.querySelector('field-ui[label="Density"] slider-ui[part="density"]')).toBeTruthy();
330
+ const sectionLabels = [...tp.querySelectorAll('text-ui[variant="label"]')].map((l) => l.textContent);
331
+ expect(sectionLabels).toContain('Preset');
332
+ expect(sectionLabels).not.toContain('Density');
333
+ });
334
+ });
335
+
336
+ describe('theme-panel — [register] Scale row (six [scale] tiers, supersedes verse/prose)', () => {
337
+ it('renders a select with the six canonical tiers plus a Default option', async () => {
338
+ const tp = mount('<theme-panel register></theme-panel>');
339
+ await tick();
340
+ const select = tp.querySelector('select-ui[part="scale"]');
341
+ expect(select).toBeTruthy();
342
+ const values = [...select.querySelectorAll('option')].map((o) => o.value);
343
+ expect(values).toEqual(['', 'ui-sm', 'ui-md', 'ui-lg', 'content-sm', 'content-md', 'content-lg']);
344
+ });
345
+
346
+ it('does not render the Scale row without [register]', async () => {
347
+ const tp = mount('<theme-panel presets></theme-panel>');
348
+ await tick();
349
+ expect(tp.querySelector('select-ui[part="scale"]')).toBeNull();
350
+ });
351
+
352
+ it('selecting a tier sets [scale="…"] on the target, never [verse]/[prose]', async () => {
353
+ const tp = mount('<theme-panel register></theme-panel>');
354
+ await tick();
355
+ const select = tp.querySelector('select-ui[part="scale"]');
356
+ select.value = 'content-md';
357
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'content-md' } }));
358
+ await tick();
359
+ expect(document.documentElement.getAttribute('scale')).toBe('content-md');
360
+ expect(document.documentElement.hasAttribute('verse')).toBe(false);
361
+ expect(document.documentElement.hasAttribute('prose')).toBe(false);
362
+ expect(tp.getAttribute('active-scale')).toBe('content-md');
363
+ });
364
+
365
+ it('selecting the Default option clears [scale] on the target', async () => {
366
+ const tp = mount('<theme-panel register></theme-panel>');
367
+ await tick();
368
+ const select = tp.querySelector('select-ui[part="scale"]');
369
+ select.value = 'ui-lg';
370
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'ui-lg' } }));
371
+ await tick();
372
+ expect(document.documentElement.getAttribute('scale')).toBe('ui-lg');
373
+
374
+ select.value = '';
375
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '' } }));
376
+ await tick();
377
+ expect(document.documentElement.hasAttribute('scale')).toBe(false);
378
+ expect(tp.getAttribute('active-scale')).toBe('');
379
+ });
380
+
381
+ it('apply({scale}) writes [scale] and emits source: "scale"', async () => {
382
+ const tp = mount('<theme-panel register></theme-panel>');
383
+ await tick();
384
+ const onChange = vi.fn();
385
+ tp.addEventListener('theme-change', onChange);
386
+ tp.apply({ scale: 'ui-sm' });
387
+ await tick();
388
+ expect(document.documentElement.getAttribute('scale')).toBe('ui-sm');
389
+ expect(onChange.mock.calls[0][0].detail.scale).toBe('ui-sm');
390
+ expect(onChange.mock.calls[0][0].detail.source).toBe('programmatic');
391
+ });
392
+
393
+ it('[persist] round-trips the scale tier through localStorage on reconnect', async () => {
394
+ const first = mount('<theme-panel register persist></theme-panel>');
395
+ await tick();
396
+ const select = first.querySelector('select-ui[part="scale"]');
397
+ select.value = 'content-lg';
398
+ select.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: 'content-lg' } }));
399
+ await tick();
400
+ expect(localStorage.getItem('adia-theme-scale')).toBe('content-lg');
401
+
402
+ first.remove();
403
+ document.documentElement.removeAttribute('scale');
404
+ const second = mount('<theme-panel register persist></theme-panel>');
405
+ await tick();
406
+ expect(document.documentElement.getAttribute('scale')).toBe('content-lg');
407
+ expect(second.getAttribute('active-scale')).toBe('content-lg');
408
+ });
409
+
410
+ it('reset() clears [scale] and resets the select to Default', async () => {
411
+ document.documentElement.setAttribute('scale', 'ui-md');
412
+ const tp = mount('<theme-panel register></theme-panel>');
413
+ await tick();
414
+ tp.reset();
269
415
  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']);
416
+ expect(document.documentElement.hasAttribute('scale')).toBe(false);
417
+ expect(tp.querySelector('select-ui[part="scale"]').value).toBe('');
273
418
  });
274
419
  });
@@ -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,6 +29,7 @@ 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
@@ -34,12 +38,17 @@ composes:
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
+ Always carries [scale="ui-md"] on itself (set in connected(), not
203
+ a consumer-configurable prop) — pins the panel's OWN chrome to the
204
+ base register, independent of whatever [scale] the Scale row is
205
+ driving on the target. Target defaults to :root, a DOM ancestor of
206
+ theme-panel, so [scale]'s inherited custom properties would
207
+ otherwise cascade into the panel's own controls too. A visual
208
+ no-op under the unscaled default (ui-md is byte-identical to it,
209
+ scale.css's own invariant) — only matters once an ancestor sets a
210
+ different tier. --a-density / --a-radius-k are pinned the same
211
+ way, declared directly in theme-panel.css (the "compact" preset's
212
+ values) rather than via an attribute — no [density] mechanism
213
+ exists in the framework the way [scale] does.
214
+ attribute: scale
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