symbiote-ui 0.3.0-alpha.39 → 0.3.0-alpha.40

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.
@@ -3657,9 +3657,9 @@
3657
3657
  }
3658
3658
  ],
3659
3659
  "agent": {
3660
- "componentDescription": "Generic hierarchical graph overview/read renderer with force layout, semantic navigation, and selection events. Use this interactive visual graph surface when composing canvas UI. Capabilities: hierarchical-graph, overview-read-renderer, force-layout, semantic-clusters, focus-selection, layout-snapshot. Data ownership: host-owned model; component renders and emits intent events. WebMCP tools: canvas_graph_set_model, canvas_graph_focus_node, canvas_graph_focus_nodes, canvas_graph_set_path.",
3660
+ "componentDescription": "Generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax. Use this interactive visual graph surface when composing canvas UI. Capabilities: hierarchical-graph, overview-read-renderer, force-layout, semantic-clusters, focus-selection, layout-snapshot, device-orientation-parallax. Data ownership: host-owned model; component renders and emits intent events. WebMCP tools: canvas_graph_set_model, canvas_graph_focus_node, canvas_graph_focus_nodes, canvas_graph_set_path.",
3661
3661
  "semanticRole": "interactive visual graph surface",
3662
- "usage": "Render or compose <canvas-graph> when the host needs generic hierarchical graph overview/read renderer with force layout, semantic navigation, and selection events.",
3662
+ "usage": "Render or compose <canvas-graph> when the host needs generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax.",
3663
3663
  "dataOwnership": "host-owned model; component renders and emits intent events",
3664
3664
  "webmcp": {
3665
3665
  "mode": "explicit-descriptor",
@@ -3259,7 +3259,7 @@ export let COMPONENTS = [
3259
3259
  className: 'CanvasGraph',
3260
3260
  module: 'canvas/CanvasGraph/CanvasGraph.js',
3261
3261
  category: 'canvas',
3262
- description: 'Generic hierarchical graph overview/read renderer with force layout, semantic navigation, and selection events.',
3262
+ description: 'Generic hierarchical graph overview/read renderer with force layout, semantic navigation, selection events, and optional device-orientation parallax.',
3263
3263
  contract: {
3264
3264
  status: 'draft',
3265
3265
  schemaVersion: 'component-descriptor-v2',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiote-ui",
3
- "version": "0.3.0-alpha.39",
3
+ "version": "0.3.0-alpha.40",
4
4
  "type": "module",
5
5
  "description": "Symbiote provider UI, Web Components, themes, manifests, schemas, graph layout helpers, WebMCP metadata, and SSR-safe UI contracts.",
6
6
  "main": "index.js",
@@ -7,189 +7,231 @@ export default css`
7
7
  align-items: center;
8
8
  color: var(--sn-text);
9
9
  font-family: var(--sn-font);
10
+ }
10
11
 
11
- &[hidden] {
12
- display: none !important;
13
- }
12
+ cascade-theme-widget[hidden] {
13
+ display: none !important;
14
+ }
14
15
 
15
- .ctw-trigger {
16
- flex: 0 0 auto;
17
- }
16
+ cascade-theme-widget .ctw-trigger {
17
+ flex: 0 0 auto;
18
+ }
18
19
 
19
- .ctw-trigger .material-symbols-outlined,
20
- .ctw-header-actions .material-symbols-outlined {
21
- font-size: var(--sn-shell-menu-action-icon-size, var(--sn-layout-header-icon-size, 16px));
22
- }
20
+ cascade-theme-widget .ctw-trigger .material-symbols-outlined,
21
+ cascade-theme-widget .ctw-header-actions .material-symbols-outlined,
22
+ .ctw-popover[data-overlay-portal] .ctw-header-actions .material-symbols-outlined {
23
+ font-size: var(--sn-shell-menu-action-icon-size, var(--sn-layout-header-icon-size, 16px));
24
+ }
23
25
 
24
- .ctw-popover {
25
- position: absolute;
26
- top: calc(100% + var(--sn-theme-widget-offset, 8px));
27
- right: 0;
28
- z-index: var(--sn-theme-widget-z, 80);
29
- display: grid;
30
- grid-template-rows: auto auto auto;
31
- gap: var(--sn-theme-widget-gap, calc(8px * var(--sn-theme-density, 1)));
32
- width: min(92vw, var(--sn-theme-widget-width, 320px));
33
- padding: var(--sn-theme-widget-padding, calc(10px * var(--sn-theme-density, 1)));
34
- border: var(--sn-node-border-width, 1px) solid var(--sn-node-border);
35
- border-radius: var(--sn-node-radius, 8px);
36
- background: var(--sn-panel-bg);
37
- box-shadow: var(--sn-panel-shadow, 0 16px 48px hsl(0 0% 0% / 0.28));
38
- color: var(--sn-text);
39
- }
26
+ cascade-theme-widget .ctw-popover,
27
+ .ctw-popover[data-overlay-portal] {
28
+ z-index: var(--sn-theme-widget-z, var(--sn-overlay-z-base, 20000));
29
+ display: grid;
30
+ grid-template-rows: auto auto auto;
31
+ gap: var(--sn-theme-widget-gap, calc(8px * var(--sn-theme-density, 1)));
32
+ width: min(92vw, var(--sn-theme-widget-width, 320px));
33
+ max-width: calc(100vw - 16px);
34
+ padding: var(--sn-theme-widget-padding, calc(10px * var(--sn-theme-density, 1)));
35
+ border: var(--sn-node-border-width, 1px) solid var(--sn-node-border);
36
+ border-radius: var(--sn-node-radius, 8px);
37
+ background: var(--sn-panel-bg);
38
+ box-shadow: var(--sn-panel-shadow, 0 16px 48px hsl(0 0% 0% / 0.28));
39
+ color: var(--sn-text);
40
+ font-family: var(--sn-font);
41
+ }
40
42
 
41
- .ctw-popover[hidden] {
42
- display: none;
43
- }
43
+ cascade-theme-widget .ctw-popover {
44
+ position: absolute;
45
+ top: calc(100% + var(--sn-theme-widget-offset, 8px));
46
+ right: 0;
47
+ }
44
48
 
45
- .ctw-header,
46
- .ctw-header-actions,
47
- .ctw-control,
48
- .ctw-control-head,
49
- .ctw-mode {
50
- display: flex;
51
- align-items: center;
52
- min-width: 0;
53
- }
49
+ .ctw-popover[data-overlay-portal] {
50
+ position: fixed;
51
+ top: 0;
52
+ left: 0;
53
+ right: auto;
54
+ }
54
55
 
55
- .ctw-header {
56
- justify-content: space-between;
57
- gap: var(--sn-theme-widget-gap, 8px);
58
- }
56
+ cascade-theme-widget .ctw-popover[hidden],
57
+ .ctw-popover[data-overlay-portal][hidden] {
58
+ display: none;
59
+ }
59
60
 
60
- .ctw-header strong {
61
- min-width: 0;
62
- overflow: hidden;
63
- font-size: var(--sn-theme-widget-title-size, var(--sn-app-title-size, 13px));
64
- text-overflow: ellipsis;
65
- white-space: nowrap;
66
- }
61
+ cascade-theme-widget .ctw-header,
62
+ cascade-theme-widget .ctw-header-actions,
63
+ cascade-theme-widget .ctw-control,
64
+ cascade-theme-widget .ctw-control-head,
65
+ cascade-theme-widget .ctw-mode,
66
+ .ctw-popover[data-overlay-portal] .ctw-header,
67
+ .ctw-popover[data-overlay-portal] .ctw-header-actions,
68
+ .ctw-popover[data-overlay-portal] .ctw-control,
69
+ .ctw-popover[data-overlay-portal] .ctw-control-head,
70
+ .ctw-popover[data-overlay-portal] .ctw-mode {
71
+ display: flex;
72
+ align-items: center;
73
+ min-width: 0;
74
+ }
67
75
 
68
- .ctw-header-actions {
69
- gap: var(--sn-theme-widget-action-gap, 4px);
70
- }
76
+ cascade-theme-widget .ctw-header,
77
+ .ctw-popover[data-overlay-portal] .ctw-header {
78
+ justify-content: space-between;
79
+ gap: var(--sn-theme-widget-gap, 8px);
80
+ }
71
81
 
72
- .ctw-header-actions button,
73
- .ctw-mode button {
74
- display: inline-flex;
75
- align-items: center;
76
- justify-content: center;
77
- border: 1px solid var(--sn-button-border, var(--sn-node-border));
78
- border-radius: var(--sn-button-radius, 6px);
79
- background: var(--sn-button-bg, var(--sn-node-bg));
80
- color: var(--sn-button-color, var(--sn-text));
81
- font: inherit;
82
- cursor: pointer;
83
- }
82
+ cascade-theme-widget .ctw-header strong,
83
+ .ctw-popover[data-overlay-portal] .ctw-header strong {
84
+ min-width: 0;
85
+ overflow: hidden;
86
+ font-size: var(--sn-theme-widget-title-size, var(--sn-app-title-size, 13px));
87
+ text-overflow: ellipsis;
88
+ white-space: nowrap;
89
+ }
84
90
 
85
- .ctw-header-actions button {
86
- width: var(--sn-theme-widget-icon-button-size, var(--sn-theme-editor-icon-button-size, 28px));
87
- min-width: var(--sn-theme-widget-icon-button-size, var(--sn-theme-editor-icon-button-size, 28px));
88
- height: var(--sn-theme-widget-icon-button-size, var(--sn-theme-editor-icon-button-size, 28px));
89
- padding: 0;
90
- }
91
+ cascade-theme-widget .ctw-header-actions,
92
+ .ctw-popover[data-overlay-portal] .ctw-header-actions {
93
+ gap: var(--sn-theme-widget-action-gap, 4px);
94
+ }
91
95
 
92
- .ctw-mode {
93
- display: grid;
94
- grid-template-columns: repeat(2, minmax(0, 1fr));
95
- gap: var(--sn-theme-widget-mode-gap, 4px);
96
- padding: var(--sn-theme-widget-mode-padding, 3px);
97
- border: 1px solid var(--sn-node-border);
98
- border-radius: var(--sn-node-radius, 8px);
99
- background: var(--sn-bg);
100
- }
96
+ cascade-theme-widget .ctw-header-actions button,
97
+ cascade-theme-widget .ctw-mode button,
98
+ .ctw-popover[data-overlay-portal] .ctw-header-actions button,
99
+ .ctw-popover[data-overlay-portal] .ctw-mode button {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ border: 1px solid var(--sn-button-border, var(--sn-node-border));
104
+ border-radius: var(--sn-button-radius, 6px);
105
+ background: var(--sn-button-bg, var(--sn-node-bg));
106
+ color: var(--sn-button-color, var(--sn-text));
107
+ font: inherit;
108
+ cursor: pointer;
109
+ }
101
110
 
102
- .ctw-mode button {
103
- min-width: 0;
104
- min-height: var(--sn-theme-widget-mode-height, var(--sn-button-min-height, 28px));
105
- padding: var(--sn-theme-widget-mode-button-padding, 5px 10px);
106
- color: var(--sn-text-dim);
107
- font-size: var(--sn-theme-widget-control-size, var(--sn-button-font-size, 12px));
108
- }
111
+ cascade-theme-widget .ctw-header-actions button,
112
+ .ctw-popover[data-overlay-portal] .ctw-header-actions button {
113
+ width: var(--sn-theme-widget-icon-button-size, var(--sn-theme-editor-icon-button-size, 28px));
114
+ min-width: var(--sn-theme-widget-icon-button-size, var(--sn-theme-editor-icon-button-size, 28px));
115
+ height: var(--sn-theme-widget-icon-button-size, var(--sn-theme-editor-icon-button-size, 28px));
116
+ padding: 0;
117
+ }
109
118
 
110
- .ctw-mode button[aria-pressed="true"] {
111
- border-color: var(--sn-button-primary-border, var(--sn-node-selected));
112
- background: var(--sn-button-primary-bg, var(--sn-node-selected));
113
- color: var(--sn-button-primary-color, var(--sn-bg));
114
- }
119
+ cascade-theme-widget .ctw-mode,
120
+ .ctw-popover[data-overlay-portal] .ctw-mode {
121
+ display: grid;
122
+ grid-template-columns: repeat(2, minmax(0, 1fr));
123
+ gap: var(--sn-theme-widget-mode-gap, 4px);
124
+ padding: var(--sn-theme-widget-mode-padding, 3px);
125
+ border: 1px solid var(--sn-node-border);
126
+ border-radius: var(--sn-node-radius, 8px);
127
+ background: var(--sn-bg);
128
+ }
115
129
 
116
- .ctw-controls {
117
- display: grid;
118
- gap: var(--sn-theme-widget-control-gap, 7px);
119
- min-width: 0;
120
- }
130
+ cascade-theme-widget .ctw-mode button,
131
+ .ctw-popover[data-overlay-portal] .ctw-mode button {
132
+ min-width: 0;
133
+ min-height: var(--sn-theme-widget-mode-height, var(--sn-button-min-height, 28px));
134
+ padding: var(--sn-theme-widget-mode-button-padding, 5px 10px);
135
+ color: var(--sn-text-dim);
136
+ font-size: var(--sn-theme-widget-control-size, var(--sn-button-font-size, 12px));
137
+ }
121
138
 
122
- .ctw-control {
123
- display: grid;
124
- grid-template-columns: minmax(82px, 0.52fr) minmax(96px, 1fr) minmax(34px, auto);
125
- gap: var(--sn-theme-widget-control-gap, 7px);
126
- color: var(--sn-text-dim);
127
- font-size: var(--sn-theme-widget-control-size, var(--sn-theme-editor-control-size, 12px));
128
- }
139
+ cascade-theme-widget .ctw-mode button[aria-pressed="true"],
140
+ .ctw-popover[data-overlay-portal] .ctw-mode button[aria-pressed="true"] {
141
+ border-color: var(--sn-button-primary-border, var(--sn-node-selected));
142
+ background: var(--sn-button-primary-bg, var(--sn-node-selected));
143
+ color: var(--sn-button-primary-color, var(--sn-bg));
144
+ }
129
145
 
130
- .ctw-control-head {
131
- gap: var(--sn-theme-widget-control-gap, 7px);
132
- }
146
+ cascade-theme-widget .ctw-controls,
147
+ .ctw-popover[data-overlay-portal] .ctw-controls {
148
+ display: grid;
149
+ gap: var(--sn-theme-widget-control-gap, 7px);
150
+ min-width: 0;
151
+ }
133
152
 
134
- .ctw-control-icon {
135
- width: var(--sn-theme-widget-control-icon-box, calc(18px * var(--sn-theme-density, 1)));
136
- color: var(--sn-node-selected);
137
- font-size: var(--sn-theme-widget-control-icon-size, var(--sn-layout-header-icon-size, 16px));
138
- line-height: 1;
139
- text-align: center;
140
- }
153
+ cascade-theme-widget .ctw-control,
154
+ .ctw-popover[data-overlay-portal] .ctw-control {
155
+ display: grid;
156
+ grid-template-columns: minmax(82px, 0.52fr) minmax(96px, 1fr) minmax(34px, auto);
157
+ gap: var(--sn-theme-widget-control-gap, 7px);
158
+ color: var(--sn-text-dim);
159
+ font-size: var(--sn-theme-widget-control-size, var(--sn-theme-editor-control-size, 12px));
160
+ }
141
161
 
142
- .ctw-control label {
143
- overflow: hidden;
144
- text-overflow: ellipsis;
145
- white-space: nowrap;
146
- }
162
+ cascade-theme-widget .ctw-control-head,
163
+ .ctw-popover[data-overlay-portal] .ctw-control-head {
164
+ gap: var(--sn-theme-widget-control-gap, 7px);
165
+ }
147
166
 
148
- .ctw-control input {
149
- width: 100%;
150
- min-width: 0;
151
- height: var(--sn-theme-widget-range-hit-size, 34px);
152
- accent-color: var(--sn-node-selected);
153
- }
167
+ cascade-theme-widget .ctw-control-icon,
168
+ .ctw-popover[data-overlay-portal] .ctw-control-icon {
169
+ width: var(--sn-theme-widget-control-icon-box, calc(18px * var(--sn-theme-density, 1)));
170
+ color: var(--sn-node-selected);
171
+ font-size: var(--sn-theme-widget-control-icon-size, var(--sn-layout-header-icon-size, 16px));
172
+ line-height: 1;
173
+ text-align: center;
174
+ }
154
175
 
155
- .ctw-control output {
156
- color: var(--sn-text);
157
- font-variant-numeric: tabular-nums;
158
- text-align: right;
159
- }
176
+ cascade-theme-widget .ctw-control label,
177
+ .ctw-popover[data-overlay-portal] .ctw-control label {
178
+ overflow: hidden;
179
+ text-overflow: ellipsis;
180
+ white-space: nowrap;
181
+ }
160
182
 
161
- .ctw-header-actions button:hover,
162
- .ctw-mode button:hover {
163
- background: var(--sn-button-hover-bg, var(--sn-node-hover));
164
- }
183
+ cascade-theme-widget .ctw-control input,
184
+ .ctw-popover[data-overlay-portal] .ctw-control input {
185
+ width: 100%;
186
+ min-width: 0;
187
+ height: var(--sn-theme-widget-range-hit-size, 34px);
188
+ accent-color: var(--sn-node-selected);
189
+ }
190
+
191
+ cascade-theme-widget .ctw-control output,
192
+ .ctw-popover[data-overlay-portal] .ctw-control output {
193
+ color: var(--sn-text);
194
+ font-variant-numeric: tabular-nums;
195
+ text-align: right;
196
+ }
197
+
198
+ cascade-theme-widget .ctw-header-actions button:hover,
199
+ cascade-theme-widget .ctw-mode button:hover,
200
+ .ctw-popover[data-overlay-portal] .ctw-header-actions button:hover,
201
+ .ctw-popover[data-overlay-portal] .ctw-mode button:hover {
202
+ background: var(--sn-button-hover-bg, var(--sn-node-hover));
203
+ }
165
204
 
166
- .ctw-header-actions button:focus-visible,
167
- .ctw-mode button:focus-visible,
168
- .ctw-control input:focus-visible,
169
- .ctw-trigger:focus-visible {
170
- outline: var(--sn-effect-focus-ring, 2px solid var(--sn-node-selected));
171
- outline-offset: 2px;
205
+ cascade-theme-widget .ctw-header-actions button:focus-visible,
206
+ cascade-theme-widget .ctw-mode button:focus-visible,
207
+ cascade-theme-widget .ctw-control input:focus-visible,
208
+ cascade-theme-widget .ctw-trigger:focus-visible,
209
+ .ctw-popover[data-overlay-portal] .ctw-header-actions button:focus-visible,
210
+ .ctw-popover[data-overlay-portal] .ctw-mode button:focus-visible,
211
+ .ctw-popover[data-overlay-portal] .ctw-control input:focus-visible {
212
+ outline: var(--sn-effect-focus-ring, 2px solid var(--sn-node-selected));
213
+ outline-offset: 2px;
214
+ }
215
+
216
+ @media (max-width: 820px) {
217
+ cascade-theme-widget .ctw-trigger-label {
218
+ display: none;
172
219
  }
173
220
 
174
- @media (max-width: 820px) {
175
- .ctw-trigger-label {
176
- display: none;
177
- }
178
-
179
- .ctw-popover {
180
- position: fixed;
181
- top: var(--sn-theme-widget-mobile-top, calc(env(safe-area-inset-top) + 88px));
182
- right: max(var(--sn-theme-widget-mobile-inset, 8px), env(safe-area-inset-right));
183
- left: max(var(--sn-theme-widget-mobile-inset, 8px), env(safe-area-inset-left));
184
- width: auto;
185
- max-width: none;
186
- max-height: calc(
187
- 100dvh - var(--sn-theme-widget-mobile-top, calc(env(safe-area-inset-top) + 88px)) -
188
- max(var(--sn-theme-widget-mobile-inset, 8px), env(safe-area-inset-bottom))
189
- );
190
- overflow: auto;
191
- transform: none;
192
- }
221
+ cascade-theme-widget .ctw-popover,
222
+ .ctw-popover[data-overlay-portal] {
223
+ position: fixed;
224
+ top: var(--sn-theme-widget-mobile-top, calc(env(safe-area-inset-top) + 88px));
225
+ right: max(var(--sn-theme-widget-mobile-inset, 8px), env(safe-area-inset-right));
226
+ left: max(var(--sn-theme-widget-mobile-inset, 8px), env(safe-area-inset-left));
227
+ width: auto;
228
+ max-width: none;
229
+ max-height: calc(
230
+ 100dvh - var(--sn-theme-widget-mobile-top, calc(env(safe-area-inset-top) + 88px)) -
231
+ max(var(--sn-theme-widget-mobile-inset, 8px), env(safe-area-inset-bottom))
232
+ );
233
+ overflow: auto;
234
+ transform: none;
193
235
  }
194
236
  }
195
237
  `;
@@ -6,6 +6,12 @@ import {
6
6
  getCascadeThemeControls,
7
7
  normalizeCascadeThemeOptions,
8
8
  } from '../cascade-theme.js';
9
+ import {
10
+ bringOverlayToFront,
11
+ mountOverlayToDocument,
12
+ restoreOverlayHome,
13
+ } from '../../ui/overlay-stack.js';
14
+ import { positionOverlay } from '../../ui/overlay-positioner.js';
9
15
  import css from './CascadeThemeWidget.css.js';
10
16
  import tpl from './CascadeThemeWidget.tpl.js';
11
17
 
@@ -47,13 +53,15 @@ export class CascadeThemeWidget extends Symbiote {
47
53
  #controls = getCascadeThemeControls().filter((control) => COMPACT_CONTROLS.includes(control.name));
48
54
  #state = normalizeCascadeThemeOptions(CASCADE_THEME_DEFAULTS);
49
55
  #ready = false;
56
+ #popoverBound = false;
57
+ #overlayListenersBound = false;
50
58
 
51
59
  init$ = {
52
60
  isOpen: false,
53
61
  triggerTitle: 'Theme quick controls',
54
62
 
55
63
  onToggle: () => {
56
- this.$.isOpen = !this.$.isOpen;
64
+ this.#setOpen(!this.$.isOpen);
57
65
  },
58
66
  };
59
67
 
@@ -62,8 +70,8 @@ export class CascadeThemeWidget extends Symbiote {
62
70
  this.addEventListener('input', this.#onInput);
63
71
  this.addEventListener('click', this.#onClick);
64
72
  this._onDocumentPointerDown = (event) => {
65
- if (!this.$.isOpen || this.contains(event.target)) return;
66
- this.$.isOpen = false;
73
+ if (!this.$.isOpen || this.#eventTargetsWidget(event)) return;
74
+ this.#setOpen(false);
67
75
  };
68
76
  if (typeof document !== 'undefined') {
69
77
  document.addEventListener('pointerdown', this._onDocumentPointerDown);
@@ -73,6 +81,8 @@ export class CascadeThemeWidget extends Symbiote {
73
81
  disconnectedCallback() {
74
82
  this.removeEventListener('input', this.#onInput);
75
83
  this.removeEventListener('click', this.#onClick);
84
+ this.#unbindPopoverEvents();
85
+ this.#setOpen(false);
76
86
  if (typeof document !== 'undefined') {
77
87
  document.removeEventListener('pointerdown', this._onDocumentPointerDown);
78
88
  }
@@ -90,9 +100,11 @@ export class CascadeThemeWidget extends Symbiote {
90
100
  renderCallback() {
91
101
  if (this.#ready) return;
92
102
  this.#ready = true;
103
+ this.#bindPopoverEvents();
93
104
  this.#renderControls();
94
105
  this.#loadStoredState();
95
106
  this.#apply('init');
107
+ if (this.$.isOpen) this.#openPopover();
96
108
  }
97
109
 
98
110
  get state() {
@@ -137,7 +149,7 @@ export class CascadeThemeWidget extends Symbiote {
137
149
 
138
150
  #onInput = (event) => {
139
151
  let input = event.target.closest?.('[data-theme-control]');
140
- if (!input || !this.contains(input)) return;
152
+ if (!input || !this.#elementTargetsWidget(input)) return;
141
153
  this.#state = normalizeCascadeThemeOptions({
142
154
  ...this.#state,
143
155
  [input.dataset.themeControl]: Number(input.value),
@@ -147,7 +159,7 @@ export class CascadeThemeWidget extends Symbiote {
147
159
 
148
160
  #onClick = (event) => {
149
161
  let modeButton = event.target.closest?.('[data-theme-mode]');
150
- if (modeButton && this.contains(modeButton)) {
162
+ if (modeButton && this.#elementTargetsWidget(modeButton)) {
151
163
  this.#state = normalizeCascadeThemeOptions({
152
164
  ...this.#state,
153
165
  mode: modeButton.dataset.themeMode,
@@ -163,7 +175,7 @@ export class CascadeThemeWidget extends Symbiote {
163
175
  } else if (action === 'reset') {
164
176
  this.reset();
165
177
  } else if (action === 'open-full') {
166
- this.$.isOpen = false;
178
+ this.#setOpen(false);
167
179
  this.dispatchEvent(new CustomEvent('cascade-theme-open-full', {
168
180
  bubbles: true,
169
181
  composed: true,
@@ -172,6 +184,108 @@ export class CascadeThemeWidget extends Symbiote {
172
184
  }
173
185
  };
174
186
 
187
+ #bindPopoverEvents() {
188
+ let popover = this.ref.popover;
189
+ if (!popover || this.#popoverBound) return;
190
+ popover.addEventListener('input', this.#onInput);
191
+ popover.addEventListener('click', this.#onClick);
192
+ this.#popoverBound = true;
193
+ }
194
+
195
+ #unbindPopoverEvents() {
196
+ let popover = this.ref.popover;
197
+ if (!popover || !this.#popoverBound) return;
198
+ popover.removeEventListener('input', this.#onInput);
199
+ popover.removeEventListener('click', this.#onClick);
200
+ this.#popoverBound = false;
201
+ }
202
+
203
+ #setOpen(open) {
204
+ let nextOpen = Boolean(open);
205
+ if (nextOpen === this.$.isOpen) {
206
+ if (nextOpen) this.#openPopover();
207
+ return;
208
+ }
209
+ this.$.isOpen = nextOpen;
210
+ if (nextOpen) {
211
+ this.#openPopover();
212
+ } else {
213
+ this.#closePopover();
214
+ }
215
+ }
216
+
217
+ #openPopover() {
218
+ let popover = this.ref.popover;
219
+ if (!popover) return;
220
+ this.#bindPopoverEvents();
221
+ popover.hidden = false;
222
+ mountOverlayToDocument(popover, this);
223
+ bringOverlayToFront(popover);
224
+ this.#positionPopover();
225
+ this.#bindOverlayListeners();
226
+ if (typeof requestAnimationFrame === 'function') {
227
+ requestAnimationFrame(() => {
228
+ if (this.$.isOpen) this.#positionPopover();
229
+ });
230
+ }
231
+ }
232
+
233
+ #closePopover() {
234
+ let popover = this.ref.popover;
235
+ this.#unbindOverlayListeners();
236
+ if (!popover) return;
237
+ popover.hidden = true;
238
+ popover.style.removeProperty('top');
239
+ popover.style.removeProperty('left');
240
+ restoreOverlayHome(popover);
241
+ }
242
+
243
+ #bindOverlayListeners() {
244
+ if (this.#overlayListenersBound || typeof window === 'undefined') return;
245
+ window.addEventListener('resize', this.#onOverlayReposition);
246
+ window.addEventListener('scroll', this.#onOverlayReposition, true);
247
+ this.#overlayListenersBound = true;
248
+ }
249
+
250
+ #unbindOverlayListeners() {
251
+ if (!this.#overlayListenersBound || typeof window === 'undefined') return;
252
+ window.removeEventListener('resize', this.#onOverlayReposition);
253
+ window.removeEventListener('scroll', this.#onOverlayReposition, true);
254
+ this.#overlayListenersBound = false;
255
+ }
256
+
257
+ #onOverlayReposition = () => {
258
+ if (this.$.isOpen) this.#positionPopover();
259
+ };
260
+
261
+ #positionPopover() {
262
+ let popover = this.ref.popover;
263
+ let trigger = this.ref.trigger || this.querySelector('.ctw-trigger');
264
+ if (!popover || !trigger || typeof window === 'undefined') return;
265
+ if (window.matchMedia?.('(max-width: 820px)')?.matches) {
266
+ popover.style.removeProperty('top');
267
+ popover.style.removeProperty('left');
268
+ return;
269
+ }
270
+ let offset = Number.parseFloat(
271
+ getComputedStyle(trigger).getPropertyValue('--sn-theme-widget-offset') || '8'
272
+ );
273
+ positionOverlay(trigger, popover, 'bottom-end', {
274
+ offset: Number.isFinite(offset) ? offset : 8,
275
+ });
276
+ }
277
+
278
+ #eventTargetsWidget(event) {
279
+ let path = event.composedPath?.() || [];
280
+ let popover = this.ref.popover;
281
+ return path.includes(this) || (popover && (path.includes(popover) || popover.contains(event.target)));
282
+ }
283
+
284
+ #elementTargetsWidget(element) {
285
+ let popover = this.ref.popover;
286
+ return this.contains(element) || Boolean(popover?.contains(element));
287
+ }
288
+
175
289
  #renderControls() {
176
290
  let controls = this.ref.controls;
177
291
  if (!controls) return;
@@ -2,6 +2,7 @@ import { html } from '@symbiotejs/symbiote';
2
2
 
3
3
  export default html`
4
4
  <button
5
+ ref="trigger"
5
6
  class="ctw-trigger shell-action"
6
7
  type="button"
7
8
  aria-haspopup="dialog"
@@ -15,7 +16,13 @@ export default html`
15
16
  <span class="material-symbols-outlined" aria-hidden="true">palette</span>
16
17
  <span class="ctw-trigger-label">Theme</span>
17
18
  </button>
18
- <section class="ctw-popover" role="dialog" aria-label="Theme quick controls" ${{ '@hidden': '!isOpen' }}>
19
+ <section
20
+ ref="popover"
21
+ class="ctw-popover"
22
+ role="dialog"
23
+ aria-label="Theme quick controls"
24
+ ${{ '@hidden': '!isOpen' }}
25
+ >
19
26
  <header class="ctw-header">
20
27
  <strong>Theme</strong>
21
28
  <div class="ctw-header-actions">