@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.
- package/CHANGELOG.md +30 -0
- package/README.md +29 -15
- package/editor/editor-shell/css/editor-shell.bespoke.css +7 -0
- package/editor/editor-shell/css/editor-shell.layout.css +15 -4
- package/editor/editor-sidebar/editor-sidebar.js +14 -2
- package/index.js +2 -0
- package/package.json +10 -3
- package/shell/admin-shell/css/admin-shell.main.css +35 -0
- package/simple/index.js +2 -0
- package/simple/simple-content/simple-content.a2ui.json +67 -0
- package/simple/simple-content/simple-content.css +29 -0
- package/simple/simple-content/simple-content.examples.html +13 -0
- package/simple/simple-content/simple-content.html +42 -0
- package/simple/simple-content/simple-content.yaml +54 -0
- package/simple/simple-hero/simple-hero.a2ui.json +76 -0
- package/simple/simple-hero/simple-hero.css +45 -0
- package/simple/simple-hero/simple-hero.examples.html +33 -0
- package/simple/simple-hero/simple-hero.html +42 -0
- package/simple/simple-hero/simple-hero.yaml +57 -0
- package/simple/simple-shell/simple-shell.a2ui.json +87 -0
- package/simple/simple-shell/simple-shell.css +40 -0
- package/simple/simple-shell/simple-shell.examples.html +42 -0
- package/simple/simple-shell/simple-shell.html +42 -0
- package/simple/simple-shell/simple-shell.js +47 -0
- package/simple/simple-shell/simple-shell.test.js +83 -0
- package/simple/simple-shell/simple-shell.yaml +78 -0
- package/theme/index.js +1 -0
- package/theme/theme-panel/theme-panel.a2ui.json +173 -0
- package/theme/theme-panel/theme-panel.css +50 -0
- package/theme/theme-panel/theme-panel.examples.html +104 -0
- package/theme/theme-panel/theme-panel.html +73 -0
- package/theme/theme-panel/theme-panel.js +533 -0
- package/theme/theme-panel/theme-panel.test.js +274 -0
- 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 };
|