@hyperframes/studio 0.5.0-alpha.14 → 0.5.0-alpha.15

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 (33) hide show
  1. package/dist/assets/{hyperframes-player-vibA20NC.js → hyperframes-player-Cd8vYWxP.js} +2 -2
  2. package/dist/assets/index-DFLVGWTx.js +106 -0
  3. package/dist/assets/index-mXJ-UH9F.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +785 -377
  7. package/src/captions/generator.test.ts +19 -0
  8. package/src/captions/generator.ts +9 -2
  9. package/src/captions/hooks/useCaptionSync.ts +6 -1
  10. package/src/captions/parser.test.ts +14 -0
  11. package/src/captions/parser.ts +1 -0
  12. package/src/components/editor/DomEditOverlay.test.ts +241 -0
  13. package/src/components/editor/DomEditOverlay.tsx +970 -115
  14. package/src/components/editor/PropertyPanel.tsx +91 -83
  15. package/src/components/editor/domEditing.test.ts +161 -29
  16. package/src/components/editor/domEditing.ts +84 -113
  17. package/src/components/editor/manualEdits.test.ts +945 -0
  18. package/src/components/editor/manualEdits.ts +1397 -0
  19. package/src/components/editor/manualOffsetDrag.test.ts +140 -0
  20. package/src/components/editor/manualOffsetDrag.ts +307 -0
  21. package/src/components/renders/RenderQueue.tsx +10 -3
  22. package/src/hooks/usePersistentEditHistory.test.ts +1 -0
  23. package/src/hooks/usePersistentEditHistory.ts +3 -2
  24. package/src/player/components/CompositionThumbnail.test.ts +1 -1
  25. package/src/player/components/CompositionThumbnail.tsx +1 -1
  26. package/src/player/components/Player.tsx +54 -9
  27. package/src/player/hooks/useTimelinePlayer.test.ts +1 -0
  28. package/src/utils/clipboard.test.ts +1 -0
  29. package/src/utils/frameCapture.ts +3 -1
  30. package/src/utils/projectRouting.test.ts +87 -0
  31. package/src/utils/projectRouting.ts +27 -0
  32. package/dist/assets/index-JhhmFie-.js +0 -105
  33. package/dist/assets/index-KioPDrX6.css +0 -1
@@ -35,9 +35,13 @@ const CURATED_STYLE_PROPERTIES = [
35
35
  export interface DomEditCapabilities {
36
36
  canSelect: boolean;
37
37
  canEditStyles: boolean;
38
+ /** Directly editable authored left/top style fields. Canvas drag uses manual edits instead. */
38
39
  canMove: boolean;
40
+ /** Directly editable authored width/height style fields. Canvas resize uses manual edits instead. */
39
41
  canResize: boolean;
40
- canDetachFromLayout: boolean;
42
+ canApplyManualOffset: boolean;
43
+ canApplyManualSize: boolean;
44
+ canApplyManualRotation: boolean;
41
45
  reasonIfDisabled?: string;
42
46
  }
43
47
 
@@ -119,54 +123,6 @@ function isIdentityTransform(value: string | undefined): boolean {
119
123
  return values.every((part, index) => Math.abs(part - identity[index]) < 0.0001);
120
124
  }
121
125
 
122
- function isClipClassName(className: string | undefined): boolean {
123
- return Boolean(className?.split(/\s+/).includes("clip"));
124
- }
125
-
126
- function isInlineTextTag(tagName: string | undefined): boolean {
127
- return Boolean(
128
- tagName &&
129
- ["a", "b", "em", "i", "small", "span", "strong", "sub", "sup"].includes(tagName.toLowerCase()),
130
- );
131
- }
132
-
133
- function isBlockishTag(tagName: string | undefined): boolean {
134
- return Boolean(
135
- tagName &&
136
- [
137
- "article",
138
- "aside",
139
- "canvas",
140
- "div",
141
- "figure",
142
- "footer",
143
- "header",
144
- "img",
145
- "main",
146
- "section",
147
- "svg",
148
- "video",
149
- ].includes(tagName.toLowerCase()),
150
- );
151
- }
152
-
153
- function isBlockishDisplay(display: string | undefined): boolean {
154
- return Boolean(
155
- display &&
156
- [
157
- "block",
158
- "flex",
159
- "flow-root",
160
- "grid",
161
- "inline-block",
162
- "inline-flex",
163
- "inline-grid",
164
- "list-item",
165
- "table",
166
- ].includes(display),
167
- );
168
- }
169
-
170
126
  function isTextBearingTag(tagName: string): boolean {
171
127
  return ["div", "span", "p", "strong", "h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
172
128
  }
@@ -196,16 +152,15 @@ function findClosestByAttribute(el: HTMLElement, attributeNames: string[]): HTML
196
152
  return null;
197
153
  }
198
154
 
199
- function getCompositionHost(el: HTMLElement): HTMLElement | null {
200
- return findClosestByAttribute(el, ["data-composition-src", "data-composition-file"]);
201
- }
202
-
203
155
  function getSourceFileForElement(
204
156
  el: HTMLElement,
205
157
  activeCompositionPath: string | null,
206
158
  ): { sourceFile: string; compositionPath: string } {
159
+ const sourceHost = findClosestByAttribute(el, ["data-composition-file", "data-composition-src"]);
207
160
  const ownerRoot = findClosestByAttribute(el, ["data-composition-id"]);
208
161
  const sourceFile =
162
+ sourceHost?.getAttribute("data-composition-file") ??
163
+ sourceHost?.getAttribute("data-composition-src") ??
209
164
  ownerRoot?.getAttribute("data-composition-file") ??
210
165
  ownerRoot?.getAttribute("data-composition-src") ??
211
166
  activeCompositionPath ??
@@ -217,21 +172,28 @@ function getSourceFileForElement(
217
172
  };
218
173
  }
219
174
 
175
+ function getPreferredClipAncestor(startEl: HTMLElement): HTMLElement | null {
176
+ let current: HTMLElement | null = startEl;
177
+ while (current) {
178
+ if (current.classList.contains("clip")) {
179
+ const isCompositionHost =
180
+ current.hasAttribute("data-composition-src") ||
181
+ current.hasAttribute("data-composition-file");
182
+ if (!isCompositionHost || current === startEl) return current;
183
+ }
184
+ current = current.parentElement;
185
+ }
186
+ return null;
187
+ }
188
+
220
189
  function getSelectionCandidate(startEl: HTMLElement, options: DomEditContextOptions): HTMLElement {
221
190
  if (options.preferClipAncestor) {
222
- const clipAncestor = startEl.closest(".clip");
223
- if (isHtmlElement(clipAncestor)) {
191
+ const clipAncestor = getPreferredClipAncestor(startEl);
192
+ if (clipAncestor) {
224
193
  return clipAncestor;
225
194
  }
226
195
  }
227
196
 
228
- if (!options.isMasterView) return startEl;
229
-
230
- const compositionHost = getCompositionHost(startEl);
231
- if (compositionHost && compositionHost !== startEl) {
232
- return compositionHost;
233
- }
234
-
235
197
  return startEl;
236
198
  }
237
199
 
@@ -442,7 +404,9 @@ export function resolveDomEditCapabilities(args: {
442
404
  canEditStyles: false,
443
405
  canMove: false,
444
406
  canResize: false,
445
- canDetachFromLayout: false,
407
+ canApplyManualOffset: false,
408
+ canApplyManualSize: false,
409
+ canApplyManualRotation: false,
446
410
  reasonIfDisabled: "Studio could not resolve a stable patch target for this element.",
447
411
  };
448
412
  }
@@ -461,21 +425,13 @@ export function resolveDomEditCapabilities(args: {
461
425
  !hasTransformDrivenGeometry;
462
426
 
463
427
  const canResize = canMove && (width != null || height != null);
464
- const isBlockishLayer =
465
- args.isCompositionHost ||
466
- isClipClassName(args.className) ||
467
- isBlockishTag(args.tagName) ||
468
- isBlockishDisplay(args.computedStyles.display);
469
- const canDetachFromLayout =
470
- !canMove &&
471
- !hasTransformDrivenGeometry &&
472
- isBlockishLayer &&
473
- (!isInlineTextTag(args.tagName) || isClipClassName(args.className));
474
- const reasonIfDisabled = !canMove
475
- ? canDetachFromLayout
476
- ? "This layer is controlled by layout."
477
- : "Direct move/resize is limited to absolute or fixed elements with px geometry and no transform-driven layout."
478
- : undefined;
428
+ const canApplyManualGeometry = !args.isCompositionHost;
429
+ const canApplyManualOffset = canApplyManualGeometry;
430
+ const canApplyManualSize = canApplyManualGeometry;
431
+ const canApplyManualRotation = canApplyManualGeometry;
432
+ const reasonIfDisabled = canApplyManualGeometry
433
+ ? undefined
434
+ : "Select an internal layer to transform it.";
479
435
 
480
436
  if (args.isCompositionHost && args.isMasterView) {
481
437
  return {
@@ -483,7 +439,9 @@ export function resolveDomEditCapabilities(args: {
483
439
  canEditStyles: false,
484
440
  canMove,
485
441
  canResize,
486
- canDetachFromLayout,
442
+ canApplyManualOffset,
443
+ canApplyManualSize,
444
+ canApplyManualRotation,
487
445
  reasonIfDisabled,
488
446
  };
489
447
  }
@@ -493,7 +451,9 @@ export function resolveDomEditCapabilities(args: {
493
451
  canEditStyles: true,
494
452
  canMove,
495
453
  canResize,
496
- canDetachFromLayout,
454
+ canApplyManualOffset,
455
+ canApplyManualSize,
456
+ canApplyManualRotation,
497
457
  reasonIfDisabled,
498
458
  };
499
459
  }
@@ -585,6 +545,49 @@ export function refreshDomEditSelection(
585
545
  : null;
586
546
  }
587
547
 
548
+ export function getDomEditTargetKey(
549
+ selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
550
+ ): string {
551
+ return [
552
+ selection.sourceFile || "index.html",
553
+ selection.id ?? "",
554
+ selection.selector ?? "",
555
+ selection.selectorIndex ?? "",
556
+ ].join("|");
557
+ }
558
+
559
+ function hasSupportedDirectEdit(capabilities: DomEditCapabilities): boolean {
560
+ return (
561
+ capabilities.canEditStyles ||
562
+ capabilities.canMove ||
563
+ capabilities.canResize ||
564
+ capabilities.canApplyManualOffset ||
565
+ capabilities.canApplyManualSize ||
566
+ capabilities.canApplyManualRotation
567
+ );
568
+ }
569
+
570
+ export function getDomEditNonEditableReason(
571
+ element: HTMLElement,
572
+ selection: DomEditSelection | null,
573
+ ): string | null {
574
+ if (!selection) {
575
+ return "No stable source target";
576
+ }
577
+
578
+ if (selection.element !== element) {
579
+ return selection.isCompositionHost
580
+ ? "Nested composition boundary"
581
+ : `Selection resolves to ${selection.label}`;
582
+ }
583
+
584
+ if (!hasSupportedDirectEdit(selection.capabilities)) {
585
+ return selection.capabilities.reasonIfDisabled ?? "No supported direct edits";
586
+ }
587
+
588
+ return null;
589
+ }
590
+
588
591
  export function findElementForSelection(
589
592
  doc: Document,
590
593
  selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
@@ -624,38 +627,6 @@ export function findElementForSelection(
624
627
  return matches[0] ?? null;
625
628
  }
626
629
 
627
- export function buildDomEditMovePatchOperations(left: number, top: number): PatchOperation[] {
628
- return [
629
- { type: "inline-style", property: "left", value: `${Math.round(left)}px` },
630
- { type: "inline-style", property: "top", value: `${Math.round(top)}px` },
631
- ];
632
- }
633
-
634
- export function buildDomEditResizePatchOperations(width: number, height: number): PatchOperation[] {
635
- return [
636
- { type: "inline-style", property: "width", value: `${Math.round(width)}px` },
637
- { type: "inline-style", property: "height", value: `${Math.round(height)}px` },
638
- ];
639
- }
640
-
641
- export function buildDomEditDetachPatchOperations(rect: {
642
- left: number;
643
- top: number;
644
- width: number;
645
- height: number;
646
- }): PatchOperation[] {
647
- return [
648
- { type: "inline-style", property: "position", value: "absolute" },
649
- { type: "inline-style", property: "left", value: `${Math.round(rect.left)}px` },
650
- { type: "inline-style", property: "top", value: `${Math.round(rect.top)}px` },
651
- { type: "inline-style", property: "width", value: `${Math.round(rect.width)}px` },
652
- { type: "inline-style", property: "height", value: `${Math.round(rect.height)}px` },
653
- { type: "inline-style", property: "margin", value: "0" },
654
- { type: "inline-style", property: "right", value: "auto" },
655
- { type: "inline-style", property: "bottom", value: "auto" },
656
- ];
657
- }
658
-
659
630
  export function buildDomEditStylePatchOperation(property: string, value: string): PatchOperation {
660
631
  return {
661
632
  type: "inline-style",
@@ -687,7 +658,7 @@ function formatTextFields(fields: DomEditTextField[]): string {
687
658
  return fields
688
659
  .map(
689
660
  (field) =>
690
- `- key=${field.key}; tag=<${field.tagName}>; source=${field.source}; text="${field.value.replace(/"/g, '\\"')}"`,
661
+ `- key=${field.key}; tag=<${field.tagName}>; source=${field.source}; text=${JSON.stringify(field.value)}`,
691
662
  )
692
663
  .join("\n");
693
664
  }