Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cd4308b
chore(producer): shim __filename/__dirname in the CJS banner
miguel-heygen Jun 18, 2026
31fcadf
chore(producer): use a template literal for the CJS banner (review nit)
miguel-heygen Jun 20, 2026
7d97fa6
feat(core): add GSAP keyframe + motion-path source mutations
miguel-heygen Jun 18, 2026
fd0a505
fix(core): address #1554 review — data-exclusion test, split-fix doc,…
miguel-heygen Jun 20, 2026
663955d
feat(core): route motion-path mutations through studio-api + fix clip…
miguel-heygen Jun 18, 2026
343dbcf
feat(core): strip legacy path-offset/rotation + drop obsolete studio …
miguel-heygen Jun 19, 2026
1612463
fix(core): address #1555 review — complete hold-sync, invalidate clip…
miguel-heygen Jun 20, 2026
105c865
feat(studio): GSAP runtime read layer + shared helpers
miguel-heygen Jun 20, 2026
e28b305
fix(studio): address #1607 review — cold-parse vs fetch-error budgets…
miguel-heygen Jun 20, 2026
0631e78
feat(studio): GSAP drag/commit/bridge editing infra
miguel-heygen Jun 20, 2026
c66e82a
fix(studio): address #1608 review — facade awaits commit, strict stal…
miguel-heygen Jun 20, 2026
a72c70e
feat(studio): motion-path geometry + commit helpers
miguel-heygen Jun 20, 2026
395c6f7
docs(studio): address #1609 review — document occlusion fade-in invar…
miguel-heygen Jun 20, 2026
6953ebb
feat(studio): on-canvas motion-path overlay
miguel-heygen Jun 20, 2026
0392a98
fix(studio): address #1610 review — scope dblclick to pan-surface, ki…
miguel-heygen Jun 20, 2026
5ab1ce7
feat(studio): keyframes flag, gesture recording + timeline/selection …
miguel-heygen Jun 20, 2026
151f36b
fix(studio): address #1611 review — fetch-first keyframe path, gated …
miguel-heygen Jun 20, 2026
2759947
feat(studio): single-source manual offset + rotation via the GSAP tim…
miguel-heygen Jun 19, 2026
5ef68d0
fix(studio): address #1567 review — drop drag-intercept flag, harden …
miguel-heygen Jun 20, 2026
03d7927
feat(studio): patchRuntimeTweenInPlace — update a tween's values in p…
miguel-heygen Jun 19, 2026
2a0df60
fix(studio): address #1612 review — channel-aware set resolution + de…
miguel-heygen Jun 20, 2026
a339c96
feat(studio): instantPatch fast path in runCommit
miguel-heygen Jun 19, 2026
57b49d8
feat(studio): route static position/rotation set drags through instan…
miguel-heygen Jun 19, 2026
ff2be9d
fix(studio): address #1613 review — derive instantPatch from the muta…
miguel-heygen Jun 20, 2026
77b1415
feat(studio): no full iframe remount for soft-reloadable edits
miguel-heygen Jun 19, 2026
987bf98
feat(studio): pre-load MotionPathPlugin so motion-path edits don't as…
miguel-heygen Jun 19, 2026
1cec120
fix(studio): don't re-save + reload when source editor syncs externally
miguel-heygen Jun 20, 2026
56d76a9
fix(core): inject MotionPathPlugin into preview when a composition us…
miguel-heygen Jun 20, 2026
cbf506b
fix(studio): dedup __hfMotionPathPluginLoading type decl (restack art…
miguel-heygen Jun 20, 2026
ca5d144
fix(studio): address #1605 review — distinguish soft-reload failure m…
miguel-heygen Jun 20, 2026
c7929e4
fix(core): align __clipTree and __clipManifest ids via stableClipId
miguel-heygen Jun 21, 2026
001a9e6
fix(core): strip baked runtime + tag comp root in preview assembly
miguel-heygen Jun 21, 2026
ce3df89
feat(studio): set motion-path destination from a toolbar toggle
miguel-heygen Jun 21, 2026
1ba2c42
fix(studio): center timeline keyframe diamonds on their percentage
miguel-heygen Jun 21, 2026
b551c03
fix(studio): resize static elements via tl.set, not a single-stop key…
miguel-heygen Jun 21, 2026
fbbc87a
fix(studio): negative-cache failed media probes
miguel-heygen Jun 21, 2026
55304b3
fix(studio): type window.setTimeout handle as number
miguel-heygen Jun 21, 2026
859d4f8
fix(studio): drag/resize disappearance, stale-ID duplicates, soft-rel…
miguel-heygen Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,6 @@ hyperframes-bench/
tmp/
# Studio-generated preview thumbnails
.thumbnails/

# Local editor settings
.zed/
100 changes: 0 additions & 100 deletions packages/core/src/lint/rules/gsap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -978,106 +978,6 @@ describe("GSAP rules", () => {
expect(finding).toBeUndefined();
});

// gsap_studio_edit_blocked
it("warns when script registers timeline AND has GSAP tweens targeting #id selectors", async () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="headline" style="position:absolute;left:120px;top:200px;">Hello</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.set("#headline", { opacity: 0 });
tl.to("#headline", { opacity: 1, duration: 0.5 }, 0);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_studio_edit_blocked");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
expect(finding?.message).toContain('"#headline"');
});

it("warns when script registers timeline AND has GSAP tweens targeting .class selectors", async () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div class="box" style="position:absolute;left:120px;top:200px;"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from(".box", { y: 80, opacity: 0, duration: 0.4 }, 0);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_studio_edit_blocked");
expect(finding).toBeDefined();
expect(finding?.message).toContain('".box"');
});

it("does NOT warn when timeline is registered but no GSAP element selectors are called", async () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080"></div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_studio_edit_blocked");
expect(finding).toBeUndefined();
});

it("does NOT warn when script has GSAP calls but does not register on window.__timelines", async () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="box"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.to("#box", { x: 100, duration: 1 }, 0);
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_studio_edit_blocked");
expect(finding).toBeUndefined();
});

it("lists all unique targeted selectors in the warning message", async () => {
const html = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080">
<div id="title"></div>
<div id="sub"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from("#title", { opacity: 0, duration: 0.3 }, 0);
tl.from("#sub", { opacity: 0, duration: 0.3 }, 0.2);
window.__timelines["c1"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_studio_edit_blocked");
expect(finding).toBeDefined();
expect(finding?.message).toContain('"#title"');
expect(finding?.message).toContain('"#sub"');
});

it("scene_layer_missing_visibility_kill: fires when multi-scene exit lacks hard kill", async () => {
const html = `
<html><body>
Expand Down
36 changes: 0 additions & 36 deletions packages/core/src/lint/rules/gsap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
truncateSnippet,
stripJsComments,
WINDOW_TIMELINE_ASSIGN_PATTERN,
TIMELINE_REGISTRY_ASSIGN_PATTERN,
} from "../utils";

// ── GSAP-specific types ────────────────────────────────────────────────────
Expand Down Expand Up @@ -855,39 +854,4 @@ export const gsapRules: LintRule<LintContext>[] = [
}
return findings;
},

// gsap_studio_edit_blocked
// When a script both registers a timeline on window.__timelines AND contains
// GSAP mutation calls targeting element selectors, Studio's isElementGsapTargeted
// check returns true for those elements and silently skips saving drag/resize
// position changes back to source HTML.
({ scripts }) => {
const findings: HyperframeLintFinding[] = [];
const GSAP_MUTATION_SELECTOR_RE = /\.\s*(?:set|to|from|fromTo)\s*\(\s*["']([#.][^"']+)["']/g;

for (const script of scripts) {
const content = stripJsComments(script.content);
if (!TIMELINE_REGISTRY_ASSIGN_PATTERN.test(content)) continue;

const targets = new Set<string>();
let match: RegExpExecArray | null;
const re = new RegExp(GSAP_MUTATION_SELECTOR_RE.source, "g");
while ((match = re.exec(content)) !== null) {
if (match[1]) targets.add(match[1]);
}
if (targets.size === 0) continue;

const selList = [...targets].map((s) => `"${s}"`).join(", ");
findings.push({
code: "gsap_studio_edit_blocked",
severity: "warning",
message: `GSAP tweens target ${selList} in a registered timeline. Studio cannot save drag/resize edits to these elements — the runtime skips write-back for any element that appears in a registered window.__timelines timeline.`,
fixHint:
"The hyperframes runtime registers timelines automatically. Do not add a manual window.__timelines script unless GSAP intentionally controls element positions. " +
"For initial visibility states, use CSS (e.g. opacity:0) instead of gsap.set(). " +
"If GSAP must own these elements' positions, avoid drag-editing them in Studio.",
});
}
return findings;
},
];
5 changes: 4 additions & 1 deletion packages/core/src/parsers/gsapConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export function classifyTweenPropertyGroup(
): PropertyGroupName | undefined {
const groups = new Set<PropertyGroupName>();
for (const key of Object.keys(properties)) {
if (key === "transformOrigin") continue;
// transformOrigin is a modifier; `_auto` is Studio's internal endpoint marker;
// `data` is GSAP-reserved (carries the Studio hold-set tag). None is an animated
// property, so none should affect the group.
if (key === "transformOrigin" || key === "_auto" || key === "data") continue;
const g = classifyPropertyGroup(key);
groups.add(g);
}
Expand Down
Loading
Loading