@materialwebunofficial/antigravity-themes 1.0.5 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -34
- package/bin/cli.mjs +91 -47
- package/lib/asar.mjs +163 -0
- package/lib/bundle.mjs +70 -17
- package/lib/injector.mjs +39 -6
- package/lib/keeper.mjs +73 -0
- package/lib/native.mjs +165 -0
- package/lib/paths.mjs +3 -2
- package/lib/plugin.mjs +98 -0
- package/package.json +1 -1
- package/src/code-tools.js +1 -1
- package/src/context-card.js +1 -1
- package/src/frame-host.js +282 -0
- package/src/loading-indicator.js +1 -1
- package/src/progress-indicators.js +49 -1
- package/src/theme-host.js +97 -66
- package/src/turn-tokens.js +1 -1
- package/themes/frames/matte.css +211 -0
- package/themes/frames/md3e.css +1410 -0
- package/themes/frames/nordic.css +252 -0
- package/themes/frames/paper.css +298 -0
- package/themes/index.json +18 -12
- package/themes/matte-tokens.css +98 -0
- package/themes/matte.css +465 -135
- package/themes/md3e.css +1124 -164
- package/themes/nordic-tokens.css +60 -0
- package/themes/nordic.css +457 -67
- package/themes/paper-tokens.css +54 -0
- package/themes/paper.css +508 -68
package/lib/plugin.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The Antigravity plugin that carries the keeper (keeper.mjs). Antigravity
|
|
2
|
+
// discovers plugins in ~/.gemini/config/plugins/, outside its install folder,
|
|
3
|
+
// and starts their sidecars itself at every launch, with its own Node: so the
|
|
4
|
+
// native install survives updates with no process of ours left running.
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, renameSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { removeTree } from './asar.mjs';
|
|
9
|
+
import { installDir } from './paths.mjs';
|
|
10
|
+
|
|
11
|
+
export const PLUGIN = 'antigravity-themes';
|
|
12
|
+
export const pluginDir = () => path.join(os.homedir(), '.gemini', 'config', 'plugins', PLUGIN);
|
|
13
|
+
|
|
14
|
+
// What the keeper needs to build the scripts again after an update
|
|
15
|
+
const RUNTIME = ['lib', 'src', 'themes', 'vendor', 'package.json', 'LICENSE', 'NOTICE'];
|
|
16
|
+
|
|
17
|
+
// Material Symbols Rounded "palette"
|
|
18
|
+
const LOGO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="#6f9fe8" d="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 -444Z"/></svg>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const README = `# Antigravity Themes
|
|
22
|
+
|
|
23
|
+
Installed by \`antigravity-themes install --native\`. The themes themselves are
|
|
24
|
+
written into Antigravity's own files (resources/app); this plugin only keeps
|
|
25
|
+
them there. Its one sidecar, the keeper, runs once each time Antigravity
|
|
26
|
+
starts:
|
|
27
|
+
|
|
28
|
+
- when the themes are in place, it exits at once;
|
|
29
|
+
- after an Antigravity update, which replaces the app's files, it puts the
|
|
30
|
+
themes back and applies them to the open windows straight away.
|
|
31
|
+
|
|
32
|
+
Turning this plugin off stops the keeper: the themes then last until the next
|
|
33
|
+
Antigravity update. To remove everything, run
|
|
34
|
+
\`npx -y @materialwebunofficial/antigravity-themes uninstall\`.
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const KEEPER = `// Started by Antigravity with every launch (restart_policy "never": once per
|
|
38
|
+
// start). The work is in runtime/lib/keeper.mjs.
|
|
39
|
+
import { runKeeper } from '../../runtime/lib/keeper.mjs';
|
|
40
|
+
|
|
41
|
+
await runKeeper();
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
export function installPlugin(root, { build, version }) {
|
|
45
|
+
const staging = path.join(installDir(), 'plugin-staging');
|
|
46
|
+
removeTree(staging);
|
|
47
|
+
mkdirSync(path.join(staging, 'sidecars', 'keeper'), { recursive: true });
|
|
48
|
+
mkdirSync(path.join(staging, 'assets'), { recursive: true });
|
|
49
|
+
for (const entry of RUNTIME) {
|
|
50
|
+
const from = path.join(root, entry);
|
|
51
|
+
if (existsSync(from)) cpSync(from, path.join(staging, 'runtime', entry), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
writeFileSync(path.join(staging, 'runtime', 'build.json'), `${JSON.stringify({ build, version }, null, 2)}\n`);
|
|
54
|
+
writeFileSync(path.join(staging, 'plugin.json'), `${JSON.stringify({
|
|
55
|
+
name: PLUGIN,
|
|
56
|
+
displayName: 'Antigravity Themes',
|
|
57
|
+
description: 'Keeps the Matte, Paper, Nordic and Material 3 Expressive themes installed across Antigravity updates.',
|
|
58
|
+
version,
|
|
59
|
+
logo: 'assets/logo.svg',
|
|
60
|
+
homepage: 'https://github.com/materialwebunofficial/antigravity-themes',
|
|
61
|
+
}, null, 2)}\n`);
|
|
62
|
+
writeFileSync(path.join(staging, 'README.md'), README);
|
|
63
|
+
writeFileSync(path.join(staging, 'assets', 'logo.svg'), LOGO);
|
|
64
|
+
writeFileSync(path.join(staging, 'sidecars', 'keeper', 'sidecar.json'), `${JSON.stringify({
|
|
65
|
+
command: 'node',
|
|
66
|
+
args: ['keeper.mjs'],
|
|
67
|
+
restart_policy: 'never',
|
|
68
|
+
display_name: 'Antigravity Themes keeper',
|
|
69
|
+
description: 'At each start, puts the themes back if an Antigravity update removed them; otherwise exits at once.',
|
|
70
|
+
}, null, 2)}\n`);
|
|
71
|
+
writeFileSync(path.join(staging, 'sidecars', 'keeper', 'keeper.mjs'), KEEPER);
|
|
72
|
+
|
|
73
|
+
// Moved in whole (same volume: a rename), so Antigravity never discovers
|
|
74
|
+
// half a plugin. A keeper still running keeps its folder busy: then copy.
|
|
75
|
+
const dest = pluginDir();
|
|
76
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
77
|
+
try {
|
|
78
|
+
if (existsSync(dest)) {
|
|
79
|
+
const old = path.join(installDir(), 'plugin-old');
|
|
80
|
+
removeTree(old);
|
|
81
|
+
renameSync(dest, old);
|
|
82
|
+
removeTree(old);
|
|
83
|
+
}
|
|
84
|
+
renameSync(staging, dest);
|
|
85
|
+
} catch {
|
|
86
|
+
cpSync(staging, dest, { recursive: true, force: true });
|
|
87
|
+
removeTree(staging);
|
|
88
|
+
}
|
|
89
|
+
try { rmdirSync(installDir()); } catch { /* not empty (a background install) or gone */ }
|
|
90
|
+
return dest;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removePlugin() {
|
|
94
|
+
const dest = pluginDir();
|
|
95
|
+
if (!existsSync(dest)) return null;
|
|
96
|
+
removeTree(dest);
|
|
97
|
+
return dest;
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@materialwebunofficial/antigravity-themes",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Matte, Paper, Nordic, and Material 3 Expressive themes for Antigravity Desktop, applied without changing the app's files.",
|
|
5
5
|
"homepage": "https://github.com/materialwebunofficial/antigravity-themes#readme",
|
|
6
6
|
"repository": {
|
package/src/code-tools.js
CHANGED
package/src/context-card.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// seen part-way says which step its count starts from.
|
|
22
22
|
// Warnings at 80% and 95% of the window: one quiet line under the summary.
|
|
23
23
|
function agyContextCard() {
|
|
24
|
-
const WINDOWS = [[/^gemini/i,
|
|
24
|
+
const WINDOWS = [[/^gemini/i, 256000], [/^claude/i, 200000], [/gpt-oss/i, 131072]];
|
|
25
25
|
const LEVELS = [[0.95, 'critical', 'The context window is almost full.'],
|
|
26
26
|
[0.8, 'warn', 'The context window is filling up.']];
|
|
27
27
|
const GROUP_NAMES = { 1: 'System prompt', 2: 'Tools', 3: 'Rules', 4: 'MCP tools', 5: 'Skills', 6: 'Subagents', 7: 'Messages', 8: 'Workflows', 9: 'Plugins', 10: 'Flows' };
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// AGY frame host: the themes inside a sidecar frame (Swarm and other plugin
|
|
2
|
+
// panes). Each is a cross-origin plugin:// iframe, so the main window's theme
|
|
3
|
+
// host can neither style it nor share its fonts; this script is injected into
|
|
4
|
+
// the frame itself and takes only what a frame needs: the theme's frame
|
|
5
|
+
// stylesheet (themes/frames/*.css) and the mode, seed and font the window
|
|
6
|
+
// tells it about.
|
|
7
|
+
//
|
|
8
|
+
// Protocol (postMessage with the parent window, which runs theme-host.js):
|
|
9
|
+
// frame -> parent { type: 'agy-theme-query', fonts?: true }
|
|
10
|
+
// parent -> frame { type: 'agy-theme-change', theme, seed, mode }
|
|
11
|
+
// parent -> frame { type: 'agy-theme-fonts', theme, fonts: [{ family, weight, stretch, buffer }] }
|
|
12
|
+
// The last answer is kept in localStorage, so a frame that loads later paints
|
|
13
|
+
// with its theme at once instead of flashing the stock look.
|
|
14
|
+
function agyFrameHost(bundle) {
|
|
15
|
+
const VERSION = 1;
|
|
16
|
+
const existing = window.__agyFrameHost;
|
|
17
|
+
if (existing?.version === VERSION) return existing.update(bundle);
|
|
18
|
+
existing?.dispose();
|
|
19
|
+
// Before 1.0.13 the window's full host ran in frames too, and the window
|
|
20
|
+
// wrote every one of its custom properties inline on this <html>, where they
|
|
21
|
+
// would outrank the frame stylesheet. Only the app's own tokens belong there.
|
|
22
|
+
try { window.__agyThemeHost?.dispose?.(); } catch { /* not ours to fix */ }
|
|
23
|
+
try {
|
|
24
|
+
const inline = document.documentElement.style;
|
|
25
|
+
for (const name of [...inline]) {
|
|
26
|
+
if (/^--(sw-|o2-|md-|m-|p-|n-|agy-|nord\d)/.test(name)) inline.removeProperty(name);
|
|
27
|
+
}
|
|
28
|
+
} catch { /* no root yet: nothing was written */ }
|
|
29
|
+
|
|
30
|
+
const STORE = 'agy-frame-theme';
|
|
31
|
+
const read = () => {
|
|
32
|
+
try { return JSON.parse(localStorage.getItem(STORE)) || null; } catch { return null; }
|
|
33
|
+
};
|
|
34
|
+
const save = (state) => {
|
|
35
|
+
try { localStorage.setItem(STORE, JSON.stringify(state)); } catch { /* storage blocked: the query answers again */ }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let current = null; // { theme, seed, mode }
|
|
39
|
+
let answered = false;
|
|
40
|
+
const fontsLoaded = new Set(); // themes whose font faces are registered
|
|
41
|
+
let fontsPending = false; // asked for them, no answer yet
|
|
42
|
+
const features = new Map(); // theme -> { dispose } for the theme's own script
|
|
43
|
+
|
|
44
|
+
// A query asks for the theme's fonts too while the frame lacks them; the
|
|
45
|
+
// window may not be listening yet (at startup the frame can load first), so
|
|
46
|
+
// every later query asks again until they arrive
|
|
47
|
+
const needsFonts = (theme) => (bundle.fonts || []).includes(theme) && !fontsLoaded.has(theme);
|
|
48
|
+
const ask = () => {
|
|
49
|
+
const fonts = needsFonts(current?.theme);
|
|
50
|
+
if (fonts) fontsPending = true;
|
|
51
|
+
try { window.parent.postMessage(fonts ? { type: 'agy-theme-query', fonts: true } : { type: 'agy-theme-query' }, '*'); } catch { /* detached */ }
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function apply() {
|
|
55
|
+
const root = document.documentElement;
|
|
56
|
+
if (!root) return;
|
|
57
|
+
const theme = current?.theme;
|
|
58
|
+
const css = (theme && bundle.frames[theme]) || '';
|
|
59
|
+
let style = document.getElementById('agy-theme-css');
|
|
60
|
+
if (!css) {
|
|
61
|
+
root.removeAttribute('data-agy-theme');
|
|
62
|
+
root.removeAttribute('data-agy-seed');
|
|
63
|
+
root.removeAttribute('data-agy-mode');
|
|
64
|
+
style?.remove();
|
|
65
|
+
} else {
|
|
66
|
+
root.setAttribute('data-agy-theme', theme);
|
|
67
|
+
if (current.seed) root.setAttribute('data-agy-seed', current.seed);
|
|
68
|
+
else root.removeAttribute('data-agy-seed');
|
|
69
|
+
root.setAttribute('data-agy-mode', current.mode === 'light' ? 'light' : 'dark');
|
|
70
|
+
if (!style) {
|
|
71
|
+
style = document.createElement('style');
|
|
72
|
+
style.id = 'agy-theme-css';
|
|
73
|
+
}
|
|
74
|
+
if (style.textContent !== css) style.textContent = css;
|
|
75
|
+
// Last, after the frame's own sheets: equal-specificity ties go to the
|
|
76
|
+
// theme (before <head> exists it goes on <html>; moved at DOMContentLoaded)
|
|
77
|
+
const parent = document.head || root;
|
|
78
|
+
if (style.parentNode !== parent || style !== parent.lastElementChild) parent.appendChild(style);
|
|
79
|
+
if (needsFonts(theme) && !fontsPending) ask();
|
|
80
|
+
}
|
|
81
|
+
for (const [name, feature] of features) {
|
|
82
|
+
if (name !== theme) { feature.dispose(); features.delete(name); }
|
|
83
|
+
}
|
|
84
|
+
const start = theme && !features.has(theme) && FEATURES[theme];
|
|
85
|
+
if (start) {
|
|
86
|
+
try { features.set(theme, start()); } catch { /* the stylesheet still applies */ }
|
|
87
|
+
}
|
|
88
|
+
// A sidecar follows the app's theme by watching this element's inline
|
|
89
|
+
// style, where the SDK writes the app's tokens. The theme changes what
|
|
90
|
+
// those tokens resolve to without touching them, so it leaves its own mark
|
|
91
|
+
// there: Swarm then re-reads its light/dark scheme and hands the tokens on
|
|
92
|
+
// to its narrator frame.
|
|
93
|
+
if (css) root.style.setProperty('--agy-frame-theme', `${theme} ${current.seed ?? '-'} ${current.mode}`);
|
|
94
|
+
else root.style.removeProperty('--agy-frame-theme');
|
|
95
|
+
// md3e-web components cache their colors; this is the package's own signal
|
|
96
|
+
// to read the tokens again (a new seed or mode)
|
|
97
|
+
window.dispatchEvent(new Event('theme-change'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function onMessage(e) {
|
|
101
|
+
const d = e.data;
|
|
102
|
+
if (!d || e.source !== window.parent || e.source === window) return;
|
|
103
|
+
if (d.type === 'agy-theme-change') {
|
|
104
|
+
answered = true;
|
|
105
|
+
const next = { theme: typeof d.theme === 'string' ? d.theme : null, seed: typeof d.seed === 'string' ? d.seed : null, mode: d.mode === 'light' ? 'light' : 'dark' };
|
|
106
|
+
if (current && next.theme === current.theme && next.seed === current.seed && next.mode === current.mode) {
|
|
107
|
+
// the theme painted from storage: only its fonts may still be missing
|
|
108
|
+
if (needsFonts(next.theme) && !fontsPending) ask();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
current = next;
|
|
112
|
+
save(current);
|
|
113
|
+
apply();
|
|
114
|
+
} else if (d.type === 'agy-theme-fonts' && Array.isArray(d.fonts) && document.fonts) {
|
|
115
|
+
fontsPending = false;
|
|
116
|
+
if (fontsLoaded.has(d.theme)) return;
|
|
117
|
+
fontsLoaded.add(d.theme);
|
|
118
|
+
for (const f of d.fonts) {
|
|
119
|
+
try {
|
|
120
|
+
const face = new FontFace(f.family, f.buffer, { weight: f.weight, stretch: f.stretch, style: 'normal', display: 'swap' });
|
|
121
|
+
document.fonts.add(face);
|
|
122
|
+
face.load().catch(() => {});
|
|
123
|
+
} catch {
|
|
124
|
+
// the theme still reads on its fallback stack
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
window.addEventListener('message', onMessage);
|
|
130
|
+
|
|
131
|
+
// ---- Posts written with literal \uXXXX escapes -------------------------------
|
|
132
|
+
// A model that passes its post as a JSON string sometimes escapes it twice,
|
|
133
|
+
// and the board then shows "\u00e9" where an accented letter belongs. Swarm
|
|
134
|
+
// prints a post as it was stored, so the escapes are decoded here, in the
|
|
135
|
+
// text nodes of what the page adds (never inside code: there an escape is
|
|
136
|
+
// meant). Every theme gets this, the app's own included. Swarm clamps a post
|
|
137
|
+
// to two lines by measuring it, which it did on the longer text: measured
|
|
138
|
+
// again.
|
|
139
|
+
const ESCAPE = /\\u([0-9a-fA-F]{4})/g;
|
|
140
|
+
function decodeEscapes(node) {
|
|
141
|
+
if (node.nodeType !== 1 || !node.textContent.includes('\\u')) return;
|
|
142
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
143
|
+
const clamps = new Set();
|
|
144
|
+
for (let text = walker.nextNode(); text; text = walker.nextNode()) {
|
|
145
|
+
const value = text.nodeValue;
|
|
146
|
+
if (!value.includes('\\u') || text.parentElement?.closest('code, pre, script, style')) continue;
|
|
147
|
+
const decoded = value.replace(ESCAPE, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
148
|
+
if (decoded === value) continue;
|
|
149
|
+
text.nodeValue = decoded;
|
|
150
|
+
const body = text.parentElement?.closest('.feed .msg-body');
|
|
151
|
+
if (body) clamps.add(body);
|
|
152
|
+
}
|
|
153
|
+
for (const body of clamps) {
|
|
154
|
+
const open = body.classList.contains('open');
|
|
155
|
+
body.classList.add('clamped');
|
|
156
|
+
body.classList.remove('open');
|
|
157
|
+
const overflows = body.scrollHeight > body.clientHeight + 4;
|
|
158
|
+
if (open) body.classList.add('open');
|
|
159
|
+
body.classList.toggle('clamped', overflows);
|
|
160
|
+
body.nextElementSibling?.classList.toggle('hidden', !overflows);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const escapeObserver = new MutationObserver((records) => {
|
|
164
|
+
for (const record of records) {
|
|
165
|
+
for (const node of record.addedNodes) decodeEscapes(node);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
function watchEscapes() {
|
|
169
|
+
if (!document.body) return;
|
|
170
|
+
decodeEscapes(document.body);
|
|
171
|
+
escapeObserver.observe(document.body, { childList: true, subtree: true });
|
|
172
|
+
}
|
|
173
|
+
if (document.body) watchEscapes();
|
|
174
|
+
else document.addEventListener('DOMContentLoaded', watchEscapes, { once: true });
|
|
175
|
+
|
|
176
|
+
// ---- MD3E: the Live status is the package's <md-loading-indicator> --------
|
|
177
|
+
// Swarm marks "updates automatically" with a pulsing dot. M3 Expressive has
|
|
178
|
+
// a component for exactly that, live activity: the loading indicator. It
|
|
179
|
+
// rests as its determinate shape (one drawn frame, no animation loop), morphs
|
|
180
|
+
// for three steps when new posts arrive, and runs while the board is
|
|
181
|
+
// reconnecting. frames/md3e.css hides the dot it replaces.
|
|
182
|
+
function md3eFrame() {
|
|
183
|
+
if (!customElements.get('md-loading-indicator')) {
|
|
184
|
+
try { bundle.md3e?.(); } catch { return { dispose() {} }; }
|
|
185
|
+
if (!customElements.get('md-loading-indicator')) return { dispose() {} };
|
|
186
|
+
}
|
|
187
|
+
const MORPH_MS = 650 * 3;
|
|
188
|
+
let live = null, indicator = null, still = null, observer = null, timer = 0, mounted = false;
|
|
189
|
+
// At rest the indicator is determinate: one drawn frame, no frame loop
|
|
190
|
+
const make = () => {
|
|
191
|
+
const el = document.createElement('md-loading-indicator');
|
|
192
|
+
el.className = 'agy-live';
|
|
193
|
+
el.setAttribute('size', '24');
|
|
194
|
+
el.setAttribute('progress', '1');
|
|
195
|
+
el.setAttribute('aria-hidden', 'true'); // the label next to it says it
|
|
196
|
+
return el;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
function sync() {
|
|
200
|
+
if (!indicator) return;
|
|
201
|
+
const busy = live.classList.contains('stale') || timer !== 0;
|
|
202
|
+
if (busy) indicator.removeAttribute('progress');
|
|
203
|
+
else if (indicator.getAttribute('progress') !== '1') indicator.setAttribute('progress', '1');
|
|
204
|
+
}
|
|
205
|
+
function onClass() {
|
|
206
|
+
if (live.classList.contains('flash') && !timer) {
|
|
207
|
+
timer = setTimeout(() => { timer = 0; sync(); }, MORPH_MS);
|
|
208
|
+
}
|
|
209
|
+
sync();
|
|
210
|
+
}
|
|
211
|
+
function mount() {
|
|
212
|
+
if (mounted) return;
|
|
213
|
+
live = document.getElementById('live');
|
|
214
|
+
if (!live) return;
|
|
215
|
+
mounted = true;
|
|
216
|
+
indicator = make();
|
|
217
|
+
live.prepend(indicator);
|
|
218
|
+
// "Watching for a swarm", on the empty state: the same mark, still
|
|
219
|
+
const hint = document.querySelector('.empty-hint');
|
|
220
|
+
if (hint) {
|
|
221
|
+
still = make();
|
|
222
|
+
hint.prepend(still);
|
|
223
|
+
}
|
|
224
|
+
observer = new MutationObserver(onClass);
|
|
225
|
+
observer.observe(live, { attributes: true, attributeFilter: ['class'] });
|
|
226
|
+
sync();
|
|
227
|
+
}
|
|
228
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount, { once: true });
|
|
229
|
+
else mount();
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
dispose() {
|
|
233
|
+
document.removeEventListener('DOMContentLoaded', mount);
|
|
234
|
+
observer?.disconnect();
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
indicator?.remove();
|
|
237
|
+
still?.remove();
|
|
238
|
+
mounted = true; // a late DOMContentLoaded must not mount after dispose
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const FEATURES = { md3e: md3eFrame };
|
|
243
|
+
|
|
244
|
+
function update(nextBundle) {
|
|
245
|
+
bundle = nextBundle;
|
|
246
|
+
apply();
|
|
247
|
+
return api;
|
|
248
|
+
}
|
|
249
|
+
function dispose() {
|
|
250
|
+
window.removeEventListener('message', onMessage);
|
|
251
|
+
document.removeEventListener('DOMContentLoaded', watchEscapes);
|
|
252
|
+
escapeObserver.disconnect();
|
|
253
|
+
for (const feature of features.values()) feature.dispose();
|
|
254
|
+
features.clear();
|
|
255
|
+
document.getElementById('agy-theme-css')?.remove();
|
|
256
|
+
const root = document.documentElement;
|
|
257
|
+
root?.removeAttribute('data-agy-theme');
|
|
258
|
+
root?.removeAttribute('data-agy-seed');
|
|
259
|
+
root?.removeAttribute('data-agy-mode');
|
|
260
|
+
root?.style.removeProperty('--agy-frame-theme');
|
|
261
|
+
delete window.__agyFrameHost;
|
|
262
|
+
delete window.__agyFrameScript; // the frame script may run here again
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const api = { version: VERSION, update, dispose, get: () => current?.theme ?? null };
|
|
266
|
+
window.__agyFrameHost = api;
|
|
267
|
+
|
|
268
|
+
current = read();
|
|
269
|
+
apply();
|
|
270
|
+
// Injected at the start of the document, the stylesheet may have gone on
|
|
271
|
+
// <html> ahead of the frame's own: put it last in <head> once it exists
|
|
272
|
+
if (document.readyState === 'loading') {
|
|
273
|
+
document.addEventListener('DOMContentLoaded', () => { if (window.__agyFrameHost === api) apply(); }, { once: true });
|
|
274
|
+
}
|
|
275
|
+
// The window's host may start after this frame (the injector reaches the
|
|
276
|
+
// targets in any order): ask again until it answers
|
|
277
|
+
ask();
|
|
278
|
+
for (const delay of [400, 1500, 4000]) setTimeout(() => { if (!answered && window.__agyFrameHost === api) ask(); }, delay);
|
|
279
|
+
return api;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (typeof module === 'object' && module.exports) module.exports = agyFrameHost;
|
package/src/loading-indicator.js
CHANGED
|
@@ -84,7 +84,7 @@ function agyProgressIndicators() {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const timer = setInterval(scan,
|
|
87
|
+
const timer = setInterval(scan, 10000);
|
|
88
88
|
scan();
|
|
89
89
|
|
|
90
90
|
return {
|
|
@@ -96,3 +96,51 @@ function agyProgressIndicators() {
|
|
|
96
96
|
},
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
// Usage rings (Settings > Models & Usage, the model menu's usage view): M3
|
|
101
|
+
// Expressive's circular indicator leaves a gap between the arc and its
|
|
102
|
+
// track; the app draws a full track circle under the arc. In MD3E the track
|
|
103
|
+
// is cut to what the arc leaves, minus the gap at both ends, through two
|
|
104
|
+
// variables md3e.css reads (other themes ignore them). A 1ms CSS animation on
|
|
105
|
+
// the arc announces each ring as it mounts, so nothing watches the page; one
|
|
106
|
+
// observer follows the value of the rings already seen.
|
|
107
|
+
function agyQuotaRings() {
|
|
108
|
+
const ARC = '[data-testid="quota-progress-circle"]';
|
|
109
|
+
const GAP = 4; // M3's indicator-track gap, px (the ring is drawn 1:1)
|
|
110
|
+
const style = document.createElement('style');
|
|
111
|
+
style.id = 'agy-rings-css';
|
|
112
|
+
style.textContent = `
|
|
113
|
+
:root[data-agy-theme="md3e"] ${ARC} { animation: agy-ring-mount 1ms; }
|
|
114
|
+
@keyframes agy-ring-mount { to { opacity: 1; } }`;
|
|
115
|
+
(document.head || document.documentElement).appendChild(style);
|
|
116
|
+
|
|
117
|
+
function fit(arc) {
|
|
118
|
+
const track = arc.parentElement?.querySelector('circle:first-child');
|
|
119
|
+
if (!track || track === arc) return;
|
|
120
|
+
const length = parseFloat(arc.getAttribute('stroke-dasharray'));
|
|
121
|
+
const offset = parseFloat(arc.getAttribute('stroke-dashoffset')) || 0;
|
|
122
|
+
if (!(length > 0)) return;
|
|
123
|
+
const cap = (parseFloat(getComputedStyle(arc).strokeWidth) || 4) / 2;
|
|
124
|
+
const value = Math.max(0, length - offset);
|
|
125
|
+
const skip = value ? GAP + 2 * cap : 0; // round caps reach past both ends
|
|
126
|
+
const rest = Math.max(0, length - value - 2 * skip);
|
|
127
|
+
track.style.setProperty('--agy-track-dash', `${rest.toFixed(2)} ${length.toFixed(2)}`);
|
|
128
|
+
track.style.setProperty('--agy-track-offset', `${(-(value + skip)).toFixed(2)}`);
|
|
129
|
+
track.style.setProperty('--agy-track-opacity', rest > 0.5 ? '1' : '0'); // a 0-length dash still paints its round caps
|
|
130
|
+
}
|
|
131
|
+
const observer = new MutationObserver((records) => records.forEach((r) => fit(r.target)));
|
|
132
|
+
function onAnimationStart(e) {
|
|
133
|
+
if (e.animationName !== 'agy-ring-mount') return;
|
|
134
|
+
fit(e.target);
|
|
135
|
+
observer.observe(e.target, { attributes: true, attributeFilter: ['stroke-dashoffset', 'stroke-dasharray'] });
|
|
136
|
+
}
|
|
137
|
+
document.addEventListener('animationstart', onAnimationStart, true);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
dispose() {
|
|
141
|
+
document.removeEventListener('animationstart', onAnimationStart, true);
|
|
142
|
+
observer.disconnect();
|
|
143
|
+
style.remove();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|