@hyperframes/studio 0.7.59 → 0.7.61

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 (78) hide show
  1. package/dist/assets/hyperframes-player-Xvx2hkrc.js +459 -0
  2. package/dist/assets/{index-CrkAdJkb.js → index-2mxh_HSy.js} +201 -201
  3. package/dist/assets/{index-B995FG46.js → index-BCpoiv9S.js} +1 -1
  4. package/dist/assets/index-BhWig0mx.css +1 -0
  5. package/dist/assets/{index-FvzmPhfG.js → index-D-uFclFj.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +1441 -770
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +4 -9
  11. package/src/components/DesignPanelPromoteProvider.tsx +27 -1
  12. package/src/components/StudioHeader.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +47 -28
  14. package/src/components/editor/PromotableControl.tsx +4 -2
  15. package/src/components/editor/PropertyPanel.tsx +151 -149
  16. package/src/components/editor/PropertyPanelFlat.tsx +44 -42
  17. package/src/components/editor/manualEditingAvailability.test.ts +6 -6
  18. package/src/components/editor/manualEditingAvailability.ts +5 -3
  19. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +2 -2
  20. package/src/components/editor/propertyPanelFlatMotionSection.tsx +1 -1
  21. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
  22. package/src/components/editor/propertyPanelFlatPrimitives.tsx +3 -2
  23. package/src/components/editor/propertyPanelFlatSelectRow.tsx +7 -1
  24. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
  25. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
  26. package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
  27. package/src/components/editor/propertyPanelFlatTextSection.tsx +3 -3
  28. package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
  29. package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
  30. package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
  31. package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
  32. package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
  33. package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
  34. package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
  35. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
  36. package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
  37. package/src/components/storyboard/storyboardReviewStage.ts +45 -0
  38. package/src/components/storyboard/useFrameComments.ts +22 -6
  39. package/src/contexts/FileManagerContext.tsx +3 -0
  40. package/src/contexts/PanelLayoutContext.tsx +3 -0
  41. package/src/contexts/ViewModeContext.tsx +60 -7
  42. package/src/hooks/timelineEditingHelpers.ts +2 -2
  43. package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
  44. package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
  45. package/src/hooks/useDomEditCommits.test.tsx +41 -1
  46. package/src/hooks/useDomEditCommits.ts +2 -2
  47. package/src/hooks/useDomEditSession.ts +1 -1
  48. package/src/hooks/useEditorSave.ts +1 -1
  49. package/src/hooks/useFileManager.projectOwnership.test.tsx +14 -3
  50. package/src/hooks/useFileManager.ts +76 -12
  51. package/src/hooks/usePanelLayout.test.ts +118 -0
  52. package/src/hooks/usePanelLayout.ts +24 -1
  53. package/src/hooks/usePreviewPersistence.ts +4 -1
  54. package/src/hooks/useRazorSplit.history.test.tsx +12 -4
  55. package/src/hooks/useRazorSplit.test.tsx +88 -0
  56. package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
  57. package/src/hooks/useRazorSplit.ts +50 -16
  58. package/src/hooks/useSlideshowTabState.test.ts +96 -0
  59. package/src/hooks/useSlideshowTabState.ts +61 -0
  60. package/src/hooks/useTimelineEditing.ts +2 -0
  61. package/src/hooks/useTimelineEditingTypes.ts +2 -1
  62. package/src/hooks/useTimelineGroupEditing.ts +1 -1
  63. package/src/player/lib/playbackTypes.ts +7 -0
  64. package/src/player/lib/runtimeProtocol.test.ts +6 -1
  65. package/src/player/lib/timelineDOM.ts +8 -11
  66. package/src/player/lib/timelineIframeHelpers.ts +153 -107
  67. package/src/utils/domEditSaveQueue.test.ts +19 -0
  68. package/src/utils/domEditSaveQueue.ts +2 -1
  69. package/src/utils/sdkCutover.test.ts +21 -5
  70. package/src/utils/sdkEditTransaction.ts +5 -4
  71. package/src/utils/studioFileHistory.ts +6 -4
  72. package/src/utils/studioFileVersion.test.ts +36 -0
  73. package/src/utils/studioFileVersion.ts +23 -0
  74. package/src/utils/studioSaveDiagnostics.test.ts +18 -0
  75. package/src/utils/studioSaveDiagnostics.ts +21 -0
  76. package/dist/assets/hyperframes-player-Bm07FLkl.js +0 -459
  77. package/dist/assets/index-Dj5p8U_A.css +0 -1
  78. 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 &&
@@ -388,48 +388,50 @@ export function PropertyPanelFlat({
388
388
  ),
389
389
  });
390
390
  }
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
- });
391
+ if (sections.layout) {
392
+ groups.push({
393
+ id: "layout",
394
+ title: "Layout",
395
+ // No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
396
+ // (wheel/arrow keys only) advertising "drag values to scrub" here lies.
397
+ summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`,
398
+ content: (
399
+ <FlatLayoutSection
400
+ element={element}
401
+ styles={styles}
402
+ onSetStyle={onSetStyle}
403
+ disabled={!element.capabilities.canEditStyles}
404
+ displayX={displayX}
405
+ displayY={displayY}
406
+ displayW={displayW}
407
+ displayH={displayH}
408
+ displayR={displayR}
409
+ manualOffsetEditingDisabled={manualOffsetEditingDisabled}
410
+ manualSizeEditingDisabled={manualSizeEditingDisabled}
411
+ manualRotationEditingDisabled={manualRotationEditingDisabled}
412
+ commitManualOffset={commitManualOffset}
413
+ commitManualSize={commitManualSize}
414
+ commitManualRotation={commitManualRotation}
415
+ gsapAnimId={gsapAnimId}
416
+ navKeyframes={navKeyframes}
417
+ currentPct={currentPct}
418
+ seekFromKfPct={seekFromKfPct}
419
+ animIdForProp={animIdForProp}
420
+ resolveAnimIdForProp={animIdForProp}
421
+ gsapRuntimeValues={gsapRuntimeValues}
422
+ gsapKeyframes={navKeyframes}
423
+ elStart={elStart}
424
+ elDuration={elDuration}
425
+ onCommitAnimatedProperty={onCommitAnimatedProperty}
426
+ onCommitAnimatedProperties={onCommitAnimatedProperties}
427
+ onSeekToTime={onSeekToTime}
428
+ onRemoveKeyframe={onRemoveKeyframe}
429
+ onConvertToKeyframes={onConvertToKeyframes}
430
+ onLivePreviewProps={createGsapLivePreview(previewIframeRef ?? { current: null })}
431
+ />
432
+ ),
433
+ });
434
+ }
433
435
  if (showMotionGroup) {
434
436
  groups.push({
435
437
  id: "motion",
@@ -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
  });
@@ -98,12 +98,14 @@ export const STUDIO_SDK_RESOLVER_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
98
98
  );
99
99
 
100
100
  // Studio inspector redesign ("Ledger, flat" — design_handoff_studio_inspector):
101
- // flat identity header/footer/groups behind a flag for incremental review.
102
- // Default false; enable via VITE_STUDIO_FLAT_INSPECTOR_ENABLED=true.
101
+ // flat identity header/footer/groups. Default true as of v0.7.59+ bug-fix pass
102
+ // (right-aligned values, Stroke select-only, promote-badge overlap, Layout/
103
+ // Style section gating); disable via VITE_STUDIO_FLAT_INSPECTOR_ENABLED=false
104
+ // to fall back to the legacy panel.
103
105
  export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
104
106
  env,
105
107
  ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
106
- false,
108
+ true,
107
109
  );
108
110
 
109
111
  export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
@@ -377,7 +377,7 @@ export function FlatColorGradingSection({
377
377
  track("select", "Custom LUT");
378
378
  applyLut(src || null, src && lut?.src === src ? lut.intensity : 1);
379
379
  }}
380
- className="bg-transparent font-mono text-[10px] text-panel-text-3 outline-none"
380
+ className="border-b border-panel-border-input/50 bg-transparent font-mono text-[10px] text-panel-text-3 outline-none hover:border-panel-border-input"
381
381
  >
382
382
  <option value="">None</option>
383
383
  {lutAssets.map((asset) => (
@@ -545,7 +545,7 @@ export function FlatColorGradingSection({
545
545
  onSetApplyScope(e.target.value as "source-file" | "project");
546
546
  }}
547
547
  disabled={applyBusy}
548
- className="bg-transparent font-mono text-[11px] text-panel-text-0 outline-none disabled:opacity-50"
548
+ className="border-b border-panel-border-input/50 bg-transparent font-mono text-[11px] text-panel-text-0 outline-none hover:border-panel-border-input disabled:opacity-50"
549
549
  >
550
550
  <option value="source-file">Current file media</option>
551
551
  <option value="project">All project media</option>
@@ -85,7 +85,7 @@ export function FlatTimingRow({
85
85
  const cell = (label: string, value: string, onCommit: (next: string) => void) => (
86
86
  <div className="grid gap-px">
87
87
  <span className="text-[9px] text-panel-text-4">{label}</span>
88
- <span className="border-b border-transparent font-mono text-[11px] text-panel-text-0 hover:border-panel-border-input">
88
+ <span className="border-b border-panel-border-input/50 font-mono text-[11px] text-panel-text-0 hover:border-panel-border-input">
89
89
  <CommitField
90
90
  value={value}
91
91
  onCommit={(next) => {
@@ -38,6 +38,16 @@ describe("FlatRow", () => {
38
38
  act(() => root.unmount());
39
39
  });
40
40
 
41
+ it("right-aligns the value input — the flat inspector's justify-between row layout leaves a left-aligned value looking stranded at the edge of its own box", () => {
42
+ const { host, root } = renderInto(
43
+ <FlatRow label="Size" value="72px" tier="explicitCustom" onCommit={vi.fn()} />,
44
+ );
45
+ const input = host.querySelector("input");
46
+ expect(input?.className).toContain("text-right");
47
+ expect(input?.className).not.toContain("text-left");
48
+ act(() => root.unmount());
49
+ });
50
+
41
51
  it("renders the explicitCustom tier with a mint value and a reset button", () => {
42
52
  const onReset = vi.fn();
43
53
  const { host, root } = renderInto(
@@ -43,14 +43,15 @@ export function FlatRow({
43
43
  data-flat-row-value="true"
44
44
  className={`min-w-0 border-b pb-px font-mono text-[11px] ${VALUE_TIER_VALUE_CLASS[tier]} ${
45
45
  tier === "explicitCustom"
46
- ? "border-transparent group-hover:border-panel-accent/35"
47
- : "border-transparent group-hover:border-panel-border-input"
46
+ ? "border-panel-accent/30 group-hover:border-panel-accent/70"
47
+ : "border-panel-border-input/50 group-hover:border-panel-border-input"
48
48
  }`}
49
49
  >
50
50
  <CommitField
51
51
  value={value}
52
52
  disabled={disabled}
53
53
  liveCommit={liveCommit}
54
+ align="right"
54
55
  onCommit={(nextValue) => {
55
56
  track("metric", label);
56
57
  onCommit(nextValue);
@@ -53,7 +53,13 @@ export function FlatSelectRow({
53
53
  <div className="group flex min-h-[30px] items-center justify-between">
54
54
  <span className={`text-[11px] ${VALUE_TIER_LABEL_CLASS[tier]}`}>{label}</span>
55
55
  <span className="flex items-center gap-2">
56
- <label className="flex items-center gap-1.5">
56
+ <label
57
+ className={`flex items-center gap-1.5 border-b pb-px ${
58
+ tier === "explicitCustom"
59
+ ? "border-panel-accent/30 group-hover:border-panel-accent/70"
60
+ : "border-panel-border-input/50 group-hover:border-panel-border-input"
61
+ }`}
62
+ >
57
63
  <select
58
64
  value={value}
59
65
  disabled={disabled}
@@ -12,16 +12,3 @@ export const STROKE_STYLE_OPTIONS: string[] = [
12
12
  "inset",
13
13
  "outset",
14
14
  ];
15
-
16
- export function formatStrokeSummary(widthPx: number, style: string): string {
17
- return `${widthPx}px ${style}`;
18
- }
19
-
20
- export function parseStrokeSummary(text: string): { widthPx: number; style: string } | null {
21
- const match = /^\s*(-?\d+(?:\.\d+)?)px\s+(\S+)\s*$/.exec(text);
22
- if (!match) return null;
23
- const widthPx = Number.parseFloat(match[1]);
24
- const style = match[2];
25
- if (!Number.isFinite(widthPx) || !style) return null;
26
- return { widthPx, style };
27
- }
@@ -175,31 +175,43 @@ function setInputValue(input: HTMLInputElement, nextValue: string) {
175
175
  }
176
176
 
177
177
  describe("FlatStyleSection — Stroke and Radius", () => {
178
- it("renders the combined stroke row and commits width+style together on blur", () => {
178
+ it("renders a width-only Stroke width row style is only ever set through the Stroke style select below it", () => {
179
179
  const { host, root } = renderSection(STROKE_STYLES);
180
- expect(host.textContent).toContain("Stroke");
181
- expect(getFlatRowInput(host, "Stroke").value).toBe("1px solid");
180
+ expect(host.textContent).toContain("Stroke width");
181
+ expect(host.textContent).toContain("Stroke style");
182
+ expect(getFlatRowInput(host, "Stroke width").value).toBe("1px");
182
183
  act(() => root.unmount());
183
184
  });
184
185
 
185
- it("commits the stroke row's new width and style together on blur", async () => {
186
+ it("commits a new stroke width without touching the existing style", async () => {
186
187
  const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
187
- await commitFlatRowInput(host, "Stroke", "2px dashed");
188
+ await commitFlatRowInput(host, "Stroke width", "2px");
188
189
  expect(onSetStyle).toHaveBeenCalledWith("border-width", "2px");
189
- expect(onSetStyle).toHaveBeenCalledWith("border-style", "dashed");
190
+ expect(onSetStyle).not.toHaveBeenCalledWith("border-style", expect.anything());
191
+ act(() => root.unmount());
192
+ });
193
+
194
+ it("defaults a from-zero width commit to solid, since a width with no style renders no visible border", async () => {
195
+ const { host, root, onSetStyle } = renderSection({
196
+ "border-width": "0px",
197
+ "border-style": "none",
198
+ "border-color": "rgba(255,255,255,.12)",
199
+ });
200
+ await commitFlatRowInput(host, "Stroke width", "3px");
201
+ expect(onSetStyle).toHaveBeenCalledWith("border-style", "solid");
190
202
  act(() => root.unmount());
191
203
  });
192
204
 
193
205
  it("clamps an out-of-range stroke width commit to 200px (fix 2)", async () => {
194
206
  const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
195
- await commitFlatRowInput(host, "Stroke", "9999px solid");
207
+ await commitFlatRowInput(host, "Stroke width", "9999px");
196
208
  expect(onSetStyle).toHaveBeenCalledWith("border-width", "200px");
197
209
  act(() => root.unmount());
198
210
  });
199
211
 
200
- it("rejects a stroke commit whose style token is not a valid border-style (fix 2)", async () => {
212
+ it("ignores an unparseable stroke width commit", async () => {
201
213
  const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
202
- await commitFlatRowInput(host, "Stroke", "12px bogus");
214
+ await commitFlatRowInput(host, "Stroke width", "bogus");
203
215
  expect(onSetStyle).not.toHaveBeenCalled();
204
216
  act(() => root.unmount());
205
217
  });