keyframes 0.1.0__py3-none-any.whl

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.
Files changed (77) hide show
  1. keyframes/__init__.py +1 -0
  2. keyframes/build.py +102 -0
  3. keyframes/check.py +107 -0
  4. keyframes/cli.py +149 -0
  5. keyframes/deck.py +67 -0
  6. keyframes/export.py +106 -0
  7. keyframes/guide/__init__.py +25 -0
  8. keyframes/guide/components/keyframes-code.md +60 -0
  9. keyframes/guide/components/keyframes-excalidraw.md +148 -0
  10. keyframes/guide/components/keyframes-mujoco.md +76 -0
  11. keyframes/guide/components/keyframes-slider.md +65 -0
  12. keyframes/guide/components/keyframes-three.md +81 -0
  13. keyframes/guide/core.md +113 -0
  14. keyframes/patch.py +77 -0
  15. keyframes/render.py +32 -0
  16. keyframes/scaffold/.gitignore +3 -0
  17. keyframes/scaffold/CLAUDE.md +1 -0
  18. keyframes/scaffold/assets/deck.css +1 -0
  19. keyframes/scaffold/assets/figure.svg +1 -0
  20. keyframes/scaffold/deck.toml +10 -0
  21. keyframes/scaffold/references/.gitkeep +0 -0
  22. keyframes/scaffold/slides/01-title.html +4 -0
  23. keyframes/scaffold/slides/02-intro.html +17 -0
  24. keyframes/server.py +204 -0
  25. keyframes/static/components/keyframes-code.js +423 -0
  26. keyframes/static/components/keyframes-excalidraw.js +189 -0
  27. keyframes/static/components/keyframes-mujoco.js +515 -0
  28. keyframes/static/components/keyframes-slider.js +255 -0
  29. keyframes/static/components/keyframes-three.js +224 -0
  30. keyframes/static/editor/editor.css +228 -0
  31. keyframes/static/editor/editor.js +594 -0
  32. keyframes/static/editor/panel.css +127 -0
  33. keyframes/static/editor/panel.js +357 -0
  34. keyframes/static/index.html +23 -0
  35. keyframes/static/runtime/component.js +126 -0
  36. keyframes/static/runtime/live.js +31 -0
  37. keyframes/static/runtime/math.js +33 -0
  38. keyframes/static/runtime/presenter.js +91 -0
  39. keyframes/static/runtime/runtime.css +91 -0
  40. keyframes/static/runtime/runtime.js +215 -0
  41. keyframes/static/runtime/steps.js +15 -0
  42. keyframes/terminal.py +144 -0
  43. keyframes/themes/default/STYLE_GUIDE.md +73 -0
  44. keyframes/themes/default/assets/arc-top.webp +0 -0
  45. keyframes/themes/default/assets/orbit-ring.webp +0 -0
  46. keyframes/themes/default/assets/streaks-top.webp +0 -0
  47. keyframes/themes/default/assets/streaks-wide.webp +0 -0
  48. keyframes/themes/default/assets/sun-top.webp +0 -0
  49. keyframes/themes/default/assets/title-orbit.webp +0 -0
  50. keyframes/themes/default/layouts/content.html +16 -0
  51. keyframes/themes/default/layouts/figure.html +7 -0
  52. keyframes/themes/default/layouts/grid-2.html +19 -0
  53. keyframes/themes/default/layouts/grid-3x2.html +28 -0
  54. keyframes/themes/default/layouts/grid-4.html +44 -0
  55. keyframes/themes/default/layouts/media-side.html +16 -0
  56. keyframes/themes/default/layouts/section-arc.html +4 -0
  57. keyframes/themes/default/layouts/section-streaks.html +4 -0
  58. keyframes/themes/default/layouts/section.html +4 -0
  59. keyframes/themes/default/layouts/split-wide.html +7 -0
  60. keyframes/themes/default/layouts/split.html +13 -0
  61. keyframes/themes/default/layouts/statement.html +4 -0
  62. keyframes/themes/default/layouts/title.html +5 -0
  63. keyframes/themes/default/layouts/two-column.html +5 -0
  64. keyframes/themes/default/theme.css +330 -0
  65. keyframes/themes/minimal/STYLE_GUIDE.md +41 -0
  66. keyframes/themes/minimal/layouts/figure.html +7 -0
  67. keyframes/themes/minimal/layouts/section.html +4 -0
  68. keyframes/themes/minimal/layouts/statement.html +3 -0
  69. keyframes/themes/minimal/layouts/title-body.html +10 -0
  70. keyframes/themes/minimal/layouts/title.html +5 -0
  71. keyframes/themes/minimal/layouts/two-column.html +11 -0
  72. keyframes/themes/minimal/theme.css +196 -0
  73. keyframes/vendor.py +181 -0
  74. keyframes-0.1.0.dist-info/METADATA +119 -0
  75. keyframes-0.1.0.dist-info/RECORD +77 -0
  76. keyframes-0.1.0.dist-info/WHEEL +4 -0
  77. keyframes-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,255 @@
1
+ import { SwComponent } from "../runtime/component.js";
2
+
3
+ const PRINT = new URLSearchParams(location.search).has("print");
4
+ const PLAY_ICON = `<svg viewBox="0 0 24 24"><path d="M7 4.5v15l13-7.5z"/></svg>`;
5
+ const PAUSE_ICON = `<svg viewBox="0 0 24 24"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>`;
6
+
7
+ const style = document.createElement("style");
8
+ style.textContent = `
9
+ keyframes-slider {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 0.6em;
13
+ width: 100%;
14
+ height: 100%;
15
+ box-sizing: border-box;
16
+ font: 400 var(--keyframes-slider-size, 28px)/1.2 var(--font-sans);
17
+ color: var(--ink, var(--fg));
18
+ user-select: none;
19
+ }
20
+ keyframes-slider .keyframes-slider-label { flex: none; font-weight: 500; white-space: nowrap; }
21
+ keyframes-slider .keyframes-slider-value {
22
+ flex: none;
23
+ min-width: 4ch;
24
+ text-align: right;
25
+ font-family: var(--font-mono);
26
+ font-variant-numeric: tabular-nums;
27
+ color: var(--muted);
28
+ white-space: nowrap;
29
+ }
30
+ keyframes-slider input {
31
+ flex: 1 1 auto;
32
+ min-width: 0;
33
+ height: 1.4em;
34
+ margin: 0;
35
+ font: inherit;
36
+ background: transparent;
37
+ appearance: none;
38
+ cursor: pointer;
39
+ }
40
+ keyframes-slider input:focus { outline: none; }
41
+ keyframes-slider input::-webkit-slider-runnable-track {
42
+ height: 0.28em;
43
+ border-radius: 1em;
44
+ background: linear-gradient(to right, var(--accent) var(--keyframes-slider-fill, 0%), var(--border) 0);
45
+ }
46
+ keyframes-slider input::-moz-range-track { height: 0.28em; border-radius: 1em; background: var(--border); }
47
+ keyframes-slider input::-moz-range-progress { height: 0.28em; border-radius: 1em; background: var(--accent); }
48
+ keyframes-slider input::-webkit-slider-thumb {
49
+ appearance: none;
50
+ width: 1em;
51
+ height: 1em;
52
+ margin-top: -0.36em;
53
+ border: 0.14em solid var(--bg, #fff);
54
+ border-radius: 50%;
55
+ box-sizing: border-box;
56
+ background: var(--accent);
57
+ box-shadow: 0 1px 4px rgb(0 0 0 / 0.3);
58
+ }
59
+ keyframes-slider input::-moz-range-thumb {
60
+ width: 1em;
61
+ height: 1em;
62
+ border: 0.14em solid var(--bg, #fff);
63
+ border-radius: 50%;
64
+ box-sizing: border-box;
65
+ background: var(--accent);
66
+ }
67
+ keyframes-slider input:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 0.2em color-mix(in srgb, var(--accent) 35%, transparent); }
68
+ keyframes-slider .keyframes-slider-play {
69
+ flex: none;
70
+ display: grid;
71
+ place-items: center;
72
+ width: 1.6em;
73
+ height: 1.6em;
74
+ padding: 0;
75
+ font: inherit;
76
+ border: 0;
77
+ border-radius: 50%;
78
+ background: var(--accent);
79
+ color: #fff;
80
+ cursor: pointer;
81
+ }
82
+ keyframes-slider .keyframes-slider-play svg { width: 0.75em; height: 0.75em; fill: currentColor; }
83
+ `;
84
+ document.head.append(style);
85
+
86
+ const isTarget = (el) => el.localName.startsWith("keyframes-") && el.localName !== "keyframes-slider" && "setParam" in el;
87
+
88
+ function decimals(step) {
89
+ const fraction = String(step).split(".")[1] ?? "";
90
+ return Math.min(fraction.length, 3);
91
+ }
92
+
93
+ class SwSlider extends SwComponent {
94
+ #target = null;
95
+ #input = null;
96
+ #output = null;
97
+ #button = null;
98
+ #time = 0;
99
+ #playing = false;
100
+ #live = false;
101
+ #scrubbing = false;
102
+ #raf = 0;
103
+ #last = 0;
104
+
105
+ get timeline() {
106
+ return this.hasAttribute("timeline");
107
+ }
108
+
109
+ get value() {
110
+ return this.timeline ? this.#time : this.#input?.valueAsNumber;
111
+ }
112
+
113
+ async prepare() {
114
+ const target = this.#findTarget();
115
+ if (!target) throw this.#fail(`no component for="${this.getAttribute("for")}" on this slide`);
116
+ if (this.timeline && typeof target.seek !== "function") throw this.#fail(`<${target.localName}> has no timeline`);
117
+ if (!this.timeline && !this.getAttribute("param")) throw this.#fail("needs a param attribute (or timeline)");
118
+ this.#target = target;
119
+ this.#build();
120
+ this.#playing = this.timeline && !this.hasAttribute("paused") && !PRINT;
121
+ const initial = this.getAttribute(PRINT && this.hasAttribute("snapshot") ? "snapshot" : "value");
122
+ this.#set(Number(initial ?? (this.timeline ? 0 : NaN)));
123
+ }
124
+
125
+ play() {
126
+ this.#live = true;
127
+ if (this.#target) this.#apply();
128
+ this.#sync();
129
+ }
130
+
131
+ pause() {
132
+ this.#live = false;
133
+ this.#sync();
134
+ }
135
+
136
+ step(n) {
137
+ const values = this.getAttribute("steps")?.split(",").map(Number);
138
+ if (values?.length && this.#input) this.#set(values[Math.min(n, values.length - 1)]);
139
+ }
140
+
141
+ #fail(message) {
142
+ const error = new Error(message);
143
+ this.showError(error);
144
+ return error;
145
+ }
146
+
147
+ #findTarget() {
148
+ const id = this.getAttribute("for") ?? "";
149
+ const el = (this.slide ?? document).querySelector(`[data-block="${CSS.escape(id)}"], #${CSS.escape(id)}`);
150
+ if (!el) return null;
151
+ return isTarget(el) ? el : [...el.querySelectorAll("*")].find(isTarget) ?? null;
152
+ }
153
+
154
+ #build() {
155
+ const timeline = this.timeline;
156
+ const input = document.createElement("input");
157
+ input.type = "range";
158
+ input.min = timeline ? 0 : this.getAttribute("min") ?? 0;
159
+ input.max = timeline ? this.getAttribute("duration") ?? 10 : this.getAttribute("max") ?? 1;
160
+ input.step = this.getAttribute("step") ?? (input.max - input.min) / (timeline ? 1000 : 100);
161
+ input.setAttribute("aria-label", this.getAttribute("label") ?? (timeline ? "Time" : this.getAttribute("param")));
162
+ input.addEventListener("input", () => this.#set(input.valueAsNumber));
163
+ input.addEventListener("pointerdown", () => (this.#scrubbing = true));
164
+ input.addEventListener("change", () => (this.#scrubbing = false));
165
+ this.addEventListener("keydown", (e) => this.#onKey(e));
166
+ this.#input = input;
167
+ this.#output = document.createElement("output");
168
+ this.#output.className = "keyframes-slider-value";
169
+ this.replaceChildren(...[this.#playButton(), this.#label(), input, this.#output].filter(Boolean));
170
+ }
171
+
172
+ #playButton() {
173
+ if (!this.timeline) return null;
174
+ this.#button = document.createElement("button");
175
+ this.#button.className = "keyframes-slider-play";
176
+ this.#button.addEventListener("click", () => this.#toggle());
177
+ return this.#button;
178
+ }
179
+
180
+ #label() {
181
+ const text = this.getAttribute("label");
182
+ if (!text) return null;
183
+ const label = document.createElement("span");
184
+ label.className = "keyframes-slider-label";
185
+ label.textContent = text;
186
+ return label;
187
+ }
188
+
189
+ #onKey(e) {
190
+ if (e.key === "Escape") document.activeElement?.blur();
191
+ else if (e.key === " " && this.timeline && e.target === this.#input) {
192
+ e.preventDefault();
193
+ this.#toggle();
194
+ }
195
+ }
196
+
197
+ #set(value) {
198
+ if (Number.isFinite(value)) this.#input.value = value;
199
+ this.#time = this.#input.valueAsNumber;
200
+ this.#apply();
201
+ }
202
+
203
+ #apply() {
204
+ const value = this.value;
205
+ const { min, max } = this.#input;
206
+ const digits = this.timeline ? 1 : decimals(this.#input.step);
207
+ const unit = this.getAttribute("unit") ?? (this.timeline ? " s" : "");
208
+ this.#output.textContent = `${value.toFixed(digits)}${unit}`;
209
+ this.#input.style.setProperty("--keyframes-slider-fill", `${(100 * (value - min)) / (max - min)}%`);
210
+ if (this.timeline) this.#target.seek(value);
211
+ else this.#target.setParam(this.getAttribute("param"), value);
212
+ }
213
+
214
+ #toggle() {
215
+ if (!this.#playing && this.#time >= Number(this.#input.max)) this.#set(0);
216
+ this.#playing = !this.#playing;
217
+ this.#sync();
218
+ }
219
+
220
+ #sync() {
221
+ if (this.#button) this.#button.innerHTML = this.#playing ? PAUSE_ICON : PLAY_ICON;
222
+ this.#button?.setAttribute("aria-label", this.#playing ? "Pause" : "Play");
223
+ const run = this.#live && this.#playing;
224
+ if (run && !this.#raf) {
225
+ this.#last = performance.now();
226
+ this.#raf = requestAnimationFrame(this.#tick);
227
+ } else if (!run && this.#raf) {
228
+ cancelAnimationFrame(this.#raf);
229
+ this.#raf = 0;
230
+ }
231
+ }
232
+
233
+ #tick = (now) => {
234
+ this.#raf = requestAnimationFrame(this.#tick);
235
+ const dt = Math.min((now - this.#last) / 1000, 0.1);
236
+ this.#last = now;
237
+ if (!this.#scrubbing) this.#advance(dt);
238
+ };
239
+
240
+ #advance(dt) {
241
+ const duration = Number(this.#input.max);
242
+ let t = this.#time + dt;
243
+ if (t >= duration && this.hasAttribute("loop")) t -= duration;
244
+ else if (t >= duration) {
245
+ t = duration;
246
+ this.#playing = false;
247
+ this.#sync();
248
+ }
249
+ this.#input.value = t;
250
+ this.#time = t;
251
+ this.#apply();
252
+ }
253
+ }
254
+
255
+ customElements.define("keyframes-slider", SwSlider);
@@ -0,0 +1,224 @@
1
+ import { SwComponent, onResize } from "../runtime/component.js";
2
+
3
+ const MAX_PIXEL_RATIO = 3;
4
+ const SNAPSHOT_TIME = 2;
5
+
6
+ const style = document.createElement("style");
7
+ style.textContent = `
8
+ keyframes-three { display: block; position: relative; width: 100%; height: 100%; }
9
+ keyframes-three > canvas, keyframes-three > img { display: block; width: 100%; height: 100%; }
10
+ `;
11
+ document.head.append(style);
12
+
13
+ function parseVector(value, fallback) {
14
+ const parts = (value || "").split(",").map(Number);
15
+ return parts.length === 3 && parts.every(Number.isFinite) ? parts : fallback;
16
+ }
17
+
18
+ function disposeScene(scene) {
19
+ scene.traverse((obj) => {
20
+ obj.geometry?.dispose();
21
+ for (const material of [obj.material].flat().filter(Boolean)) {
22
+ for (const value of Object.values(material)) value?.isTexture && value.dispose();
23
+ material.dispose();
24
+ }
25
+ });
26
+ for (const texture of [scene.background, scene.environment]) texture?.isTexture && texture.dispose();
27
+ }
28
+
29
+ class SwThree extends SwComponent {
30
+ #three = null;
31
+ #hooks = null;
32
+ #cleanup = null;
33
+ #unsubscribe = [];
34
+ #raf = 0;
35
+ #last = 0;
36
+ #time = 0;
37
+ #driven = false;
38
+ #step = 0;
39
+
40
+ async init() {
41
+ const THREE = await import("three");
42
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
43
+ const scene = new THREE.Scene();
44
+ const camera = new THREE.PerspectiveCamera(Number(this.getAttribute("fov")) || 45, 1, 0.01, 1000);
45
+ const target = new THREE.Vector3(...parseVector(this.getAttribute("target"), [0, 0, 0]));
46
+ camera.position.set(...parseVector(this.getAttribute("camera"), [4, 3, 6]));
47
+ camera.lookAt(target);
48
+ this.#applyBackground(THREE, renderer, scene);
49
+ this.replaceChildren(renderer.domElement);
50
+ this.#three = { THREE, renderer, scene, camera, controls: null };
51
+ this.#hooks = { frame: [], step: [], resize: [], param: [] };
52
+
53
+ try {
54
+ this.#three.controls = await this.#createControls(camera, renderer.domElement, target);
55
+ this.#watchSize();
56
+ const module = await import(this.#moduleUrl());
57
+ const cleanup = await module.default(this.#context());
58
+ this.#cleanup = typeof cleanup === "function" ? cleanup : null;
59
+ this.#emit("resize", this.clientWidth, this.clientHeight);
60
+ this.#emit("step", this.#step);
61
+ for (const [name, value] of Object.entries(this.params)) this.#emitParam(name, value);
62
+ this.#render();
63
+ } catch (e) {
64
+ this.destroy();
65
+ throw e;
66
+ }
67
+ }
68
+
69
+ play() {
70
+ if (this.#raf) return;
71
+ this.#last = performance.now();
72
+ const loop = (now) => {
73
+ this.#raf = requestAnimationFrame(loop);
74
+ this.#tick(Math.min((now - this.#last) / 1000, 0.1));
75
+ this.#last = now;
76
+ this.#render();
77
+ };
78
+ this.#raf = requestAnimationFrame(loop);
79
+ }
80
+
81
+ pause() {
82
+ cancelAnimationFrame(this.#raf);
83
+ this.#raf = 0;
84
+ }
85
+
86
+ step(n) {
87
+ this.#step = n;
88
+ if (!this.#hooks) return;
89
+ this.#emit("step", n);
90
+ if (!this.#raf) this.#render();
91
+ }
92
+
93
+ paramChanged(name, value) {
94
+ if (!this.#hooks) return;
95
+ this.#emitParam(name, value);
96
+ if (!this.#raf) this.#render();
97
+ }
98
+
99
+ seek(t) {
100
+ this.#driven = true;
101
+ this.#time = t;
102
+ }
103
+
104
+ async snapshot() {
105
+ if (!this.#three) return;
106
+ this.pause();
107
+ this.#settle();
108
+ this.#render();
109
+ const img = new Image();
110
+ img.src = this.#three.renderer.domElement.toDataURL("image/png");
111
+ await img.decode();
112
+ this.replaceChildren(img);
113
+ }
114
+
115
+ destroy() {
116
+ this.pause();
117
+ for (const off of this.#unsubscribe.splice(0)) off();
118
+ const three = this.#three;
119
+ const cleanup = this.#cleanup;
120
+ this.#three = this.#hooks = this.#cleanup = null;
121
+ this.#time = 0;
122
+ if (!three) return;
123
+ try {
124
+ cleanup?.();
125
+ } finally {
126
+ three.controls?.dispose();
127
+ disposeScene(three.scene);
128
+ three.renderer.dispose();
129
+ three.renderer.forceContextLoss();
130
+ this.replaceChildren();
131
+ }
132
+ }
133
+
134
+ #moduleUrl() {
135
+ const url = new URL(this.resolveUrl(this.getAttribute("src")));
136
+ url.searchParams.set("v", Date.now());
137
+ return url.href;
138
+ }
139
+
140
+ #applyBackground(THREE, renderer, scene) {
141
+ const background = this.getAttribute("background") || "transparent";
142
+ if (background === "transparent") renderer.setClearColor(0x000000, 0);
143
+ else scene.background = new THREE.Color(background);
144
+ }
145
+
146
+ async #createControls(camera, canvas, target) {
147
+ if ((this.getAttribute("controls") || "orbit") !== "orbit") return null;
148
+ const { OrbitControls } = await import("three/addons/controls/OrbitControls.js");
149
+ const controls = new OrbitControls(camera, canvas);
150
+ controls.target.copy(target);
151
+ controls.enableDamping = true;
152
+ controls.update();
153
+ return controls;
154
+ }
155
+
156
+ #watchSize() {
157
+ const resize = (w, h) => {
158
+ if (!this.#three || !w || !h) return;
159
+ const { renderer, camera } = this.#three;
160
+ renderer.setPixelRatio(Math.min(this.pixelRatio, MAX_PIXEL_RATIO));
161
+ renderer.setSize(w, h, false);
162
+ camera.aspect = w / h;
163
+ camera.updateProjectionMatrix();
164
+ this.#emit("resize", w, h);
165
+ if (!this.#raf) this.#render();
166
+ };
167
+ const onScale = () => resize(this.clientWidth, this.clientHeight);
168
+ window.keyframes?.addEventListener("scale", onScale);
169
+ this.#unsubscribe.push(
170
+ onResize(this, resize),
171
+ () => window.keyframes?.removeEventListener("scale", onScale),
172
+ );
173
+ onScale();
174
+ }
175
+
176
+ #context() {
177
+ const { THREE, scene, camera, renderer, controls } = this.#three;
178
+ const hooks = this.#hooks;
179
+ return {
180
+ THREE,
181
+ scene,
182
+ camera,
183
+ renderer,
184
+ controls,
185
+ container: this,
186
+ onFrame: (cb) => hooks.frame.push(cb),
187
+ onStep: (cb) => hooks.step.push(cb),
188
+ onResize: (cb) => hooks.resize.push(cb),
189
+ params: this.params,
190
+ onParam: (name, cb) => hooks.param.push([name, cb]),
191
+ addons: (path) => import(`three/addons/${path}`),
192
+ };
193
+ }
194
+
195
+ #tick(dt) {
196
+ if (!this.#driven) this.#time += dt;
197
+ this.#emit("frame", this.#time, dt);
198
+ }
199
+
200
+ #settle() {
201
+ if (!this.#driven) {
202
+ while (this.#time < SNAPSHOT_TIME) this.#tick(1 / 60);
203
+ return;
204
+ }
205
+ for (let i = 0; i < SNAPSHOT_TIME * 60; i++) this.#tick(1 / 60);
206
+ }
207
+
208
+ #emitParam(name, value) {
209
+ for (const [key, cb] of this.#hooks.param) if (key === name) cb(value);
210
+ }
211
+
212
+ #emit(name, ...args) {
213
+ for (const cb of this.#hooks?.[name] ?? []) cb(...args);
214
+ }
215
+
216
+ #render() {
217
+ if (!this.#three) return;
218
+ const { renderer, scene, camera, controls } = this.#three;
219
+ controls?.update();
220
+ renderer.render(scene, camera);
221
+ }
222
+ }
223
+
224
+ customElements.define("keyframes-three", SwThree);
@@ -0,0 +1,228 @@
1
+ .keyframes-editing .keyframes-slide.is-current [data-block] {
2
+ cursor: move;
3
+ outline: 1px dashed rgba(47, 111, 237, 0.35);
4
+ outline-offset: -1px;
5
+ }
6
+
7
+ .keyframes-editing .keyframes-slide.is-current [data-block]:hover {
8
+ outline: 1px solid rgba(47, 111, 237, 0.8);
9
+ }
10
+
11
+ .keyframes-editing [data-block]:not(.keyframes-ed-live) * {
12
+ pointer-events: none !important;
13
+ }
14
+
15
+ .keyframes-editing .keyframes-ed-live {
16
+ outline: 2px solid #f2994a !important;
17
+ cursor: auto !important;
18
+ }
19
+
20
+ .keyframes-editing .keyframes-step-hidden {
21
+ opacity: 0.3 !important;
22
+ visibility: visible;
23
+ }
24
+
25
+ .keyframes-ed-overlay {
26
+ position: absolute;
27
+ inset: 0;
28
+ z-index: 100000;
29
+ pointer-events: none;
30
+ }
31
+
32
+ .keyframes-ed-box {
33
+ position: absolute;
34
+ outline: 2px solid #2f6fed;
35
+ outline-offset: 0;
36
+ }
37
+
38
+ .keyframes-ed-handle {
39
+ position: absolute;
40
+ width: 14px;
41
+ height: 14px;
42
+ margin: -7px 0 0 -7px;
43
+ background: #fff;
44
+ border: 2px solid #2f6fed;
45
+ border-radius: 3px;
46
+ box-sizing: border-box;
47
+ pointer-events: auto;
48
+ scale: calc(1 / var(--keyframes-scale, 1));
49
+ }
50
+
51
+ .keyframes-ed-nw { left: 0; top: 0; cursor: nwse-resize; }
52
+ .keyframes-ed-n { left: 50%; top: 0; cursor: ns-resize; }
53
+ .keyframes-ed-ne { left: 100%; top: 0; cursor: nesw-resize; }
54
+ .keyframes-ed-e { left: 100%; top: 50%; cursor: ew-resize; }
55
+ .keyframes-ed-se { left: 100%; top: 100%; cursor: nwse-resize; }
56
+ .keyframes-ed-s { left: 50%; top: 100%; cursor: ns-resize; }
57
+ .keyframes-ed-sw { left: 0; top: 100%; cursor: nesw-resize; }
58
+ .keyframes-ed-w { left: 0; top: 50%; cursor: ew-resize; }
59
+
60
+ .keyframes-ed-guide {
61
+ position: absolute;
62
+ background: #ff3d81;
63
+ }
64
+
65
+ .keyframes-ed-vguide {
66
+ top: 0;
67
+ bottom: 0;
68
+ width: calc(1px / var(--keyframes-scale, 1));
69
+ }
70
+
71
+ .keyframes-ed-hguide {
72
+ left: 0;
73
+ right: 0;
74
+ height: calc(1px / var(--keyframes-scale, 1));
75
+ }
76
+
77
+ .keyframes-ed-marquee {
78
+ display: none;
79
+ position: absolute;
80
+ background: rgba(47, 111, 237, 0.08);
81
+ border: 1px solid rgba(47, 111, 237, 0.6);
82
+ }
83
+
84
+ .keyframes-ed-label {
85
+ display: none;
86
+ position: fixed;
87
+ z-index: 100001;
88
+ padding: 2px 6px;
89
+ background: #2f6fed;
90
+ border-radius: 4px;
91
+ pointer-events: none;
92
+ font-variant-numeric: tabular-nums;
93
+ }
94
+
95
+ .keyframes-ed-toolbar,
96
+ .keyframes-ed-help,
97
+ .keyframes-ed-toast,
98
+ .keyframes-ed-label {
99
+ font: 13px/1.3 system-ui, -apple-system, sans-serif;
100
+ color: #e9ecf2;
101
+ }
102
+
103
+ .keyframes-ed-toolbar {
104
+ position: fixed;
105
+ top: 10px;
106
+ left: calc((100vw - var(--keyframes-panel-space, 0px)) / 2);
107
+ translate: -50% 0;
108
+ z-index: 100001;
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 10px;
112
+ padding: 6px 10px;
113
+ background: rgba(24, 26, 32, 0.92);
114
+ border-radius: 10px;
115
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
116
+ backdrop-filter: blur(8px);
117
+ white-space: nowrap;
118
+ }
119
+
120
+ .keyframes-ed-toolbar[hidden],
121
+ .keyframes-ed-help[hidden] {
122
+ display: none;
123
+ }
124
+
125
+ .keyframes-ed-badge {
126
+ font-weight: 700;
127
+ font-size: 11px;
128
+ letter-spacing: 0.08em;
129
+ color: #fff;
130
+ background: #2f6fed;
131
+ padding: 2px 6px;
132
+ border-radius: 4px;
133
+ }
134
+
135
+ .keyframes-ed-slide {
136
+ color: #9aa3b2;
137
+ }
138
+
139
+ .keyframes-ed-block {
140
+ font-family: ui-monospace, Menlo, monospace;
141
+ min-width: 80px;
142
+ }
143
+
144
+ .keyframes-ed-toolbar label {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 3px;
148
+ color: #9aa3b2;
149
+ }
150
+
151
+ .keyframes-ed-toolbar input {
152
+ width: 52px;
153
+ padding: 3px 4px;
154
+ color: inherit;
155
+ background: #2a2d36;
156
+ border: 1px solid #3a3e4a;
157
+ border-radius: 5px;
158
+ font: inherit;
159
+ font-variant-numeric: tabular-nums;
160
+ }
161
+
162
+ .keyframes-ed-toolbar input:disabled {
163
+ opacity: 0.4;
164
+ }
165
+
166
+ .keyframes-ed-toolbar button {
167
+ min-width: 26px;
168
+ padding: 3px 7px;
169
+ color: inherit;
170
+ background: #2a2d36;
171
+ border: 1px solid #3a3e4a;
172
+ border-radius: 5px;
173
+ cursor: pointer;
174
+ font: inherit;
175
+ }
176
+
177
+ .keyframes-ed-toolbar button:hover {
178
+ background: #3a3e4a;
179
+ }
180
+
181
+ .keyframes-ed-status {
182
+ color: #7fd19b;
183
+ min-width: 70px;
184
+ }
185
+
186
+ .keyframes-ed-status.keyframes-ed-error {
187
+ color: #ff7a7a;
188
+ }
189
+
190
+ .keyframes-ed-help {
191
+ position: fixed;
192
+ top: 56px;
193
+ left: calc((100vw - var(--keyframes-panel-space, 0px)) / 2);
194
+ translate: -50% 0;
195
+ z-index: 100001;
196
+ padding: 14px 18px;
197
+ background: rgba(24, 26, 32, 0.95);
198
+ border-radius: 10px;
199
+ }
200
+
201
+ .keyframes-ed-help td {
202
+ padding: 3px 10px 3px 0;
203
+ }
204
+
205
+ .keyframes-ed-help td:first-child,
206
+ .keyframes-ed-help kbd {
207
+ font-family: ui-monospace, Menlo, monospace;
208
+ color: #fff;
209
+ }
210
+
211
+ .keyframes-ed-toast {
212
+ position: fixed;
213
+ bottom: 20px;
214
+ left: calc((100vw - var(--keyframes-panel-space, 0px)) / 2);
215
+ translate: -50% 20px;
216
+ z-index: 100001;
217
+ padding: 8px 14px;
218
+ background: rgba(24, 26, 32, 0.92);
219
+ border-radius: 8px;
220
+ opacity: 0;
221
+ transition: opacity 0.2s, translate 0.2s;
222
+ pointer-events: none;
223
+ }
224
+
225
+ .keyframes-ed-toast.keyframes-ed-show {
226
+ opacity: 1;
227
+ translate: -50% 0;
228
+ }