@materialwebunofficial/antigravity-themes 0.0.0-stage → 1.0.4

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.
@@ -0,0 +1,904 @@
1
+ // AGY theme host: applies one theme from a bundle, remembers the choice, and
2
+ // adds a "Theme" menu to the title bar (Ctrl+Shift+F12 cycles themes; not
3
+ // Ctrl+Alt+<letter>: Windows reads Ctrl+Alt as AltGr, and on the Turkish Q
4
+ // layout AltGr+T types "₺"). In MD3E the title bar's menus and Theme fold
5
+ // into one app menu button (the package's <md-menu>).
6
+ //
7
+ // Context-free on purpose: it only touches the DOM and document.cookie, so it
8
+ // runs the same when inject.mjs evaluates it in the page and when a preload
9
+ // calls it from Electron's isolated world.
10
+ function agyThemeHost(bundle) {
11
+ // Bump when this file's code changes. A same-version host only takes the
12
+ // new bundle; an older one would keep running its old code through
13
+ // update(), so it is disposed and replaced (same task: no flash).
14
+ const HOST_VERSION = 20; // also bump when a module it starts (context-card, turn-tokens, loading-/progress-indicator, code-tools) changes: the host starts them once
15
+ const existing = window.__agyThemeHost;
16
+ if (existing?.version === HOST_VERSION) return existing.update(bundle);
17
+ existing?.dispose();
18
+
19
+ const COOKIE = 'agy-theme';
20
+ const readChoice = () => {
21
+ try {
22
+ return (document.cookie.match(/(?:^|;\s*)agy-theme=([^;]+)/) || [])[1];
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+ const saveChoice = (key) => {
28
+ try {
29
+ document.cookie = `${COOKIE}=${key}; Max-Age=31536000; Path=/; SameSite=Strict; Secure`;
30
+ } catch {}
31
+ };
32
+
33
+ let options = new Map();
34
+ let current = null;
35
+
36
+ // Fonts come as bytes and are registered with the FontFace API: no
37
+ // network fetch, no CSP font-src, and no 300 KB data URI in the CSS text.
38
+ const loadedFonts = new Set();
39
+ function ensureFonts() {
40
+ for (const f of bundle.fonts?.[current.theme] ?? []) {
41
+ const key = `${f.family}|${f.weight}|${f.stretch}`;
42
+ if (loadedFonts.has(key) || !document.fonts) continue;
43
+ loadedFonts.add(key);
44
+ try {
45
+ const bytes = Uint8Array.from(atob(f.data), (c) => c.charCodeAt(0));
46
+ const face = new FontFace(f.family, bytes.buffer, { weight: f.weight, stretch: f.stretch, style: 'normal', display: 'swap' });
47
+ document.fonts.add(face);
48
+ face.load().catch(() => {});
49
+ } catch {
50
+ // a theme without its font still works on the fallback stack
51
+ }
52
+ }
53
+ }
54
+
55
+ // Light or dark follows the app's own setting (System / Light / Dark),
56
+ // which it marks on <body> (theme-light / theme-dark). The themes' token
57
+ // blocks live on :root, so the mode is mirrored there as data-agy-mode.
58
+ // No mark yet (startup) counts as dark, the themes' default.
59
+ function readMode() {
60
+ const body = document.body;
61
+ if (!body) return 'dark';
62
+ const light = body.classList.contains('theme-light')
63
+ || (body.classList.contains('theme-standalone') && !body.classList.contains('dark'));
64
+ return light ? 'light' : 'dark';
65
+ }
66
+ function syncMode() {
67
+ const root = document.documentElement;
68
+ const mode = readMode();
69
+ if (!root || root.getAttribute('data-agy-mode') === mode) return false;
70
+ root.setAttribute('data-agy-mode', mode);
71
+ return true;
72
+ }
73
+ const modeObserver = new MutationObserver(() => {
74
+ watchMode();
75
+ if (!syncMode()) return;
76
+ announceTheme();
77
+ // The app re-sends its own (mis-parsed) overlay color on theme changes
78
+ requestAnimationFrame(() => syncTitleBarOverlay());
79
+ setTimeout(syncTitleBarOverlay, 300);
80
+ });
81
+ // The vendored md3e-web components cache their colors; the package's own
82
+ // window event makes them read the tokens again (the app uses no such event)
83
+ const announceTheme = () => window.dispatchEvent(new Event('theme-change'));
84
+ let modeTarget = null;
85
+ function watchMode() {
86
+ const target = document.body ?? document.documentElement;
87
+ if (!target || target === modeTarget) return;
88
+ modeObserver.disconnect();
89
+ modeTarget = target;
90
+ if (target === document.body) modeObserver.observe(target, { attributes: true, attributeFilter: ['class'] });
91
+ else modeObserver.observe(target, { childList: true }); // until <body> exists
92
+ }
93
+
94
+ function apply() {
95
+ const root = document.documentElement;
96
+ if (!root || !current) return;
97
+ ensureFonts();
98
+ watchMode();
99
+ syncMode();
100
+ if (current.theme === 'antigravity') {
101
+ root.removeAttribute('data-agy-theme');
102
+ root.removeAttribute('data-agy-seed');
103
+ const style = document.getElementById('agy-theme-css');
104
+ if (style) style.textContent = '';
105
+ } else {
106
+ root.setAttribute('data-agy-theme', current.theme);
107
+ if (current.seed) root.setAttribute('data-agy-seed', current.seed);
108
+ else root.removeAttribute('data-agy-seed');
109
+ let style = document.getElementById('agy-theme-css');
110
+ if (!style) {
111
+ style = document.createElement('style');
112
+ style.id = 'agy-theme-css';
113
+ }
114
+ const css = bundle.themes[current.theme] || '';
115
+ if (style.textContent !== css) style.textContent = css;
116
+ // Last in <head> so equal-specificity ties go to the theme
117
+ const parent = document.head || root;
118
+ if (style.parentNode !== parent || style !== parent.lastElementChild) parent.appendChild(style);
119
+ }
120
+ announceTheme();
121
+ renderMenu();
122
+ if (appMenuBuilt) buildAppMenu(false);
123
+ if (current.theme !== 'md3e') closeAppMenu();
124
+ // Themes change the title bar's padding and font: re-anchor the menu
125
+ requestAnimationFrame(() => { position(); syncTitleBarOverlay(); });
126
+ }
127
+
128
+ // The window controls (minimize/maximize/close) are drawn by Windows in
129
+ // the color the app passes to setTitleBarOverlay. The app derives it from
130
+ // --sidebar by taking the first three numbers of the computed color, which
131
+ // turns a non-RGB serialization such as lch(12% 4 190) into rgb(12, 4, 190):
132
+ // the bright blue behind the close button. Resolve through a canvas, which
133
+ // always reads back sRGB bytes. electronNative is frozen (contextBridge),
134
+ // so it cannot be wrapped; re-assert on focus instead.
135
+ let overlayCanvas = null;
136
+ function cssHex(varName) {
137
+ const probe = document.createElement('div');
138
+ probe.style.color = `var(${varName})`;
139
+ document.body.appendChild(probe);
140
+ const color = getComputedStyle(probe).color;
141
+ probe.remove();
142
+ overlayCanvas ??= document.createElement('canvas');
143
+ overlayCanvas.width = overlayCanvas.height = 1;
144
+ const ctx = overlayCanvas.getContext('2d', { willReadFrequently: true });
145
+ ctx.clearRect(0, 0, 1, 1);
146
+ ctx.fillStyle = color;
147
+ ctx.fillRect(0, 0, 1, 1);
148
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
149
+ return `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
150
+ }
151
+ function syncTitleBarOverlay() {
152
+ if (!document.body || !window.electronNative?.setTitleBarOverlay) return;
153
+ try {
154
+ window.electronNative.setTitleBarOverlay({ color: cssHex('--sidebar'), symbolColor: cssHex('--foreground') });
155
+ } catch {
156
+ // older shells without the overlay API
157
+ }
158
+ }
159
+
160
+ function select(key) {
161
+ const next = options.get(key);
162
+ if (!next) return;
163
+ current = next;
164
+ saveChoice(key);
165
+ apply();
166
+ }
167
+
168
+ function update(nextBundle) {
169
+ bundle = nextBundle;
170
+ options = new Map(bundle.options.map((o) => [o.key, o]));
171
+ current = options.get(readChoice()) || options.get(current?.key) || options.get(bundle.default) || bundle.options[0];
172
+ apply();
173
+ return api;
174
+ }
175
+
176
+ // ---- Title bar menu (shadow DOM on <body>, outside React's tree) --------
177
+ let hostEl = null, shadow = null, trigger = null, menu = null, retry = 0;
178
+ let appMenu = null, appTrigger = null;
179
+
180
+ function mount() {
181
+ if (hostEl || !document.body) return;
182
+ hostEl = document.createElement('div');
183
+ hostEl.id = 'agy-theme-switcher';
184
+ hostEl.style.cssText = 'position:fixed;top:0;left:0;z-index:2147483000;display:none;';
185
+ shadow = hostEl.attachShadow({ mode: 'open' });
186
+ shadow.innerHTML = `
187
+ <style>
188
+ :host {
189
+ all: initial;
190
+ font-family: var(--font-sans, "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif);
191
+ font-size: 13px;
192
+ }
193
+ button, [role="menuitemradio"] { -webkit-app-region: no-drag; }
194
+ .trigger {
195
+ display: flex; align-items: center; padding: 0 8px; border: 0; border-radius: 6px;
196
+ background: transparent; color: var(--muted-foreground, #999); font: inherit; cursor: default;
197
+ }
198
+ .trigger:hover, .trigger[aria-expanded="true"] { background: var(--secondary, #333); color: var(--foreground, #eee); }
199
+ .menu {
200
+ position: absolute; top: calc(100% + 6px); left: 0; min-width: 230px; padding: 6px;
201
+ border: 1px solid var(--border, #333); border-radius: 12px;
202
+ background: var(--card, #1c1c1f); color: var(--foreground, #eee);
203
+ box-shadow: 0 16px 40px -12px rgb(0 0 0 / 0.75);
204
+ }
205
+ .menu[hidden] { display: none; }
206
+ [role="menuitemradio"], .menu-sub-trigger {
207
+ display: flex; align-items: center; gap: 10px; height: 32px; padding: 0 10px;
208
+ border-radius: 8px; font: inherit; color: inherit; cursor: default; outline: none;
209
+ }
210
+ [role="menuitemradio"]:hover, [role="menuitemradio"]:focus-visible,
211
+ .menu-sub-trigger:hover, .menu-sub-wrapper:hover .menu-sub-trigger { background: var(--secondary, #333); }
212
+ .swatch { width: 12px; height: 12px; border-radius: 999px; box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.2); }
213
+ .name { flex: 1; }
214
+ .check { visibility: hidden; }
215
+ [aria-checked="true"] .check { visibility: visible; }
216
+ .chevron { color: var(--muted-foreground, #999); font-size: 13px; margin-left: auto; }
217
+ .menu-sub-wrapper { position: relative; }
218
+ .menu-sub-flyout {
219
+ display: none; position: absolute; top: -6px; left: calc(100% + 4px); min-width: 170px; padding: 6px;
220
+ border: 1px solid var(--border, #333); border-radius: 12px;
221
+ background: var(--card, #1c1c1f); color: var(--foreground, #eee);
222
+ box-shadow: 0 16px 40px -12px rgb(0 0 0 / 0.75);
223
+ }
224
+ .menu-sub-wrapper:hover .menu-sub-flyout { display: block; }
225
+ .hint { padding: 6px 10px 2px; color: var(--muted-foreground, #999); font-size: 11px; }
226
+
227
+ /* MD3E: the md-menu anatomy of md3e.css section 50 (one
228
+ surface-container-low group, 16px, level 2; 44px items with their
229
+ shape 4px in, 12px at the ends; selected = tertiary-container pill
230
+ and the Rounded check), opening with the menu's scale + fade */
231
+ :host-context([data-agy-theme="md3e"]) .trigger:hover { background: var(--md-hover); color: var(--md-sys-color-on-surface); }
232
+ :host-context([data-agy-theme="md3e"]) .trigger[aria-expanded="true"] {
233
+ background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container);
234
+ }
235
+ :host-context([data-agy-theme="md3e"]) .menu {
236
+ top: calc(100% + 4px); min-width: 260px; padding: 4px 0; border: 0; border-radius: 16px;
237
+ background: var(--md-sys-color-surface-container-low); color: var(--md-sys-color-on-surface);
238
+ box-shadow: var(--md-sys-elevation-level2);
239
+ font: var(--md-sys-typescale-label-large); letter-spacing: var(--md-sys-typescale-label-large-tracking);
240
+ transform-origin: top left; transition: scale var(--md-fast), opacity var(--md-color-fast);
241
+ }
242
+ @starting-style { :host-context([data-agy-theme="md3e"]) .menu { scale: 0.8; opacity: 0; } }
243
+ :host-context([data-agy-theme="md3e"]) [role="menuitemradio"] {
244
+ position: relative; isolation: isolate; height: 44px; gap: 12px; padding: 0 16px; border-radius: 0;
245
+ }
246
+ :host-context([data-agy-theme="md3e"]) [role="menuitemradio"]::before {
247
+ content: ""; position: absolute; inset: 0 4px; z-index: -1; border-radius: 4px;
248
+ transition: background-color var(--md-state);
249
+ }
250
+ :host-context([data-agy-theme="md3e"]) [role="menuitemradio"]:first-child::before { border-radius: 12px 12px 4px 4px; }
251
+ :host-context([data-agy-theme="md3e"]) [role="menuitemradio"]:hover,
252
+ :host-context([data-agy-theme="md3e"]) [role="menuitemradio"]:focus-visible { background: transparent; }
253
+ :host-context([data-agy-theme="md3e"]) [role="menuitemradio"]:hover::before { background: color-mix(in srgb, currentColor 8%, transparent); }
254
+ :host-context([data-agy-theme="md3e"]) [role="menuitemradio"]:focus-visible::before { background: color-mix(in srgb, currentColor 10%, transparent); }
255
+ :host-context([data-agy-theme="md3e"]) [aria-checked="true"] { color: var(--md-sys-color-on-tertiary-container); }
256
+ :host-context([data-agy-theme="md3e"]) [aria-checked="true"]::before { border-radius: 12px; background: var(--md-sys-color-tertiary-container); }
257
+ :host-context([data-agy-theme="md3e"]) [aria-checked="true"]:hover::before {
258
+ background: color-mix(in srgb, var(--md-sys-color-on-tertiary-container) 8%, var(--md-sys-color-tertiary-container));
259
+ }
260
+ :host-context([data-agy-theme="md3e"]) .swatch { width: 16px; height: 16px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) 24%, transparent); }
261
+ :host-context([data-agy-theme="md3e"]) .check {
262
+ width: 20px; height: 20px; font-size: 0; background: currentColor;
263
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M389 -369 688 -668Q699 -679 714 -679Q728 -679 739 -668Q750 -657 750 -642Q750 -628 739 -617L415 -292Q404 -281 390 -281Q375 -281 364 -292L221 -435Q210 -446 210 -460Q210 -475 221 -486Q232 -497 246 -497Q261 -497 272 -486Z'/%3E%3C/svg%3E") center / contain no-repeat;
264
+ }
265
+ :host-context([data-agy-theme="md3e"]) .hint {
266
+ padding: 8px 16px 6px; font: var(--md-sys-typescale-label-medium); color: var(--md-sys-color-on-surface-variant);
267
+ }
268
+
269
+ /* MD3E app menu: an xsmall M3 icon button (32px, 20px icon); the
270
+ <md-menu> it opens draws itself */
271
+ .appmenu { display: none; min-width: 220px; }
272
+ :host-context([data-agy-theme="md3e"][data-agy-appmenu]) .trigger,
273
+ :host-context([data-agy-theme="md3e"][data-agy-appmenu]) .menu { display: none !important; }
274
+ :host-context([data-agy-theme="md3e"][data-agy-appmenu]) .appmenu { display: block; }
275
+ .appmenu md-menu {
276
+ min-width: 220px;
277
+ }
278
+ .appmenu-trigger {
279
+ display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0;
280
+ border-radius: 9999px; background: transparent; color: var(--md-sys-color-on-surface-variant);
281
+ cursor: default; outline: none; transition: background-color var(--md-state), color var(--md-state);
282
+ }
283
+ .appmenu-trigger:hover { background: var(--md-hover); color: var(--md-sys-color-on-surface); }
284
+ .appmenu-trigger[aria-expanded="true"] {
285
+ background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container);
286
+ }
287
+ .appmenu-trigger:focus-visible { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; }
288
+ .appmenu svg { display: block; width: 20px; height: 20px; fill: currentColor; }
289
+ .appmenu .swatch { width: 16px; height: 16px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) 24%, transparent); }
290
+ </style>
291
+ <button class="trigger" aria-haspopup="menu" aria-expanded="false">Theme</button>
292
+ <div class="menu" role="menu" aria-label="Theme" hidden></div>`;
293
+ trigger = shadow.querySelector('.trigger');
294
+ menu = shadow.querySelector('.menu');
295
+ mountAppMenu();
296
+ trigger.addEventListener('click', () => (menu.hidden ? openMenu() : closeMenu()));
297
+ menu.addEventListener('click', (e) => {
298
+ const item = e.target.closest('[role="menuitemradio"]');
299
+ if (item) { select(item.dataset.key); closeMenu(); return; }
300
+ const subTrig = e.target.closest('.menu-sub-trigger');
301
+ if (subTrig) {
302
+ const activeSeed = [...options.values()].find((o) => o.theme === 'md3e' && o.key === current?.key);
303
+ select(activeSeed ? activeSeed.key : 'md3e-teal');
304
+ closeMenu();
305
+ }
306
+ });
307
+ menu.addEventListener('keydown', onMenuKey);
308
+ document.body.appendChild(hostEl);
309
+ renderMenu();
310
+ if (appMenuBuilt) buildAppMenu(false);
311
+ position();
312
+ }
313
+
314
+ function renderMenu() {
315
+ if (!menu) return;
316
+ menu.innerHTML = '';
317
+ // Standalone options
318
+ for (const o of options.values()) {
319
+ if (o.theme === 'md3e') continue;
320
+ const item = document.createElement('div');
321
+ item.setAttribute('role', 'menuitemradio');
322
+ item.setAttribute('aria-checked', String(o.key === current?.key));
323
+ item.tabIndex = -1;
324
+ item.dataset.key = o.key;
325
+ item.innerHTML = '<span class="swatch"></span><span class="name"></span><span class="check">✓</span>';
326
+ item.querySelector('.swatch').style.background = o.swatch || 'transparent';
327
+ item.querySelector('.name').textContent = o.name;
328
+ menu.appendChild(item);
329
+ }
330
+ // Group MD3E into single entry with submenu flyout
331
+ const md3eOptions = [...options.values()].filter((o) => o.theme === 'md3e');
332
+ if (md3eOptions.length) {
333
+ const isMd3eActive = current?.theme === 'md3e';
334
+ const subWrapper = document.createElement('div');
335
+ subWrapper.className = 'menu-sub-wrapper';
336
+ const subTrigger = document.createElement('div');
337
+ subTrigger.className = 'menu-sub-trigger';
338
+ subTrigger.setAttribute('role', 'button');
339
+ subTrigger.tabIndex = -1;
340
+ subTrigger.innerHTML = '<span class="swatch" style="background:#a5faf9"></span><span class="name">Material 3 Expressive</span><span class="check">✓</span><span class="chevron">›</span>';
341
+ if (!isMd3eActive) subTrigger.querySelector('.check').style.visibility = 'hidden';
342
+ else subTrigger.querySelector('.check').style.visibility = 'visible';
343
+ subWrapper.appendChild(subTrigger);
344
+
345
+ const flyout = document.createElement('div');
346
+ flyout.className = 'menu-sub-flyout';
347
+ flyout.setAttribute('role', 'menu');
348
+ for (const o of md3eOptions) {
349
+ const item = document.createElement('div');
350
+ item.setAttribute('role', 'menuitemradio');
351
+ item.setAttribute('aria-checked', String(o.key === current?.key));
352
+ item.tabIndex = -1;
353
+ item.dataset.key = o.key;
354
+ item.innerHTML = '<span class="swatch"></span><span class="name"></span><span class="check">✓</span>';
355
+ item.querySelector('.swatch').style.background = o.swatch || 'transparent';
356
+ item.querySelector('.name').textContent = o.name;
357
+ flyout.appendChild(item);
358
+ }
359
+ subWrapper.appendChild(flyout);
360
+ menu.appendChild(subWrapper);
361
+ }
362
+ const hint = document.createElement('div');
363
+ hint.className = 'hint';
364
+ hint.textContent = 'Ctrl+Shift+F12: next theme';
365
+ menu.appendChild(hint);
366
+ }
367
+
368
+ // ---- MD3E app menu ----------------------------------------------------------
369
+ // A text menu bar has no place in an M3 top app bar, so in MD3E the title
370
+ // bar's menus (Antigravity, Workspace, View, Window) and Theme sit behind one
371
+ // icon button that opens the package's <md-menu>, each menu a cascading
372
+ // submenu. The app stays the source of truth: md3e.css only hides its menu
373
+ // bar (while data-agy-appmenu is set), the items are read from its menu
374
+ // components' props on every open, and choosing one runs the app's own
375
+ // handler. Without the vendored component the app's menu bar stays.
376
+ const SVG_NS = 'http://www.w3.org/2000/svg';
377
+ // Material Symbols Rounded (FILL 0, opsz 20, wght 400), as in md3e-icons.css
378
+ const GLYPHS = {
379
+ menu: 'M180 -264Q165 -264 154 -274Q144 -285 144 -300Q144 -315 154 -326Q165 -336 180 -336H780Q795 -336 806 -326Q816 -315 816 -300Q816 -285 806 -274Q795 -264 780 -264ZM180 -444Q165 -444 154 -454Q144 -465 144 -480Q144 -495 154 -506Q165 -516 180 -516H780Q795 -516 806 -506Q816 -495 816 -480Q816 -465 806 -454Q795 -444 780 -444ZM180 -624Q165 -624 154 -634Q144 -645 144 -660Q144 -675 154 -686Q165 -696 180 -696H780Q795 -696 806 -686Q816 -675 816 -660Q816 -645 806 -634Q795 -624 780 -624Z',
380
+ info: 'M516 -324V-492Q516 -507 506 -518Q495 -528 480 -528Q465 -528 454 -518Q444 -507 444 -492V-324Q444 -309 454 -298Q465 -288 480 -288Q495 -288 506 -298Q516 -309 516 -324ZM516 -636Q516 -651 506 -662Q495 -672 480 -672Q465 -672 454 -662Q444 -651 444 -636Q444 -621 454 -610Q465 -600 480 -600Q495 -600 506 -610Q516 -621 516 -636ZM480 -96Q401 -96 331 -126Q261 -156 208 -208Q156 -261 126 -331Q96 -401 96 -480Q96 -560 126 -630Q156 -699 208 -752Q261 -804 331 -834Q401 -864 480 -864Q560 -864 630 -834Q699 -804 752 -752Q804 -699 834 -629Q864 -560 864 -480Q864 -401 834 -331Q804 -261 752 -208Q699 -156 629 -126Q560 -96 480 -96ZM480 -168Q610 -168 701 -259Q792 -350 792 -480Q792 -610 701 -701Q610 -792 480 -792Q350 -792 259 -701Q168 -610 168 -480Q168 -350 259 -259Q350 -168 480 -168Z',
381
+ workspaces: 'M240 -144Q180 -144 138 -186Q96 -228 96 -288Q96 -348 138 -390Q180 -432 240 -432Q300 -432 342 -390Q384 -348 384 -288Q384 -228 342 -186Q300 -144 240 -144ZM720 -144Q660 -144 618 -186Q576 -228 576 -288Q576 -348 618 -390Q660 -432 720 -432Q780 -432 822 -390Q864 -348 864 -288Q864 -228 822 -186Q780 -144 720 -144ZM240 -216Q270 -216 291 -237Q312 -258 312 -288Q312 -318 291 -339Q270 -360 240 -360Q210 -360 189 -339Q168 -318 168 -288Q168 -258 189 -237Q210 -216 240 -216ZM720 -216Q750 -216 771 -237Q792 -258 792 -288Q792 -318 771 -339Q750 -360 720 -360Q690 -360 669 -339Q648 -318 648 -288Q648 -258 669 -237Q690 -216 720 -216ZM480 -528Q420 -528 378 -570Q336 -612 336 -672Q336 -732 378 -774Q420 -816 480 -816Q540 -816 582 -774Q624 -732 624 -672Q624 -612 582 -570Q540 -528 480 -528ZM480 -600Q510 -600 531 -621Q552 -642 552 -672Q552 -702 531 -723Q510 -744 480 -744Q450 -744 429 -723Q408 -702 408 -672Q408 -642 429 -621Q450 -600 480 -600Z',
382
+ visibility: 'M480 -312Q550 -312 599 -361Q648 -410 648 -480Q648 -550 599 -599Q550 -648 480 -648Q410 -648 361 -599Q312 -550 312 -480Q312 -410 361 -361Q410 -312 480 -312ZM480 -384Q440 -384 412 -412Q384 -440 384 -480Q384 -520 412 -548Q440 -576 480 -576Q520 -576 548 -548Q576 -520 576 -480Q576 -440 548 -412Q520 -384 480 -384ZM68 -445Q63 -453 61 -462Q59 -471 59 -480Q59 -489 61 -498Q63 -507 68 -515Q132 -629 241 -698Q350 -768 480 -768Q610 -768 719 -698Q828 -629 892 -515Q897 -507 899 -498Q901 -489 901 -480Q901 -471 899 -462Q897 -453 892 -445Q828 -331 719 -262Q610 -192 480 -192Q350 -192 241 -262Q132 -331 68 -445ZM833 -480Q782 -580 687 -638Q592 -696 480 -696Q368 -696 273 -638Q178 -580 127 -480Q178 -380 273 -322Q368 -264 480 -264Q592 -264 687 -322Q782 -380 833 -480Z',
383
+ select_window: 'M168 -96Q138 -96 117 -117Q96 -138 96 -168V-504Q96 -534 117 -555Q138 -576 168 -576H240V-792Q240 -822 261 -843Q282 -864 312 -864H792Q822 -864 843 -843Q864 -822 864 -792V-456Q864 -426 843 -405Q822 -384 792 -384H720V-168Q720 -138 699 -117Q678 -96 648 -96ZM168 -168H648Q648 -168 648 -168Q648 -168 648 -168V-432H168V-168Q168 -168 168 -168Q168 -168 168 -168ZM720 -456H792Q792 -456 792 -456Q792 -456 792 -456V-720H312V-576H648Q678 -576 699 -555Q720 -534 720 -504Z',
384
+ palette: 'M480 -96Q401 -96 331 -126Q261 -156 208 -208Q156 -261 126 -331Q96 -401 96 -480Q96 -560 126 -630Q157 -699 210 -752Q264 -804 336 -834Q407 -864 488 -864Q566 -864 634 -837Q703 -810 754 -763Q805 -716 834 -653Q864 -590 864 -518Q864 -422 797 -355Q730 -288 634 -288H566Q558 -288 552 -283Q546 -278 546 -270Q546 -255 561 -245Q576 -235 576 -192Q576 -155 549 -126Q522 -96 480 -96ZM264 -444Q289 -444 306 -462Q324 -479 324 -504Q324 -529 306 -546Q289 -564 264 -564Q239 -564 222 -546Q204 -529 204 -504Q204 -479 222 -462Q239 -444 264 -444ZM384 -588Q409 -588 426 -606Q444 -623 444 -648Q444 -673 426 -690Q409 -708 384 -708Q359 -708 342 -690Q324 -673 324 -648Q324 -623 342 -606Q359 -588 384 -588ZM576 -588Q601 -588 618 -606Q636 -623 636 -648Q636 -673 618 -690Q601 -708 576 -708Q551 -708 534 -690Q516 -673 516 -648Q516 -623 534 -606Q551 -588 576 -588ZM696 -444Q721 -444 738 -462Q756 -479 756 -504Q756 -529 738 -546Q721 -564 696 -564Q671 -564 654 -546Q636 -529 636 -504Q636 -479 654 -462Q671 -444 696 -444ZM480 -168Q491 -168 498 -176Q504 -185 504 -192Q504 -208 489 -220Q474 -232 474 -270Q474 -308 500 -334Q527 -360 565 -360H634Q700 -360 746 -406Q792 -452 792 -518Q792 -633 704 -712Q615 -792 488 -792Q354 -792 261 -701Q168 -610 168 -480Q168 -350 259 -259Q350 -168 480 -168Z',
385
+ };
386
+ // the first menu is the product's own (its title is the app name)
387
+ const MENU_GLYPHS = { Workspace: 'workspaces', View: 'visibility', Window: 'select_window' };
388
+ // "Version …" is a label in the app's menu, not a dead command: read, not 38%
389
+ const INFO_COLORS = JSON.stringify({ disabledTextColor: 'var(--md-sys-color-on-surface-variant)' });
390
+ let appMenus = [];
391
+
392
+ function glyph(name, slot) {
393
+ const svg = document.createElementNS(SVG_NS, 'svg');
394
+ svg.setAttribute('viewBox', '0 -960 960 960');
395
+ svg.setAttribute('aria-hidden', 'true');
396
+ if (slot) svg.setAttribute('slot', slot);
397
+ const path = document.createElementNS(SVG_NS, 'path');
398
+ path.setAttribute('d', GLYPHS[name]);
399
+ svg.appendChild(path);
400
+ return svg;
401
+ }
402
+ function menuItem(headline, attrs = {}) {
403
+ const item = document.createElement('md-menu-item');
404
+ item.setAttribute('headline', headline);
405
+ for (const [name, value] of Object.entries(attrs)) {
406
+ if (value != null && value !== false) item.setAttribute(name, value === true ? '' : String(value));
407
+ }
408
+ return item;
409
+ }
410
+ function submenu(label, children) {
411
+ const sub = document.createElement('md-menu');
412
+ sub.slot = 'submenu';
413
+ sub.setAttribute('label', label);
414
+ sub.append(...children);
415
+ return sub;
416
+ }
417
+
418
+ function patchComponentSheets() {
419
+ try {
420
+ const dummyItem = document.createElement('md-menu-item');
421
+ if (dummyItem.shadowRoot?.adoptedStyleSheets?.length) {
422
+ const s = dummyItem.shadowRoot.adoptedStyleSheets[0];
423
+ s.insertRule('.text { white-space: nowrap !important; overflow-wrap: normal !important; max-width: none !important; }', 0);
424
+ s.insertRule('.item { min-width: 220px !important; max-width: 320px !important; }', 0);
425
+ }
426
+ const dummyMenu = document.createElement('md-menu');
427
+ if (dummyMenu.shadowRoot?.adoptedStyleSheets?.length) {
428
+ const ms = dummyMenu.shadowRoot.adoptedStyleSheets[0];
429
+ ms.insertRule('.menu { min-width: 220px !important; max-width: 320px !important; will-change: transform, opacity !important; }', 0);
430
+ }
431
+ } catch {}
432
+ }
433
+
434
+ function mountAppMenu() {
435
+ if (!customElements.get('md-menu')) return;
436
+ patchComponentSheets();
437
+ appMenu = document.createElement('md-menu');
438
+ appMenu.className = 'appmenu';
439
+ appMenu.setAttribute('label', 'Application menu');
440
+ appTrigger = document.createElement('button');
441
+ appTrigger.className = 'appmenu-trigger';
442
+ appTrigger.slot = 'trigger';
443
+ appTrigger.setAttribute('aria-label', 'Application menu');
444
+ appTrigger.title = 'Menu';
445
+ appTrigger.appendChild(glyph('menu'));
446
+ appMenu.appendChild(appTrigger);
447
+ // these run before the menu's own handlers on its trigger slot
448
+ appTrigger.addEventListener('click', () => buildAppMenu(false));
449
+ appTrigger.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') buildAppMenu(false); });
450
+ appMenu.addEventListener('select', onAppMenuSelect);
451
+ shadow.appendChild(appMenu);
452
+ document.documentElement.setAttribute('data-agy-appmenu', '');
453
+ }
454
+
455
+ // The app's menu bar renders one component per menu, { title, options:
456
+ // [{ label, shortcut, onClick, nonSelectable }] }: read from React's fiber
457
+ function readAppMenus() {
458
+ const bar = document.querySelector('[data-testid="title-menu-bar"]');
459
+ const key = bar && Object.keys(bar).find((k) => k.startsWith('__reactFiber$'));
460
+ const menus = [];
461
+ for (let f = key ? bar[key].child : null; f; f = f.sibling) {
462
+ const p = f.memoizedProps;
463
+ if (typeof p?.title === 'string' && Array.isArray(p.options) && p.options.length) menus.push({ title: p.title, options: p.options });
464
+ }
465
+ return menus;
466
+ }
467
+
468
+ function fixItemStyle(el) {
469
+ if (!el || !el.shadowRoot) return;
470
+ try {
471
+ let s = el.shadowRoot.querySelector('#agy-item-fix');
472
+ if (!s) {
473
+ s = document.createElement('style');
474
+ s.id = 'agy-item-fix';
475
+ s.textContent = `
476
+ .text { white-space: nowrap !important; overflow-wrap: normal !important; max-width: none !important; }
477
+ .item { min-width: 220px !important; max-width: 320px !important; }
478
+ `;
479
+ el.shadowRoot.appendChild(s);
480
+ }
481
+ } catch {}
482
+ }
483
+
484
+ function fixMenuStyle(menu) {
485
+ if (!menu || !menu.shadowRoot) return;
486
+ try {
487
+ let s = menu.shadowRoot.querySelector('#agy-menu-fix');
488
+ if (!s) {
489
+ s = document.createElement('style');
490
+ s.id = 'agy-menu-fix';
491
+ s.textContent = `
492
+ .menu { min-width: 220px !important; max-width: 320px !important; will-change: transform, opacity !important; }
493
+ `;
494
+ menu.shadowRoot.appendChild(s);
495
+ }
496
+ } catch {}
497
+ for (const it of menu.querySelectorAll('md-menu-item')) fixItemStyle(it);
498
+ for (const sub of menu.querySelectorAll('md-menu')) fixMenuStyle(sub);
499
+ }
500
+
501
+ let appMenuBuilt = false;
502
+ function buildAppMenu(force = false) {
503
+ const forced = force === true;
504
+ if (!appMenu || (!forced && appMenu.open)) return;
505
+ if (appMenuBuilt && !forced) {
506
+ window.__buildAppMenuHitCache = (window.__buildAppMenuHitCache || 0) + 1;
507
+ for (const item of appMenu.querySelectorAll('md-menu-item[value^="theme:"]')) {
508
+ const val = item.getAttribute('value') || '';
509
+ item.selected = (val === `theme:${current?.key}`);
510
+ }
511
+ fixMenuStyle(appMenu);
512
+ return;
513
+ }
514
+ window.__buildAppMenuFullBuild = (window.__buildAppMenuFullBuild || 0) + 1;
515
+ appMenus = readAppMenus();
516
+ if (!appMenus.length && !forced) return;
517
+ const items = appMenus.map((m, i) => {
518
+ const top = menuItem(m.title);
519
+ top.append(glyph(MENU_GLYPHS[m.title] || 'info', 'start'), submenu(m.title, m.options.map((o, j) => menuItem(String(o.label), {
520
+ value: `app:${i}:${j}`,
521
+ 'trailing-text': typeof o.shortcut === 'string' && o.shortcut ? o.shortcut : null,
522
+ disabled: !!o.nonSelectable,
523
+ colors: o.nonSelectable ? INFO_COLORS : null,
524
+ }))));
525
+ return top;
526
+ });
527
+ // Theme: standalone themes, MD3E submenu, and Next theme
528
+ const choices = document.createElement('md-menu-group');
529
+ for (const o of options.values()) {
530
+ if (o.theme === 'md3e') continue;
531
+ const item = menuItem(o.name, { value: `theme:${o.key}`, 'selection-mode': 'single', selected: o.key === current?.key, 'selected-icon': 'check' });
532
+ if (o.swatch && o.swatch !== 'transparent') {
533
+ const swatch = document.createElement('span');
534
+ swatch.className = 'swatch';
535
+ swatch.slot = 'end';
536
+ swatch.style.background = o.swatch;
537
+ item.appendChild(swatch);
538
+ }
539
+ choices.appendChild(item);
540
+ }
541
+ const md3eItems = [];
542
+ for (const o of options.values()) {
543
+ if (o.theme !== 'md3e') continue;
544
+ const item = menuItem(o.name, { value: `theme:${o.key}`, 'selection-mode': 'single', selected: o.key === current?.key, 'selected-icon': 'check' });
545
+ if (o.swatch) {
546
+ const swatch = document.createElement('span');
547
+ swatch.className = 'swatch';
548
+ swatch.slot = 'end';
549
+ swatch.style.background = o.swatch;
550
+ item.appendChild(swatch);
551
+ }
552
+ md3eItems.push(item);
553
+ }
554
+ if (md3eItems.length) {
555
+ const md3eMenu = menuItem('Material 3 Expressive');
556
+ md3eMenu.append(glyph('palette', 'start'), submenu('Material 3 Expressive', md3eItems));
557
+ choices.appendChild(md3eMenu);
558
+ }
559
+ const cycle = document.createElement('md-menu-group');
560
+ cycle.appendChild(menuItem('Next theme', { value: 'theme:next', 'trailing-text': 'Ctrl+Shift+F12' }));
561
+ const theme = menuItem('Theme');
562
+ theme.append(glyph('palette', 'start'), submenu('Theme', [choices, cycle]));
563
+ for (const el of [...appMenu.children]) if (el !== appTrigger) el.remove();
564
+ appMenu.append(...items, theme);
565
+ appMenuBuilt = true;
566
+ fixMenuStyle(appMenu);
567
+ }
568
+
569
+ function onAppMenuSelect(e) {
570
+ const [kind, a, b] = String(e.detail?.item?.value ?? '').split(':');
571
+ // Close without handing focus back to the button: the command may move it
572
+ // (Command Palette, New Conversation)
573
+ e.preventDefault();
574
+ closeAppMenu();
575
+ if (kind === 'theme') {
576
+ if (a === 'next') cycleTheme();
577
+ else select(a);
578
+ } else if (kind === 'app') {
579
+ try {
580
+ appMenus[Number(a)]?.options[Number(b)]?.onClick?.();
581
+ } catch {
582
+ // the app's own handler failed; its menu would have failed the same
583
+ }
584
+ }
585
+ }
586
+ function closeAppMenu() {
587
+ if (!appMenu?.open) return;
588
+ if (typeof appMenu._requestClose === 'function') appMenu._requestClose(false);
589
+ else appMenu.open = false;
590
+ }
591
+
592
+ function position() {
593
+ if (!hostEl) return;
594
+ // MD3E: the app menu button at the bar's start, its icon in line with
595
+ // the drawer's icons below
596
+ if (appMenu && current?.theme === 'md3e') {
597
+ const bar = document.querySelector('[data-testid="title-menu-bar"]');
598
+ if (!bar) {
599
+ hostEl.style.display = 'none';
600
+ if (retry++ < 60) setTimeout(position, 500);
601
+ return;
602
+ }
603
+ const r = bar.getBoundingClientRect();
604
+ hostEl.style.display = 'block';
605
+ hostEl.style.transform = `translate(${Math.round(r.left + 22)}px, ${Math.round(r.top + (r.height - 32) / 2)}px)`;
606
+ return;
607
+ }
608
+ const items = document.querySelectorAll('[data-testid="title-menu-bar-item"]');
609
+ const last = items[items.length - 1];
610
+ if (!last) {
611
+ hostEl.style.display = 'none';
612
+ if (retry++ < 60) setTimeout(position, 500);
613
+ return;
614
+ }
615
+ const r = last.getBoundingClientRect();
616
+ const prev = items[items.length - 2]?.getBoundingClientRect();
617
+ const gap = prev ? Math.max(0, r.left - prev.right) : 2;
618
+ const cs = getComputedStyle(last);
619
+ hostEl.style.display = 'block';
620
+ hostEl.style.transform = `translate(${Math.round(r.right + gap)}px, ${Math.round(r.top)}px)`;
621
+ trigger.style.height = `${r.height}px`;
622
+ trigger.style.font = cs.font;
623
+ trigger.style.letterSpacing = cs.letterSpacing;
624
+ trigger.style.padding = cs.padding;
625
+ trigger.style.borderRadius = cs.borderRadius;
626
+ }
627
+
628
+ function openMenu() {
629
+ position();
630
+ menu.hidden = false;
631
+ trigger.setAttribute('aria-expanded', 'true');
632
+ (menu.querySelector('[aria-checked="true"]') || menu.querySelector('[role="menuitemradio"]'))?.focus();
633
+ }
634
+ function closeMenu() {
635
+ if (!menu || menu.hidden) return;
636
+ menu.hidden = true;
637
+ trigger.setAttribute('aria-expanded', 'false');
638
+ }
639
+ function onMenuKey(e) {
640
+ const items = [...menu.querySelectorAll('[role="menuitemradio"]')];
641
+ const i = items.indexOf(shadow.activeElement);
642
+ if (e.key === 'ArrowDown') { items[(i + 1) % items.length].focus(); e.preventDefault(); }
643
+ else if (e.key === 'ArrowUp') { items[(i - 1 + items.length) % items.length].focus(); e.preventDefault(); }
644
+ else if (e.key === 'Enter' || e.key === ' ') { if (items[i]) { select(items[i].dataset.key); closeMenu(); } e.preventDefault(); }
645
+ else if (e.key === 'Escape') { closeMenu(); trigger.focus(); }
646
+ }
647
+
648
+ // ---- M3 ripple ------------------------------------------------------------
649
+ const RIPPLE_TARGETS = 'button, a[href], [role="button"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"], [role="tab"], [data-testid="conversation-row-sidebar"]';
650
+ const PRESSED_OPACITY = 0.1;
651
+ let rippleLayer = null;
652
+ const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)');
653
+
654
+ function startRipple(e) {
655
+ if (!(bundle.ripple ?? []).includes(current?.theme) || e.button !== 0 || reducedMotion?.matches) return;
656
+ if (hostEl && e.composedPath().includes(hostEl)) return;
657
+ const target = e.target instanceof Element ? e.target.closest(RIPPLE_TARGETS) : null;
658
+ if (!target || target.isContentEditable) return;
659
+ const x = e.clientX, y = e.clientY, start = performance.now();
660
+ let released = false, onRelease = null;
661
+ const release = () => {
662
+ released = true;
663
+ window.removeEventListener('pointerup', release, true);
664
+ window.removeEventListener('pointercancel', release, true);
665
+ onRelease?.();
666
+ };
667
+ window.addEventListener('pointerup', release, true);
668
+ window.addEventListener('pointercancel', release, true);
669
+ requestAnimationFrame(() => {
670
+ const box = target.getBoundingClientRect();
671
+ if (box.width < 8 || box.height < 8) return;
672
+ const cs = getComputedStyle(target);
673
+ // md-menu items draw their shape 4px inside the row (::before): ripple there
674
+ const shape = target.matches('[role="menu"] [role^="menuitem"]') ? getComputedStyle(target, '::before') : null;
675
+ const inset = shape?.content && shape.content !== 'none' ? parseFloat(shape.left) || 0 : 0;
676
+ const r = { left: box.left + inset, top: box.top, width: box.width - inset * 2, height: box.height };
677
+ const radius = inset ? shape.borderRadius : cs.borderRadius;
678
+ if (!rippleLayer?.isConnected) {
679
+ rippleLayer = document.createElement('div');
680
+ rippleLayer.id = 'agy-ripple-layer';
681
+ rippleLayer.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147482000;contain:strict;';
682
+ document.body.appendChild(rippleLayer);
683
+ }
684
+ const clip = document.createElement('div');
685
+ clip.style.cssText = `position:absolute;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;border-radius:${radius};overflow:hidden;`;
686
+ const endR = Math.hypot(r.width, r.height) / 2 + 10;
687
+ const startScale = (Math.max(r.width, r.height) * 0.3) / endR;
688
+ const cx = r.width / 2, cy = r.height / 2;
689
+ const ink = document.createElement('div');
690
+ ink.style.cssText = `position:absolute;left:${cx - endR}px;top:${cy - endR}px;width:${endR * 2}px;height:${endR * 2}px;border-radius:50%;background:${cs.color};opacity:0;`;
691
+ clip.appendChild(ink);
692
+ rippleLayer.appendChild(clip);
693
+ ink.animate(
694
+ [{ transform: `translate(${x - r.left - cx}px, ${y - r.top - cy}px) scale(${startScale})` }, { transform: 'none' }],
695
+ { duration: 225, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', fill: 'forwards' },
696
+ );
697
+ ink.animate([{ opacity: 0 }, { opacity: PRESSED_OPACITY }], { duration: 75, fill: 'forwards' });
698
+ const fadeOut = () => {
699
+ const wait = Math.max(0, 225 - (performance.now() - start));
700
+ setTimeout(() => {
701
+ ink.animate([{ opacity: PRESSED_OPACITY }, { opacity: 0 }], { duration: 150, fill: 'forwards' })
702
+ .finished.finally(() => clip.remove());
703
+ setTimeout(() => clip.remove(), 400);
704
+ }, wait);
705
+ };
706
+ if (released) fadeOut();
707
+ else onRelease = fadeOut;
708
+ });
709
+ }
710
+
711
+ // ---- Expansion: animated open and close ------------------------------------
712
+ // The app swaps the region in and out at once. Opening grows the new region
713
+ // from 0; closing plays a clone of the old one down to 0. Both ride the M3
714
+ // Expressive springs from the tokens: height the spatial (medium) spring,
715
+ // opacity the effects (fast) one.
716
+ const COLLAPSIBLES = [
717
+ { toggle: '[data-testid="changed-files-card-header"]', card: '[data-testid="changed-files-card"]', region: ':scope > .relative:not([data-agy-ghost])' },
718
+ ];
719
+ function spring(name) {
720
+ const cs = getComputedStyle(document.documentElement);
721
+ const duration = parseFloat(cs.getPropertyValue(`--md-sys-motion-${name}-duration`));
722
+ const easing = cs.getPropertyValue(`--md-sys-motion-${name}-easing`).trim();
723
+ return duration && easing ? { duration, easing } : { duration: 300, easing: 'cubic-bezier(0.2, 0, 0, 1)' };
724
+ }
725
+ // closing holds the end state until the clone is removed; opening must not,
726
+ // or the region would keep a fixed height after it
727
+ function animateRegion(el, from, to, closing) {
728
+ const fill = closing ? 'forwards' : 'none';
729
+ el.animate({ opacity: closing ? [1, 0] : [0, 1] }, { ...spring('effect-fast'), fill });
730
+ return el.animate([{ height: `${from}px`, clipPath: 'inset(0)' }, { height: `${to}px`, clipPath: 'inset(0)' }], { ...spring('spatial-medium'), fill });
731
+ }
732
+ function onClickCapture(e) {
733
+ if (!(bundle.ripple ?? []).includes(current?.theme) || reducedMotion?.matches) return;
734
+ const t = e.target instanceof Element ? e.target : null;
735
+ for (const c of COLLAPSIBLES) {
736
+ const toggle = t?.closest(c.toggle);
737
+ if (!toggle || t.closest('button, a') && toggle.contains(t.closest('button, a'))) continue;
738
+ const card = toggle.closest(c.card);
739
+ if (!card) return;
740
+ card.querySelectorAll(':scope > [data-agy-ghost]').forEach((g) => g.remove());
741
+ const region = card.querySelector(c.region);
742
+ if (!region) {
743
+ // collapsed: React adds the region before the next frame
744
+ requestAnimationFrame(() => {
745
+ const opened = card.isConnected && card.querySelector(c.region);
746
+ if (opened) animateRegion(opened, 0, opened.getBoundingClientRect().height, false);
747
+ });
748
+ return;
749
+ }
750
+ const height = region.getBoundingClientRect().height;
751
+ const scrolled = [...region.querySelectorAll('.overflow-y-auto')].map((el) => el.scrollTop);
752
+ const ghost = region.cloneNode(true);
753
+ ghost.setAttribute('data-agy-ghost', '');
754
+ ghost.setAttribute('aria-hidden', 'true');
755
+ ghost.inert = true;
756
+ ghost.style.overflow = 'clip';
757
+ requestAnimationFrame(() => {
758
+ if (region.isConnected || !toggle.isConnected) return;
759
+ toggle.after(ghost);
760
+ ghost.querySelectorAll('.overflow-y-auto').forEach((el, i) => (el.scrollTop = scrolled[i] ?? 0));
761
+ animateRegion(ghost, height, 0, true).finished.finally(() => ghost.remove());
762
+ setTimeout(() => ghost.remove(), 900);
763
+ });
764
+ return;
765
+ }
766
+ }
767
+
768
+ // ---- Global listeners -----------------------------------------------------
769
+ function onPointerDown(e) {
770
+ startRipple(e);
771
+ if (hostEl && !e.composedPath().includes(hostEl)) closeMenu();
772
+ const transcript = document.querySelector('[data-testid="autoscroll-viewport"]');
773
+ if (transcript && !transcript.classList.contains('no-focus-agent-input')) transcript.classList.add('no-focus-agent-input');
774
+ }
775
+ function cycleTheme() {
776
+ const keys = [...options.keys()];
777
+ select(keys[(keys.indexOf(current.key) + 1) % keys.length]);
778
+ }
779
+ function onKeyDown(e) {
780
+ if (e.ctrlKey && e.shiftKey && !e.altKey && e.key === 'F12') {
781
+ cycleTheme();
782
+ e.preventDefault();
783
+ }
784
+ }
785
+ window.addEventListener('pointerdown', onPointerDown, true);
786
+ window.addEventListener('click', onClickCapture, true);
787
+ window.addEventListener('keydown', onKeyDown, true);
788
+ window.addEventListener('resize', position);
789
+ window.addEventListener('focus', syncTitleBarOverlay);
790
+ document.fonts?.addEventListener('loadingdone', position);
791
+
792
+ // ---- Virtual list layout guard --------------------------------------------
793
+ // Antigravity virtualizes the sidebar and auxiliary panels with TanStack
794
+ // Virtual. On mounting long lists (e.g. 299 diffs in Review or 95 sidebar
795
+ // items), TanStack Virtual synchronously reads offsetHeight on every
796
+ // [data-index] element while dirtying container styles between each item.
797
+ // In a large conversation, each forced reflow costs 80-100ms, causing a 2s+
798
+ // freeze upon entry. This guard serves known fixed heights or cached values
799
+ // for virtual rows, preventing layout thrashing while letting ResizeObserver
800
+ // handle real dynamic size changes asynchronously on idle frames.
801
+ const origOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')?.get;
802
+ const virtualHeightCache = new WeakMap();
803
+ if (origOffsetHeight) {
804
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
805
+ get() {
806
+ if (this.hasAttribute('data-index')) {
807
+ const cached = virtualHeightCache.get(this);
808
+ if (cached !== undefined) return cached;
809
+ if (this.firstElementChild?.classList.contains('sticky')) {
810
+ virtualHeightCache.set(this, 33);
811
+ return 33;
812
+ }
813
+ if (this.classList.contains('bg-sidebar') || this.querySelector('h2')) {
814
+ virtualHeightCache.set(this, 24);
815
+ return 24;
816
+ }
817
+ if (this.querySelector('.agy-sidebar-row') || this.classList.contains('pb-[1px]')) {
818
+ virtualHeightCache.set(this, 33);
819
+ return 33;
820
+ }
821
+ if (!this.textContent.trim()) {
822
+ virtualHeightCache.set(this, 16);
823
+ return 16;
824
+ }
825
+ }
826
+ const val = origOffsetHeight.call(this);
827
+ if (this.hasAttribute('data-index')) virtualHeightCache.set(this, val);
828
+ return val;
829
+ },
830
+ configurable: true,
831
+ });
832
+ }
833
+
834
+ // Start the Overview panel's Context card; every theme gets it
835
+ const contextCard = typeof agyContextCard === 'function' ? agyContextCard() : null;
836
+ // ...and the token line under each finished turn
837
+ const turnTokens = typeof agyTurnTokens === 'function' ? agyTurnTokens() : null;
838
+ // ...and, in MD3E, the M3 loading indicator in the agent's "Working" row
839
+ const loadingIndicator = typeof agyLoadingIndicator === 'function' ? agyLoadingIndicator() : null;
840
+ // ...and wavy progress bars on running background work
841
+ const progressIndicators = typeof agyProgressIndicators === 'function' ? agyProgressIndicators() : null;
842
+ // ...and Wrap lines / Line numbers in every code block's header
843
+ const codeTools = typeof agyCodeTools === 'function' ? agyCodeTools() : null;
844
+
845
+ function dispose() {
846
+ window.removeEventListener('pointerdown', onPointerDown, true);
847
+ window.removeEventListener('click', onClickCapture, true);
848
+ window.removeEventListener('keydown', onKeyDown, true);
849
+ window.removeEventListener('resize', position);
850
+ window.removeEventListener('focus', syncTitleBarOverlay);
851
+ document.fonts?.removeEventListener('loadingdone', position);
852
+ hostEl?.remove();
853
+ document.documentElement?.removeAttribute('data-agy-appmenu');
854
+ rippleLayer?.remove();
855
+ contextCard?.dispose();
856
+ turnTokens?.dispose();
857
+ loadingIndicator?.dispose();
858
+ progressIndicators?.dispose();
859
+ codeTools?.dispose();
860
+ if (origOffsetHeight) {
861
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
862
+ get: origOffsetHeight,
863
+ configurable: true,
864
+ });
865
+ }
866
+ document.querySelectorAll('[data-agy-ghost]').forEach((g) => g.remove());
867
+ document.getElementById('agy-theme-css')?.remove();
868
+ document.documentElement?.removeAttribute('data-agy-theme');
869
+ document.documentElement?.removeAttribute('data-agy-seed');
870
+ modeObserver.disconnect();
871
+ document.documentElement?.removeAttribute('data-agy-mode');
872
+ document.querySelectorAll('[data-testid="autoscroll-viewport"].no-focus-agent-input')
873
+ .forEach((el) => el.classList.remove('no-focus-agent-input'));
874
+ syncTitleBarOverlay();
875
+ delete window.__agyThemeHost;
876
+ delete window.agyTheme;
877
+ }
878
+
879
+ const api = {
880
+ version: HOST_VERSION,
881
+ update,
882
+ dispose,
883
+ select,
884
+ get: () => current?.key,
885
+ options: () => bundle.options.map(({ key, name }) => ({ key, name })),
886
+ };
887
+ window.__agyThemeHost = api;
888
+ window.agyTheme = api;
889
+
890
+ update(bundle);
891
+ if (!document.documentElement) {
892
+ new MutationObserver((_, mo) => {
893
+ if (document.documentElement) { mo.disconnect(); apply(); }
894
+ }).observe(document, { childList: true });
895
+ }
896
+ if (document.readyState === 'loading') {
897
+ document.addEventListener('DOMContentLoaded', () => { apply(); mount(); }, { once: true });
898
+ } else {
899
+ mount();
900
+ }
901
+ return api;
902
+ }
903
+
904
+ if (typeof module === 'object' && module.exports) module.exports = agyThemeHost;