@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,274 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import '../../../web-components/core/element.js';
|
|
3
|
+
// Stub primitives that theme-panel composes — they don't need to render
|
|
4
|
+
// anything visible for these behavior tests; we just need them to register
|
|
5
|
+
// so customElements.define() inside theme-panel sees its DOM children as
|
|
6
|
+
// known elements (otherwise happy-dom treats them as HTMLUnknownElement,
|
|
7
|
+
// which is fine for our assertions).
|
|
8
|
+
import './theme-panel.js';
|
|
9
|
+
|
|
10
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
11
|
+
|
|
12
|
+
function mount(html) {
|
|
13
|
+
const wrap = document.createElement('div');
|
|
14
|
+
wrap.innerHTML = html;
|
|
15
|
+
document.body.appendChild(wrap);
|
|
16
|
+
return wrap.firstElementChild;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// happy-dom doesn't implement matchMedia; stub a non-dark default.
|
|
20
|
+
let mqlListeners = [];
|
|
21
|
+
function installMatchMedia({ dark = false } = {}) {
|
|
22
|
+
globalThis.matchMedia = (query) => ({
|
|
23
|
+
matches: query.includes('dark') ? dark : false,
|
|
24
|
+
media: query,
|
|
25
|
+
addEventListener: (_evt, fn) => { mqlListeners.push(fn); },
|
|
26
|
+
removeEventListener: (_evt, fn) => {
|
|
27
|
+
mqlListeners = mqlListeners.filter((f) => f !== fn);
|
|
28
|
+
},
|
|
29
|
+
addListener: (fn) => { mqlListeners.push(fn); },
|
|
30
|
+
removeListener: (fn) => {
|
|
31
|
+
mqlListeners = mqlListeners.filter((f) => f !== fn);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// happy-dom's requestAnimationFrame is sync via setTimeout; force it
|
|
37
|
+
// synchronous so #onThemeClick's slider sync runs in the test's tick().
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
document.body.innerHTML = '';
|
|
40
|
+
try { localStorage.clear(); } catch {}
|
|
41
|
+
mqlListeners = [];
|
|
42
|
+
installMatchMedia({ dark: false });
|
|
43
|
+
// Synchronous rAF for deterministic tests
|
|
44
|
+
globalThis.requestAnimationFrame = (cb) => { cb(); return 0; };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
// Always clean any html-level inline state so tests don't leak
|
|
49
|
+
document.documentElement.removeAttribute('data-theme');
|
|
50
|
+
document.documentElement.style.removeProperty('--a-density');
|
|
51
|
+
document.documentElement.style.removeProperty('--a-radius-k');
|
|
52
|
+
document.documentElement.style.removeProperty('color-scheme');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('theme-panel', () => {
|
|
56
|
+
it('registers theme-panel as a custom element', () => {
|
|
57
|
+
expect(customElements.get('theme-panel')).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('stamps internal DOM on connect (themes row)', async () => {
|
|
61
|
+
const tp = mount('<theme-panel></theme-panel>');
|
|
62
|
+
await tick();
|
|
63
|
+
const themes = tp.querySelector('[part="themes"]');
|
|
64
|
+
expect(themes).toBeTruthy();
|
|
65
|
+
// 8 default themes
|
|
66
|
+
expect(themes.querySelectorAll('button-ui').length).toBe(8);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders only minimum surface by default (no sliders, presets, scheme)', async () => {
|
|
70
|
+
const tp = mount('<theme-panel></theme-panel>');
|
|
71
|
+
await tick();
|
|
72
|
+
expect(tp.querySelector('[part="presets"]')).toBeNull();
|
|
73
|
+
expect(tp.querySelector('[part="scheme"]')).toBeNull();
|
|
74
|
+
expect(tp.querySelector('slider-ui')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('[parametric] adds density + radius sliders', async () => {
|
|
78
|
+
const tp = mount('<theme-panel parametric></theme-panel>');
|
|
79
|
+
await tick();
|
|
80
|
+
expect(tp.querySelector('slider-ui[part="density"]')).toBeTruthy();
|
|
81
|
+
expect(tp.querySelector('slider-ui[part="radius"]')).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('[presets] adds the preset row (compact/default/spacious)', async () => {
|
|
85
|
+
const tp = mount('<theme-panel presets></theme-panel>');
|
|
86
|
+
await tick();
|
|
87
|
+
const presets = tp.querySelector('[part="presets"]');
|
|
88
|
+
expect(presets).toBeTruthy();
|
|
89
|
+
const buttons = [...presets.querySelectorAll('button-ui')];
|
|
90
|
+
expect(buttons.map((b) => b.getAttribute('data-preset'))).toEqual(['compact', 'default', 'spacious']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('[scheme-toggle] adds the segmented scheme picker', async () => {
|
|
94
|
+
const tp = mount('<theme-panel scheme-toggle></theme-panel>');
|
|
95
|
+
await tick();
|
|
96
|
+
expect(tp.querySelector('[part="scheme"] segmented-ui')).toBeTruthy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('theme button click sets [data-theme] on the target (defaults to <html>)', async () => {
|
|
100
|
+
const tp = mount('<theme-panel></theme-panel>');
|
|
101
|
+
await tick();
|
|
102
|
+
const oceanBtn = tp.querySelector('button-ui[data-theme-slug="ocean"]');
|
|
103
|
+
oceanBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
104
|
+
await tick();
|
|
105
|
+
expect(document.documentElement.getAttribute('data-theme')).toBe('ocean');
|
|
106
|
+
expect(tp.getAttribute('active-theme')).toBe('ocean');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('theme button click for "default" clears [data-theme]', async () => {
|
|
110
|
+
document.documentElement.setAttribute('data-theme', 'ocean');
|
|
111
|
+
const tp = mount('<theme-panel></theme-panel>');
|
|
112
|
+
await tick();
|
|
113
|
+
const defaultBtn = tp.querySelector('button-ui[data-theme-slug="default"]');
|
|
114
|
+
defaultBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
115
|
+
await tick();
|
|
116
|
+
expect(document.documentElement.hasAttribute('data-theme')).toBe(false);
|
|
117
|
+
expect(tp.getAttribute('active-theme')).toBe('');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('fires theme-change event with source: "theme" on theme click', async () => {
|
|
121
|
+
const tp = mount('<theme-panel></theme-panel>');
|
|
122
|
+
await tick();
|
|
123
|
+
const onChange = vi.fn();
|
|
124
|
+
tp.addEventListener('theme-change', onChange);
|
|
125
|
+
tp.querySelector('button-ui[data-theme-slug="forest"]').dispatchEvent(new Event('click', { bubbles: true }));
|
|
126
|
+
await tick();
|
|
127
|
+
expect(onChange).toHaveBeenCalled();
|
|
128
|
+
const detail = onChange.mock.calls[0][0].detail;
|
|
129
|
+
expect(detail.theme).toBe('forest');
|
|
130
|
+
expect(detail.source).toBe('theme');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('sliders write --a-density / --a-radius-k on target', async () => {
|
|
134
|
+
const tp = mount('<theme-panel parametric></theme-panel>');
|
|
135
|
+
await tick();
|
|
136
|
+
const density = tp.querySelector('slider-ui[part="density"]');
|
|
137
|
+
density.value = '1.2';
|
|
138
|
+
density.dispatchEvent(new Event('input', { bubbles: true }));
|
|
139
|
+
await tick();
|
|
140
|
+
expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('1.2');
|
|
141
|
+
|
|
142
|
+
const radius = tp.querySelector('slider-ui[part="radius"]');
|
|
143
|
+
radius.value = '0.6';
|
|
144
|
+
radius.dispatchEvent(new Event('input', { bubbles: true }));
|
|
145
|
+
await tick();
|
|
146
|
+
expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('0.6');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('preset "compact" applies (0.85, 0.75)', async () => {
|
|
150
|
+
const tp = mount('<theme-panel parametric presets></theme-panel>');
|
|
151
|
+
await tick();
|
|
152
|
+
const compact = tp.querySelector('button-ui[data-preset="compact"]');
|
|
153
|
+
compact.dispatchEvent(new Event('click', { bubbles: true }));
|
|
154
|
+
await tick();
|
|
155
|
+
expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('0.85');
|
|
156
|
+
expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('0.75');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('preset "spacious" applies (1.15, 1.25)', async () => {
|
|
160
|
+
const tp = mount('<theme-panel parametric presets></theme-panel>');
|
|
161
|
+
await tick();
|
|
162
|
+
const spacious = tp.querySelector('button-ui[data-preset="spacious"]');
|
|
163
|
+
spacious.dispatchEvent(new Event('click', { bubbles: true }));
|
|
164
|
+
await tick();
|
|
165
|
+
expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('1.15');
|
|
166
|
+
expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('1.25');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('preset "default" (Reset) clears [data-theme]', async () => {
|
|
170
|
+
document.documentElement.setAttribute('data-theme', 'ocean');
|
|
171
|
+
const tp = mount('<theme-panel parametric presets></theme-panel>');
|
|
172
|
+
await tick();
|
|
173
|
+
const reset = tp.querySelector('button-ui[data-preset="default"]');
|
|
174
|
+
reset.dispatchEvent(new Event('click', { bubbles: true }));
|
|
175
|
+
await tick();
|
|
176
|
+
expect(document.documentElement.hasAttribute('data-theme')).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('[persist] writes localStorage keys on theme click', async () => {
|
|
180
|
+
const tp = mount('<theme-panel persist></theme-panel>');
|
|
181
|
+
await tick();
|
|
182
|
+
tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
|
|
183
|
+
await tick();
|
|
184
|
+
expect(localStorage.getItem('adia-theme-theme')).toBe('ocean');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('absence of [persist] does NOT write localStorage', async () => {
|
|
188
|
+
const tp = mount('<theme-panel></theme-panel>');
|
|
189
|
+
await tick();
|
|
190
|
+
tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
|
|
191
|
+
await tick();
|
|
192
|
+
expect(localStorage.getItem('adia-theme-theme')).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('reset() clears all state and emits source: "reset"', async () => {
|
|
196
|
+
document.documentElement.setAttribute('data-theme', 'ocean');
|
|
197
|
+
document.documentElement.style.setProperty('--a-density', '1.2');
|
|
198
|
+
const tp = mount('<theme-panel parametric persist></theme-panel>');
|
|
199
|
+
await tick();
|
|
200
|
+
const onChange = vi.fn();
|
|
201
|
+
tp.addEventListener('theme-change', onChange);
|
|
202
|
+
tp.reset();
|
|
203
|
+
await tick();
|
|
204
|
+
expect(document.documentElement.hasAttribute('data-theme')).toBe(false);
|
|
205
|
+
expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('');
|
|
206
|
+
expect(onChange).toHaveBeenCalled();
|
|
207
|
+
expect(onChange.mock.calls[0][0].detail.source).toBe('reset');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('[storage-prefix] override changes LS key namespace', async () => {
|
|
211
|
+
const tp = mount('<theme-panel persist storage-prefix="my-app-"></theme-panel>');
|
|
212
|
+
await tick();
|
|
213
|
+
tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
|
|
214
|
+
await tick();
|
|
215
|
+
expect(localStorage.getItem('my-app-theme')).toBe('ocean');
|
|
216
|
+
expect(localStorage.getItem('adia-theme-theme')).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('apply({theme}) is equivalent to a theme click and emits source: "programmatic"', async () => {
|
|
220
|
+
const tp = mount('<theme-panel></theme-panel>');
|
|
221
|
+
await tick();
|
|
222
|
+
const onChange = vi.fn();
|
|
223
|
+
tp.addEventListener('theme-change', onChange);
|
|
224
|
+
tp.apply({ theme: 'slate' });
|
|
225
|
+
await tick();
|
|
226
|
+
expect(document.documentElement.getAttribute('data-theme')).toBe('slate');
|
|
227
|
+
expect(onChange.mock.calls[0][0].detail.source).toBe('programmatic');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('PCM listener attaches when scheme="auto" and no persisted scheme', async () => {
|
|
231
|
+
const tp = mount('<theme-panel scheme-toggle></theme-panel>');
|
|
232
|
+
await tick();
|
|
233
|
+
// mqlListeners is populated by our matchMedia stub on addEventListener('change', ...)
|
|
234
|
+
expect(mqlListeners.length).toBeGreaterThan(0);
|
|
235
|
+
expect(tp.getAttribute('active-scheme')).toBe('light'); // our stub returns dark=false
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('PCM listener does NOT attach when [persist] has a saved scheme', async () => {
|
|
239
|
+
try { localStorage.setItem('adia-theme-scheme', 'dark'); } catch {}
|
|
240
|
+
const tp = mount('<theme-panel persist scheme-toggle></theme-panel>');
|
|
241
|
+
await tick();
|
|
242
|
+
expect(mqlListeners.length).toBe(0);
|
|
243
|
+
expect(tp.getAttribute('active-scheme')).toBe('dark');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('[target] scoped selector writes to that element, not <html>', async () => {
|
|
247
|
+
const preview = document.createElement('div');
|
|
248
|
+
preview.id = 'preview';
|
|
249
|
+
document.body.appendChild(preview);
|
|
250
|
+
const tp = mount('<theme-panel target="#preview"></theme-panel>');
|
|
251
|
+
await tick();
|
|
252
|
+
tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
|
|
253
|
+
await tick();
|
|
254
|
+
expect(preview.getAttribute('data-theme')).toBe('ocean');
|
|
255
|
+
expect(document.documentElement.getAttribute('data-theme')).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('cleans up listeners on disconnect (no zombie listeners)', async () => {
|
|
259
|
+
const tp = mount('<theme-panel scheme-toggle></theme-panel>');
|
|
260
|
+
await tick();
|
|
261
|
+
expect(mqlListeners.length).toBeGreaterThan(0);
|
|
262
|
+
tp.remove();
|
|
263
|
+
await tick();
|
|
264
|
+
expect(mqlListeners.length).toBe(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('restricted [themes] list renders only the listed slugs', async () => {
|
|
268
|
+
const tp = mount('<theme-panel themes="default ocean"></theme-panel>');
|
|
269
|
+
await tick();
|
|
270
|
+
const buttons = [...tp.querySelectorAll('[part="themes"] button-ui')];
|
|
271
|
+
expect(buttons.length).toBe(2);
|
|
272
|
+
expect(buttons.map((b) => b.getAttribute('data-theme-slug'))).toEqual(['default', 'ocean']);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: ThemePanel
|
|
4
|
+
tag: theme-panel
|
|
5
|
+
component: ThemePanel
|
|
6
|
+
category: layout
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Module-tier appearance-preferences control surface. Owns the three knobs
|
|
10
|
+
of the AdiaUI theming contract: [data-theme=<slug>] named themes,
|
|
11
|
+
--a-density / --a-radius-k parametric overrides, and color-scheme
|
|
12
|
+
light/dark switching, plus optional localStorage persistence behind a
|
|
13
|
+
small attribute API.
|
|
14
|
+
|
|
15
|
+
Drops into any consumer's <popover-ui slot="content"> (the canonical
|
|
16
|
+
composition) or directly into a sidebar section. Eight named themes
|
|
17
|
+
ship by default; section visibility flips on/off via boolean attributes
|
|
18
|
+
([parametric], [presets], [scheme-toggle]).
|
|
19
|
+
|
|
20
|
+
Promoted from the duplicated <div id="theme-panel"> block in site/ and
|
|
21
|
+
playgrounds/admin-shell/ — see docs/specs/theme-panel-module.md.
|
|
22
|
+
|
|
23
|
+
props:
|
|
24
|
+
themes:
|
|
25
|
+
description: |
|
|
26
|
+
Space-separated list of theme slugs to render as buttons. Author
|
|
27
|
+
may restrict ([themes="default ocean"]) or extend. Tolerant —
|
|
28
|
+
unknown slugs render a button; clicking applies [data-theme=slug]
|
|
29
|
+
regardless of whether themes.css has a matching block.
|
|
30
|
+
type: string
|
|
31
|
+
default: "default ocean forest sunset lavender rose slate midnight"
|
|
32
|
+
reflect: true
|
|
33
|
+
|
|
34
|
+
parametric:
|
|
35
|
+
description: |
|
|
36
|
+
Renders the density + radius slider block. Sliders bind to
|
|
37
|
+
--a-density and --a-radius-k on the target element.
|
|
38
|
+
type: boolean
|
|
39
|
+
default: false
|
|
40
|
+
reflect: true
|
|
41
|
+
|
|
42
|
+
presets:
|
|
43
|
+
description: |
|
|
44
|
+
Renders the compact / reset / spacious preset row. Each preset
|
|
45
|
+
applies a (density, radius) pair; "default" also clears the
|
|
46
|
+
[data-theme] attribute.
|
|
47
|
+
type: boolean
|
|
48
|
+
default: false
|
|
49
|
+
reflect: true
|
|
50
|
+
|
|
51
|
+
scheme-toggle:
|
|
52
|
+
description: |
|
|
53
|
+
Renders an integrated light/dark <segmented-ui> at the top of the
|
|
54
|
+
panel. When absent, the panel omits scheme entirely — consumers
|
|
55
|
+
can still drive scheme via the .apply({scheme}) public method.
|
|
56
|
+
type: boolean
|
|
57
|
+
default: false
|
|
58
|
+
reflect: true
|
|
59
|
+
attribute: scheme-toggle
|
|
60
|
+
|
|
61
|
+
persist:
|
|
62
|
+
description: |
|
|
63
|
+
Persists selections to localStorage. Defaults to ephemeral so
|
|
64
|
+
playgrounds and embedded demos do not silently mutate a user's
|
|
65
|
+
docs-shell preferences.
|
|
66
|
+
type: boolean
|
|
67
|
+
default: false
|
|
68
|
+
reflect: true
|
|
69
|
+
|
|
70
|
+
storage-prefix:
|
|
71
|
+
description: |
|
|
72
|
+
Namespace for localStorage keys when [persist] is set. Four keys
|
|
73
|
+
are written: {prefix}theme, {prefix}scheme, {prefix}density,
|
|
74
|
+
{prefix}radius.
|
|
75
|
+
type: string
|
|
76
|
+
default: "adia-theme-"
|
|
77
|
+
reflect: true
|
|
78
|
+
attribute: storage-prefix
|
|
79
|
+
|
|
80
|
+
target:
|
|
81
|
+
description: |
|
|
82
|
+
CSS selector for the element that receives [data-theme] and
|
|
83
|
+
--a-density / --a-radius-k writes. Defaults to :root (the
|
|
84
|
+
<html> element). Scoped targets are useful for preview-pane
|
|
85
|
+
demos.
|
|
86
|
+
type: string
|
|
87
|
+
default: ":root"
|
|
88
|
+
reflect: true
|
|
89
|
+
|
|
90
|
+
scheme:
|
|
91
|
+
description: |
|
|
92
|
+
Initial color-scheme. "auto" follows prefers-color-scheme via a
|
|
93
|
+
matchMedia listener; user clicks on the scheme toggle promote to
|
|
94
|
+
an explicit light or dark choice.
|
|
95
|
+
type: string
|
|
96
|
+
default: "auto"
|
|
97
|
+
enum: ["light", "dark", "auto"]
|
|
98
|
+
reflect: true
|
|
99
|
+
|
|
100
|
+
active-theme:
|
|
101
|
+
description: |
|
|
102
|
+
Reflected — the currently selected theme slug. Empty string when
|
|
103
|
+
unset (default theme). Read-only externally; use .apply({theme})
|
|
104
|
+
to change.
|
|
105
|
+
type: string
|
|
106
|
+
default: ""
|
|
107
|
+
reflect: true
|
|
108
|
+
attribute: active-theme
|
|
109
|
+
|
|
110
|
+
active-scheme:
|
|
111
|
+
description: |
|
|
112
|
+
Reflected — the resolved color-scheme (auto collapsed to a
|
|
113
|
+
concrete value). Either "light" or "dark".
|
|
114
|
+
type: string
|
|
115
|
+
default: ""
|
|
116
|
+
reflect: true
|
|
117
|
+
attribute: active-scheme
|
|
118
|
+
|
|
119
|
+
active-density:
|
|
120
|
+
description: |
|
|
121
|
+
Reflected — mirrors the current --a-density value on the target
|
|
122
|
+
element. Stringified number.
|
|
123
|
+
type: string
|
|
124
|
+
default: ""
|
|
125
|
+
reflect: true
|
|
126
|
+
attribute: active-density
|
|
127
|
+
|
|
128
|
+
active-radius:
|
|
129
|
+
description: |
|
|
130
|
+
Reflected — mirrors the current --a-radius-k value on the target
|
|
131
|
+
element. Stringified number.
|
|
132
|
+
type: string
|
|
133
|
+
default: ""
|
|
134
|
+
reflect: true
|
|
135
|
+
attribute: active-radius
|
|
136
|
+
|
|
137
|
+
events:
|
|
138
|
+
theme-change:
|
|
139
|
+
description: |
|
|
140
|
+
Bubbles when any user-visible state changes (theme click, slider
|
|
141
|
+
input, preset click, scheme flip, reset). One event per change.
|
|
142
|
+
No event on auto-rehydrate-from-storage at boot.
|
|
143
|
+
detail:
|
|
144
|
+
theme: string
|
|
145
|
+
scheme: string
|
|
146
|
+
density: number
|
|
147
|
+
radius: number
|
|
148
|
+
source: string
|
|
149
|
+
|
|
150
|
+
slots: {}
|
|
151
|
+
|
|
152
|
+
states:
|
|
153
|
+
- name: idle
|
|
154
|
+
description: Default — panel rendered, awaiting input.
|
|
155
|
+
- name: persisted
|
|
156
|
+
description: |
|
|
157
|
+
[persist] is set; selections write to localStorage under
|
|
158
|
+
[storage-prefix].
|
|
159
|
+
attribute: persist
|
|
160
|
+
- name: scheme-auto
|
|
161
|
+
description: |
|
|
162
|
+
[scheme="auto"] and no user override this session; a
|
|
163
|
+
prefers-color-scheme listener reapplies on system flip.
|
|
164
|
+
|
|
165
|
+
traits: []
|
|
166
|
+
|
|
167
|
+
a2ui:
|
|
168
|
+
rules:
|
|
169
|
+
- >-
|
|
170
|
+
theme-panel is the canonical appearance-preferences popover.
|
|
171
|
+
Compose inside a <popover-ui slot="content"> in the topbar of a
|
|
172
|
+
shell (admin, chat, editor, simple) when the page exposes user
|
|
173
|
+
theming. Avoid hardcoding it as a shell child — placement is the
|
|
174
|
+
consumer's call.
|
|
175
|
+
- >-
|
|
176
|
+
Use [persist] for the docs surface or a real product app; omit
|
|
177
|
+
[persist] for playgrounds and embedded demos so state stays
|
|
178
|
+
ephemeral. The default is ephemeral.
|
|
179
|
+
- >-
|
|
180
|
+
Add boolean attributes [parametric], [presets], [scheme-toggle]
|
|
181
|
+
to enable sections; the minimum panel is the theme button grid.
|
|
182
|
+
|
|
183
|
+
keywords:
|
|
184
|
+
- theme
|
|
185
|
+
- theme-panel
|
|
186
|
+
- palette
|
|
187
|
+
- density
|
|
188
|
+
- radius
|
|
189
|
+
- scheme
|
|
190
|
+
- dark-mode
|
|
191
|
+
- light-mode
|
|
192
|
+
- color-scheme
|
|
193
|
+
- preferences
|
|
194
|
+
- settings
|
|
195
|
+
- color
|
|
196
|
+
- appearance
|
|
197
|
+
- skin
|
|
198
|
+
|
|
199
|
+
synonyms:
|
|
200
|
+
theme: [palette, skin, appearance]
|
|
201
|
+
scheme: [mode, color-scheme, dark-mode]
|
|
202
|
+
density: [compactness, spacing-scale]
|
|
203
|
+
radius: [corner-roundness, border-radius]
|
|
204
|
+
|
|
205
|
+
related:
|
|
206
|
+
- Popover
|
|
207
|
+
- Button
|
|
208
|
+
- Slider
|
|
209
|
+
- Field
|
|
210
|
+
- Divider
|
|
211
|
+
- Text
|
|
212
|
+
- Segmented
|
|
213
|
+
- AdminShell
|