@hyperframes/studio 0.6.83 → 0.6.85
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/dist/assets/{hyperframes-player-WXAuftNy.js → hyperframes-player-DRpY3xHh.js} +1 -1
- package/dist/assets/{index-FFuagZGD.js → index-DtSCUvYQ.js} +34 -34
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/editor/manualEditsTypes.ts +7 -5
- package/src/utils/sourcePatcher.test.ts +79 -8
- package/src/utils/sourcePatcher.ts +48 -41
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DtSCUvYQ.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-DHcptK1_.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.85",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@codemirror/view": "6.40.0",
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"mediabunny": "^1.45.3",
|
|
34
|
-
"@hyperframes/
|
|
35
|
-
"@hyperframes/
|
|
34
|
+
"@hyperframes/core": "0.6.85",
|
|
35
|
+
"@hyperframes/player": "0.6.85"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "19",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vite": "^6.4.2",
|
|
47
47
|
"vitest": "^3.2.4",
|
|
48
48
|
"zustand": "^5.0.0",
|
|
49
|
-
"@hyperframes/producer": "0.6.
|
|
49
|
+
"@hyperframes/producer": "0.6.85"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "19",
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/* ── Public constants ──────────────────────────────────────────────── */
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export {
|
|
3
|
+
STUDIO_OFFSET_X_PROP,
|
|
4
|
+
STUDIO_OFFSET_Y_PROP,
|
|
5
|
+
STUDIO_WIDTH_PROP,
|
|
6
|
+
STUDIO_HEIGHT_PROP,
|
|
7
|
+
STUDIO_MANUAL_EDIT_GESTURE_ATTR,
|
|
8
|
+
} from "@hyperframes/core/studio-api/draft-markers";
|
|
6
9
|
export const STUDIO_ROTATION_PROP = "--hf-studio-rotation";
|
|
7
10
|
|
|
8
11
|
/* ── Internal DOM attribute names ─────────────────────────────────── */
|
|
9
12
|
export const STUDIO_PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
10
|
-
export const STUDIO_MANUAL_EDIT_GESTURE_ATTR = "data-hf-studio-manual-edit-gesture";
|
|
11
13
|
export const STUDIO_BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
12
14
|
export const STUDIO_ROTATION_ATTR = "data-hf-studio-rotation";
|
|
13
15
|
export const STUDIO_ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
@@ -517,17 +517,88 @@ describe("motion attribute round-trip via sourcePatcher", () => {
|
|
|
517
517
|
});
|
|
518
518
|
});
|
|
519
519
|
|
|
520
|
-
// T3 — id-based targeting (
|
|
521
|
-
// R1 adds `hfId?: string` to PatchTarget and a `[data-hf-id="…"]` lookup branch
|
|
522
|
-
// in findTagByTarget. Convert from it.todo to real assertions in the R1 PR.
|
|
520
|
+
// T3 — id-based targeting (R1).
|
|
523
521
|
describe("T3 — hfId targeting (spec for R1)", () => {
|
|
524
|
-
it
|
|
522
|
+
it("updates inline style by data-hf-id", () => {
|
|
523
|
+
const html = `<h1 data-hf-id="hf-x7k2" style="color: red">Hello</h1>`;
|
|
524
|
+
const result = applyPatchByTarget(
|
|
525
|
+
html,
|
|
526
|
+
{ hfId: "hf-x7k2" },
|
|
527
|
+
{
|
|
528
|
+
type: "inline-style",
|
|
529
|
+
property: "color",
|
|
530
|
+
value: "blue",
|
|
531
|
+
},
|
|
532
|
+
);
|
|
533
|
+
expect(result).toContain("color: blue");
|
|
534
|
+
expect(result).toContain('data-hf-id="hf-x7k2"');
|
|
535
|
+
});
|
|
525
536
|
|
|
526
|
-
it
|
|
537
|
+
it("updates text content by data-hf-id", () => {
|
|
538
|
+
const html = `<p data-hf-id="hf-a1b2">Old text</p>`;
|
|
539
|
+
const result = applyPatchByTarget(
|
|
540
|
+
html,
|
|
541
|
+
{ hfId: "hf-a1b2" },
|
|
542
|
+
{
|
|
543
|
+
type: "text-content",
|
|
544
|
+
property: "",
|
|
545
|
+
value: "New text",
|
|
546
|
+
},
|
|
547
|
+
);
|
|
548
|
+
expect(result).toContain(">New text<");
|
|
549
|
+
});
|
|
527
550
|
|
|
528
|
-
it
|
|
551
|
+
it("updates attribute by data-hf-id", () => {
|
|
552
|
+
const html = `<div data-hf-id="hf-c3d4" data-start="0"></div>`;
|
|
553
|
+
const result = applyPatchByTarget(
|
|
554
|
+
html,
|
|
555
|
+
{ hfId: "hf-c3d4" },
|
|
556
|
+
{
|
|
557
|
+
type: "attribute",
|
|
558
|
+
property: "start",
|
|
559
|
+
value: "2.5",
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
expect(result).toContain('data-start="2.5"');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("data-hf-id attribute is preserved after a style patch", () => {
|
|
566
|
+
const html = `<h1 data-hf-id="hf-x7k2" style="color: red">Hello</h1>`;
|
|
567
|
+
const patched = applyPatchByTarget(
|
|
568
|
+
html,
|
|
569
|
+
{ hfId: "hf-x7k2" },
|
|
570
|
+
{
|
|
571
|
+
type: "inline-style",
|
|
572
|
+
property: "color",
|
|
573
|
+
value: "blue",
|
|
574
|
+
},
|
|
575
|
+
);
|
|
576
|
+
expect(readAttributeByTarget(patched, { hfId: "hf-x7k2" }, "data-hf-id")).toBe("hf-x7k2");
|
|
577
|
+
});
|
|
529
578
|
|
|
530
|
-
it
|
|
579
|
+
it("hfId lookup falls through to selector when hfId not found", () => {
|
|
580
|
+
const html = `<h1 class="headline" style="color: red">Hello</h1>`;
|
|
581
|
+
const result = applyPatchByTarget(
|
|
582
|
+
html,
|
|
583
|
+
{ hfId: "hf-missing", selector: ".headline" },
|
|
584
|
+
{ type: "inline-style", property: "color", value: "blue" },
|
|
585
|
+
);
|
|
586
|
+
expect(result).toContain("color: blue");
|
|
587
|
+
});
|
|
531
588
|
|
|
532
|
-
it
|
|
589
|
+
it("hfId match is authoritative — selector is not used as a narrowing filter", () => {
|
|
590
|
+
// hfId matches h1; selector points at h2. hfId wins — patch lands on h1, h2 untouched.
|
|
591
|
+
const html = `<h1 data-hf-id="hf-x7k2" class="a">A</h1><h2 class="b">B</h2>`;
|
|
592
|
+
const result = applyPatchByTarget(
|
|
593
|
+
html,
|
|
594
|
+
{ hfId: "hf-x7k2", selector: ".b" },
|
|
595
|
+
{ type: "inline-style", property: "color", value: "blue" },
|
|
596
|
+
);
|
|
597
|
+
expect(result).toContain('data-hf-id="hf-x7k2"');
|
|
598
|
+
const h1End = result.indexOf("</h1>");
|
|
599
|
+
const bluePos = result.indexOf("color: blue");
|
|
600
|
+
expect(bluePos).toBeGreaterThan(-1);
|
|
601
|
+
expect(bluePos).toBeLessThan(h1End);
|
|
602
|
+
expect(result).toContain('<h2 class="b">B</h2>');
|
|
603
|
+
});
|
|
533
604
|
});
|
|
@@ -94,6 +94,7 @@ export interface PatchOperation {
|
|
|
94
94
|
|
|
95
95
|
export interface PatchTarget {
|
|
96
96
|
id?: string | null;
|
|
97
|
+
hfId?: string;
|
|
97
98
|
selector?: string;
|
|
98
99
|
selectorIndex?: number;
|
|
99
100
|
}
|
|
@@ -232,61 +233,67 @@ function replaceTagAtMatch(html: string, match: TagMatch, newTag: string): strin
|
|
|
232
233
|
return `${html.slice(0, match.start)}${newTag}${html.slice(match.end)}`;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
function execDataAttrPattern(html: string, attr: string, value: string): TagMatch | null {
|
|
237
|
+
const pattern = new RegExp(`(<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\2[^>]*)>`, "i");
|
|
238
|
+
const match = pattern.exec(html);
|
|
239
|
+
if (match?.index == null) return null;
|
|
240
|
+
// Defensive: a second exact match means a duplicate id/attr in the source
|
|
241
|
+
// (id drift). Don't silently patch the first while leaving the other stale —
|
|
242
|
+
// surface it. By the mint contract this should never fire.
|
|
243
|
+
const all = html.match(new RegExp(`<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\1[^>]*>`, "gi"));
|
|
244
|
+
if (all && all.length > 1) {
|
|
245
|
+
// eslint-disable-next-line no-console
|
|
246
|
+
console.warn(
|
|
247
|
+
`sourcePatcher: ${attr}="${value}" matched ${all.length} elements; patching the first. ids/attrs must be unique per document.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return { tag: match[1], start: match.index, end: match.index + match[1].length };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function findTagByClass(html: string, target: PatchTarget): TagMatch | null {
|
|
254
|
+
const classMatch = target.selector?.match(/^\.([a-zA-Z0-9_-]+)$/);
|
|
255
|
+
if (!classMatch) return null;
|
|
256
|
+
const cls = classMatch[1];
|
|
257
|
+
const pattern = new RegExp(
|
|
258
|
+
`(<[^>]*\\bclass=(["'])[^"']*\\b${escapeRegex(cls)}\\b[^"']*\\2[^>]*)>`,
|
|
259
|
+
"gi",
|
|
260
|
+
);
|
|
261
|
+
const selectorIndex = target.selectorIndex ?? 0;
|
|
262
|
+
let match: RegExpExecArray | null;
|
|
263
|
+
let currentIndex = 0;
|
|
264
|
+
while ((match = pattern.exec(html)) !== null) {
|
|
265
|
+
if (currentIndex === selectorIndex && match.index != null) {
|
|
240
266
|
return {
|
|
241
267
|
tag: match[1],
|
|
242
268
|
start: match.index,
|
|
243
269
|
end: match.index + match[1].length,
|
|
244
270
|
};
|
|
245
271
|
}
|
|
272
|
+
currentIndex += 1;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function findTagByTarget(html: string, target: PatchTarget): TagMatch | null {
|
|
278
|
+
if (target.hfId) {
|
|
279
|
+
const result = execDataAttrPattern(html, "data-hf-id", target.hfId);
|
|
280
|
+
if (result) return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (target.id) {
|
|
284
|
+
const result = execDataAttrPattern(html, "id", target.id);
|
|
285
|
+
if (result) return result;
|
|
246
286
|
}
|
|
247
287
|
|
|
248
288
|
if (!target.selector) return null;
|
|
249
289
|
|
|
250
290
|
const compositionIdMatch = target.selector.match(/^\[data-composition-id="([^"]+)"\]$/);
|
|
251
291
|
if (compositionIdMatch) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
`(<[^>]*\\bdata-composition-id=(["'])${escapeRegex(compId)}\\2[^>]*)>`,
|
|
255
|
-
"i",
|
|
256
|
-
);
|
|
257
|
-
const match = pattern.exec(html);
|
|
258
|
-
if (match?.index != null) {
|
|
259
|
-
return {
|
|
260
|
-
tag: match[1],
|
|
261
|
-
start: match.index,
|
|
262
|
-
end: match.index + match[1].length,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
292
|
+
const result = execDataAttrPattern(html, "data-composition-id", compositionIdMatch[1]);
|
|
293
|
+
if (result) return result;
|
|
265
294
|
}
|
|
266
295
|
|
|
267
|
-
|
|
268
|
-
if (classMatch) {
|
|
269
|
-
const cls = classMatch[1];
|
|
270
|
-
const pattern = new RegExp(
|
|
271
|
-
`(<[^>]*\\bclass=(["'])[^"']*\\b${escapeRegex(cls)}\\b[^"']*\\2[^>]*)>`,
|
|
272
|
-
"gi",
|
|
273
|
-
);
|
|
274
|
-
const selectorIndex = target.selectorIndex ?? 0;
|
|
275
|
-
let match: RegExpExecArray | null;
|
|
276
|
-
let currentIndex = 0;
|
|
277
|
-
while ((match = pattern.exec(html)) !== null) {
|
|
278
|
-
if (currentIndex === selectorIndex && match.index != null) {
|
|
279
|
-
return {
|
|
280
|
-
tag: match[1],
|
|
281
|
-
start: match.index,
|
|
282
|
-
end: match.index + match[1].length,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
currentIndex += 1;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return null;
|
|
296
|
+
return findTagByClass(html, target);
|
|
290
297
|
}
|
|
291
298
|
|
|
292
299
|
export function readAttributeByTarget(
|