@hyperframes/studio 0.6.0-alpha.12 → 0.6.0-alpha.13
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-DjsVzYFP.js → hyperframes-player-DMgdgHZd.js} +1 -1
- package/dist/assets/index-B0OzpJPU.css +1 -0
- package/dist/assets/index-SEkerIt9.js +110 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +35 -302
- package/src/components/editor/DomEditOverlay.tsx +15 -1
- package/src/components/editor/PropertyPanel.tsx +158 -27
- package/src/components/editor/domEditing.ts +38 -5
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/nle/NLELayout.tsx +0 -10
- package/src/player/components/Player.tsx +19 -1
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +2 -83
- package/src/player/components/TimelineClip.tsx +9 -244
- package/dist/assets/index-FWg79aJz.css +0 -1
- package/dist/assets/index-xdyn_qRZ.js +0 -110
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -547,18 +547,65 @@ function MetricField({
|
|
|
547
547
|
value,
|
|
548
548
|
disabled,
|
|
549
549
|
liveCommit,
|
|
550
|
+
scrub,
|
|
550
551
|
onCommit,
|
|
551
552
|
}: {
|
|
552
553
|
label: string;
|
|
553
554
|
value: string;
|
|
554
555
|
disabled?: boolean;
|
|
555
556
|
liveCommit?: boolean;
|
|
557
|
+
scrub?: boolean;
|
|
556
558
|
onCommit: (nextValue: string) => void;
|
|
557
559
|
}) {
|
|
560
|
+
const scrubRef = useRef<{
|
|
561
|
+
startX: number;
|
|
562
|
+
startValue: number;
|
|
563
|
+
pointerId: number;
|
|
564
|
+
} | null>(null);
|
|
565
|
+
|
|
566
|
+
const handleScrubPointerDown = useCallback(
|
|
567
|
+
(e: React.PointerEvent<HTMLSpanElement>) => {
|
|
568
|
+
if (disabled || !scrub) return;
|
|
569
|
+
const parsed = parseFloat(value);
|
|
570
|
+
if (!Number.isFinite(parsed)) return;
|
|
571
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
572
|
+
scrubRef.current = { startX: e.clientX, startValue: parsed, pointerId: e.pointerId };
|
|
573
|
+
},
|
|
574
|
+
[disabled, scrub, value],
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const handleScrubPointerMove = useCallback(
|
|
578
|
+
(e: React.PointerEvent<HTMLSpanElement>) => {
|
|
579
|
+
const state = scrubRef.current;
|
|
580
|
+
if (!state) return;
|
|
581
|
+
const delta = e.clientX - state.startX;
|
|
582
|
+
const next = Math.round(state.startValue + delta);
|
|
583
|
+
onCommit(String(next));
|
|
584
|
+
},
|
|
585
|
+
[onCommit],
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const handleScrubPointerUp = useCallback(() => {
|
|
589
|
+
scrubRef.current = null;
|
|
590
|
+
}, []);
|
|
591
|
+
|
|
592
|
+
const scrubProps =
|
|
593
|
+
scrub && !disabled
|
|
594
|
+
? ({
|
|
595
|
+
className:
|
|
596
|
+
"flex-shrink-0 text-[11px] font-medium text-neutral-500 cursor-ew-resize select-none",
|
|
597
|
+
onPointerDown: handleScrubPointerDown,
|
|
598
|
+
onPointerMove: handleScrubPointerMove,
|
|
599
|
+
onPointerUp: handleScrubPointerUp,
|
|
600
|
+
} as const)
|
|
601
|
+
: ({
|
|
602
|
+
className: "flex-shrink-0 text-[11px] font-medium text-neutral-500",
|
|
603
|
+
} as const);
|
|
604
|
+
|
|
558
605
|
return (
|
|
559
606
|
<div className={FIELD}>
|
|
560
607
|
<div className="flex min-w-0 items-center gap-3">
|
|
561
|
-
<span
|
|
608
|
+
<span {...scrubProps}>{label}</span>
|
|
562
609
|
<CommitField
|
|
563
610
|
value={value}
|
|
564
611
|
disabled={disabled}
|
|
@@ -769,8 +816,8 @@ function fontSourceRank(source: FontSource): number {
|
|
|
769
816
|
if (source === "Current") return 0;
|
|
770
817
|
if (source === "Document") return 1;
|
|
771
818
|
if (source === "Imported") return 2;
|
|
772
|
-
if (source === "
|
|
773
|
-
if (source === "
|
|
819
|
+
if (source === "Google") return 3;
|
|
820
|
+
if (source === "Local") return 4;
|
|
774
821
|
return 5;
|
|
775
822
|
}
|
|
776
823
|
|
|
@@ -841,16 +888,44 @@ function loadImportedFontStylesheet(asset: ImportedFontAsset): void {
|
|
|
841
888
|
document.head.appendChild(style);
|
|
842
889
|
}
|
|
843
890
|
|
|
891
|
+
const ALL_WEIGHTS = ["100", "200", "300", "400", "500", "600", "700", "800", "900"];
|
|
892
|
+
const WEIGHT_LABELS: Record<string, string> = {
|
|
893
|
+
"100": "100 · Thin",
|
|
894
|
+
"200": "200 · Extra Light",
|
|
895
|
+
"300": "300 · Light",
|
|
896
|
+
"400": "400 · Regular",
|
|
897
|
+
"500": "500 · Medium",
|
|
898
|
+
"600": "600 · Semi Bold",
|
|
899
|
+
"700": "700 · Bold",
|
|
900
|
+
"800": "800 · Extra Bold",
|
|
901
|
+
"900": "900 · Black",
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
function detectAvailableWeights(fontFamily: string): string[] {
|
|
905
|
+
const fonts = document.fonts;
|
|
906
|
+
if (!fonts) return ALL_WEIGHTS;
|
|
907
|
+
const family = fontFamily.split(",")[0]?.trim().replace(/['"]/g, "");
|
|
908
|
+
if (!family) return ALL_WEIGHTS;
|
|
909
|
+
const available: string[] = [];
|
|
910
|
+
for (const w of ALL_WEIGHTS) {
|
|
911
|
+
if (fonts.check(`${w} 16px "${family}"`)) available.push(w);
|
|
912
|
+
}
|
|
913
|
+
return available.length > 0 ? available : ALL_WEIGHTS;
|
|
914
|
+
}
|
|
915
|
+
|
|
844
916
|
function FontWeightField({
|
|
845
917
|
value,
|
|
846
918
|
disabled,
|
|
919
|
+
fontFamily,
|
|
847
920
|
onCommit,
|
|
848
921
|
}: {
|
|
849
922
|
value: string;
|
|
850
923
|
disabled?: boolean;
|
|
924
|
+
fontFamily?: string;
|
|
851
925
|
onCommit: (nextValue: string) => void;
|
|
852
926
|
}) {
|
|
853
|
-
const options =
|
|
927
|
+
const options = fontFamily ? detectAvailableWeights(fontFamily) : ALL_WEIGHTS;
|
|
928
|
+
const displayOptions = value && !options.includes(value) ? [value, ...options] : options;
|
|
854
929
|
return (
|
|
855
930
|
<div className={FIELD}>
|
|
856
931
|
<div className="flex min-w-0 items-center gap-3">
|
|
@@ -861,9 +936,9 @@ function FontWeightField({
|
|
|
861
936
|
onChange={(e) => onCommit(e.target.value)}
|
|
862
937
|
className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
|
|
863
938
|
>
|
|
864
|
-
{
|
|
939
|
+
{displayOptions.map((option) => (
|
|
865
940
|
<option key={option} value={option}>
|
|
866
|
-
{option}
|
|
941
|
+
{WEIGHT_LABELS[option] ?? option}
|
|
867
942
|
</option>
|
|
868
943
|
))}
|
|
869
944
|
</select>
|
|
@@ -1022,13 +1097,20 @@ function FontFamilyField({
|
|
|
1022
1097
|
|
|
1023
1098
|
const options = useMemo(() => {
|
|
1024
1099
|
const documentFonts = collectDocumentFontFamilies();
|
|
1100
|
+
const googleSet = new Set(googleFonts.map((f) => f.toLowerCase()));
|
|
1101
|
+
const taggedLocalFonts = localFonts.map(
|
|
1102
|
+
(family): FontOption => ({
|
|
1103
|
+
family,
|
|
1104
|
+
source: googleSet.has(family.toLowerCase()) ? "Google" : "Local",
|
|
1105
|
+
}),
|
|
1106
|
+
);
|
|
1025
1107
|
return sortFontOptions(
|
|
1026
1108
|
uniqueFontOptions([
|
|
1027
1109
|
{ family: currentFamily, source: "Current" },
|
|
1028
1110
|
...documentFonts.map((family): FontOption => ({ family, source: "Document" })),
|
|
1029
1111
|
...projectFontAssets,
|
|
1030
|
-
...localFonts.map((family): FontOption => ({ family, source: "Local" })),
|
|
1031
1112
|
...googleFonts.map((family): FontOption => ({ family, source: "Google" })),
|
|
1113
|
+
...taggedLocalFonts,
|
|
1032
1114
|
...DEFAULT_FONT_FAMILIES.map((family): FontOption => ({ family, source: "System" })),
|
|
1033
1115
|
]),
|
|
1034
1116
|
);
|
|
@@ -1036,7 +1118,21 @@ function FontFamilyField({
|
|
|
1036
1118
|
|
|
1037
1119
|
const filteredOptions = useMemo(() => {
|
|
1038
1120
|
const matches = options.filter((option) => fontMatchesQuery(option.family, query));
|
|
1039
|
-
|
|
1121
|
+
if (query.trim()) return matches.slice(0, 200);
|
|
1122
|
+
const bySource = new Map<string, FontOption[]>();
|
|
1123
|
+
for (const m of matches) {
|
|
1124
|
+
const list = bySource.get(m.source) ?? [];
|
|
1125
|
+
list.push(m);
|
|
1126
|
+
bySource.set(m.source, list);
|
|
1127
|
+
}
|
|
1128
|
+
const result: FontOption[] = [];
|
|
1129
|
+
for (const s of ["Current", "Document", "Imported"]) {
|
|
1130
|
+
result.push(...(bySource.get(s) ?? []));
|
|
1131
|
+
}
|
|
1132
|
+
result.push(...(bySource.get("Google") ?? []).slice(0, 100));
|
|
1133
|
+
result.push(...(bySource.get("Local") ?? []).slice(0, 80));
|
|
1134
|
+
result.push(...(bySource.get("System") ?? []));
|
|
1135
|
+
return result;
|
|
1040
1136
|
}, [options, query]);
|
|
1041
1137
|
|
|
1042
1138
|
const importLocalFont = async (family: string): Promise<ImportedFontAsset | null> => {
|
|
@@ -1226,21 +1322,36 @@ function AdvancedTextControls({
|
|
|
1226
1322
|
return (
|
|
1227
1323
|
<div className="space-y-4">
|
|
1228
1324
|
<div className={RESPONSIVE_GRID}>
|
|
1229
|
-
<
|
|
1325
|
+
<SelectField
|
|
1230
1326
|
label="Line"
|
|
1231
1327
|
value={getTextStyleValue(field, inheritedStyles, "line-height", "normal")}
|
|
1232
1328
|
disabled={disabled}
|
|
1233
|
-
|
|
1234
|
-
|
|
1329
|
+
options={["normal", "1", "1.1", "1.2", "1.25", "1.3", "1.4", "1.5", "1.6", "1.75", "2"]}
|
|
1330
|
+
onChange={(next) =>
|
|
1235
1331
|
onCommit("line-height", normalizeTextMetricValue("line-height", next))
|
|
1236
1332
|
}
|
|
1237
1333
|
/>
|
|
1238
|
-
<
|
|
1334
|
+
<SelectField
|
|
1239
1335
|
label="Track"
|
|
1240
1336
|
value={getTextStyleValue(field, inheritedStyles, "letter-spacing", "0px")}
|
|
1241
1337
|
disabled={disabled}
|
|
1242
|
-
|
|
1243
|
-
|
|
1338
|
+
options={[
|
|
1339
|
+
"0px",
|
|
1340
|
+
"-0.05em",
|
|
1341
|
+
"-0.04em",
|
|
1342
|
+
"-0.03em",
|
|
1343
|
+
"-0.02em",
|
|
1344
|
+
"-0.01em",
|
|
1345
|
+
"0em",
|
|
1346
|
+
"0.01em",
|
|
1347
|
+
"0.02em",
|
|
1348
|
+
"0.03em",
|
|
1349
|
+
"0.05em",
|
|
1350
|
+
"0.1em",
|
|
1351
|
+
"0.15em",
|
|
1352
|
+
"0.2em",
|
|
1353
|
+
]}
|
|
1354
|
+
onChange={(next) =>
|
|
1244
1355
|
onCommit("letter-spacing", normalizeTextMetricValue("letter-spacing", next))
|
|
1245
1356
|
}
|
|
1246
1357
|
/>
|
|
@@ -1266,7 +1377,7 @@ function AdvancedTextControls({
|
|
|
1266
1377
|
value={getTextStyleValue(field, inheritedStyles, "font-style", "normal")}
|
|
1267
1378
|
disabled={disabled}
|
|
1268
1379
|
onChange={(next) => onCommit("font-style", next)}
|
|
1269
|
-
options={["normal", "italic"
|
|
1380
|
+
options={["normal", "italic"]}
|
|
1270
1381
|
/>
|
|
1271
1382
|
</div>
|
|
1272
1383
|
);
|
|
@@ -2245,6 +2356,16 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2245
2356
|
parsePxMetricValue(styles["border-width"] ?? "") ??
|
|
2246
2357
|
parsePxMetricValue(styles["border-top-width"] ?? "") ??
|
|
2247
2358
|
0;
|
|
2359
|
+
const hasVisualBackground =
|
|
2360
|
+
(styles.background != null && styles.background !== "none" && styles.background !== "") ||
|
|
2361
|
+
(styles["background-color"] != null &&
|
|
2362
|
+
styles["background-color"] !== "transparent" &&
|
|
2363
|
+
styles["background-color"] !== "rgba(0, 0, 0, 0)" &&
|
|
2364
|
+
styles["background-color"] !== "") ||
|
|
2365
|
+
(styles["background-image"] != null &&
|
|
2366
|
+
styles["background-image"] !== "none" &&
|
|
2367
|
+
styles["background-image"] !== "") ||
|
|
2368
|
+
borderWidthValue > 0;
|
|
2248
2369
|
const borderStyleValue = styles["border-style"] || styles["border-top-style"] || "none";
|
|
2249
2370
|
const borderColorValue =
|
|
2250
2371
|
styles["border-color"] || styles["border-top-color"] || "rgba(255, 255, 255, 0.18)";
|
|
@@ -2402,6 +2523,9 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2402
2523
|
styles["font-weight"] ||
|
|
2403
2524
|
"400"
|
|
2404
2525
|
}
|
|
2526
|
+
fontFamily={
|
|
2527
|
+
activeField.computedStyles["font-family"] || styles["font-family"]
|
|
2528
|
+
}
|
|
2405
2529
|
disabled={false}
|
|
2406
2530
|
onCommit={(next) =>
|
|
2407
2531
|
onSetTextFieldStyle(activeField.key, "font-weight", next)
|
|
@@ -2530,6 +2654,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2530
2654
|
/>
|
|
2531
2655
|
<FontWeightField
|
|
2532
2656
|
value={activeField.computedStyles["font-weight"] || "400"}
|
|
2657
|
+
fontFamily={activeField.computedStyles["font-family"]}
|
|
2533
2658
|
disabled={false}
|
|
2534
2659
|
onCommit={(next) =>
|
|
2535
2660
|
onSetTextFieldStyle(activeField.key, "font-weight", next)
|
|
@@ -2570,24 +2695,28 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2570
2695
|
label="X"
|
|
2571
2696
|
value={formatPxMetricValue(manualOffset.x)}
|
|
2572
2697
|
disabled={manualOffsetEditingDisabled}
|
|
2698
|
+
scrub
|
|
2573
2699
|
onCommit={(next) => commitManualOffset("x", next)}
|
|
2574
2700
|
/>
|
|
2575
2701
|
<MetricField
|
|
2576
2702
|
label="Y"
|
|
2577
2703
|
value={formatPxMetricValue(manualOffset.y)}
|
|
2578
2704
|
disabled={manualOffsetEditingDisabled}
|
|
2705
|
+
scrub
|
|
2579
2706
|
onCommit={(next) => commitManualOffset("y", next)}
|
|
2580
2707
|
/>
|
|
2581
2708
|
<MetricField
|
|
2582
2709
|
label="W"
|
|
2583
2710
|
value={formatPxMetricValue(resolvedWidth)}
|
|
2584
2711
|
disabled={manualSizeEditingDisabled}
|
|
2712
|
+
scrub
|
|
2585
2713
|
onCommit={(next) => commitManualSize("width", next)}
|
|
2586
2714
|
/>
|
|
2587
2715
|
<MetricField
|
|
2588
2716
|
label="H"
|
|
2589
2717
|
value={formatPxMetricValue(resolvedHeight)}
|
|
2590
2718
|
disabled={manualSizeEditingDisabled}
|
|
2719
|
+
scrub
|
|
2591
2720
|
onCommit={(next) => commitManualSize("height", next)}
|
|
2592
2721
|
/>
|
|
2593
2722
|
</div>
|
|
@@ -2640,18 +2769,20 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2640
2769
|
|
|
2641
2770
|
{showEditableSections && (
|
|
2642
2771
|
<>
|
|
2643
|
-
|
|
2644
|
-
<
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2772
|
+
{hasVisualBackground && (
|
|
2773
|
+
<Section title="Radius" icon={<Settings size={15} />}>
|
|
2774
|
+
<SliderControl
|
|
2775
|
+
value={radiusValue}
|
|
2776
|
+
min={0}
|
|
2777
|
+
max={Math.max(240, Math.ceil(radiusValue))}
|
|
2778
|
+
step={1}
|
|
2779
|
+
disabled={styleEditingDisabled}
|
|
2780
|
+
displayValue={`${formatNumericValue(radiusValue)}px`}
|
|
2781
|
+
formatDisplayValue={(next) => `${formatNumericValue(next)}px`}
|
|
2782
|
+
onCommit={(next) => onSetStyle("border-radius", `${formatNumericValue(next)}px`)}
|
|
2783
|
+
/>
|
|
2784
|
+
</Section>
|
|
2785
|
+
)}
|
|
2655
2786
|
|
|
2656
2787
|
<Section title="Stroke" icon={<Square size={15} />}>
|
|
2657
2788
|
<div className="space-y-4">
|
|
@@ -453,16 +453,49 @@ function getElementDepth(el: HTMLElement): number {
|
|
|
453
453
|
return depth;
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
const VISUAL_LEAF_TAGS = new Set(["img", "video", "canvas", "svg", "audio"]);
|
|
457
|
+
|
|
458
|
+
function isElementComputedVisible(el: HTMLElement): boolean {
|
|
459
|
+
const win = el.ownerDocument.defaultView;
|
|
460
|
+
if (!win) return true;
|
|
461
|
+
let current: HTMLElement | null = el;
|
|
462
|
+
while (current) {
|
|
463
|
+
const computed = win.getComputedStyle(current);
|
|
464
|
+
if (computed.display === "none" || computed.visibility === "hidden") return false;
|
|
465
|
+
const opacity = Number.parseFloat(computed.opacity);
|
|
466
|
+
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
|
|
467
|
+
current = current.parentElement;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function isEmptyVisualContainer(el: HTMLElement): boolean {
|
|
473
|
+
const tag = el.tagName.toLowerCase();
|
|
474
|
+
if (VISUAL_LEAF_TAGS.has(tag)) return false;
|
|
475
|
+
|
|
476
|
+
const children = el.children;
|
|
477
|
+
if (children.length === 0) {
|
|
478
|
+
const text = (el.textContent ?? "").trim();
|
|
479
|
+
return text.length === 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
483
|
+
const child = children[i];
|
|
484
|
+
if (!isHtmlElement(child)) continue;
|
|
485
|
+
if (VISUAL_LEAF_TAGS.has(child.tagName.toLowerCase())) return false;
|
|
486
|
+
if (isElementComputedVisible(child)) return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
456
492
|
function hasRenderedBox(el: HTMLElement): boolean {
|
|
457
493
|
const rect = el.getBoundingClientRect();
|
|
458
494
|
if (rect.width <= 1 || rect.height <= 1) return false;
|
|
459
495
|
|
|
460
|
-
|
|
461
|
-
if (!computed) return true;
|
|
462
|
-
if (computed.display === "none" || computed.visibility === "hidden") return false;
|
|
496
|
+
if (!isElementComputedVisible(el)) return false;
|
|
463
497
|
|
|
464
|
-
|
|
465
|
-
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
|
|
498
|
+
if (isEmptyVisualContainer(el)) return false;
|
|
466
499
|
|
|
467
500
|
return true;
|
|
468
501
|
}
|
|
@@ -16,10 +16,10 @@ describe("manual editing availability", () => {
|
|
|
16
16
|
vi.resetModules();
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it("enables inspector selection by default while motion
|
|
19
|
+
it("enables inspector selection and manual dragging by default while motion stays opt-in", async () => {
|
|
20
20
|
const availability = await loadAvailabilityWithEnv({});
|
|
21
21
|
|
|
22
|
-
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(
|
|
22
|
+
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(true);
|
|
23
23
|
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true);
|
|
24
24
|
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
|
|
25
25
|
expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false);
|
|
@@ -32,7 +32,7 @@ const env = import.meta.env as StudioFeatureFlagEnv;
|
|
|
32
32
|
export const STUDIO_PREVIEW_MANUAL_EDITING_ENABLED = resolveStudioBooleanEnvFlag(
|
|
33
33
|
env,
|
|
34
34
|
[STUDIO_PREVIEW_MANUAL_DRAGGING_ENV, "VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED"],
|
|
35
|
-
|
|
35
|
+
true,
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
|
|
@@ -52,9 +52,6 @@ interface NLELayoutProps {
|
|
|
52
52
|
) => Promise<void> | void;
|
|
53
53
|
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
54
54
|
onSelectTimelineElement?: (element: TimelineElement | null) => void;
|
|
55
|
-
onInspectTimelineElement?: (element: TimelineElement) => void;
|
|
56
|
-
inspectedTimelineElementId?: string | null;
|
|
57
|
-
timelineLayerChildCounts?: ReadonlyMap<string, number>;
|
|
58
55
|
/** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
|
|
59
56
|
onCompIdToSrcChange?: (map: Map<string, string>) => void;
|
|
60
57
|
/** Whether the timeline panel is visible (default: true) */
|
|
@@ -91,9 +88,6 @@ export const NLELayout = memo(function NLELayout({
|
|
|
91
88
|
onResizeElement,
|
|
92
89
|
onBlockedEditAttempt,
|
|
93
90
|
onSelectTimelineElement,
|
|
94
|
-
onInspectTimelineElement,
|
|
95
|
-
inspectedTimelineElementId,
|
|
96
|
-
timelineLayerChildCounts,
|
|
97
91
|
onCompIdToSrcChange,
|
|
98
92
|
timelineVisible,
|
|
99
93
|
onToggleTimeline,
|
|
@@ -460,10 +454,6 @@ export const NLELayout = memo(function NLELayout({
|
|
|
460
454
|
onResizeElement={onResizeElement}
|
|
461
455
|
onBlockedEditAttempt={onBlockedEditAttempt}
|
|
462
456
|
onSelectElement={onSelectTimelineElement}
|
|
463
|
-
onInspectElement={onInspectTimelineElement}
|
|
464
|
-
inspectedElementId={inspectedTimelineElementId}
|
|
465
|
-
layerChildCounts={timelineLayerChildCounts}
|
|
466
|
-
disabled={timelineDisabled}
|
|
467
457
|
/>
|
|
468
458
|
</div>
|
|
469
459
|
{timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
|
|
@@ -36,6 +36,8 @@ function getShaderTransitionLoading(event: Event): boolean | null {
|
|
|
36
36
|
return state.loading === true && state.ready !== true;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const COMPOSITION_LOADING_OVERLAY_DELAY_MS = 400;
|
|
40
|
+
|
|
39
41
|
export function shouldShowCompositionLoadingOverlay(compositionLoading: boolean): boolean {
|
|
40
42
|
return compositionLoading;
|
|
41
43
|
}
|
|
@@ -124,6 +126,20 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
124
126
|
const [assetOverlayFading, setAssetOverlayFading] = useState(false);
|
|
125
127
|
const [shaderTransitionLoading, setShaderTransitionLoading] = useState(false);
|
|
126
128
|
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
129
|
+
const [compositionOverlayDeferred, setCompositionOverlayDeferred] = useState(true);
|
|
130
|
+
|
|
131
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!compositionLoading) {
|
|
134
|
+
setCompositionOverlayDeferred(true);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const timer = setTimeout(
|
|
138
|
+
() => setCompositionOverlayDeferred(false),
|
|
139
|
+
COMPOSITION_LOADING_OVERLAY_DELAY_MS,
|
|
140
|
+
);
|
|
141
|
+
return () => clearTimeout(timer);
|
|
142
|
+
}, [compositionLoading]);
|
|
127
143
|
|
|
128
144
|
useMountEffect(() => {
|
|
129
145
|
const container = containerRef.current;
|
|
@@ -281,7 +297,9 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
281
297
|
}, [assetsLoading]);
|
|
282
298
|
|
|
283
299
|
const showCompositionOverlay =
|
|
284
|
-
!suppressLoadingOverlay &&
|
|
300
|
+
!suppressLoadingOverlay &&
|
|
301
|
+
!compositionOverlayDeferred &&
|
|
302
|
+
shouldShowCompositionLoadingOverlay(compositionLoading);
|
|
285
303
|
const showAssetOverlay =
|
|
286
304
|
assetOverlayVisible && !shaderTransitionLoading && !showCompositionOverlay;
|
|
287
305
|
|
|
@@ -12,8 +12,6 @@ import {
|
|
|
12
12
|
shouldHandleTimelineDeleteKey,
|
|
13
13
|
shouldAutoScrollTimeline,
|
|
14
14
|
} from "./Timeline";
|
|
15
|
-
import { TIMELINE_CLIP_CONTROL_Z_INDEX } from "./TimelineClip";
|
|
16
|
-
import { COMPOSITION_THUMBNAIL_LABEL_Z_INDEX } from "./CompositionThumbnail";
|
|
17
15
|
import { formatTime } from "../lib/time";
|
|
18
16
|
|
|
19
17
|
describe("generateTicks", () => {
|
|
@@ -166,12 +164,6 @@ describe("shouldAutoScrollTimeline", () => {
|
|
|
166
164
|
});
|
|
167
165
|
});
|
|
168
166
|
|
|
169
|
-
describe("timeline clip controls", () => {
|
|
170
|
-
it("renders layer controls above composition thumbnail chrome", () => {
|
|
171
|
-
expect(TIMELINE_CLIP_CONTROL_Z_INDEX).toBeGreaterThan(COMPOSITION_THUMBNAIL_LABEL_Z_INDEX);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
167
|
describe("getTimelineScrollLeftForZoomTransition", () => {
|
|
176
168
|
it("resets horizontal scroll when switching from manual zoom back to fit", () => {
|
|
177
169
|
expect(getTimelineScrollLeftForZoomTransition("manual", "fit", 480)).toBe(0);
|