@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `<a2ui-root-ui>` — 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.
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-ui>` — Composition shell for chat + canvas generative UI. Manages layout modes
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-ui>` — Module-tier command palette wrapper — wraps a native <dialog> and the
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-ui>` — Behavior-only application shell. Canonical authoring shape uses
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-ui>` — Module-tier shell sidebar — owns resize, snap-to-collapsed, persistence,
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
- /* Opt-in resize affordanceresize needs overflow visible (satisfied above). */
23
- embed-shell[traits~="resizable"] {
24
- resize: both;
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-ui>` — Embedded multi-surface shell — composes a primary [app] surface with secondary
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
- class EmbedShell extends HTMLElement {
30
- #wired = false;
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
- connectedCallback() {
33
- if (this.#wired) return;
34
- this.#wired = true;
35
- this.addEventListener('click', (e) => {
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-ui>` — Module-tier minimal shell for marketing / landing / error pages.
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: [data-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",
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[data-theme] attribute.\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 [data-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",
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 [data-theme=slug]\nregardless of whether themes.css has a matching block.\n",
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-ui>` — Module-tier appearance-preferences control surface. Owns the three knobs
3
- of the AdiaUI theming contract: [data-theme=<slug>] named themes,
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
- [data-theme] attribute.
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 [data-theme] and
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 [data-theme=slug]
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: [data-theme] named themes, --a-density /
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 [data-theme], inline parametric
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('data-theme');
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 [data-theme] on target)
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 [data-theme] block has taken effect.
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 [data-theme] too
367
+ // "Reset" preset — clears [theme] too
368
368
  const target = this.#resolveTarget();
369
- target.removeAttribute('data-theme');
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('data-theme');
387
+ target.removeAttribute('theme');
388
388
  this.activeTheme = '';
389
389
  } else {
390
- target.setAttribute('data-theme', partial.theme);
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('data-theme', theme);
488
+ target.setAttribute('theme', theme);
489
489
  this.activeTheme = theme;
490
490
  } else {
491
- this.activeTheme = target.getAttribute('data-theme') || '';
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('data-theme');
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 [data-theme] on the target (defaults to <html>)', async () => {
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('data-theme')).toBe('ocean');
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 [data-theme]', async () => {
110
- document.documentElement.setAttribute('data-theme', 'ocean');
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('data-theme')).toBe(false);
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 [data-theme]', async () => {
170
- document.documentElement.setAttribute('data-theme', 'ocean');
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('data-theme')).toBe(false);
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('data-theme', 'ocean');
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('data-theme')).toBe(false);
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('data-theme')).toBe('slate');
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('data-theme')).toBe('ocean');
255
- expect(document.documentElement.getAttribute('data-theme')).toBeNull();
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: [data-theme=<slug>] named themes,
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 [data-theme=slug]
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
- [data-theme] attribute.
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 [data-theme] and
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.