@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.
- package/CHANGELOG.md +22 -0
- package/dist/everything.min.js +73 -73
- package/dist/shell/admin-shell.min.css +1 -1
- package/dist/web-modules.min.css +1 -1
- package/dist/web-modules.sheet.js +1 -1
- package/package.json +1 -1
- package/shell/admin-shell/css/admin-shell.collapsed.css +46 -15
- package/shell/admin-shell/css/admin-shell.tokens.css +1 -1
- package/theme/theme-panel/theme-panel.a2ui.json +16 -8
- package/theme/theme-panel/theme-panel.css +20 -9
- package/theme/theme-panel/theme-panel.d.ts +28 -14
- package/theme/theme-panel/theme-panel.js +172 -95
- package/theme/theme-panel/theme-panel.test.js +191 -46
- package/theme/theme-panel/theme-panel.yaml +55 -23
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
10
|
-
ship by default
|
|
11
|
-
|
|
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 /
|
|
49
|
-
applies a (density, radius) pair
|
|
50
|
-
[
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 =
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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('
|
|
117
|
-
target.removeAttribute('prose');
|
|
151
|
+
target.removeAttribute('scale');
|
|
118
152
|
if (this.persist) {
|
|
119
|
-
for (const k of ['theme', 'scheme', 'density', 'radius', '
|
|
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.
|
|
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.#
|
|
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
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 = '
|
|
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',
|
|
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
|
|
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
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
343
|
+
// Theme select — fires '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
|
-
|
|
280
|
-
|
|
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
|
|
319
|
-
if (this.#
|
|
398
|
+
// Scale
|
|
399
|
+
if (this.#scaleEl) {
|
|
320
400
|
const handler = (e) => {
|
|
321
|
-
const v = e.detail?.value ?? this.#
|
|
322
|
-
this.#apply({
|
|
401
|
+
const v = e.detail?.value ?? this.#scaleEl.value;
|
|
402
|
+
this.#apply({ scale: v }, 'scale');
|
|
323
403
|
};
|
|
324
|
-
this.#
|
|
325
|
-
this.#cleanups.push(() => this.#
|
|
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.
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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.
|
|
446
|
-
else localStorage.removeItem(`${this.storagePrefix}
|
|
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.#
|
|
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,
|
|
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
|
-
|
|
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
|
|
536
|
-
if (this.#
|
|
537
|
-
const
|
|
538
|
-
target.
|
|
539
|
-
|
|
540
|
-
this
|
|
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
|
|