@adia-ai/web-modules 0.7.10 → 0.7.12
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 +13 -5
- package/README.md +1 -1
- package/chat/chat-composer/chat-composer.d.ts +1 -1
- package/chat/chat-shell/chat-shell.d.ts +1 -1
- package/chat/chat-sidebar/chat-sidebar.d.ts +1 -1
- package/chat/chat-thread/chat-thread.d.ts +1 -1
- package/dist/everything.min.js +67 -67
- package/dist/web-modules.min.css +1 -1
- package/dist/web-modules.sheet.js +11 -0
- package/editor/editor-canvas/editor-canvas.d.ts +1 -1
- package/editor/editor-shell/editor-shell.d.ts +1 -1
- package/editor/editor-sidebar/editor-sidebar.d.ts +1 -1
- package/editor/editor-toolbar/editor-toolbar.d.ts +1 -1
- package/package.json +5 -1
- package/runtime/a2ui-root/a2ui-root.d.ts +1 -1
- package/runtime/gen-root/gen-root.d.ts +1 -1
- package/shell/admin-command/admin-command.d.ts +1 -1
- package/shell/admin-shell/admin-shell.d.ts +1 -1
- package/shell/admin-sidebar/admin-sidebar.d.ts +1 -1
- package/shell/embed-shell/embed-shell.css +3 -21
- package/shell/embed-shell/embed-shell.d.ts +1 -1
- package/shell/embed-shell/embed-shell.js +27 -15
- package/shell/embed-shell/embed-shell.test.js +127 -0
- package/simple/simple-shell/simple-shell.d.ts +1 -1
- package/theme/theme-panel/theme-panel.a2ui.json +4 -4
- package/theme/theme-panel/theme-panel.d.ts +5 -5
- package/theme/theme-panel/theme-panel.js +11 -11
- package/theme/theme-panel/theme-panel.test.js +14 -14
- package/theme/theme-panel/theme-panel.yaml +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<a2ui-root
|
|
2
|
+
* `<a2ui-root>` — A2UI protocol surface. Connects to a stream source (SSE, WebSocket, JSONL, MCP) and renders A2UI messages as AdiaUI components via the `@adia-ai/a2ui-utils` renderer.
|
|
3
3
|
*
|
|
4
4
|
* @see https://ui-kit.exe.xyz/site/components/a2ui-root
|
|
5
5
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<gen-root
|
|
2
|
+
* `<gen-root>` — Composition shell for chat + canvas generative UI. Manages layout modes
|
|
3
3
|
(chat-only / split / canvas-only) and delegates a unified API across
|
|
4
4
|
child chat-ui, canvas-ui, and inspector-ui elements.
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<admin-command
|
|
2
|
+
* `<admin-command>` — Module-tier command palette wrapper — wraps a native <dialog> and the
|
|
3
3
|
inner <command-ui>. Owns the keyboard shortcut listener, focus
|
|
4
4
|
management, and dismiss handlers. Reflects [open].
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<admin-shell
|
|
2
|
+
* `<admin-shell>` — Behavior-only application shell. Canonical authoring shape uses
|
|
3
3
|
cluster-namespaced bespoke children — <admin-topbar> at the top,
|
|
4
4
|
<admin-sidebar slot="leading|trailing"> rails, <admin-content> for
|
|
5
5
|
the main content region, <admin-command> for the Cmd+K palette, and
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<admin-sidebar
|
|
2
|
+
* `<admin-sidebar>` — Module-tier shell sidebar — owns resize, snap-to-collapsed, persistence,
|
|
3
3
|
and the [collapsed] reflected attribute. Sits inside <admin-shell> as
|
|
4
4
|
slot="leading" or slot="trailing". Authors compose chrome bars + content
|
|
5
5
|
inside via slot vocabulary.
|
|
@@ -19,27 +19,9 @@ embed-shell {
|
|
|
19
19
|
font-family: var(--a-font-family);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
/* The native ::-webkit-resizer is tiny + faint + half-hidden by the rounded
|
|
27
|
-
corner, so it reads as "not resizable". Draw a clear diagonal-grip cue inset
|
|
28
|
-
from the corner (clear of the radius); pointer-events:none keeps the native
|
|
29
|
-
resize working underneath it. */
|
|
30
|
-
embed-shell[traits~="resizable"]::after {
|
|
31
|
-
content: "";
|
|
32
|
-
position: absolute;
|
|
33
|
-
inset-block-end: 4px;
|
|
34
|
-
inset-inline-end: 4px;
|
|
35
|
-
inline-size: 12px;
|
|
36
|
-
block-size: 12px;
|
|
37
|
-
pointer-events: none;
|
|
38
|
-
opacity: 0.6;
|
|
39
|
-
background:
|
|
40
|
-
linear-gradient(135deg, transparent 0 46%, var(--a-fg-subtle) 46% 62%, transparent 62%),
|
|
41
|
-
linear-gradient(135deg, transparent 0 72%, var(--a-fg-subtle) 72% 88%, transparent 88%);
|
|
42
|
-
}
|
|
22
|
+
/* Resize is opt-in via traits="resizable" — the real `resizable` trait owns it
|
|
23
|
+
entirely (invisible edge handles + a resize-end event). No grabber is drawn; the
|
|
24
|
+
resize cursor on edge-hover is the affordance. */
|
|
43
25
|
|
|
44
26
|
/* primary app surface: borderless, fills the frame (or 50% when a panel opens),
|
|
45
27
|
position:relative so it stays the containing block for its OWN sheets (and uses
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<embed-shell
|
|
2
|
+
* `<embed-shell>` — Embedded multi-surface shell — composes a primary [app] surface with secondary
|
|
3
3
|
[panel="<name>"] surfaces the consumer provides, arranging them responsively.
|
|
4
4
|
Peer of <admin-shell>. Content-agnostic: it orchestrates whatever [app] +
|
|
5
5
|
[panel] children it is given (it owns layout, not content), so any embedded app
|
|
@@ -25,25 +25,37 @@
|
|
|
25
25
|
*
|
|
26
26
|
* Layout lives in embed-shell.css: ≥760px the open panel is a 50/50 column; <760px
|
|
27
27
|
* it's a cover sheet (translateY — identity at rest, so popovers inside still anchor).
|
|
28
|
+
*
|
|
29
|
+
* Extends UIElement (the shell base) so the declarative trait pattern works:
|
|
30
|
+
* `<embed-shell traits="resizable">` applies the real `resizable` trait (drag any
|
|
31
|
+
* edge to resize + a `resize-end` event), not a CSS stand-in. The trait is imported
|
|
32
|
+
* here so it's registered for declarative lookup.
|
|
28
33
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
35
|
+
import '../../../web-components/traits/resizable/resizable.js'; // register `resizable` for traits="resizable"
|
|
36
|
+
|
|
37
|
+
class EmbedShell extends UIElement {
|
|
38
|
+
static template = () => null; // behavior-only, light-DOM — keep the consumer's children
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.#
|
|
35
|
-
this.addEventListener('
|
|
36
|
-
const opener = e.target.closest?.('[opens]');
|
|
37
|
-
if (opener && this.contains(opener)) { this.toggle(opener.getAttribute('opens')); return; }
|
|
38
|
-
const closer = e.target.closest?.('[close]');
|
|
39
|
-
if (closer && this.contains(closer)) this.close();
|
|
40
|
-
});
|
|
41
|
-
this.addEventListener('embed:open', (e) => this.toggle(e.detail?.panel));
|
|
42
|
-
this.addEventListener('keydown', (e) => {
|
|
43
|
-
if (e.key === 'Escape' && this.panel) { e.stopPropagation(); this.close(); }
|
|
44
|
-
});
|
|
40
|
+
connected() {
|
|
41
|
+
this.addEventListener('click', this.#onClick);
|
|
42
|
+
this.addEventListener('embed:open', this.#onEmbedOpen);
|
|
43
|
+
this.addEventListener('keydown', this.#onKeydown);
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
// Stable #field arrows — connect/reconnect re-adds are idempotent (the browser
|
|
47
|
+
// dedups identical listeners), satisfying the trait system's symmetric-lifecycle rule.
|
|
48
|
+
#onClick = (e) => {
|
|
49
|
+
const opener = e.target.closest?.('[opens]');
|
|
50
|
+
if (opener && this.contains(opener)) { this.toggle(opener.getAttribute('opens')); return; }
|
|
51
|
+
const closer = e.target.closest?.('[close]');
|
|
52
|
+
if (closer && this.contains(closer)) this.close();
|
|
53
|
+
};
|
|
54
|
+
#onEmbedOpen = (e) => this.toggle(e.detail?.panel);
|
|
55
|
+
#onKeydown = (e) => {
|
|
56
|
+
if (e.key === 'Escape' && this.panel) { e.stopPropagation(); this.close(); }
|
|
57
|
+
};
|
|
58
|
+
|
|
47
59
|
/** the open panel's name, or '' */
|
|
48
60
|
get panel() { return this.getAttribute('panel') || ''; }
|
|
49
61
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import './embed-shell.js'; // registers <embed-shell>
|
|
3
|
+
|
|
4
|
+
// A minimal consumer shape: one [app] surface with two [opens] triggers, plus
|
|
5
|
+
// two named [panel] surfaces (one carrying a [close] trigger). The shell owns
|
|
6
|
+
// layout, not content — divs stand in for the real consumer surfaces.
|
|
7
|
+
function mount() {
|
|
8
|
+
document.body.innerHTML = `
|
|
9
|
+
<embed-shell>
|
|
10
|
+
<div app>
|
|
11
|
+
<button id="open-chat" opens="chat">chat</button>
|
|
12
|
+
<button id="open-settings" opens="settings">settings</button>
|
|
13
|
+
</div>
|
|
14
|
+
<div panel="chat"><button id="close" close>x</button></div>
|
|
15
|
+
<div panel="settings"></div>
|
|
16
|
+
</embed-shell>`;
|
|
17
|
+
return document.body.querySelector('embed-shell');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
document.body.innerHTML = '';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ── open / close / toggle ─────────────────────────────────────────
|
|
25
|
+
describe('embed-shell — open / close / toggle API', () => {
|
|
26
|
+
it('open(name) reflects [panel] on the shell + sets [active] on the matching surface', () => {
|
|
27
|
+
const shell = mount();
|
|
28
|
+
shell.open('chat');
|
|
29
|
+
expect(shell.getAttribute('panel')).toBe('chat');
|
|
30
|
+
expect(shell.querySelector('[panel="chat"]').hasAttribute('active')).toBe(true);
|
|
31
|
+
expect(shell.panel).toBe('chat');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('open(name) switches the active panel — only one [active] at a time', () => {
|
|
35
|
+
const shell = mount();
|
|
36
|
+
shell.open('chat');
|
|
37
|
+
shell.open('settings');
|
|
38
|
+
expect(shell.getAttribute('panel')).toBe('settings');
|
|
39
|
+
expect(shell.querySelector('[panel="chat"]').hasAttribute('active')).toBe(false);
|
|
40
|
+
expect(shell.querySelector('[panel="settings"]').hasAttribute('active')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('open() with no matching [panel] is a no-op (consumer must provide the surface)', () => {
|
|
44
|
+
const shell = mount();
|
|
45
|
+
shell.open('nope');
|
|
46
|
+
expect(shell.hasAttribute('panel')).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('open() ignores an empty name', () => {
|
|
50
|
+
const shell = mount();
|
|
51
|
+
shell.open('');
|
|
52
|
+
expect(shell.hasAttribute('panel')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('close() clears [panel] + [active]; .panel reads ""', () => {
|
|
56
|
+
const shell = mount();
|
|
57
|
+
shell.open('chat');
|
|
58
|
+
shell.close();
|
|
59
|
+
expect(shell.hasAttribute('panel')).toBe(false);
|
|
60
|
+
expect(shell.querySelector('[panel="chat"]').hasAttribute('active')).toBe(false);
|
|
61
|
+
expect(shell.panel).toBe('');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('toggle() opens when closed, closes when the same panel is already open', () => {
|
|
65
|
+
const shell = mount();
|
|
66
|
+
shell.toggle('chat');
|
|
67
|
+
expect(shell.panel).toBe('chat');
|
|
68
|
+
shell.toggle('chat');
|
|
69
|
+
expect(shell.panel).toBe('');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('emits embed:change with the new panel name on open, "" on close', () => {
|
|
73
|
+
const shell = mount();
|
|
74
|
+
const seen = [];
|
|
75
|
+
shell.addEventListener('embed:change', (e) => seen.push(e.detail.panel));
|
|
76
|
+
shell.open('chat');
|
|
77
|
+
shell.close();
|
|
78
|
+
expect(seen).toEqual(['chat', '']);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── delegated triggers + inbound events ───────────────────────────
|
|
83
|
+
describe('embed-shell — delegated triggers + events', () => {
|
|
84
|
+
it('[opens] click toggles the named panel', () => {
|
|
85
|
+
const shell = mount();
|
|
86
|
+
shell.querySelector('#open-chat').click();
|
|
87
|
+
expect(shell.panel).toBe('chat');
|
|
88
|
+
shell.querySelector('#open-chat').click(); // same trigger toggles it back off
|
|
89
|
+
expect(shell.panel).toBe('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('[opens] switches directly between panels', () => {
|
|
93
|
+
const shell = mount();
|
|
94
|
+
shell.querySelector('#open-chat').click();
|
|
95
|
+
shell.querySelector('#open-settings').click();
|
|
96
|
+
expect(shell.panel).toBe('settings');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('[close] click dismisses the open panel', () => {
|
|
100
|
+
const shell = mount();
|
|
101
|
+
shell.open('chat');
|
|
102
|
+
shell.querySelector('#close').click();
|
|
103
|
+
expect(shell.panel).toBe('');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('embed:open event opens/toggles a panel', () => {
|
|
107
|
+
const shell = mount();
|
|
108
|
+
shell.querySelector('[app]').dispatchEvent(
|
|
109
|
+
new CustomEvent('embed:open', { detail: { panel: 'settings' }, bubbles: true }),
|
|
110
|
+
);
|
|
111
|
+
expect(shell.panel).toBe('settings');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('Escape closes the open panel', () => {
|
|
115
|
+
const shell = mount();
|
|
116
|
+
shell.open('chat');
|
|
117
|
+
shell.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
118
|
+
expect(shell.panel).toBe('');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('Escape is a no-op when no panel is open', () => {
|
|
122
|
+
const shell = mount();
|
|
123
|
+
// Should not throw; panel stays closed.
|
|
124
|
+
shell.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
125
|
+
expect(shell.panel).toBe('');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<simple-shell
|
|
2
|
+
* `<simple-shell>` — Module-tier minimal shell for marketing / landing / error pages.
|
|
3
3
|
Behavior-only orchestrator with two reflected attributes — [centered]
|
|
4
4
|
for vertical centering, [full-bleed] for dropping the max-width
|
|
5
5
|
constraint.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/ThemePanel.json",
|
|
4
4
|
"title": "ThemePanel",
|
|
5
|
-
"description": "Module-tier appearance-preferences control surface. Owns the three knobs\nof the AdiaUI theming contract: [
|
|
5
|
+
"description": "Module-tier appearance-preferences control surface. Owns the three knobs\nof the AdiaUI theming contract: [theme=<slug>] named themes,\n--a-density / --a-radius-k parametric overrides, and color-scheme\nlight/dark switching, plus optional localStorage persistence behind a\nsmall attribute API.\n\nDrops into any consumer's <popover-ui slot=\"content\"> (the canonical\ncomposition) or directly into a sidebar section. Eight named themes\nship by default; section visibility flips on/off via boolean attributes\n([parametric], [presets], [scheme-toggle]).\n\nPromoted from the duplicated <div id=\"theme-panel\"> block in site/ and\nplaygrounds/admin-shell/ — see docs/specs/theme-panel-module.md.\n",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"default": false
|
|
53
53
|
},
|
|
54
54
|
"presets": {
|
|
55
|
-
"description": "Renders the compact / reset / spacious preset row. Each preset\napplies a (density, radius) pair; \"default\" also clears the\n[
|
|
55
|
+
"description": "Renders the compact / reset / spacious preset row. Each preset\napplies a (density, radius) pair; \"default\" also clears the\n[theme] attribute.\n",
|
|
56
56
|
"type": "boolean",
|
|
57
57
|
"default": false
|
|
58
58
|
},
|
|
@@ -82,12 +82,12 @@
|
|
|
82
82
|
"default": "adia-theme-"
|
|
83
83
|
},
|
|
84
84
|
"target": {
|
|
85
|
-
"description": "CSS selector for the element that receives [
|
|
85
|
+
"description": "CSS selector for the element that receives [theme] and\n--a-density / --a-radius-k writes. Defaults to :root (the\n<html> element). Scoped targets are useful for preview-pane\ndemos.\n",
|
|
86
86
|
"type": "string",
|
|
87
87
|
"default": ":root"
|
|
88
88
|
},
|
|
89
89
|
"themes": {
|
|
90
|
-
"description": "Space-separated list of theme slugs to render as buttons. Author\nmay restrict ([themes=\"default ocean\"]) or extend. Tolerant —\nunknown slugs render a button; clicking applies [
|
|
90
|
+
"description": "Space-separated list of theme slugs to render as buttons. Author\nmay restrict ([themes=\"default ocean\"]) or extend. Tolerant —\nunknown slugs render a button; clicking applies [theme=slug]\nregardless of whether themes.css has a matching block.\n",
|
|
91
91
|
"type": "string",
|
|
92
92
|
"default": "default ocean forest sunset lavender rose slate midnight"
|
|
93
93
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<theme-panel
|
|
3
|
-
of the AdiaUI theming contract: [
|
|
2
|
+
* `<theme-panel>` — Module-tier appearance-preferences control surface. Owns the three knobs
|
|
3
|
+
of the AdiaUI theming contract: [theme=<slug>] named themes,
|
|
4
4
|
--a-density / --a-radius-k parametric overrides, and color-scheme
|
|
5
5
|
light/dark switching, plus optional localStorage persistence behind a
|
|
6
6
|
small attribute API.
|
|
@@ -47,7 +47,7 @@ docs-shell preferences.
|
|
|
47
47
|
persist: boolean;
|
|
48
48
|
/** Renders the compact / reset / spacious preset row. Each preset
|
|
49
49
|
applies a (density, radius) pair; "default" also clears the
|
|
50
|
-
[
|
|
50
|
+
[theme] attribute.
|
|
51
51
|
*/
|
|
52
52
|
presets: boolean;
|
|
53
53
|
/** Renders a "Scale Context" row (verse / regular / prose) — a
|
|
@@ -61,7 +61,7 @@ matchMedia listener; user clicks on the scheme toggle promote to
|
|
|
61
61
|
an explicit light or dark choice.
|
|
62
62
|
*/
|
|
63
63
|
scheme: 'light' | 'dark' | 'auto';
|
|
64
|
-
/** CSS selector for the element that receives [
|
|
64
|
+
/** CSS selector for the element that receives [theme] and
|
|
65
65
|
--a-density / --a-radius-k writes. Defaults to :root (the
|
|
66
66
|
<html> element). Scoped targets are useful for preview-pane
|
|
67
67
|
demos.
|
|
@@ -69,7 +69,7 @@ demos.
|
|
|
69
69
|
target: string;
|
|
70
70
|
/** Space-separated list of theme slugs to render as buttons. Author
|
|
71
71
|
may restrict ([themes="default ocean"]) or extend. Tolerant —
|
|
72
|
-
unknown slugs render a button; clicking applies [
|
|
72
|
+
unknown slugs render a button; clicking applies [theme=slug]
|
|
73
73
|
regardless of whether themes.css has a matching block.
|
|
74
74
|
*/
|
|
75
75
|
themes: string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* <theme-panel persist parametric presets scheme-toggle>
|
|
3
3
|
*
|
|
4
4
|
* Module-tier appearance-preferences control surface. Owns the three knobs
|
|
5
|
-
* of the AdiaUI theming contract: [
|
|
5
|
+
* of the AdiaUI theming contract: [theme] named themes, --a-density /
|
|
6
6
|
* --a-radius-k parametric overrides, and color-scheme light/dark switching,
|
|
7
7
|
* plus optional localStorage persistence.
|
|
8
8
|
*
|
|
@@ -103,13 +103,13 @@ class ThemePanel extends UIElement {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* Clear all state on target — remove [
|
|
106
|
+
* Clear all state on target — remove [theme], inline parametric
|
|
107
107
|
* overrides, color-scheme, and (if [persist]) localStorage keys.
|
|
108
108
|
* Emits theme-change with source: 'reset'.
|
|
109
109
|
*/
|
|
110
110
|
reset() {
|
|
111
111
|
const target = this.#resolveTarget();
|
|
112
|
-
target.removeAttribute('
|
|
112
|
+
target.removeAttribute('theme');
|
|
113
113
|
target.style.removeProperty('--a-density');
|
|
114
114
|
target.style.removeProperty('--a-radius-k');
|
|
115
115
|
target.style.removeProperty('color-scheme');
|
|
@@ -340,10 +340,10 @@ class ThemePanel extends UIElement {
|
|
|
340
340
|
localStorage.removeItem(`${this.storagePrefix}radius`);
|
|
341
341
|
} catch {}
|
|
342
342
|
}
|
|
343
|
-
// Apply the theme (which may write [
|
|
343
|
+
// Apply the theme (which may write [theme] on target)
|
|
344
344
|
this.#apply({ theme: slug }, 'theme');
|
|
345
345
|
// Then re-read computed values into sliders on next frame, so the
|
|
346
|
-
// theme's [
|
|
346
|
+
// theme's [theme] block has taken effect.
|
|
347
347
|
const sync = () => {
|
|
348
348
|
const cs = getComputedStyle(target);
|
|
349
349
|
const density = parseFloat(cs.getPropertyValue('--a-density')) || 1;
|
|
@@ -364,9 +364,9 @@ class ThemePanel extends UIElement {
|
|
|
364
364
|
const preset = PRESETS[name];
|
|
365
365
|
if (!preset) return;
|
|
366
366
|
if (preset.clearTheme) {
|
|
367
|
-
// "Reset" preset — clears [
|
|
367
|
+
// "Reset" preset — clears [theme] too
|
|
368
368
|
const target = this.#resolveTarget();
|
|
369
|
-
target.removeAttribute('
|
|
369
|
+
target.removeAttribute('theme');
|
|
370
370
|
this.activeTheme = '';
|
|
371
371
|
if (this.persist) {
|
|
372
372
|
try { localStorage.removeItem(`${this.storagePrefix}theme`); } catch {}
|
|
@@ -384,10 +384,10 @@ class ThemePanel extends UIElement {
|
|
|
384
384
|
|
|
385
385
|
if (partial.theme !== undefined) {
|
|
386
386
|
if (partial.theme === '' || partial.theme === 'default') {
|
|
387
|
-
target.removeAttribute('
|
|
387
|
+
target.removeAttribute('theme');
|
|
388
388
|
this.activeTheme = '';
|
|
389
389
|
} else {
|
|
390
|
-
target.setAttribute('
|
|
390
|
+
target.setAttribute('theme', partial.theme);
|
|
391
391
|
this.activeTheme = partial.theme;
|
|
392
392
|
}
|
|
393
393
|
if (this.persist) {
|
|
@@ -485,10 +485,10 @@ class ThemePanel extends UIElement {
|
|
|
485
485
|
|
|
486
486
|
// Apply theme
|
|
487
487
|
if (theme) {
|
|
488
|
-
target.setAttribute('
|
|
488
|
+
target.setAttribute('theme', theme);
|
|
489
489
|
this.activeTheme = theme;
|
|
490
490
|
} else {
|
|
491
|
-
this.activeTheme = target.getAttribute('
|
|
491
|
+
this.activeTheme = target.getAttribute('theme') || '';
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
// Apply scheme
|
|
@@ -46,7 +46,7 @@ beforeEach(() => {
|
|
|
46
46
|
|
|
47
47
|
afterEach(() => {
|
|
48
48
|
// Always clean any html-level inline state so tests don't leak
|
|
49
|
-
document.documentElement.removeAttribute('
|
|
49
|
+
document.documentElement.removeAttribute('theme');
|
|
50
50
|
document.documentElement.style.removeProperty('--a-density');
|
|
51
51
|
document.documentElement.style.removeProperty('--a-radius-k');
|
|
52
52
|
document.documentElement.style.removeProperty('color-scheme');
|
|
@@ -96,24 +96,24 @@ describe('theme-panel', () => {
|
|
|
96
96
|
expect(tp.querySelector('[part="scheme"] segmented-ui')).toBeTruthy();
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
it('theme button click sets [
|
|
99
|
+
it('theme button click sets [theme] on the target (defaults to <html>)', async () => {
|
|
100
100
|
const tp = mount('<theme-panel></theme-panel>');
|
|
101
101
|
await tick();
|
|
102
102
|
const oceanBtn = tp.querySelector('button-ui[data-theme-slug="ocean"]');
|
|
103
103
|
oceanBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
104
104
|
await tick();
|
|
105
|
-
expect(document.documentElement.getAttribute('
|
|
105
|
+
expect(document.documentElement.getAttribute('theme')).toBe('ocean');
|
|
106
106
|
expect(tp.getAttribute('active-theme')).toBe('ocean');
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it('theme button click for "default" clears [
|
|
110
|
-
document.documentElement.setAttribute('
|
|
109
|
+
it('theme button click for "default" clears [theme]', async () => {
|
|
110
|
+
document.documentElement.setAttribute('theme', 'ocean');
|
|
111
111
|
const tp = mount('<theme-panel></theme-panel>');
|
|
112
112
|
await tick();
|
|
113
113
|
const defaultBtn = tp.querySelector('button-ui[data-theme-slug="default"]');
|
|
114
114
|
defaultBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
115
115
|
await tick();
|
|
116
|
-
expect(document.documentElement.hasAttribute('
|
|
116
|
+
expect(document.documentElement.hasAttribute('theme')).toBe(false);
|
|
117
117
|
expect(tp.getAttribute('active-theme')).toBe('');
|
|
118
118
|
});
|
|
119
119
|
|
|
@@ -166,14 +166,14 @@ describe('theme-panel', () => {
|
|
|
166
166
|
expect(document.documentElement.style.getPropertyValue('--a-radius-k')).toBe('1.25');
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
-
it('preset "default" (Reset) clears [
|
|
170
|
-
document.documentElement.setAttribute('
|
|
169
|
+
it('preset "default" (Reset) clears [theme]', async () => {
|
|
170
|
+
document.documentElement.setAttribute('theme', 'ocean');
|
|
171
171
|
const tp = mount('<theme-panel parametric presets></theme-panel>');
|
|
172
172
|
await tick();
|
|
173
173
|
const reset = tp.querySelector('button-ui[data-preset="default"]');
|
|
174
174
|
reset.dispatchEvent(new Event('click', { bubbles: true }));
|
|
175
175
|
await tick();
|
|
176
|
-
expect(document.documentElement.hasAttribute('
|
|
176
|
+
expect(document.documentElement.hasAttribute('theme')).toBe(false);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
it('[persist] writes localStorage keys on theme click', async () => {
|
|
@@ -193,7 +193,7 @@ describe('theme-panel', () => {
|
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
it('reset() clears all state and emits source: "reset"', async () => {
|
|
196
|
-
document.documentElement.setAttribute('
|
|
196
|
+
document.documentElement.setAttribute('theme', 'ocean');
|
|
197
197
|
document.documentElement.style.setProperty('--a-density', '1.2');
|
|
198
198
|
const tp = mount('<theme-panel parametric persist></theme-panel>');
|
|
199
199
|
await tick();
|
|
@@ -201,7 +201,7 @@ describe('theme-panel', () => {
|
|
|
201
201
|
tp.addEventListener('theme-change', onChange);
|
|
202
202
|
tp.reset();
|
|
203
203
|
await tick();
|
|
204
|
-
expect(document.documentElement.hasAttribute('
|
|
204
|
+
expect(document.documentElement.hasAttribute('theme')).toBe(false);
|
|
205
205
|
expect(document.documentElement.style.getPropertyValue('--a-density')).toBe('');
|
|
206
206
|
expect(onChange).toHaveBeenCalled();
|
|
207
207
|
expect(onChange.mock.calls[0][0].detail.source).toBe('reset');
|
|
@@ -223,7 +223,7 @@ describe('theme-panel', () => {
|
|
|
223
223
|
tp.addEventListener('theme-change', onChange);
|
|
224
224
|
tp.apply({ theme: 'slate' });
|
|
225
225
|
await tick();
|
|
226
|
-
expect(document.documentElement.getAttribute('
|
|
226
|
+
expect(document.documentElement.getAttribute('theme')).toBe('slate');
|
|
227
227
|
expect(onChange.mock.calls[0][0].detail.source).toBe('programmatic');
|
|
228
228
|
});
|
|
229
229
|
|
|
@@ -251,8 +251,8 @@ describe('theme-panel', () => {
|
|
|
251
251
|
await tick();
|
|
252
252
|
tp.querySelector('button-ui[data-theme-slug="ocean"]').dispatchEvent(new Event('click', { bubbles: true }));
|
|
253
253
|
await tick();
|
|
254
|
-
expect(preview.getAttribute('
|
|
255
|
-
expect(document.documentElement.getAttribute('
|
|
254
|
+
expect(preview.getAttribute('theme')).toBe('ocean');
|
|
255
|
+
expect(document.documentElement.getAttribute('theme')).toBeNull();
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
it('cleans up listeners on disconnect (no zombie listeners)', async () => {
|
|
@@ -8,7 +8,7 @@ category: layout
|
|
|
8
8
|
version: 1
|
|
9
9
|
description: |
|
|
10
10
|
Module-tier appearance-preferences control surface. Owns the three knobs
|
|
11
|
-
of the AdiaUI theming contract: [
|
|
11
|
+
of the AdiaUI theming contract: [theme=<slug>] named themes,
|
|
12
12
|
--a-density / --a-radius-k parametric overrides, and color-scheme
|
|
13
13
|
light/dark switching, plus optional localStorage persistence behind a
|
|
14
14
|
small attribute API.
|
|
@@ -36,7 +36,7 @@ props:
|
|
|
36
36
|
description: |
|
|
37
37
|
Space-separated list of theme slugs to render as buttons. Author
|
|
38
38
|
may restrict ([themes="default ocean"]) or extend. Tolerant —
|
|
39
|
-
unknown slugs render a button; clicking applies [
|
|
39
|
+
unknown slugs render a button; clicking applies [theme=slug]
|
|
40
40
|
regardless of whether themes.css has a matching block.
|
|
41
41
|
type: string
|
|
42
42
|
default: "default ocean forest sunset lavender rose slate midnight"
|
|
@@ -54,7 +54,7 @@ props:
|
|
|
54
54
|
description: |
|
|
55
55
|
Renders the compact / reset / spacious preset row. Each preset
|
|
56
56
|
applies a (density, radius) pair; "default" also clears the
|
|
57
|
-
[
|
|
57
|
+
[theme] attribute.
|
|
58
58
|
type: boolean
|
|
59
59
|
default: false
|
|
60
60
|
reflect: true
|
|
@@ -100,7 +100,7 @@ props:
|
|
|
100
100
|
|
|
101
101
|
target:
|
|
102
102
|
description: |
|
|
103
|
-
CSS selector for the element that receives [
|
|
103
|
+
CSS selector for the element that receives [theme] and
|
|
104
104
|
--a-density / --a-radius-k writes. Defaults to :root (the
|
|
105
105
|
<html> element). Scoped targets are useful for preview-pane
|
|
106
106
|
demos.
|