@adia-ai/web-modules 0.8.12 → 0.8.14
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 +26 -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 +17 -10
- package/theme/theme-panel/theme-panel.css +23 -10
- package/theme/theme-panel/theme-panel.d.ts +28 -14
- package/theme/theme-panel/theme-panel.js +191 -104
- package/theme/theme-panel/theme-panel.test.js +203 -46
- package/theme/theme-panel/theme-panel.yaml +56 -24
|
@@ -11,7 +11,6 @@ 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);
|
|
16
15
|
}
|
|
17
16
|
|
|
@@ -26,6 +25,20 @@ theme-panel {
|
|
|
26
25
|
min-width: var(--theme-panel-min-width);
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
/* The panel's own chrome is pinned to a fixed density register
|
|
29
|
+
("compact" — 0.85/0.75) on the <theme-provider scale="ui-md"> wrapper
|
|
30
|
+
— the single place that locks the panel's own size/scale, independent
|
|
31
|
+
of whatever --a-density/[scale] the Density slider / Scale select is
|
|
32
|
+
driving on the TARGET. Both share :root as a DOM ancestor by default,
|
|
33
|
+
so without this the panel's own controls balloon in lockstep with the
|
|
34
|
+
page it's controlling (verified live: dragging Density to 1.5, or
|
|
35
|
+
Scale to content-lg, also enlarged the popover's own buttons/select).
|
|
36
|
+
theme-provider is display:contents, so this adds no box of its own. */
|
|
37
|
+
theme-provider {
|
|
38
|
+
--a-density: 0.85;
|
|
39
|
+
--a-radius-k: 0.75;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
[part="scheme"] {
|
|
30
43
|
display: flex;
|
|
31
44
|
}
|
|
@@ -34,14 +47,9 @@ theme-panel {
|
|
|
34
47
|
flex: 1;
|
|
35
48
|
}
|
|
36
49
|
|
|
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. */
|
|
50
|
+
/* Labelled control row — "Theme" / "Preset" / "Scale" label on the left,
|
|
51
|
+
the select-ui / preset buttons pushed to the right. */
|
|
52
|
+
[part="theme-row"],
|
|
45
53
|
[part="preset-row"] {
|
|
46
54
|
display: flex;
|
|
47
55
|
align-items: center;
|
|
@@ -55,5 +63,10 @@ theme-panel {
|
|
|
55
63
|
flex-wrap: wrap;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
/*
|
|
66
|
+
/* Reset — its own row at the very bottom (separated by a divider in
|
|
67
|
+
markup order), distinct from the Preset row's "Default" button. */
|
|
68
|
+
[part="reset-row"] {
|
|
69
|
+
display: flex;
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
}
|
|
@@ -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,7 +99,9 @@ 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() {
|
|
@@ -113,24 +139,24 @@ class ThemePanel extends UIElement {
|
|
|
113
139
|
target.style.removeProperty('--a-density');
|
|
114
140
|
target.style.removeProperty('--a-radius-k');
|
|
115
141
|
target.style.removeProperty('color-scheme');
|
|
116
|
-
target.removeAttribute('
|
|
117
|
-
target.removeAttribute('prose');
|
|
142
|
+
target.removeAttribute('scale');
|
|
118
143
|
if (this.persist) {
|
|
119
|
-
for (const k of ['theme', 'scheme', 'density', 'radius', '
|
|
144
|
+
for (const k of ['theme', 'scheme', 'density', 'radius', 'scale']) {
|
|
120
145
|
try { localStorage.removeItem(`${this.storagePrefix}${k}`); } catch {}
|
|
121
146
|
}
|
|
122
147
|
}
|
|
123
148
|
this.activeTheme = '';
|
|
124
149
|
this.activeDensity = '';
|
|
125
150
|
this.activeRadius = '';
|
|
126
|
-
this.
|
|
151
|
+
this.activeScale = '';
|
|
127
152
|
// Resolved scheme falls back to prefers-color-scheme
|
|
128
153
|
const resolved = this.#resolvePrefersScheme();
|
|
129
154
|
this.activeScheme = resolved;
|
|
130
155
|
if (this.#schemeEl) this.#schemeEl.value = resolved;
|
|
131
156
|
if (this.#densityEl) this.#densityEl.value = 1;
|
|
132
157
|
if (this.#radiusEl) this.#radiusEl.value = 1;
|
|
133
|
-
if (this.#
|
|
158
|
+
if (this.#scaleEl) this.#scaleEl.value = '';
|
|
159
|
+
if (this.#themeEl) this.#themeEl.value = '';
|
|
134
160
|
this.#emit('reset');
|
|
135
161
|
}
|
|
136
162
|
|
|
@@ -157,24 +183,40 @@ class ThemePanel extends UIElement {
|
|
|
157
183
|
this.#schemeEl = seg;
|
|
158
184
|
}
|
|
159
185
|
|
|
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');
|
|
186
|
+
// Theme row — a <select-ui> (matches the Scale row's control pattern)
|
|
187
|
+
// offering every slug in [themes], only when that list is non-empty.
|
|
168
188
|
const themeSlugs = (this.themes || '').trim().split(/\s+/).filter(Boolean);
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
189
|
+
if (themeSlugs.length) {
|
|
190
|
+
const themeWrap = document.createElement('div');
|
|
191
|
+
themeWrap.setAttribute('part', 'theme-row');
|
|
192
|
+
const themeLabel = document.createElement('text-ui');
|
|
193
|
+
themeLabel.setAttribute('variant', 'label');
|
|
194
|
+
themeLabel.textContent = 'Theme';
|
|
195
|
+
themeWrap.appendChild(themeLabel);
|
|
196
|
+
|
|
197
|
+
const select = document.createElement('select-ui');
|
|
198
|
+
select.setAttribute('part', 'themes');
|
|
199
|
+
select.setAttribute('size', 'sm');
|
|
200
|
+
select.id = `${this.#uid}-theme`;
|
|
201
|
+
// themeSlugs is attribute/A2UI-driven and tolerant of unknown values
|
|
202
|
+
// (theme-panel.d.ts's documented contract) — build <option> elements
|
|
203
|
+
// via the DOM rather than interpolating slugs into innerHTML, so a
|
|
204
|
+
// slug containing `"` or `<` can't break out of the option markup.
|
|
205
|
+
const mkOption = (value, label) => {
|
|
206
|
+
const opt = document.createElement('option');
|
|
207
|
+
opt.value = value;
|
|
208
|
+
opt.textContent = label;
|
|
209
|
+
return opt;
|
|
210
|
+
};
|
|
211
|
+
select.append(
|
|
212
|
+
mkOption('', 'Default'),
|
|
213
|
+
...themeSlugs.map((slug) => mkOption(slug, titleCase(slug))),
|
|
214
|
+
);
|
|
215
|
+
select.setAttribute('value', '');
|
|
216
|
+
themeWrap.appendChild(select);
|
|
217
|
+
frag.appendChild(themeWrap);
|
|
218
|
+
this.#themeEl = select;
|
|
176
219
|
}
|
|
177
|
-
frag.appendChild(themesRow);
|
|
178
220
|
|
|
179
221
|
// Optional parametric block
|
|
180
222
|
if (this.parametric) {
|
|
@@ -185,30 +227,32 @@ class ThemePanel extends UIElement {
|
|
|
185
227
|
paramLabel.textContent = 'Parametric';
|
|
186
228
|
frag.appendChild(paramLabel);
|
|
187
229
|
|
|
188
|
-
|
|
189
|
-
|
|
230
|
+
// slider-ui renders [label] ... [value][suffix] on one header row and
|
|
231
|
+
// the track below natively (its own doc comment: "Wrap in <field-ui>
|
|
232
|
+
// only when you need external label-row composition") — passing
|
|
233
|
+
// [label] directly here, not via a wrapping <field-ui>, is what
|
|
234
|
+
// produces the one-row label+value layout instead of three stacked
|
|
235
|
+
// rows.
|
|
190
236
|
const density = document.createElement('slider-ui');
|
|
191
237
|
density.setAttribute('part', 'density');
|
|
238
|
+
density.setAttribute('label', 'Density');
|
|
192
239
|
density.setAttribute('value', '1');
|
|
193
240
|
density.setAttribute('min', '0.5');
|
|
194
241
|
density.setAttribute('max', '1.5');
|
|
195
242
|
density.setAttribute('step', '0.05');
|
|
196
243
|
density.setAttribute('suffix', '×');
|
|
197
|
-
|
|
198
|
-
frag.appendChild(densityField);
|
|
244
|
+
frag.appendChild(density);
|
|
199
245
|
this.#densityEl = density;
|
|
200
246
|
|
|
201
|
-
const radiusField = document.createElement('field-ui');
|
|
202
|
-
radiusField.setAttribute('label', 'Radius');
|
|
203
247
|
const radius = document.createElement('slider-ui');
|
|
204
248
|
radius.setAttribute('part', 'radius');
|
|
249
|
+
radius.setAttribute('label', 'Radius');
|
|
205
250
|
radius.setAttribute('value', '1');
|
|
206
251
|
radius.setAttribute('min', '0');
|
|
207
252
|
radius.setAttribute('max', '2');
|
|
208
253
|
radius.setAttribute('step', '0.1');
|
|
209
254
|
radius.setAttribute('suffix', '×');
|
|
210
|
-
|
|
211
|
-
frag.appendChild(radiusField);
|
|
255
|
+
frag.appendChild(radius);
|
|
212
256
|
this.#radiusEl = radius;
|
|
213
257
|
}
|
|
214
258
|
|
|
@@ -217,11 +261,14 @@ class ThemePanel extends UIElement {
|
|
|
217
261
|
frag.appendChild(document.createElement('divider-ui'));
|
|
218
262
|
|
|
219
263
|
if (this.presets) {
|
|
264
|
+
// Labelled "Preset" (not "Density") — it's a combined density+radius
|
|
265
|
+
// quick-pick, distinct from the Density slider above; sharing the
|
|
266
|
+
// "Density" label with the slider row read as two Density rows.
|
|
220
267
|
const presetWrap = document.createElement('div');
|
|
221
268
|
presetWrap.setAttribute('part', 'preset-row');
|
|
222
269
|
const presetLabel = document.createElement('text-ui');
|
|
223
270
|
presetLabel.setAttribute('variant', 'label');
|
|
224
|
-
presetLabel.textContent = '
|
|
271
|
+
presetLabel.textContent = 'Preset';
|
|
225
272
|
presetWrap.appendChild(presetLabel);
|
|
226
273
|
|
|
227
274
|
const presetsRow = document.createElement('div');
|
|
@@ -229,7 +276,7 @@ class ThemePanel extends UIElement {
|
|
|
229
276
|
for (const name of ['compact', 'default', 'spacious']) {
|
|
230
277
|
const btn = document.createElement('button-ui');
|
|
231
278
|
btn.setAttribute('data-preset', name);
|
|
232
|
-
btn.setAttribute('text',
|
|
279
|
+
btn.setAttribute('text', titleCase(name));
|
|
233
280
|
btn.setAttribute('variant', 'outline');
|
|
234
281
|
btn.setAttribute('size', 'sm');
|
|
235
282
|
presetsRow.appendChild(btn);
|
|
@@ -238,46 +285,89 @@ class ThemePanel extends UIElement {
|
|
|
238
285
|
frag.appendChild(presetWrap);
|
|
239
286
|
}
|
|
240
287
|
|
|
241
|
-
// Scale
|
|
288
|
+
// Scale — the six ancestor [scale] tiers (styles/scale.css) applied
|
|
289
|
+
// directly to target, matching theme-provider.class.js's own `scale`
|
|
290
|
+
// prop values exactly. Supersedes the old verse/regular/prose row —
|
|
291
|
+
// [verse]/[prose] are deprecated aliases now, not something a NEW
|
|
292
|
+
// control should write.
|
|
242
293
|
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;
|
|
294
|
+
const scaleWrap = document.createElement('div');
|
|
295
|
+
scaleWrap.setAttribute('part', 'preset-row');
|
|
296
|
+
const scaleLabel = document.createElement('text-ui');
|
|
297
|
+
scaleLabel.setAttribute('variant', 'label');
|
|
298
|
+
scaleLabel.textContent = 'Scale';
|
|
299
|
+
scaleWrap.appendChild(scaleLabel);
|
|
300
|
+
|
|
301
|
+
const select = document.createElement('select-ui');
|
|
302
|
+
select.setAttribute('part', 'scale');
|
|
303
|
+
select.setAttribute('size', 'sm');
|
|
304
|
+
select.id = `${this.#uid}-scale`;
|
|
305
|
+
select.innerHTML = SCALE_TIERS
|
|
306
|
+
.map((tier) => `<option value="${tier}">${tier || 'Default'}</option>`)
|
|
307
|
+
.join('');
|
|
308
|
+
select.setAttribute('value', '');
|
|
309
|
+
scaleWrap.appendChild(select);
|
|
310
|
+
frag.appendChild(scaleWrap);
|
|
311
|
+
this.#scaleEl = select;
|
|
264
312
|
}
|
|
265
313
|
}
|
|
266
314
|
|
|
267
|
-
|
|
315
|
+
// Reset — its own row at the very bottom, separated by a divider.
|
|
316
|
+
// Distinct from the Preset row's "Default" button: Default only sets
|
|
317
|
+
// density/radius; Reset clears everything (theme, scheme, density,
|
|
318
|
+
// radius, scale) via the public .reset() method.
|
|
319
|
+
frag.appendChild(document.createElement('divider-ui'));
|
|
320
|
+
const resetRow = document.createElement('div');
|
|
321
|
+
resetRow.setAttribute('part', 'reset-row');
|
|
322
|
+
const resetBtn = document.createElement('button-ui');
|
|
323
|
+
resetBtn.setAttribute('part', 'reset');
|
|
324
|
+
resetBtn.setAttribute('text', 'Reset');
|
|
325
|
+
resetBtn.setAttribute('variant', 'ghost');
|
|
326
|
+
resetBtn.setAttribute('size', 'sm');
|
|
327
|
+
resetRow.appendChild(resetBtn);
|
|
328
|
+
frag.appendChild(resetRow);
|
|
329
|
+
this.#resetEl = resetBtn;
|
|
330
|
+
|
|
331
|
+
// Wrap the stamped content in <theme-provider scale="ui-md"> — locks
|
|
332
|
+
// the panel's own chrome to the base size/scale register, independent
|
|
333
|
+
// of whatever [scale]/--a-density the panel's own Scale select or
|
|
334
|
+
// Density slider is driving on the target. Target defaults to :root,
|
|
335
|
+
// a DOM ancestor of theme-panel, so those inherited custom properties
|
|
336
|
+
// would otherwise cascade into the panel's own controls too (verified
|
|
337
|
+
// live: switching Scale to content-lg, or Density to 1.5, enlarged the
|
|
338
|
+
// popover's own controls alongside the page). display:contents, so it
|
|
339
|
+
// adds no box of its own — theme-panel's flex column layout is
|
|
340
|
+
// unaffected. ui-md is byte-identical to the unscaled default
|
|
341
|
+
// (scale.css's own invariant), so this is a visual no-op until an
|
|
342
|
+
// ancestor sets a different tier. Deliberately does NOT set [theme] —
|
|
343
|
+
// the panel's own accent color is meant to track whatever theme is
|
|
344
|
+
// active, only size/scale are locked.
|
|
345
|
+
const provider = document.createElement('theme-provider');
|
|
346
|
+
provider.setAttribute('scale', 'ui-md');
|
|
347
|
+
provider.appendChild(frag);
|
|
348
|
+
this.appendChild(provider);
|
|
268
349
|
this.#wireListeners();
|
|
269
350
|
}
|
|
270
351
|
|
|
271
352
|
#wireListeners() {
|
|
272
|
-
// Theme
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
353
|
+
// Theme select — fires 'change' (select-ui)
|
|
354
|
+
if (this.#themeEl) {
|
|
355
|
+
const handler = (e) => {
|
|
356
|
+
const v = e.detail?.value ?? this.#themeEl.value;
|
|
357
|
+
this.#onThemeClick(v);
|
|
358
|
+
};
|
|
359
|
+
this.#themeEl.addEventListener('change', handler);
|
|
360
|
+
this.#cleanups.push(() => this.#themeEl.removeEventListener('change', handler));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Reset button
|
|
364
|
+
if (this.#resetEl) {
|
|
365
|
+
const handler = () => this.reset();
|
|
366
|
+
this.#resetEl.addEventListener('press', handler);
|
|
367
|
+
this.#resetEl.addEventListener('click', handler);
|
|
278
368
|
this.#cleanups.push(() => {
|
|
279
|
-
|
|
280
|
-
|
|
369
|
+
this.#resetEl.removeEventListener('press', handler);
|
|
370
|
+
this.#resetEl.removeEventListener('click', handler);
|
|
281
371
|
});
|
|
282
372
|
}
|
|
283
373
|
|
|
@@ -315,14 +405,14 @@ class ThemePanel extends UIElement {
|
|
|
315
405
|
this.#cleanups.push(() => this.#schemeEl.removeEventListener('change', handler));
|
|
316
406
|
}
|
|
317
407
|
|
|
318
|
-
// Scale
|
|
319
|
-
if (this.#
|
|
408
|
+
// Scale
|
|
409
|
+
if (this.#scaleEl) {
|
|
320
410
|
const handler = (e) => {
|
|
321
|
-
const v = e.detail?.value ?? this.#
|
|
322
|
-
this.#apply({
|
|
411
|
+
const v = e.detail?.value ?? this.#scaleEl.value;
|
|
412
|
+
this.#apply({ scale: v }, 'scale');
|
|
323
413
|
};
|
|
324
|
-
this.#
|
|
325
|
-
this.#cleanups.push(() => this.#
|
|
414
|
+
this.#scaleEl.addEventListener('change', handler);
|
|
415
|
+
this.#cleanups.push(() => this.#scaleEl.removeEventListener('change', handler));
|
|
326
416
|
}
|
|
327
417
|
}
|
|
328
418
|
|
|
@@ -363,15 +453,6 @@ class ThemePanel extends UIElement {
|
|
|
363
453
|
#onPresetClick(name) {
|
|
364
454
|
const preset = PRESETS[name];
|
|
365
455
|
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
456
|
this.#apply({ density: preset.density, radius: preset.radius }, 'preset');
|
|
376
457
|
if (this.#densityEl) this.#densityEl.value = preset.density;
|
|
377
458
|
if (this.#radiusEl) this.#radiusEl.value = preset.radius;
|
|
@@ -396,6 +477,7 @@ class ThemePanel extends UIElement {
|
|
|
396
477
|
else localStorage.removeItem(`${this.storagePrefix}theme`);
|
|
397
478
|
} catch {}
|
|
398
479
|
}
|
|
480
|
+
if (this.#themeEl) this.#themeEl.value = this.activeTheme;
|
|
399
481
|
}
|
|
400
482
|
|
|
401
483
|
if (partial.density !== undefined) {
|
|
@@ -435,18 +517,22 @@ class ThemePanel extends UIElement {
|
|
|
435
517
|
if (this.#schemeEl) this.#schemeEl.value = this.activeScheme;
|
|
436
518
|
}
|
|
437
519
|
|
|
438
|
-
if (partial.
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
520
|
+
if (partial.scale !== undefined) {
|
|
521
|
+
const s = partial.scale;
|
|
522
|
+
if (s && SCALE_TIERS.includes(s)) {
|
|
523
|
+
target.setAttribute('scale', s);
|
|
524
|
+
this.activeScale = s;
|
|
525
|
+
} else {
|
|
526
|
+
target.removeAttribute('scale');
|
|
527
|
+
this.activeScale = '';
|
|
528
|
+
}
|
|
443
529
|
if (this.persist) {
|
|
444
530
|
try {
|
|
445
|
-
if (this.
|
|
446
|
-
else localStorage.removeItem(`${this.storagePrefix}
|
|
531
|
+
if (this.activeScale) localStorage.setItem(`${this.storagePrefix}scale`, this.activeScale);
|
|
532
|
+
else localStorage.removeItem(`${this.storagePrefix}scale`);
|
|
447
533
|
} catch {}
|
|
448
534
|
}
|
|
449
|
-
if (this.#
|
|
535
|
+
if (this.#scaleEl) this.#scaleEl.value = this.activeScale;
|
|
450
536
|
}
|
|
451
537
|
|
|
452
538
|
this.#emit(source);
|
|
@@ -460,6 +546,7 @@ class ThemePanel extends UIElement {
|
|
|
460
546
|
scheme: this.activeScheme,
|
|
461
547
|
density: this.activeDensity ? parseFloat(this.activeDensity) : 1,
|
|
462
548
|
radius: this.activeRadius ? parseFloat(this.activeRadius) : 1,
|
|
549
|
+
scale: this.activeScale,
|
|
463
550
|
source,
|
|
464
551
|
},
|
|
465
552
|
}));
|
|
@@ -469,7 +556,7 @@ class ThemePanel extends UIElement {
|
|
|
469
556
|
|
|
470
557
|
#initState() {
|
|
471
558
|
const target = this.#resolveTarget();
|
|
472
|
-
let theme, scheme, density, radius,
|
|
559
|
+
let theme, scheme, density, radius, scale;
|
|
473
560
|
|
|
474
561
|
if (this.persist) {
|
|
475
562
|
try {
|
|
@@ -479,7 +566,7 @@ class ThemePanel extends UIElement {
|
|
|
479
566
|
const r = localStorage.getItem(`${this.storagePrefix}radius`);
|
|
480
567
|
density = d != null ? parseFloat(d) : undefined;
|
|
481
568
|
radius = r != null ? parseFloat(r) : undefined;
|
|
482
|
-
|
|
569
|
+
scale = localStorage.getItem(`${this.storagePrefix}scale`) || '';
|
|
483
570
|
} catch {}
|
|
484
571
|
}
|
|
485
572
|
|
|
@@ -490,6 +577,7 @@ class ThemePanel extends UIElement {
|
|
|
490
577
|
} else {
|
|
491
578
|
this.activeTheme = target.getAttribute('theme') || '';
|
|
492
579
|
}
|
|
580
|
+
if (this.#themeEl) this.#themeEl.value = this.activeTheme;
|
|
493
581
|
|
|
494
582
|
// Apply scheme
|
|
495
583
|
if (scheme === 'light' || scheme === 'dark') {
|
|
@@ -532,13 +620,12 @@ class ThemePanel extends UIElement {
|
|
|
532
620
|
this.activeRadius = String(r);
|
|
533
621
|
}
|
|
534
622
|
|
|
535
|
-
// Apply scale
|
|
536
|
-
if (this.#
|
|
537
|
-
const
|
|
538
|
-
target.
|
|
539
|
-
|
|
540
|
-
this
|
|
541
|
-
this.activeRegister = (r === 'regular') ? '' : r;
|
|
623
|
+
// Apply scale
|
|
624
|
+
if (this.#scaleEl) {
|
|
625
|
+
const s = SCALE_TIERS.includes(scale) ? scale : (target.getAttribute('scale') || '');
|
|
626
|
+
if (s) target.setAttribute('scale', s);
|
|
627
|
+
this.#scaleEl.value = s;
|
|
628
|
+
this.activeScale = s;
|
|
542
629
|
}
|
|
543
630
|
}
|
|
544
631
|
|