@adia-ai/web-modules 0.4.0 → 0.4.2

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 (34) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +29 -15
  3. package/editor/editor-shell/css/editor-shell.bespoke.css +7 -0
  4. package/editor/editor-shell/css/editor-shell.layout.css +15 -4
  5. package/editor/editor-sidebar/editor-sidebar.js +14 -2
  6. package/index.js +2 -0
  7. package/package.json +10 -3
  8. package/shell/admin-shell/css/admin-shell.main.css +35 -0
  9. package/simple/index.js +2 -0
  10. package/simple/simple-content/simple-content.a2ui.json +67 -0
  11. package/simple/simple-content/simple-content.css +29 -0
  12. package/simple/simple-content/simple-content.examples.html +13 -0
  13. package/simple/simple-content/simple-content.html +42 -0
  14. package/simple/simple-content/simple-content.yaml +54 -0
  15. package/simple/simple-hero/simple-hero.a2ui.json +76 -0
  16. package/simple/simple-hero/simple-hero.css +45 -0
  17. package/simple/simple-hero/simple-hero.examples.html +33 -0
  18. package/simple/simple-hero/simple-hero.html +42 -0
  19. package/simple/simple-hero/simple-hero.yaml +57 -0
  20. package/simple/simple-shell/simple-shell.a2ui.json +87 -0
  21. package/simple/simple-shell/simple-shell.css +40 -0
  22. package/simple/simple-shell/simple-shell.examples.html +42 -0
  23. package/simple/simple-shell/simple-shell.html +42 -0
  24. package/simple/simple-shell/simple-shell.js +47 -0
  25. package/simple/simple-shell/simple-shell.test.js +83 -0
  26. package/simple/simple-shell/simple-shell.yaml +78 -0
  27. package/theme/index.js +1 -0
  28. package/theme/theme-panel/theme-panel.a2ui.json +173 -0
  29. package/theme/theme-panel/theme-panel.css +50 -0
  30. package/theme/theme-panel/theme-panel.examples.html +104 -0
  31. package/theme/theme-panel/theme-panel.html +73 -0
  32. package/theme/theme-panel/theme-panel.js +533 -0
  33. package/theme/theme-panel/theme-panel.test.js +274 -0
  34. package/theme/theme-panel/theme-panel.yaml +213 -0
@@ -0,0 +1,533 @@
1
+ /**
2
+ * <theme-panel persist parametric presets scheme-toggle>
3
+ *
4
+ * Module-tier appearance-preferences control surface. Owns the three knobs
5
+ * of the AdiaUI theming contract: [data-theme] named themes, --a-density /
6
+ * --a-radius-k parametric overrides, and color-scheme light/dark switching,
7
+ * plus optional localStorage persistence.
8
+ *
9
+ * Replaces the ~30-line inline panel + ~90-line wiring previously duplicated
10
+ * in site/ and playgrounds/admin-shell/.
11
+ *
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
16
+ * [scheme-toggle] renders an integrated light/dark segmented-ui
17
+ * [persist] writes selections to localStorage
18
+ * [storage-prefix="adia-theme-"] LS key namespace
19
+ * [target=":root"] CSS selector for theme target (default <html>)
20
+ * [scheme="auto"|"light"|"dark"] initial color-scheme
21
+ *
22
+ * Reflected state (read-only externally; use .apply() to mutate):
23
+ * [active-theme] currently selected theme slug; empty for default
24
+ * [active-scheme] resolved scheme (auto collapsed to light|dark)
25
+ * [active-density] mirrors current --a-density on target
26
+ * [active-radius] mirrors current --a-radius-k on target
27
+ *
28
+ * Events:
29
+ * theme-change — bubbles. detail: { theme, scheme, density, radius, source }
30
+ * source is one of: 'theme'|'slider'|'preset'|'scheme'|'reset'|'programmatic'
31
+ *
32
+ * Public methods:
33
+ * .apply({theme?, scheme?, density?, radius?}) — programmatic write
34
+ * .reset() — clear all state + emit reset event
35
+ *
36
+ * The element is light-DOM and stamps its internals imperatively in
37
+ * connected(). External CSS / JS reacts via reflected attributes, e.g.
38
+ * theme-panel[active-scheme="dark"] button-ui { … }
39
+ */
40
+
41
+ import { UIElement } from '../../../web-components/core/element.js';
42
+
43
+ const DEFAULT_THEMES = ['default', 'ocean', 'forest', 'sunset', 'lavender', 'rose', 'slate', 'midnight'];
44
+ const PRESETS = {
45
+ compact: { density: 0.85, radius: 0.75 },
46
+ default: { density: 1, radius: 1, clearTheme: true },
47
+ spacious: { density: 1.15, radius: 1.25 },
48
+ };
49
+
50
+ const titleCase = (s) => s.length ? s[0].toUpperCase() + s.slice(1) : s;
51
+
52
+ class ThemePanel extends UIElement {
53
+ static properties = {
54
+ themes: { type: String, default: DEFAULT_THEMES.join(' '), reflect: true },
55
+ parametric: { type: Boolean, default: false, reflect: true },
56
+ presets: { type: Boolean, default: false, reflect: true },
57
+ schemeToggle: { type: Boolean, default: false, reflect: true, attribute: 'scheme-toggle' },
58
+ persist: { type: Boolean, default: false, reflect: true },
59
+ storagePrefix: { type: String, default: 'adia-theme-', reflect: true, attribute: 'storage-prefix' },
60
+ target: { type: String, default: ':root', reflect: true },
61
+ scheme: { type: String, default: 'auto', reflect: true },
62
+ activeTheme: { type: String, default: '', reflect: true, attribute: 'active-theme' },
63
+ activeScheme: { type: String, default: '', reflect: true, attribute: 'active-scheme' },
64
+ activeDensity: { type: String, default: '', reflect: true, attribute: 'active-density' },
65
+ activeRadius: { type: String, default: '', reflect: true, attribute: 'active-radius' },
66
+ };
67
+
68
+ static template = () => null;
69
+
70
+ #mql = null;
71
+ #mqlHandler = null;
72
+ #cleanups = [];
73
+ #densityEl = null;
74
+ #radiusEl = null;
75
+ #schemeEl = null;
76
+ #stamped = false;
77
+
78
+ connected() {
79
+ if (this.#stamped) return;
80
+ this.#stamp();
81
+ this.#stamped = true;
82
+ this.#initState();
83
+ this.#wirePrefersColorScheme();
84
+ }
85
+
86
+ disconnected() {
87
+ for (const cleanup of this.#cleanups) cleanup();
88
+ this.#cleanups = [];
89
+ this.#detachPcm();
90
+ }
91
+
92
+ // ── Public API ──────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Programmatic write. Same code path as user input — emits theme-change
96
+ * with source: 'programmatic'.
97
+ */
98
+ apply(partial = {}) {
99
+ this.#apply(partial, 'programmatic');
100
+ }
101
+
102
+ /**
103
+ * Clear all state on target — remove [data-theme], inline parametric
104
+ * overrides, color-scheme, and (if [persist]) localStorage keys.
105
+ * Emits theme-change with source: 'reset'.
106
+ */
107
+ reset() {
108
+ const target = this.#resolveTarget();
109
+ target.removeAttribute('data-theme');
110
+ target.style.removeProperty('--a-density');
111
+ target.style.removeProperty('--a-radius-k');
112
+ target.style.removeProperty('color-scheme');
113
+ if (this.persist) {
114
+ for (const k of ['theme', 'scheme', 'density', 'radius']) {
115
+ try { localStorage.removeItem(`${this.storagePrefix}${k}`); } catch {}
116
+ }
117
+ }
118
+ this.activeTheme = '';
119
+ this.activeDensity = '';
120
+ this.activeRadius = '';
121
+ // Resolved scheme falls back to prefers-color-scheme
122
+ const resolved = this.#resolvePrefersScheme();
123
+ this.activeScheme = resolved;
124
+ if (this.#schemeEl) this.#schemeEl.value = resolved;
125
+ if (this.#densityEl) this.#densityEl.value = 1;
126
+ if (this.#radiusEl) this.#radiusEl.value = 1;
127
+ this.#emit('reset');
128
+ }
129
+
130
+ // ── Internal — DOM stamping ─────────────────────────────────────────
131
+
132
+ #stamp() {
133
+ const frag = document.createDocumentFragment();
134
+
135
+ // Optional scheme toggle (always at top per spec § 7.1)
136
+ if (this.schemeToggle) {
137
+ const scheme = document.createElement('div');
138
+ scheme.setAttribute('part', 'scheme');
139
+ const seg = document.createElement('segmented-ui');
140
+ seg.id = `${this.#uid}-scheme`;
141
+ const segLight = document.createElement('segment-ui');
142
+ segLight.setAttribute('value', 'light');
143
+ segLight.setAttribute('text', 'Light');
144
+ const segDark = document.createElement('segment-ui');
145
+ segDark.setAttribute('value', 'dark');
146
+ segDark.setAttribute('text', 'Dark');
147
+ seg.append(segLight, segDark);
148
+ scheme.appendChild(seg);
149
+ frag.appendChild(scheme);
150
+ this.#schemeEl = seg;
151
+ }
152
+
153
+ // Theme button row
154
+ const themeLabel = document.createElement('text-ui');
155
+ themeLabel.setAttribute('variant', 'label');
156
+ themeLabel.textContent = 'Theme';
157
+ frag.appendChild(themeLabel);
158
+
159
+ const themesRow = document.createElement('div');
160
+ themesRow.setAttribute('part', 'themes');
161
+ const themeSlugs = (this.themes || '').trim().split(/\s+/).filter(Boolean);
162
+ for (const slug of themeSlugs) {
163
+ const btn = document.createElement('button-ui');
164
+ btn.setAttribute('data-theme-slug', slug);
165
+ btn.setAttribute('text', titleCase(slug));
166
+ btn.setAttribute('variant', 'outline');
167
+ btn.setAttribute('size', 'sm');
168
+ themesRow.appendChild(btn);
169
+ }
170
+ frag.appendChild(themesRow);
171
+
172
+ // Optional parametric block
173
+ if (this.parametric) {
174
+ frag.appendChild(document.createElement('divider-ui'));
175
+
176
+ const paramLabel = document.createElement('text-ui');
177
+ paramLabel.setAttribute('variant', 'label');
178
+ paramLabel.textContent = 'Parametric';
179
+ frag.appendChild(paramLabel);
180
+
181
+ const densityField = document.createElement('field-ui');
182
+ densityField.setAttribute('label', 'Density');
183
+ const density = document.createElement('slider-ui');
184
+ density.setAttribute('part', 'density');
185
+ density.setAttribute('value', '1');
186
+ density.setAttribute('min', '0.5');
187
+ density.setAttribute('max', '1.5');
188
+ density.setAttribute('step', '0.05');
189
+ density.setAttribute('suffix', '×');
190
+ densityField.appendChild(density);
191
+ frag.appendChild(densityField);
192
+ this.#densityEl = density;
193
+
194
+ const radiusField = document.createElement('field-ui');
195
+ radiusField.setAttribute('label', 'Radius');
196
+ const radius = document.createElement('slider-ui');
197
+ radius.setAttribute('part', 'radius');
198
+ radius.setAttribute('value', '1');
199
+ radius.setAttribute('min', '0');
200
+ radius.setAttribute('max', '2');
201
+ radius.setAttribute('step', '0.1');
202
+ radius.setAttribute('suffix', '×');
203
+ radiusField.appendChild(radius);
204
+ frag.appendChild(radiusField);
205
+ this.#radiusEl = radius;
206
+ }
207
+
208
+ // Optional presets row
209
+ if (this.presets) {
210
+ frag.appendChild(document.createElement('divider-ui'));
211
+
212
+ const presetsRow = document.createElement('div');
213
+ presetsRow.setAttribute('part', 'presets');
214
+ for (const name of ['compact', 'default', 'spacious']) {
215
+ const btn = document.createElement('button-ui');
216
+ btn.setAttribute('data-preset', name);
217
+ btn.setAttribute('text', name === 'default' ? 'Reset' : titleCase(name));
218
+ btn.setAttribute('variant', 'outline');
219
+ btn.setAttribute('size', 'sm');
220
+ presetsRow.appendChild(btn);
221
+ }
222
+ frag.appendChild(presetsRow);
223
+ }
224
+
225
+ this.appendChild(frag);
226
+ this.#wireListeners();
227
+ }
228
+
229
+ #wireListeners() {
230
+ // Theme buttons — fire 'press' (button-ui), fallback to 'click' for tests
231
+ for (const btn of this.querySelectorAll('[part="themes"] button-ui')) {
232
+ const slug = btn.getAttribute('data-theme-slug');
233
+ const handler = () => this.#onThemeClick(slug);
234
+ btn.addEventListener('press', handler);
235
+ btn.addEventListener('click', handler);
236
+ this.#cleanups.push(() => {
237
+ btn.removeEventListener('press', handler);
238
+ btn.removeEventListener('click', handler);
239
+ });
240
+ }
241
+
242
+ // Parametric sliders — fire 'input'
243
+ if (this.#densityEl) {
244
+ const handler = () => this.#apply({ density: parseFloat(this.#densityEl.value) }, 'slider');
245
+ this.#densityEl.addEventListener('input', handler);
246
+ this.#cleanups.push(() => this.#densityEl.removeEventListener('input', handler));
247
+ }
248
+ if (this.#radiusEl) {
249
+ const handler = () => this.#apply({ radius: parseFloat(this.#radiusEl.value) }, 'slider');
250
+ this.#radiusEl.addEventListener('input', handler);
251
+ this.#cleanups.push(() => this.#radiusEl.removeEventListener('input', handler));
252
+ }
253
+
254
+ // Preset buttons
255
+ for (const btn of this.querySelectorAll('[part="presets"] button-ui')) {
256
+ const name = btn.getAttribute('data-preset');
257
+ const handler = () => this.#onPresetClick(name);
258
+ btn.addEventListener('press', handler);
259
+ btn.addEventListener('click', handler);
260
+ this.#cleanups.push(() => {
261
+ btn.removeEventListener('press', handler);
262
+ btn.removeEventListener('click', handler);
263
+ });
264
+ }
265
+
266
+ // Scheme toggle
267
+ if (this.#schemeEl) {
268
+ const handler = (e) => {
269
+ const v = e.detail?.value ?? this.#schemeEl.value;
270
+ if (v === 'light' || v === 'dark') this.#apply({ scheme: v }, 'scheme');
271
+ };
272
+ this.#schemeEl.addEventListener('change', handler);
273
+ this.#cleanups.push(() => this.#schemeEl.removeEventListener('change', handler));
274
+ }
275
+ }
276
+
277
+ // ── Internal — handlers ─────────────────────────────────────────────
278
+
279
+ #onThemeClick(slug) {
280
+ const target = this.#resolveTarget();
281
+ // Theme click clears parametric overrides so the theme's computed
282
+ // values surface (per spec § 7.3, OD-003 lean A — docs-shell behavior).
283
+ target.style.removeProperty('--a-density');
284
+ target.style.removeProperty('--a-radius-k');
285
+ if (this.persist) {
286
+ try {
287
+ localStorage.removeItem(`${this.storagePrefix}density`);
288
+ localStorage.removeItem(`${this.storagePrefix}radius`);
289
+ } catch {}
290
+ }
291
+ // Apply the theme (which may write [data-theme] on target)
292
+ this.#apply({ theme: slug }, 'theme');
293
+ // Then re-read computed values into sliders on next frame, so the
294
+ // theme's [data-theme] block has taken effect.
295
+ const sync = () => {
296
+ const cs = getComputedStyle(target);
297
+ const density = parseFloat(cs.getPropertyValue('--a-density')) || 1;
298
+ const radius = parseFloat(cs.getPropertyValue('--a-radius-k')) || 1;
299
+ if (this.#densityEl) this.#densityEl.value = density;
300
+ if (this.#radiusEl) this.#radiusEl.value = radius;
301
+ this.activeDensity = String(density);
302
+ this.activeRadius = String(radius);
303
+ };
304
+ if (typeof requestAnimationFrame === 'function') {
305
+ requestAnimationFrame(sync);
306
+ } else {
307
+ sync();
308
+ }
309
+ }
310
+
311
+ #onPresetClick(name) {
312
+ const preset = PRESETS[name];
313
+ if (!preset) return;
314
+ if (preset.clearTheme) {
315
+ // "Reset" preset — clears [data-theme] too
316
+ const target = this.#resolveTarget();
317
+ target.removeAttribute('data-theme');
318
+ this.activeTheme = '';
319
+ if (this.persist) {
320
+ try { localStorage.removeItem(`${this.storagePrefix}theme`); } catch {}
321
+ }
322
+ }
323
+ this.#apply({ density: preset.density, radius: preset.radius }, 'preset');
324
+ if (this.#densityEl) this.#densityEl.value = preset.density;
325
+ if (this.#radiusEl) this.#radiusEl.value = preset.radius;
326
+ }
327
+
328
+ // ── Internal — mutation (single path) ──────────────────────────────
329
+
330
+ #apply(partial, source) {
331
+ const target = this.#resolveTarget();
332
+
333
+ if (partial.theme !== undefined) {
334
+ if (partial.theme === '' || partial.theme === 'default') {
335
+ target.removeAttribute('data-theme');
336
+ this.activeTheme = '';
337
+ } else {
338
+ target.setAttribute('data-theme', partial.theme);
339
+ this.activeTheme = partial.theme;
340
+ }
341
+ if (this.persist) {
342
+ try {
343
+ if (this.activeTheme) localStorage.setItem(`${this.storagePrefix}theme`, this.activeTheme);
344
+ else localStorage.removeItem(`${this.storagePrefix}theme`);
345
+ } catch {}
346
+ }
347
+ }
348
+
349
+ if (partial.density !== undefined) {
350
+ target.style.setProperty('--a-density', partial.density);
351
+ this.activeDensity = String(partial.density);
352
+ if (this.persist) {
353
+ try { localStorage.setItem(`${this.storagePrefix}density`, String(partial.density)); } catch {}
354
+ }
355
+ }
356
+
357
+ if (partial.radius !== undefined) {
358
+ target.style.setProperty('--a-radius-k', partial.radius);
359
+ this.activeRadius = String(partial.radius);
360
+ if (this.persist) {
361
+ try { localStorage.setItem(`${this.storagePrefix}radius`, String(partial.radius)); } catch {}
362
+ }
363
+ }
364
+
365
+ if (partial.scheme !== undefined) {
366
+ const s = partial.scheme;
367
+ if (s === 'auto') {
368
+ target.style.removeProperty('color-scheme');
369
+ this.activeScheme = this.#resolvePrefersScheme();
370
+ if (this.persist) {
371
+ try { localStorage.removeItem(`${this.storagePrefix}scheme`); } catch {}
372
+ }
373
+ this.#attachPcm();
374
+ } else if (s === 'light' || s === 'dark') {
375
+ target.style.setProperty('color-scheme', s);
376
+ this.activeScheme = s;
377
+ if (this.persist) {
378
+ try { localStorage.setItem(`${this.storagePrefix}scheme`, s); } catch {}
379
+ }
380
+ // User chose explicitly — PCM listener becomes a no-op
381
+ this.#detachPcm();
382
+ }
383
+ if (this.#schemeEl) this.#schemeEl.value = this.activeScheme;
384
+ }
385
+
386
+ this.#emit(source);
387
+ }
388
+
389
+ #emit(source) {
390
+ this.dispatchEvent(new CustomEvent('theme-change', {
391
+ bubbles: true,
392
+ detail: {
393
+ theme: this.activeTheme,
394
+ scheme: this.activeScheme,
395
+ density: this.activeDensity ? parseFloat(this.activeDensity) : 1,
396
+ radius: this.activeRadius ? parseFloat(this.activeRadius) : 1,
397
+ source,
398
+ },
399
+ }));
400
+ }
401
+
402
+ // ── Internal — initial state + persistence ─────────────────────────
403
+
404
+ #initState() {
405
+ const target = this.#resolveTarget();
406
+ let theme, scheme, density, radius;
407
+
408
+ if (this.persist) {
409
+ try {
410
+ theme = localStorage.getItem(`${this.storagePrefix}theme`) || '';
411
+ scheme = localStorage.getItem(`${this.storagePrefix}scheme`) || '';
412
+ const d = localStorage.getItem(`${this.storagePrefix}density`);
413
+ const r = localStorage.getItem(`${this.storagePrefix}radius`);
414
+ density = d != null ? parseFloat(d) : undefined;
415
+ radius = r != null ? parseFloat(r) : undefined;
416
+ } catch {}
417
+ }
418
+
419
+ // Apply theme
420
+ if (theme) {
421
+ target.setAttribute('data-theme', theme);
422
+ this.activeTheme = theme;
423
+ } else {
424
+ this.activeTheme = target.getAttribute('data-theme') || '';
425
+ }
426
+
427
+ // Apply scheme
428
+ if (scheme === 'light' || scheme === 'dark') {
429
+ target.style.setProperty('color-scheme', scheme);
430
+ this.activeScheme = scheme;
431
+ if (this.#schemeEl) this.#schemeEl.value = scheme;
432
+ } else {
433
+ // No persisted scheme: use [scheme] attribute (default 'auto')
434
+ const attr = this.scheme || 'auto';
435
+ if (attr === 'light' || attr === 'dark') {
436
+ target.style.setProperty('color-scheme', attr);
437
+ this.activeScheme = attr;
438
+ } else {
439
+ this.activeScheme = this.#resolvePrefersScheme();
440
+ }
441
+ if (this.#schemeEl) this.#schemeEl.value = this.activeScheme;
442
+ }
443
+
444
+ // Apply parametric values
445
+ if (density != null && !Number.isNaN(density)) {
446
+ target.style.setProperty('--a-density', density);
447
+ this.activeDensity = String(density);
448
+ if (this.#densityEl) this.#densityEl.value = density;
449
+ } else if (this.#densityEl) {
450
+ // Read computed from target
451
+ const cs = getComputedStyle(target);
452
+ const d = parseFloat(cs.getPropertyValue('--a-density')) || 1;
453
+ this.#densityEl.value = d;
454
+ this.activeDensity = String(d);
455
+ }
456
+
457
+ if (radius != null && !Number.isNaN(radius)) {
458
+ target.style.setProperty('--a-radius-k', radius);
459
+ this.activeRadius = String(radius);
460
+ if (this.#radiusEl) this.#radiusEl.value = radius;
461
+ } else if (this.#radiusEl) {
462
+ const cs = getComputedStyle(target);
463
+ const r = parseFloat(cs.getPropertyValue('--a-radius-k')) || 1;
464
+ this.#radiusEl.value = r;
465
+ this.activeRadius = String(r);
466
+ }
467
+ }
468
+
469
+ // ── Internal — prefers-color-scheme ─────────────────────────────────
470
+
471
+ #resolvePrefersScheme() {
472
+ try {
473
+ if (typeof matchMedia === 'function' &&
474
+ matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';
475
+ } catch {}
476
+ return 'light';
477
+ }
478
+
479
+ #wirePrefersColorScheme() {
480
+ // Attach PCM listener only when scheme=auto AND no user override persisted
481
+ if (this.scheme !== 'auto') return;
482
+ if (this.persist) {
483
+ try {
484
+ const persisted = localStorage.getItem(`${this.storagePrefix}scheme`);
485
+ if (persisted === 'light' || persisted === 'dark') return;
486
+ } catch {}
487
+ }
488
+ this.#attachPcm();
489
+ }
490
+
491
+ #attachPcm() {
492
+ if (this.#mql || typeof matchMedia !== 'function') return;
493
+ try {
494
+ this.#mql = matchMedia('(prefers-color-scheme: dark)');
495
+ this.#mqlHandler = () => {
496
+ const resolved = this.#mql.matches ? 'dark' : 'light';
497
+ this.activeScheme = resolved;
498
+ if (this.#schemeEl) this.#schemeEl.value = resolved;
499
+ this.#emit('scheme');
500
+ };
501
+ this.#mql.addEventListener('change', this.#mqlHandler);
502
+ } catch {}
503
+ }
504
+
505
+ #detachPcm() {
506
+ if (this.#mql && this.#mqlHandler) {
507
+ try { this.#mql.removeEventListener('change', this.#mqlHandler); } catch {}
508
+ }
509
+ this.#mql = null;
510
+ this.#mqlHandler = null;
511
+ }
512
+
513
+ // ── Internal — target resolution ────────────────────────────────────
514
+
515
+ #resolveTarget() {
516
+ const sel = this.target || ':root';
517
+ if (sel === ':root') return document.documentElement;
518
+ try {
519
+ return document.querySelector(sel) ?? document.documentElement;
520
+ } catch {
521
+ return document.documentElement;
522
+ }
523
+ }
524
+
525
+ // ── Internal — uid for nested element ids ───────────────────────────
526
+
527
+ get #uid() {
528
+ return this.id || 'theme-panel';
529
+ }
530
+ }
531
+
532
+ customElements.define('theme-panel', ThemePanel);
533
+ export { ThemePanel };