@adia-ai/web-modules 0.8.11 → 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.
@@ -11,8 +11,18 @@ theme-panel {
11
11
  --theme-panel-gap: var(--a-space-3);
12
12
  --theme-panel-pad: var(--a-space-3);
13
13
  --theme-panel-min-width: 260px;
14
- --theme-panel-row-gap: var(--a-space-1);
15
14
  --theme-panel-preset-gap: var(--a-space-2);
15
+
16
+ /* The panel's own chrome is pinned to a fixed density register
17
+ ("compact" — 0.85/0.75), independent of whatever --a-density the
18
+ Density slider is currently driving on the TARGET. Both share
19
+ :root as a DOM ancestor by default, so without this pin the panel's
20
+ own controls balloon in lockstep with the page it's controlling
21
+ (verified live: dragging Density to 1.5 also enlarged the popover's
22
+ own buttons/sliders). [scale="ui-md"] (set on the host element in
23
+ JS) does the equivalent job for the [scale] register. */
24
+ --a-density: 0.85;
25
+ --a-radius-k: 0.75;
16
26
  }
17
27
 
18
28
  /* ── 2. Scoped layout ── */
@@ -34,14 +44,9 @@ theme-panel {
34
44
  flex: 1;
35
45
  }
36
46
 
37
- [part="themes"] {
38
- display: flex;
39
- gap: var(--theme-panel-row-gap);
40
- flex-wrap: wrap;
41
- }
42
-
43
- /* Labelled control row — "Density" / "Scale Context" label on the left,
44
- the preset buttons / register segmented pushed to the right. */
47
+ /* Labelled control row — "Theme" / "Preset" / "Scale" label on the left,
48
+ the select-ui / preset buttons pushed to the right. */
49
+ [part="theme-row"],
45
50
  [part="preset-row"] {
46
51
  display: flex;
47
52
  align-items: center;
@@ -55,5 +60,11 @@ theme-panel {
55
60
  flex-wrap: wrap;
56
61
  }
57
62
 
63
+ /* Reset — its own row at the very bottom (separated by a divider in
64
+ markup order), distinct from the Preset row's "Default" button. */
65
+ [part="reset-row"] {
66
+ display: flex;
67
+ }
68
+
58
69
  /* field-ui already provides its own label + layout; nothing extra here */
59
70
  }
@@ -6,9 +6,12 @@ light/dark switching, plus optional localStorage persistence behind a
6
6
  small attribute API.
7
7
 
8
8
  Drops into any consumer's <popover-ui slot="content"> (the canonical
9
- composition) or directly into a sidebar section. Eight named themes
10
- ship by default; section visibility flips on/off via boolean attributes
11
- ([parametric], [presets], [scheme-toggle]).
9
+ composition) or directly into a sidebar section. The 12 named identities
10
+ in styles/themes.css ship by default (a <select-ui> Theme row); pass
11
+ [themes="slug1 slug2"] to restrict, or "" to suppress the row entirely.
12
+ Section visibility flips on/off via boolean attributes ([parametric],
13
+ [presets], [register], [scheme-toggle]); a Reset row (own button, bottom
14
+ of the panel) always renders and clears everything via .reset().
12
15
 
13
16
  Promoted from the duplicated <div id="theme-panel"> block in site/ and
14
17
  playgrounds/admin-shell/ — see .claude/docs/specs/theme-panel-module.md.
@@ -29,6 +32,7 @@ export interface ThemePanelThemeChangeEventDetail {
29
32
  theme: string;
30
33
  density: string;
31
34
  radius: string;
35
+ scale: string;
32
36
  scheme: string;
33
37
  source: string;
34
38
  }
@@ -45,15 +49,20 @@ playgrounds and embedded demos do not silently mutate a user's
45
49
  docs-shell preferences.
46
50
  */
47
51
  persist: boolean;
48
- /** Renders the compact / reset / spacious preset row. Each preset
49
- applies a (density, radius) pair; "default" also clears the
50
- [theme] attribute.
52
+ /** Renders the compact / default / spacious preset row. Each preset
53
+ applies a (density, radius) pair only it does not touch [theme],
54
+ [scheme], or [scale]. Full state clearing lives in the panel's
55
+ always-rendered Reset row (bottom, own button, calls .reset()).
51
56
  */
52
57
  presets: boolean;
53
- /** Renders a "Scale Context" row (verse / regular / prose) a
54
- <segmented-ui> that toggles the [verse] / [prose] typographic register
55
- on the target element (regular clears both). Persisted like the other
56
- knobs when [persist] is set.
58
+ /** Renders a "Scale" row a <select-ui> offering the six ancestor
59
+ [scale] tiers (ui-sm | ui-md | ui-lg | content-sm | content-md |
60
+ content-lg, plus a Default option that clears the attribute)
61
+ writing [scale="<tier>"] directly on the target element, matching
62
+ theme-provider.class.js's own `scale` prop values exactly.
63
+ Supersedes the old verse/regular/prose register (deprecated
64
+ [verse]/[prose] aliases); this control never writes them.
65
+ Persisted like the other knobs when [persist] is set.
57
66
  */
58
67
  register: boolean;
59
68
  /** Initial color-scheme. "auto" follows prefers-color-scheme via a
@@ -67,10 +76,15 @@ an explicit light or dark choice.
67
76
  demos.
68
77
  */
69
78
  target: string;
70
- /** Space-separated list of theme slugs to render as buttons. Author
71
- may restrict ([themes="default ocean"]) or extend. Tolerant
72
- unknown slugs render a button; clicking applies [theme=slug]
73
- regardless of whether themes.css has a matching block.
79
+ /** Space-separated list of theme slugs to render in the Theme
80
+ <select-ui>. Defaults to the 12 named identities shipped in
81
+ styles/themes.css atlas, signal, terrarium, ledger, arcade,
82
+ meridian, solstice, glacier, ember, botanica, vector, velour
83
+ (each a real recolor + real Google/system font pairing, not a
84
+ decorative hue nudge — see that file's header). Pass "" to
85
+ suppress the Theme row entirely. Tolerant — unknown slugs still
86
+ render as an option; selecting one applies [theme=slug] regardless
87
+ of whether themes.css has a matching block.
74
88
  */
75
89
  themes: string;
76
90
 
@@ -10,9 +10,20 @@
10
10
  * in site/ and playgrounds/admin-shell/.
11
11
  *
12
12
  * Author-supplied attributes (read once at connect):
13
- * [themes="slug1 slug2 …"] space-separated theme list; default = 8 named
14
- * [parametric] renders density + radius sliders
15
- * [presets] renders compact / reset / spacious preset row
13
+ * [themes="slug1 slug2 …"] space-separated theme list; default = the
14
+ * 12 named identities shipped in
15
+ * styles/themes.css (atlas, signal,
16
+ * terrarium, ledger, arcade, meridian,
17
+ * solstice, glacier, ember, botanica,
18
+ * vector, velour). Rendered as a <select-ui>
19
+ * (Theme row); pass "" to suppress the row
20
+ * entirely.
21
+ * [parametric] renders the Density slider row (continuous --a-density)
22
+ * [presets] renders the Preset row (compact / default / spacious
23
+ * density+radius quick-picks — does not touch theme/scheme/scale)
24
+ * [register] renders the Scale row — one of the six
25
+ * [scale] tiers (ui-sm/ui-md/ui-lg/
26
+ * content-sm/content-md/content-lg)
16
27
  * [scheme-toggle] renders an integrated light/dark segmented-ui
17
28
  * [persist] writes selections to localStorage
18
29
  * [storage-prefix="adia-theme-"] LS key namespace
@@ -24,14 +35,16 @@
24
35
  * [active-scheme] resolved scheme (auto collapsed to light|dark)
25
36
  * [active-density] mirrors current --a-density on target
26
37
  * [active-radius] mirrors current --a-radius-k on target
38
+ * [active-scale] current [scale] tier on target; empty for none
27
39
  *
28
40
  * Events:
29
41
  * theme-change — bubbles. detail: { theme, scheme, density, radius, source }
30
- * source is one of: 'theme'|'slider'|'preset'|'scheme'|'reset'|'programmatic'
42
+ * source is one of: 'theme'|'slider'|'preset'|'scale'|'scheme'|'reset'|'programmatic'
31
43
  *
32
44
  * Public methods:
33
- * .apply({theme?, scheme?, density?, radius?}) — programmatic write
34
- * .reset() — clear all state + emit reset event
45
+ * .apply({theme?, scheme?, density?, radius?, scale?}) — programmatic write
46
+ * .reset() — clear all state + emit reset event; also wired to the
47
+ * panel's own Reset row (own button, bottom, always rendered)
35
48
  *
36
49
  * The element is light-DOM and stamps its internals imperatively in
37
50
  * connected(). External CSS / JS reacts via reflected attributes, e.g.
@@ -40,12 +53,23 @@
40
53
 
41
54
  import { UIElement } from '../../../web-components/core/element.js';
42
55
 
43
- const DEFAULT_THEMES = ['default', 'ocean', 'forest', 'sunset', 'lavender', 'rose', 'slate', 'midnight'];
56
+ // The 12 named identities shipped in styles/themes.css (real recolor +
57
+ // real typeface pairing per theme, not a decorative hue nudge) — see that
58
+ // file's header for the color/typography mechanism. A consumer can still
59
+ // override with [themes="slug1 slug2 …"] to offer a different set.
60
+ const DEFAULT_THEMES = [
61
+ 'atlas', 'signal', 'terrarium', 'ledger', 'arcade', 'meridian',
62
+ 'solstice', 'glacier', 'ember', 'botanica', 'vector', 'velour',
63
+ ];
44
64
  const PRESETS = {
45
65
  compact: { density: 0.85, radius: 0.75 },
46
- default: { density: 1, radius: 1, clearTheme: true },
66
+ default: { density: 1, radius: 1 },
47
67
  spacious: { density: 1.15, radius: 1.25 },
48
68
  };
69
+ // The six ancestor [scale] tiers (styles/scale.css) — matches
70
+ // theme-provider.class.js's SCALE_TIERS exactly. '' = no override (the
71
+ // unscaled base register).
72
+ const SCALE_TIERS = ['', 'ui-sm', 'ui-md', 'ui-lg', 'content-sm', 'content-md', 'content-lg'];
49
73
 
50
74
  const titleCase = (s) => s.length ? s[0].toUpperCase() + s.slice(1) : s;
51
75
 
@@ -64,7 +88,7 @@ class ThemePanel extends UIElement {
64
88
  activeDensity: { type: String, default: '', reflect: true, attribute: 'active-density' },
65
89
  activeRadius: { type: String, default: '', reflect: true, attribute: 'active-radius' },
66
90
  register: { type: Boolean, default: false, reflect: true },
67
- activeRegister:{ type: String, default: '', reflect: true, attribute: 'active-register' },
91
+ activeScale: { type: String, default: '', reflect: true, attribute: 'active-scale' },
68
92
  };
69
93
 
70
94
  static template = () => null;
@@ -75,11 +99,22 @@ class ThemePanel extends UIElement {
75
99
  #densityEl = null;
76
100
  #radiusEl = null;
77
101
  #schemeEl = null;
78
- #registerEl = null;
102
+ #scaleEl = null;
103
+ #themeEl = null;
104
+ #resetEl = null;
79
105
  #stamped = false;
80
106
 
81
107
  connected() {
82
108
  if (this.#stamped) return;
109
+ // Pin the panel's OWN chrome to the base [scale] register, independent
110
+ // of whatever [scale] the Scale row is driving on the target — target
111
+ // defaults to :root, a DOM ancestor of theme-panel, so [scale] tokens
112
+ // (inherited custom properties) would otherwise cascade into the
113
+ // panel's own controls too (verified live: switching Scale to
114
+ // content-lg also enlarged the popover's own labels/select). ui-md is
115
+ // byte-identical to the unscaled default (scale.css's own invariant),
116
+ // so this is a no-op visually until an ancestor sets a DIFFERENT tier.
117
+ this.setAttribute('scale', 'ui-md');
83
118
  this.#stamp();
84
119
  this.#stamped = true;
85
120
  this.#initState();
@@ -113,24 +148,24 @@ class ThemePanel extends UIElement {
113
148
  target.style.removeProperty('--a-density');
114
149
  target.style.removeProperty('--a-radius-k');
115
150
  target.style.removeProperty('color-scheme');
116
- target.removeAttribute('verse');
117
- target.removeAttribute('prose');
151
+ target.removeAttribute('scale');
118
152
  if (this.persist) {
119
- for (const k of ['theme', 'scheme', 'density', 'radius', 'register']) {
153
+ for (const k of ['theme', 'scheme', 'density', 'radius', 'scale']) {
120
154
  try { localStorage.removeItem(`${this.storagePrefix}${k}`); } catch {}
121
155
  }
122
156
  }
123
157
  this.activeTheme = '';
124
158
  this.activeDensity = '';
125
159
  this.activeRadius = '';
126
- this.activeRegister = '';
160
+ this.activeScale = '';
127
161
  // Resolved scheme falls back to prefers-color-scheme
128
162
  const resolved = this.#resolvePrefersScheme();
129
163
  this.activeScheme = resolved;
130
164
  if (this.#schemeEl) this.#schemeEl.value = resolved;
131
165
  if (this.#densityEl) this.#densityEl.value = 1;
132
166
  if (this.#radiusEl) this.#radiusEl.value = 1;
133
- if (this.#registerEl) this.#registerEl.value = 'regular';
167
+ if (this.#scaleEl) this.#scaleEl.value = '';
168
+ if (this.#themeEl) this.#themeEl.value = '';
134
169
  this.#emit('reset');
135
170
  }
136
171
 
@@ -157,24 +192,40 @@ class ThemePanel extends UIElement {
157
192
  this.#schemeEl = seg;
158
193
  }
159
194
 
160
- // Theme button row
161
- const themeLabel = document.createElement('text-ui');
162
- themeLabel.setAttribute('variant', 'label');
163
- themeLabel.textContent = 'Theme';
164
- frag.appendChild(themeLabel);
165
-
166
- const themesRow = document.createElement('div');
167
- themesRow.setAttribute('part', 'themes');
195
+ // Theme row — a <select-ui> (matches the Scale row's control pattern)
196
+ // offering every slug in [themes], only when that list is non-empty.
168
197
  const themeSlugs = (this.themes || '').trim().split(/\s+/).filter(Boolean);
169
- for (const slug of themeSlugs) {
170
- const btn = document.createElement('button-ui');
171
- btn.setAttribute('data-theme-slug', slug);
172
- btn.setAttribute('text', titleCase(slug));
173
- btn.setAttribute('variant', 'outline');
174
- btn.setAttribute('size', 'sm');
175
- themesRow.appendChild(btn);
198
+ if (themeSlugs.length) {
199
+ const themeWrap = document.createElement('div');
200
+ themeWrap.setAttribute('part', 'theme-row');
201
+ const themeLabel = document.createElement('text-ui');
202
+ themeLabel.setAttribute('variant', 'label');
203
+ themeLabel.textContent = 'Theme';
204
+ themeWrap.appendChild(themeLabel);
205
+
206
+ const select = document.createElement('select-ui');
207
+ select.setAttribute('part', 'themes');
208
+ select.setAttribute('size', 'sm');
209
+ select.id = `${this.#uid}-theme`;
210
+ // themeSlugs is attribute/A2UI-driven and tolerant of unknown values
211
+ // (theme-panel.d.ts's documented contract) — build <option> elements
212
+ // via the DOM rather than interpolating slugs into innerHTML, so a
213
+ // slug containing `"` or `<` can't break out of the option markup.
214
+ const mkOption = (value, label) => {
215
+ const opt = document.createElement('option');
216
+ opt.value = value;
217
+ opt.textContent = label;
218
+ return opt;
219
+ };
220
+ select.append(
221
+ mkOption('', 'Default'),
222
+ ...themeSlugs.map((slug) => mkOption(slug, titleCase(slug))),
223
+ );
224
+ select.setAttribute('value', '');
225
+ themeWrap.appendChild(select);
226
+ frag.appendChild(themeWrap);
227
+ this.#themeEl = select;
176
228
  }
177
- frag.appendChild(themesRow);
178
229
 
179
230
  // Optional parametric block
180
231
  if (this.parametric) {
@@ -217,11 +268,14 @@ class ThemePanel extends UIElement {
217
268
  frag.appendChild(document.createElement('divider-ui'));
218
269
 
219
270
  if (this.presets) {
271
+ // Labelled "Preset" (not "Density") — it's a combined density+radius
272
+ // quick-pick, distinct from the Density slider above; sharing the
273
+ // "Density" label with the slider row read as two Density rows.
220
274
  const presetWrap = document.createElement('div');
221
275
  presetWrap.setAttribute('part', 'preset-row');
222
276
  const presetLabel = document.createElement('text-ui');
223
277
  presetLabel.setAttribute('variant', 'label');
224
- presetLabel.textContent = 'Density';
278
+ presetLabel.textContent = 'Preset';
225
279
  presetWrap.appendChild(presetLabel);
226
280
 
227
281
  const presetsRow = document.createElement('div');
@@ -229,7 +283,7 @@ class ThemePanel extends UIElement {
229
283
  for (const name of ['compact', 'default', 'spacious']) {
230
284
  const btn = document.createElement('button-ui');
231
285
  btn.setAttribute('data-preset', name);
232
- btn.setAttribute('text', name === 'default' ? 'Reset' : titleCase(name));
286
+ btn.setAttribute('text', titleCase(name));
233
287
  btn.setAttribute('variant', 'outline');
234
288
  btn.setAttribute('size', 'sm');
235
289
  presetsRow.appendChild(btn);
@@ -238,46 +292,72 @@ class ThemePanel extends UIElement {
238
292
  frag.appendChild(presetWrap);
239
293
  }
240
294
 
241
- // Scale context verse / regular / prose typographic register on the target
295
+ // Scale — the six ancestor [scale] tiers (styles/scale.css) applied
296
+ // directly to target, matching theme-provider.class.js's own `scale`
297
+ // prop values exactly. Supersedes the old verse/regular/prose row —
298
+ // [verse]/[prose] are deprecated aliases now, not something a NEW
299
+ // control should write.
242
300
  if (this.register) {
243
- const regWrap = document.createElement('div');
244
- regWrap.setAttribute('part', 'preset-row');
245
- const regLabel = document.createElement('text-ui');
246
- regLabel.setAttribute('variant', 'label');
247
- regLabel.textContent = 'Scale Context';
248
- regWrap.appendChild(regLabel);
249
-
250
- const seg = document.createElement('segmented-ui');
251
- seg.setAttribute('part', 'register');
252
- seg.setAttribute('size', 'sm');
253
- seg.id = `${this.#uid}-register`;
254
- for (const name of ['verse', 'regular', 'prose']) {
255
- const s = document.createElement('segment-ui');
256
- s.setAttribute('value', name);
257
- s.setAttribute('text', name);
258
- seg.appendChild(s);
259
- }
260
- seg.setAttribute('value', 'regular');
261
- regWrap.appendChild(seg);
262
- frag.appendChild(regWrap);
263
- this.#registerEl = seg;
301
+ const scaleWrap = document.createElement('div');
302
+ scaleWrap.setAttribute('part', 'preset-row');
303
+ const scaleLabel = document.createElement('text-ui');
304
+ scaleLabel.setAttribute('variant', 'label');
305
+ scaleLabel.textContent = 'Scale';
306
+ scaleWrap.appendChild(scaleLabel);
307
+
308
+ const select = document.createElement('select-ui');
309
+ select.setAttribute('part', 'scale');
310
+ select.setAttribute('size', 'sm');
311
+ select.id = `${this.#uid}-scale`;
312
+ select.innerHTML = SCALE_TIERS
313
+ .map((tier) => `<option value="${tier}">${tier || 'Default'}</option>`)
314
+ .join('');
315
+ select.setAttribute('value', '');
316
+ scaleWrap.appendChild(select);
317
+ frag.appendChild(scaleWrap);
318
+ this.#scaleEl = select;
264
319
  }
265
320
  }
266
321
 
322
+ // Reset — its own row at the very bottom, separated by a divider.
323
+ // Distinct from the Preset row's "Default" button: Default only sets
324
+ // density/radius; Reset clears everything (theme, scheme, density,
325
+ // radius, scale) via the public .reset() method.
326
+ frag.appendChild(document.createElement('divider-ui'));
327
+ const resetRow = document.createElement('div');
328
+ resetRow.setAttribute('part', 'reset-row');
329
+ const resetBtn = document.createElement('button-ui');
330
+ resetBtn.setAttribute('part', 'reset');
331
+ resetBtn.setAttribute('text', 'Reset');
332
+ resetBtn.setAttribute('variant', 'ghost');
333
+ resetBtn.setAttribute('size', 'sm');
334
+ resetRow.appendChild(resetBtn);
335
+ frag.appendChild(resetRow);
336
+ this.#resetEl = resetBtn;
337
+
267
338
  this.appendChild(frag);
268
339
  this.#wireListeners();
269
340
  }
270
341
 
271
342
  #wireListeners() {
272
- // Theme buttonsfire 'press' (button-ui), fallback to 'click' for tests
273
- for (const btn of this.querySelectorAll('[part="themes"] button-ui')) {
274
- const slug = btn.getAttribute('data-theme-slug');
275
- const handler = () => this.#onThemeClick(slug);
276
- btn.addEventListener('press', handler);
277
- btn.addEventListener('click', handler);
343
+ // Theme selectfires 'change' (select-ui)
344
+ if (this.#themeEl) {
345
+ const handler = (e) => {
346
+ const v = e.detail?.value ?? this.#themeEl.value;
347
+ this.#onThemeClick(v);
348
+ };
349
+ this.#themeEl.addEventListener('change', handler);
350
+ this.#cleanups.push(() => this.#themeEl.removeEventListener('change', handler));
351
+ }
352
+
353
+ // Reset button
354
+ if (this.#resetEl) {
355
+ const handler = () => this.reset();
356
+ this.#resetEl.addEventListener('press', handler);
357
+ this.#resetEl.addEventListener('click', handler);
278
358
  this.#cleanups.push(() => {
279
- btn.removeEventListener('press', handler);
280
- btn.removeEventListener('click', handler);
359
+ this.#resetEl.removeEventListener('press', handler);
360
+ this.#resetEl.removeEventListener('click', handler);
281
361
  });
282
362
  }
283
363
 
@@ -315,14 +395,14 @@ class ThemePanel extends UIElement {
315
395
  this.#cleanups.push(() => this.#schemeEl.removeEventListener('change', handler));
316
396
  }
317
397
 
318
- // Scale-context register
319
- if (this.#registerEl) {
398
+ // Scale
399
+ if (this.#scaleEl) {
320
400
  const handler = (e) => {
321
- const v = e.detail?.value ?? this.#registerEl.value;
322
- this.#apply({ register: v }, 'register');
401
+ const v = e.detail?.value ?? this.#scaleEl.value;
402
+ this.#apply({ scale: v }, 'scale');
323
403
  };
324
- this.#registerEl.addEventListener('change', handler);
325
- this.#cleanups.push(() => this.#registerEl.removeEventListener('change', handler));
404
+ this.#scaleEl.addEventListener('change', handler);
405
+ this.#cleanups.push(() => this.#scaleEl.removeEventListener('change', handler));
326
406
  }
327
407
  }
328
408
 
@@ -363,15 +443,6 @@ class ThemePanel extends UIElement {
363
443
  #onPresetClick(name) {
364
444
  const preset = PRESETS[name];
365
445
  if (!preset) return;
366
- if (preset.clearTheme) {
367
- // "Reset" preset — clears [theme] too
368
- const target = this.#resolveTarget();
369
- target.removeAttribute('theme');
370
- this.activeTheme = '';
371
- if (this.persist) {
372
- try { localStorage.removeItem(`${this.storagePrefix}theme`); } catch {}
373
- }
374
- }
375
446
  this.#apply({ density: preset.density, radius: preset.radius }, 'preset');
376
447
  if (this.#densityEl) this.#densityEl.value = preset.density;
377
448
  if (this.#radiusEl) this.#radiusEl.value = preset.radius;
@@ -396,6 +467,7 @@ class ThemePanel extends UIElement {
396
467
  else localStorage.removeItem(`${this.storagePrefix}theme`);
397
468
  } catch {}
398
469
  }
470
+ if (this.#themeEl) this.#themeEl.value = this.activeTheme;
399
471
  }
400
472
 
401
473
  if (partial.density !== undefined) {
@@ -435,18 +507,22 @@ class ThemePanel extends UIElement {
435
507
  if (this.#schemeEl) this.#schemeEl.value = this.activeScheme;
436
508
  }
437
509
 
438
- if (partial.register !== undefined) {
439
- const r = partial.register;
440
- target.toggleAttribute('verse', r === 'verse');
441
- target.toggleAttribute('prose', r === 'prose');
442
- this.activeRegister = (r === 'verse' || r === 'prose') ? r : '';
510
+ if (partial.scale !== undefined) {
511
+ const s = partial.scale;
512
+ if (s && SCALE_TIERS.includes(s)) {
513
+ target.setAttribute('scale', s);
514
+ this.activeScale = s;
515
+ } else {
516
+ target.removeAttribute('scale');
517
+ this.activeScale = '';
518
+ }
443
519
  if (this.persist) {
444
520
  try {
445
- if (this.activeRegister) localStorage.setItem(`${this.storagePrefix}register`, this.activeRegister);
446
- else localStorage.removeItem(`${this.storagePrefix}register`);
521
+ if (this.activeScale) localStorage.setItem(`${this.storagePrefix}scale`, this.activeScale);
522
+ else localStorage.removeItem(`${this.storagePrefix}scale`);
447
523
  } catch {}
448
524
  }
449
- if (this.#registerEl) this.#registerEl.value = r;
525
+ if (this.#scaleEl) this.#scaleEl.value = this.activeScale;
450
526
  }
451
527
 
452
528
  this.#emit(source);
@@ -460,6 +536,7 @@ class ThemePanel extends UIElement {
460
536
  scheme: this.activeScheme,
461
537
  density: this.activeDensity ? parseFloat(this.activeDensity) : 1,
462
538
  radius: this.activeRadius ? parseFloat(this.activeRadius) : 1,
539
+ scale: this.activeScale,
463
540
  source,
464
541
  },
465
542
  }));
@@ -469,7 +546,7 @@ class ThemePanel extends UIElement {
469
546
 
470
547
  #initState() {
471
548
  const target = this.#resolveTarget();
472
- let theme, scheme, density, radius, register;
549
+ let theme, scheme, density, radius, scale;
473
550
 
474
551
  if (this.persist) {
475
552
  try {
@@ -479,7 +556,7 @@ class ThemePanel extends UIElement {
479
556
  const r = localStorage.getItem(`${this.storagePrefix}radius`);
480
557
  density = d != null ? parseFloat(d) : undefined;
481
558
  radius = r != null ? parseFloat(r) : undefined;
482
- register = localStorage.getItem(`${this.storagePrefix}register`) || '';
559
+ scale = localStorage.getItem(`${this.storagePrefix}scale`) || '';
483
560
  } catch {}
484
561
  }
485
562
 
@@ -490,6 +567,7 @@ class ThemePanel extends UIElement {
490
567
  } else {
491
568
  this.activeTheme = target.getAttribute('theme') || '';
492
569
  }
570
+ if (this.#themeEl) this.#themeEl.value = this.activeTheme;
493
571
 
494
572
  // Apply scheme
495
573
  if (scheme === 'light' || scheme === 'dark') {
@@ -532,13 +610,12 @@ class ThemePanel extends UIElement {
532
610
  this.activeRadius = String(r);
533
611
  }
534
612
 
535
- // Apply scale-context register
536
- if (this.#registerEl) {
537
- const r = (register === 'verse' || register === 'prose') ? register : 'regular';
538
- target.toggleAttribute('verse', r === 'verse');
539
- target.toggleAttribute('prose', r === 'prose');
540
- this.#registerEl.value = r;
541
- this.activeRegister = (r === 'regular') ? '' : r;
613
+ // Apply scale
614
+ if (this.#scaleEl) {
615
+ const s = SCALE_TIERS.includes(scale) ? scale : (target.getAttribute('scale') || '');
616
+ if (s) target.setAttribute('scale', s);
617
+ this.#scaleEl.value = s;
618
+ this.activeScale = s;
542
619
  }
543
620
  }
544
621