@hyperframes/studio 0.7.60 → 0.7.62

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 (110) hide show
  1. package/dist/assets/hyperframes-player-CWpmBjzv.js +459 -0
  2. package/dist/assets/{index-D6etaey-.js → index-BJzSiIBj.js} +1 -1
  3. package/dist/assets/index-Ceyz8Qt2.css +1 -0
  4. package/dist/assets/{index-Dh_WhagG.js → index-DJQPB78e.js} +1 -1
  5. package/dist/assets/index-vDRVQtTK.js +426 -0
  6. package/dist/index.d.ts +12 -1
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +4392 -3314
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +15 -15
  12. package/src/components/DesignPanelPromoteProvider.tsx +27 -1
  13. package/src/components/EditorShell.tsx +12 -0
  14. package/src/components/StudioHeader.tsx +2 -3
  15. package/src/components/StudioLeftSidebar.tsx +3 -0
  16. package/src/components/StudioRightPanel.tsx +34 -26
  17. package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
  18. package/src/components/editor/PromotableControl.tsx +4 -2
  19. package/src/components/editor/PropertyPanel.tsx +151 -149
  20. package/src/components/editor/PropertyPanelFlat.tsx +50 -42
  21. package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
  22. package/src/components/editor/domEditing.test.ts +34 -0
  23. package/src/components/editor/manualEditingAvailability.test.ts +6 -6
  24. package/src/components/editor/manualEditingAvailability.ts +5 -3
  25. package/src/components/editor/propertyPanelColor.test.tsx +31 -0
  26. package/src/components/editor/propertyPanelColor.tsx +150 -28
  27. package/src/components/editor/propertyPanelCommitField.tsx +178 -0
  28. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +44 -0
  29. package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -0
  30. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
  31. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +26 -10
  32. package/src/components/editor/propertyPanelFlatStyleSections.tsx +21 -32
  33. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
  34. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -2
  35. package/src/components/editor/propertyPanelInputCoverage.test.tsx +18 -1
  36. package/src/components/editor/propertyPanelPrimitives.tsx +3 -92
  37. package/src/components/editor/propertyPanelSections.tsx +2 -2
  38. package/src/components/editor/propertyPanelTypes.ts +2 -0
  39. package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
  40. package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
  41. package/src/components/nle/TimelinePane.tsx +6 -0
  42. package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
  43. package/src/components/sidebar/CompositionsTab.tsx +45 -2
  44. package/src/components/sidebar/LeftSidebar.tsx +3 -0
  45. package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
  46. package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
  47. package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
  48. package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
  49. package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
  50. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
  51. package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
  52. package/src/components/storyboard/storyboardReviewStage.ts +45 -0
  53. package/src/components/storyboard/useFrameComments.ts +22 -6
  54. package/src/contexts/StudioContext.tsx +4 -0
  55. package/src/contexts/ViewModeContext.tsx +60 -7
  56. package/src/hooks/timelineEditingHelpers.ts +10 -4
  57. package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
  58. package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
  59. package/src/hooks/usePreviewPersistence.ts +7 -1
  60. package/src/hooks/useRazorSplit.history.test.tsx +21 -28
  61. package/src/hooks/useRazorSplit.test.tsx +20 -9
  62. package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
  63. package/src/hooks/useRazorSplit.ts +78 -350
  64. package/src/hooks/useSlideshowTabState.test.ts +96 -0
  65. package/src/hooks/useSlideshowTabState.ts +61 -0
  66. package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
  67. package/src/hooks/useTimelineAssetDropOps.ts +52 -2
  68. package/src/hooks/useTimelineEditing.ts +19 -17
  69. package/src/hooks/useTimelineGroupEditing.ts +6 -7
  70. package/src/player/components/Timeline.tsx +12 -1
  71. package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
  72. package/src/player/components/timelineAuthoredTrack.ts +29 -0
  73. package/src/player/components/timelineCallbacks.ts +4 -0
  74. package/src/player/components/timelineClipDragCommit.test.ts +176 -0
  75. package/src/player/components/timelineClipDragCommit.ts +77 -105
  76. package/src/player/components/timelineClipDragPreview.test.ts +33 -1
  77. package/src/player/components/timelineClipDragPreview.ts +2 -1
  78. package/src/player/components/timelineCollision.test.ts +18 -0
  79. package/src/player/components/timelineCollision.ts +12 -9
  80. package/src/player/components/timelineDragDrop.ts +28 -7
  81. package/src/player/components/timelineEditCapabilities.ts +4 -1
  82. package/src/player/components/timelineGroupEditing.test.ts +10 -0
  83. package/src/player/components/timelineGroupEditing.ts +4 -3
  84. package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
  85. package/src/player/components/timelineLayout.ts +3 -1
  86. package/src/player/components/timelineZMirror.ts +1 -1
  87. package/src/player/components/useTimelineClipDrag.ts +5 -0
  88. package/src/player/components/useTimelineEditPinning.ts +12 -0
  89. package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
  90. package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
  91. package/src/player/lib/playbackTypes.ts +9 -0
  92. package/src/player/lib/runtimeProtocol.test.ts +6 -1
  93. package/src/player/lib/timelineDOM.test.ts +58 -0
  94. package/src/player/lib/timelineDOM.ts +26 -12
  95. package/src/player/lib/timelineElementHelpers.ts +21 -11
  96. package/src/player/lib/timelineIframeHelpers.ts +153 -107
  97. package/src/player/store/playerStore.ts +2 -1
  98. package/src/utils/razorSplitTransaction.test.ts +175 -0
  99. package/src/utils/razorSplitTransaction.ts +200 -0
  100. package/src/utils/studioPreviewHelpers.test.ts +27 -0
  101. package/src/utils/timelineCompositionDrop.test.ts +12 -0
  102. package/src/utils/timelineCompositionDrop.ts +16 -0
  103. package/src/utils/timelineCompositionInsert.test.ts +142 -0
  104. package/src/utils/timelineCompositionInsert.ts +90 -0
  105. package/src/utils/timelineElementSplit.test.ts +14 -2
  106. package/src/utils/timelineElementSplit.ts +5 -1
  107. package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
  108. package/dist/assets/index-DXbu6IPT.css +0 -1
  109. package/dist/assets/index-cH6NfVV_.js +0 -426
  110. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
@@ -197,11 +197,11 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
197
197
  const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize;
198
198
  const manualRotationEditingDisabled = !element.capabilities.canApplyManualRotation;
199
199
  const sourceLabel = element.id ? `#${element.id}` : (element.selector ?? "");
200
- const showEditableSections = element.capabilities.canEditStyles;
201
200
  // Capabilities are already resolved on the selection; recompute only sections,
202
201
  // feeding the live GSAP tween count (arrives on the gsapAnimations prop, not the
203
202
  // selection) so the Timing section shows for pure-GSAP elements with no data-start.
204
203
  const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
204
+ const showEditableSections = element.capabilities.canEditStyles && sections.style;
205
205
  const manualOffset = readStudioPathOffset(element.element);
206
206
  const manualSize = readStudioBoxSize(element.element);
207
207
  const resolvedWidth =
@@ -385,161 +385,163 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
385
385
  />
386
386
  )}
387
387
 
388
- <Section title="Layout" icon={<Move size={15} />}>
389
- <div className={RESPONSIVE_GRID}>
390
- <div className="flex items-center gap-1">
391
- <div className="flex-1">
392
- <MetricField
393
- label="X"
394
- value={formatPxMetricValue(displayX)}
395
- disabled={manualOffsetEditingDisabled}
396
- scrub
397
- onCommit={(next) => commitManualOffset("x", next)}
398
- />
388
+ {sections.layout && (
389
+ <Section title="Layout" icon={<Move size={15} />}>
390
+ <div className={RESPONSIVE_GRID}>
391
+ <div className="flex items-center gap-1">
392
+ <div className="flex-1">
393
+ <MetricField
394
+ label="X"
395
+ value={formatPxMetricValue(displayX)}
396
+ disabled={manualOffsetEditingDisabled}
397
+ scrub
398
+ onCommit={(next) => commitManualOffset("x", next)}
399
+ />
400
+ </div>
401
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
402
+ <KeyframeNavigation
403
+ property="x"
404
+ keyframes={navKeyframes}
405
+ currentPercentage={currentPct}
406
+ onSeek={seekFromKfPct}
407
+ onAddKeyframe={() =>
408
+ onCommitAnimatedProperty &&
409
+ void onCommitAnimatedProperty(element, "x", displayX)
410
+ }
411
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("x"), pct)}
412
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("x"))}
413
+ />
414
+ )}
399
415
  </div>
400
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
401
- <KeyframeNavigation
402
- property="x"
403
- keyframes={navKeyframes}
404
- currentPercentage={currentPct}
405
- onSeek={seekFromKfPct}
406
- onAddKeyframe={() =>
407
- onCommitAnimatedProperty &&
408
- void onCommitAnimatedProperty(element, "x", displayX)
409
- }
410
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("x"), pct)}
411
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("x"))}
412
- />
413
- )}
414
- </div>
415
- <div className="flex items-center gap-1">
416
- <div className="flex-1">
417
- <MetricField
418
- label="Y"
419
- value={formatPxMetricValue(displayY)}
420
- disabled={manualOffsetEditingDisabled}
421
- scrub
422
- onCommit={(next) => commitManualOffset("y", next)}
423
- />
416
+ <div className="flex items-center gap-1">
417
+ <div className="flex-1">
418
+ <MetricField
419
+ label="Y"
420
+ value={formatPxMetricValue(displayY)}
421
+ disabled={manualOffsetEditingDisabled}
422
+ scrub
423
+ onCommit={(next) => commitManualOffset("y", next)}
424
+ />
425
+ </div>
426
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
427
+ <KeyframeNavigation
428
+ property="y"
429
+ keyframes={navKeyframes}
430
+ currentPercentage={currentPct}
431
+ onSeek={seekFromKfPct}
432
+ onAddKeyframe={() =>
433
+ onCommitAnimatedProperty &&
434
+ void onCommitAnimatedProperty(element, "y", displayY)
435
+ }
436
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("y"), pct)}
437
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("y"))}
438
+ />
439
+ )}
424
440
  </div>
425
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
426
- <KeyframeNavigation
427
- property="y"
428
- keyframes={navKeyframes}
429
- currentPercentage={currentPct}
430
- onSeek={seekFromKfPct}
431
- onAddKeyframe={() =>
432
- onCommitAnimatedProperty &&
433
- void onCommitAnimatedProperty(element, "y", displayY)
434
- }
435
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("y"), pct)}
436
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("y"))}
437
- />
438
- )}
439
- </div>
440
- <div className="flex items-center gap-1">
441
- <div className="flex-1">
442
- <MetricField
443
- label="W"
444
- value={formatPxMetricValue(displayW)}
445
- disabled={manualSizeEditingDisabled}
446
- scrub
447
- onCommit={(next) => commitManualSize("width", next)}
448
- />
441
+ <div className="flex items-center gap-1">
442
+ <div className="flex-1">
443
+ <MetricField
444
+ label="W"
445
+ value={formatPxMetricValue(displayW)}
446
+ disabled={manualSizeEditingDisabled}
447
+ scrub
448
+ onCommit={(next) => commitManualSize("width", next)}
449
+ />
450
+ </div>
451
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
452
+ <KeyframeNavigation
453
+ property="width"
454
+ keyframes={navKeyframes}
455
+ currentPercentage={currentPct}
456
+ onSeek={seekFromKfPct}
457
+ onAddKeyframe={() =>
458
+ onCommitAnimatedProperty &&
459
+ void onCommitAnimatedProperty(element, "width", displayW)
460
+ }
461
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("width"), pct)}
462
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("width"))}
463
+ />
464
+ )}
449
465
  </div>
450
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
451
- <KeyframeNavigation
452
- property="width"
453
- keyframes={navKeyframes}
454
- currentPercentage={currentPct}
455
- onSeek={seekFromKfPct}
456
- onAddKeyframe={() =>
457
- onCommitAnimatedProperty &&
458
- void onCommitAnimatedProperty(element, "width", displayW)
459
- }
460
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("width"), pct)}
461
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("width"))}
462
- />
463
- )}
464
- </div>
465
- <div className="flex items-center gap-1">
466
- <div className="flex-1">
467
- <MetricField
468
- label="H"
469
- value={formatPxMetricValue(displayH)}
470
- disabled={manualSizeEditingDisabled}
471
- scrub
472
- onCommit={(next) => commitManualSize("height", next)}
473
- />
466
+ <div className="flex items-center gap-1">
467
+ <div className="flex-1">
468
+ <MetricField
469
+ label="H"
470
+ value={formatPxMetricValue(displayH)}
471
+ disabled={manualSizeEditingDisabled}
472
+ scrub
473
+ onCommit={(next) => commitManualSize("height", next)}
474
+ />
475
+ </div>
476
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
477
+ <KeyframeNavigation
478
+ property="height"
479
+ keyframes={navKeyframes}
480
+ currentPercentage={currentPct}
481
+ onSeek={seekFromKfPct}
482
+ onAddKeyframe={() =>
483
+ onCommitAnimatedProperty &&
484
+ void onCommitAnimatedProperty(element, "height", displayH)
485
+ }
486
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("height"), pct)}
487
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("height"))}
488
+ />
489
+ )}
474
490
  </div>
475
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
476
- <KeyframeNavigation
477
- property="height"
478
- keyframes={navKeyframes}
479
- currentPercentage={currentPct}
480
- onSeek={seekFromKfPct}
481
- onAddKeyframe={() =>
482
- onCommitAnimatedProperty &&
483
- void onCommitAnimatedProperty(element, "height", displayH)
484
- }
485
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("height"), pct)}
486
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("height"))}
487
- />
488
- )}
489
- </div>
490
- <div className="flex items-center gap-1">
491
- <div className="flex-1">
492
- <MetricField
493
- label="R"
494
- value={`${displayR}°`}
495
- disabled={manualRotationEditingDisabled}
496
- onCommit={(next) => commitManualRotation(next.replace("°", ""))}
497
- />
491
+ <div className="flex items-center gap-1">
492
+ <div className="flex-1">
493
+ <MetricField
494
+ label="R"
495
+ value={`${displayR}°`}
496
+ disabled={manualRotationEditingDisabled}
497
+ onCommit={(next) => commitManualRotation(next.replace("°", ""))}
498
+ />
499
+ </div>
500
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
501
+ <KeyframeNavigation
502
+ property="rotation"
503
+ keyframes={navKeyframes}
504
+ currentPercentage={currentPct}
505
+ onSeek={seekFromKfPct}
506
+ onAddKeyframe={() =>
507
+ onCommitAnimatedProperty &&
508
+ void onCommitAnimatedProperty(element, "rotation", displayR)
509
+ }
510
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("rotation"), pct)}
511
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("rotation"))}
512
+ />
513
+ )}
498
514
  </div>
499
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
500
- <KeyframeNavigation
501
- property="rotation"
502
- keyframes={navKeyframes}
503
- currentPercentage={currentPct}
504
- onSeek={seekFromKfPct}
505
- onAddKeyframe={() =>
506
- onCommitAnimatedProperty &&
507
- void onCommitAnimatedProperty(element, "rotation", displayR)
508
- }
509
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("rotation"), pct)}
510
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("rotation"))}
511
- />
512
- )}
513
515
  </div>
514
- </div>
515
- <PropertyPanel3dTransform
516
- gsapRuntimeValues={gsap3dValues}
517
- gsapAnimId={gsapAnimId}
518
- resolveAnimIdForProp={animIdForProp}
519
- gsapKeyframes={navKeyframes}
520
- currentPct={currentPct}
521
- elStart={elStart}
522
- elDuration={elDuration}
523
- element={element}
524
- onCommitAnimatedProperty={onCommitAnimatedProperty}
525
- onCommitAnimatedProperties={onCommitAnimatedProperties}
526
- onSeekToTime={onSeekToTime}
527
- onRemoveKeyframe={onRemoveKeyframe}
528
- onConvertToKeyframes={onConvertToKeyframes}
529
- onLivePreviewProps={createGsapLivePreview(iframeRef)}
530
- />
531
- <div className="mt-3">
532
- <div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
533
- Stacking
534
- </div>
535
- <MetricField
536
- label="Z-index"
537
- value={String(parseInt(styles["z-index"] || "auto", 10) || 0)}
538
- scrub
539
- onCommit={(next) => onSetStyle("z-index", next)}
516
+ <PropertyPanel3dTransform
517
+ gsapRuntimeValues={gsap3dValues}
518
+ gsapAnimId={gsapAnimId}
519
+ resolveAnimIdForProp={animIdForProp}
520
+ gsapKeyframes={navKeyframes}
521
+ currentPct={currentPct}
522
+ elStart={elStart}
523
+ elDuration={elDuration}
524
+ element={element}
525
+ onCommitAnimatedProperty={onCommitAnimatedProperty}
526
+ onCommitAnimatedProperties={onCommitAnimatedProperties}
527
+ onSeekToTime={onSeekToTime}
528
+ onRemoveKeyframe={onRemoveKeyframe}
529
+ onConvertToKeyframes={onConvertToKeyframes}
530
+ onLivePreviewProps={createGsapLivePreview(iframeRef)}
540
531
  />
541
- </div>
542
- </Section>
532
+ <div className="mt-3">
533
+ <div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
534
+ Stacking
535
+ </div>
536
+ <MetricField
537
+ label="Z-index"
538
+ value={String(parseInt(styles["z-index"] || "auto", 10) || 0)}
539
+ scrub
540
+ onCommit={(next) => onSetStyle("z-index", next)}
541
+ />
542
+ </div>
543
+ </Section>
544
+ )}
543
545
 
544
546
  {STUDIO_GSAP_PANEL_ENABLED &&
545
547
  onUpdateGsapProperty &&
@@ -78,6 +78,7 @@ export function PropertyPanelFlat({
78
78
  onClearSelection,
79
79
  onUngroup,
80
80
  onSetStyle,
81
+ onPreviewStyle,
81
82
  onSetAttribute,
82
83
  onSetAttributes,
83
84
  onSetAttributeLive,
@@ -86,6 +87,7 @@ export function PropertyPanelFlat({
86
87
  onRemoveBackground,
87
88
  onSetText,
88
89
  onSetTextFieldStyle,
90
+ onPreviewTextFieldStyle,
89
91
  onAddTextField,
90
92
  onRemoveTextField,
91
93
  onAskAgent,
@@ -147,6 +149,7 @@ export function PropertyPanelFlat({
147
149
  | "onClearSelection"
148
150
  | "onUngroup"
149
151
  | "onSetStyle"
152
+ | "onPreviewStyle"
150
153
  | "onSetAttribute"
151
154
  | "onSetAttributes"
152
155
  | "onSetAttributeLive"
@@ -155,6 +158,7 @@ export function PropertyPanelFlat({
155
158
  | "onRemoveBackground"
156
159
  | "onSetText"
157
160
  | "onSetTextFieldStyle"
161
+ | "onPreviewTextFieldStyle"
158
162
  | "onAddTextField"
159
163
  | "onRemoveTextField"
160
164
  | "onAskAgent"
@@ -360,6 +364,7 @@ export function PropertyPanelFlat({
360
364
  onImportFonts={onImportFonts}
361
365
  onSetText={onSetText}
362
366
  onSetTextFieldStyle={onSetTextFieldStyle}
367
+ onPreviewTextFieldStyle={onPreviewTextFieldStyle}
363
368
  onAddTextField={onAddTextField}
364
369
  onRemoveTextField={onRemoveTextField}
365
370
  />
@@ -382,54 +387,57 @@ export function PropertyPanelFlat({
382
387
  styles={styles}
383
388
  assets={assets}
384
389
  onSetStyle={onSetStyle}
390
+ onPreviewStyle={onPreviewStyle}
385
391
  onImportAssets={onImportAssets}
386
392
  gsapBorderRadius={gsapBorderRadius}
387
393
  />
388
394
  ),
389
395
  });
390
396
  }
391
- groups.push({
392
- id: "layout",
393
- title: "Layout",
394
- // No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
395
- // (wheel/arrow keys only) advertising "drag values to scrub" here lies.
396
- summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`,
397
- content: (
398
- <FlatLayoutSection
399
- element={element}
400
- styles={styles}
401
- onSetStyle={onSetStyle}
402
- disabled={!element.capabilities.canEditStyles}
403
- displayX={displayX}
404
- displayY={displayY}
405
- displayW={displayW}
406
- displayH={displayH}
407
- displayR={displayR}
408
- manualOffsetEditingDisabled={manualOffsetEditingDisabled}
409
- manualSizeEditingDisabled={manualSizeEditingDisabled}
410
- manualRotationEditingDisabled={manualRotationEditingDisabled}
411
- commitManualOffset={commitManualOffset}
412
- commitManualSize={commitManualSize}
413
- commitManualRotation={commitManualRotation}
414
- gsapAnimId={gsapAnimId}
415
- navKeyframes={navKeyframes}
416
- currentPct={currentPct}
417
- seekFromKfPct={seekFromKfPct}
418
- animIdForProp={animIdForProp}
419
- resolveAnimIdForProp={animIdForProp}
420
- gsapRuntimeValues={gsapRuntimeValues}
421
- gsapKeyframes={navKeyframes}
422
- elStart={elStart}
423
- elDuration={elDuration}
424
- onCommitAnimatedProperty={onCommitAnimatedProperty}
425
- onCommitAnimatedProperties={onCommitAnimatedProperties}
426
- onSeekToTime={onSeekToTime}
427
- onRemoveKeyframe={onRemoveKeyframe}
428
- onConvertToKeyframes={onConvertToKeyframes}
429
- onLivePreviewProps={createGsapLivePreview(previewIframeRef ?? { current: null })}
430
- />
431
- ),
432
- });
397
+ if (sections.layout) {
398
+ groups.push({
399
+ id: "layout",
400
+ title: "Layout",
401
+ // No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
402
+ // (wheel/arrow keys only) advertising "drag values to scrub" here lies.
403
+ summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`,
404
+ content: (
405
+ <FlatLayoutSection
406
+ element={element}
407
+ styles={styles}
408
+ onSetStyle={onSetStyle}
409
+ disabled={!element.capabilities.canEditStyles}
410
+ displayX={displayX}
411
+ displayY={displayY}
412
+ displayW={displayW}
413
+ displayH={displayH}
414
+ displayR={displayR}
415
+ manualOffsetEditingDisabled={manualOffsetEditingDisabled}
416
+ manualSizeEditingDisabled={manualSizeEditingDisabled}
417
+ manualRotationEditingDisabled={manualRotationEditingDisabled}
418
+ commitManualOffset={commitManualOffset}
419
+ commitManualSize={commitManualSize}
420
+ commitManualRotation={commitManualRotation}
421
+ gsapAnimId={gsapAnimId}
422
+ navKeyframes={navKeyframes}
423
+ currentPct={currentPct}
424
+ seekFromKfPct={seekFromKfPct}
425
+ animIdForProp={animIdForProp}
426
+ resolveAnimIdForProp={animIdForProp}
427
+ gsapRuntimeValues={gsapRuntimeValues}
428
+ gsapKeyframes={navKeyframes}
429
+ elStart={elStart}
430
+ elDuration={elDuration}
431
+ onCommitAnimatedProperty={onCommitAnimatedProperty}
432
+ onCommitAnimatedProperties={onCommitAnimatedProperties}
433
+ onSeekToTime={onSeekToTime}
434
+ onRemoveKeyframe={onRemoveKeyframe}
435
+ onConvertToKeyframes={onConvertToKeyframes}
436
+ onLivePreviewProps={createGsapLivePreview(previewIframeRef ?? { current: null })}
437
+ />
438
+ ),
439
+ });
440
+ }
433
441
  if (showMotionGroup) {
434
442
  groups.push({
435
443
  id: "motion",
@@ -0,0 +1,105 @@
1
+ // @vitest-environment jsdom
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { patchElementInHtml } from "@hyperframes/studio-server/source-mutation";
6
+ import { describe, expect, it } from "vitest";
7
+ import { buildDomEditStylePatchOperation } from "./domEditing";
8
+
9
+ const fixtureDir = join(
10
+ dirname(fileURLToPath(import.meta.url)),
11
+ "../../../tests/e2e/fixtures/composition-reliability",
12
+ );
13
+
14
+ function fixture(path: string): string {
15
+ return readFileSync(join(fixtureDir, path), "utf8");
16
+ }
17
+
18
+ function parse(html: string): Document {
19
+ return new DOMParser().parseFromString(html, "text/html");
20
+ }
21
+
22
+ function inTemplate(document: Document, selector: string): Element | null {
23
+ for (const template of Array.from(document.querySelectorAll("template"))) {
24
+ const match = template.content.querySelector(selector);
25
+ if (match) return match;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ describe("composition reliability acceptance fixture", () => {
31
+ const indexSource = fixture("index.html");
32
+ const titleSource = fixture("compositions/title-card.html");
33
+ const nestedSource = fixture("compositions/nested-shell.html");
34
+
35
+ it("owns repeated root hosts, a nested host, transparent headline topology, and collisions", () => {
36
+ const index = parse(indexSource);
37
+ const repeated = Array.from(
38
+ index.querySelectorAll('[data-composition-src="compositions/title-card.html"]'),
39
+ );
40
+ expect(repeated).toHaveLength(2);
41
+ expect(repeated.map((host) => host.getAttribute("data-start"))).toEqual(["0", "4"]);
42
+ expect(
43
+ index.querySelector('[data-composition-src="compositions/nested-shell.html"]'),
44
+ ).toBeTruthy();
45
+
46
+ const nested = parse(nestedSource);
47
+ const nestedHost = inTemplate(nested, '[data-composition-src="title-card.html"]');
48
+ expect(nestedHost).toBeTruthy();
49
+ const nestedDependency = nestedHost?.getAttribute("data-composition-src");
50
+ expect(
51
+ nestedDependency ? existsSync(resolve(fixtureDir, "compositions", nestedDependency)) : false,
52
+ ).toBe(true);
53
+
54
+ const title = parse(titleSource);
55
+ const mask = inTemplate(title, ".hl-mask");
56
+ const headline = inTemplate(title, ".hl-mask > .hl-text");
57
+ expect(mask?.textContent).toContain("Reliable compositions");
58
+ expect(inTemplate(title, "style")?.textContent).toMatch(
59
+ /\.hl-mask\s*\{[^}]*overflow:\s*hidden;[^}]*background:\s*transparent;/,
60
+ );
61
+ expect(headline?.tagName).toBe("H1");
62
+
63
+ const collisionA = index.querySelector('[data-hf-id="collision-a"]')!;
64
+ const collisionB = index.querySelector('[data-hf-id="collision-b"]')!;
65
+ const layered = index.querySelector('[data-hf-id="layer-overlap"]')!;
66
+ expect(collisionA.getAttribute("data-track-index")).toBe(
67
+ collisionB.getAttribute("data-track-index"),
68
+ );
69
+ expect(
70
+ Number(collisionA.getAttribute("data-start")) +
71
+ Number(collisionA.getAttribute("data-duration")),
72
+ ).toBe(Number(collisionB.getAttribute("data-start")));
73
+ expect(layered.getAttribute("data-start")).toBe(collisionB.getAttribute("data-start"));
74
+ expect(layered.getAttribute("data-track-index")).not.toBe(
75
+ collisionB.getAttribute("data-track-index"),
76
+ );
77
+ });
78
+
79
+ it("keeps timeline host edits in the root source and headline color in the template source", () => {
80
+ const moved = patchElementInHtml(indexSource, { hfId: "title-host-a" }, [
81
+ { type: "attribute", property: "start", value: "5" },
82
+ { type: "attribute", property: "duration", value: "2" },
83
+ ]);
84
+ expect(moved.matched).toBe(true);
85
+ expect(moved.html).toContain('data-hf-id="title-host-a"');
86
+ expect(moved.html).toContain('data-start="5"');
87
+ expect(moved.html).toContain('data-duration="2"');
88
+ expect(moved.html).not.toContain('data-hf-id="title-text"');
89
+ expect(titleSource).not.toContain("#12b886");
90
+
91
+ const recolored = patchElementInHtml(titleSource, { hfId: "title-text" }, [
92
+ buildDomEditStylePatchOperation("color", "#12b886"),
93
+ ]);
94
+ expect(recolored.matched).toBe(true);
95
+ const recoloredDocument = parse(recolored.html);
96
+ expect(
97
+ inTemplate(recoloredDocument, '[data-hf-id="title-text"]')?.getAttribute("style"),
98
+ ).toContain("color: #12b886");
99
+ expect(
100
+ inTemplate(recoloredDocument, '[data-hf-id="title-mask"]')?.getAttribute("style"),
101
+ ).toBeNull();
102
+ expect(recolored.html).not.toContain('data-hf-id="title-host-a"');
103
+ expect(indexSource).not.toContain("#12b886");
104
+ });
105
+ });
@@ -739,6 +739,40 @@ describe("resolveDomEditSelection", () => {
739
739
  expect(selection?.selector).toBe("#copy");
740
740
  });
741
741
 
742
+ it("keeps a transparent overflow mask structural when directly selecting its headline", async () => {
743
+ const document = createDocument(`
744
+ <template id="source-template"></template>
745
+ <section class="hl-block">
746
+ <div class="hl-mask" style="overflow: hidden; background: transparent">
747
+ <h1 class="hl-text">Launch title</h1>
748
+ </div>
749
+ </section>
750
+ `);
751
+ const headline = document.querySelector<HTMLElement>(".hl-text")!;
752
+ setElementRect(headline, { left: 44, top: 52, width: 220, height: 48 });
753
+ const selection = await resolveDomEditSelection(headline, {
754
+ activeCompositionPath: "index.html",
755
+ isMasterView: false,
756
+ preferClipAncestor: false,
757
+ });
758
+
759
+ expect(selection?.element).toBe(headline);
760
+ expect(selection?.selector).toBe(".hl-text");
761
+ expect(selection?.textFields).toMatchObject([{ source: "self", tagName: "h1" }]);
762
+ expect(selection?.boundingBox).toEqual({ x: 44, y: 52, width: 220, height: 48 });
763
+ // Explicit layer navigation remains free to resolve the structural mask.
764
+ const mask = document.querySelector<HTMLElement>(".hl-mask")!;
765
+ expect(
766
+ (
767
+ await resolveDomEditSelection(mask, {
768
+ activeCompositionPath: "index.html",
769
+ isMasterView: false,
770
+ preferClipAncestor: false,
771
+ })
772
+ )?.element,
773
+ ).toBe(mask);
774
+ });
775
+
742
776
  // fallow-ignore-next-line code-duplication
743
777
  it("collects simple child text blocks as separate editable fields", async () => {
744
778
  const document = createDocument(`
@@ -106,13 +106,13 @@ describe("manual editing availability", () => {
106
106
  expect(resolveStudioBooleanEnvFlag({ UNKNOWN: "maybe" }, ["UNKNOWN"], false)).toBe(false);
107
107
  });
108
108
 
109
- it("defaults the flat inspector flag to false and honors an explicit override", async () => {
110
- const off = await loadAvailabilityWithEnv({});
111
- expect(off.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(false);
109
+ it("defaults the flat inspector flag to true and honors an explicit override", async () => {
110
+ const on = await loadAvailabilityWithEnv({});
111
+ expect(on.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(true);
112
112
 
113
- const on = await loadAvailabilityWithEnv({
114
- VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "true",
113
+ const off = await loadAvailabilityWithEnv({
114
+ VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "false",
115
115
  });
116
- expect(on.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(true);
116
+ expect(off.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(false);
117
117
  });
118
118
  });