@hyperframes/studio 0.8.51 → 0.8.53-desktop.0
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-Cn_PpHqP.js → hyperframes-player-PTEnAHhg.js} +1 -1
- package/dist/assets/{index-CPS3Cvj8.js → index--myXp8ui.js} +1 -1
- package/dist/assets/{index-Clrb11lj.js → index-BjslOS9g.js} +1 -1
- package/dist/assets/index-BnXZuIJD.css +1 -0
- package/dist/assets/index-CNyviK_l.js +390 -0
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +808 -12
- package/dist/index.html +2 -2
- package/dist/index.js +8109 -7583
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/App.tsx +45 -50
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +56 -56
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +117 -160
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +3 -7
- package/src/components/dock/Dock.test.tsx +100 -18
- package/src/components/dock/Dock.tsx +32 -5
- package/src/components/dock/dock.css +5 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +53 -6
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +21 -4
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +4 -2
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +3 -1
- package/src/components/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- package/src/components/nle/NLEContext.tsx +0 -43
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +1 -1
- package/src/components/nle/TimelinePane.tsx +53 -67
- package/src/components/panels/SlideshowPanel.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +1 -1
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/RenderQueue.tsx +1 -1
- package/src/components/renders/RenderQueuePanel.tsx +1 -1
- package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
- package/src/components/sidebar/CompositionsPanel.tsx +15 -0
- package/src/components/sidebar/{LeftSidebar.lintBadge.test.tsx → SidebarLintButton.test.tsx} +6 -11
- package/src/components/sidebar/SidebarLintButton.tsx +50 -0
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +222 -30
- package/src/components/ui/Button.tsx +88 -125
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -57
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- package/src/hooks/timelineEditPermission.ts +31 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
- package/src/hooks/useAppHotkeys.test.ts +0 -1
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
- package/src/hooks/useAppHotkeys.ts +20 -83
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useFileManager.ts +21 -123
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useProjectFileWriter.ts +150 -0
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +63 -0
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useSlideshowTabState.test.ts +9 -31
- package/src/hooks/useSlideshowTabState.ts +6 -19
- package/src/hooks/useStudioContextValue.test.ts +6 -18
- package/src/hooks/useStudioContextValue.ts +5 -26
- package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
- package/src/hooks/useTimelineEditing.test.tsx +322 -3
- package/src/hooks/useTimelineEditing.ts +103 -16
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
- package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
- package/src/hostMount.test.tsx +222 -0
- package/src/index.ts +87 -0
- package/src/player/components/AutomationSelectionMenu.tsx +1 -1
- package/src/player/components/Timeline.tsx +1 -1
- package/src/player/components/TimelineCanvas.tsx +4 -4
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +30 -30
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/theme.css +57 -0
- package/src/styles/theme.test.ts +2 -15
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -8
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +0 -16
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-C05SNrvY.js +0 -360
- package/dist/assets/index-DUR9qzQM.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- package/src/components/StudioLeftSidebar.tsx +0 -207
- package/src/components/nle/TimelineResizeDivider.tsx +0 -101
- package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
- package/src/components/sidebar/LeftSidebar.tsx +0 -348
- package/src/hooks/useInspectorSplitResize.ts +0 -51
- package/src/utils/fitPanels.test.ts +0 -164
- package/src/utils/fitPanels.ts +0 -151
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef } from "react";
|
|
2
|
-
import { MIN_PREVIEW_H, MIN_TIMELINE_H, fitTimelineHeight } from "../../utils/fitPanels";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Horizontal drag/keyboard-resizable divider between the preview and the
|
|
6
|
-
* timeline. Implements the separator pattern: ArrowUp grows the timeline,
|
|
7
|
-
* ArrowDown shrinks it (mirrors the drag direction).
|
|
8
|
-
*/
|
|
9
|
-
export function TimelineResizeDivider({
|
|
10
|
-
timelineH,
|
|
11
|
-
setTimelineH,
|
|
12
|
-
persistTimelineH,
|
|
13
|
-
containerRef,
|
|
14
|
-
disabled,
|
|
15
|
-
}: {
|
|
16
|
-
timelineH: number;
|
|
17
|
-
setTimelineH: React.Dispatch<React.SetStateAction<number>>;
|
|
18
|
-
persistTimelineH: (h: number) => void;
|
|
19
|
-
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
20
|
-
disabled: boolean;
|
|
21
|
-
}) {
|
|
22
|
-
const isDragging = useRef(false);
|
|
23
|
-
const timelineHRef = useRef(timelineH);
|
|
24
|
-
timelineHRef.current = timelineH;
|
|
25
|
-
|
|
26
|
-
const handlePointerDown = useCallback(
|
|
27
|
-
(e: React.PointerEvent) => {
|
|
28
|
-
if (disabled) return;
|
|
29
|
-
e.preventDefault();
|
|
30
|
-
isDragging.current = true;
|
|
31
|
-
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
32
|
-
},
|
|
33
|
-
[disabled],
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const handlePointerMove = useCallback(
|
|
37
|
-
(e: React.PointerEvent) => {
|
|
38
|
-
if (disabled) return;
|
|
39
|
-
if (!isDragging.current || !containerRef.current) return;
|
|
40
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
41
|
-
const mouseY = e.clientY - rect.top;
|
|
42
|
-
setTimelineH(fitTimelineHeight(rect.height, rect.height - mouseY));
|
|
43
|
-
},
|
|
44
|
-
[disabled, containerRef, setTimelineH],
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const handlePointerUp = useCallback(() => {
|
|
48
|
-
if (isDragging.current) persistTimelineH(timelineHRef.current);
|
|
49
|
-
isDragging.current = false;
|
|
50
|
-
}, [persistTimelineH]);
|
|
51
|
-
|
|
52
|
-
const handleKeyDown = useCallback(
|
|
53
|
-
(e: React.KeyboardEvent) => {
|
|
54
|
-
if (disabled) return;
|
|
55
|
-
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
56
|
-
e.preventDefault();
|
|
57
|
-
const containerH = containerRef.current?.getBoundingClientRect().height ?? 0;
|
|
58
|
-
const delta = e.key === "ArrowUp" ? 16 : -16;
|
|
59
|
-
setTimelineH((prev) => {
|
|
60
|
-
const next = fitTimelineHeight(containerH, prev + delta);
|
|
61
|
-
persistTimelineH(next);
|
|
62
|
-
return next;
|
|
63
|
-
});
|
|
64
|
-
},
|
|
65
|
-
[disabled, containerRef, setTimelineH, persistTimelineH],
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
// Horizontal resize divider: 3px visible seam (h-[3px]), 10px pointer-capture
|
|
70
|
-
// zone via the absolutely-positioned inner hit area so the layout gap stays
|
|
71
|
-
// at 3px while draggability is preserved over the full band.
|
|
72
|
-
<div
|
|
73
|
-
role="separator"
|
|
74
|
-
aria-orientation="horizontal"
|
|
75
|
-
aria-label="Resize timeline (arrow keys)"
|
|
76
|
-
aria-valuenow={Math.round(timelineH)}
|
|
77
|
-
aria-valuemin={MIN_TIMELINE_H}
|
|
78
|
-
aria-valuemax={Math.round(
|
|
79
|
-
(containerRef.current?.getBoundingClientRect().height ?? 600) - MIN_PREVIEW_H,
|
|
80
|
-
)}
|
|
81
|
-
tabIndex={0}
|
|
82
|
-
className="group relative h-[3px] shrink-0 cursor-row-resize z-10 outline-hidden focus-visible:bg-studio-accent/20"
|
|
83
|
-
style={{ touchAction: "none" }}
|
|
84
|
-
onPointerDown={handlePointerDown}
|
|
85
|
-
onPointerMove={handlePointerMove}
|
|
86
|
-
onPointerUp={handlePointerUp}
|
|
87
|
-
onPointerCancel={handlePointerUp}
|
|
88
|
-
onKeyDown={handleKeyDown}
|
|
89
|
-
>
|
|
90
|
-
{/* Expanded hit zone, deliberately asymmetric: 4px up into the transport
|
|
91
|
-
row's vertical centring slack, the 3px seam, then 3px down into the
|
|
92
|
-
timeline card's border + toolbar padding. It stops at 10px rather than
|
|
93
|
-
the 24px WCAG 2.2 (2.5.8) target because that is all the dead space
|
|
94
|
-
there is: 28px transport buttons sit above and 28px toolbar buttons
|
|
95
|
-
below, and silently stealing their clicks is the worse bug. */}
|
|
96
|
-
<div className="absolute inset-x-0 top-[-4px] h-[10px]" />
|
|
97
|
-
{/* Visible hairline — invisible at rest, subtle wash on hover/drag/focus */}
|
|
98
|
-
<div className="h-[3px] w-full bg-transparent transition-colors group-hover:bg-white/12 group-active:bg-white/18 group-focus-visible:bg-studio-accent/60" />
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
|
|
3
|
-
import { afterEach, expect, it, vi } from "vitest";
|
|
4
|
-
import { getPersistedTab } from "./LeftSidebar";
|
|
5
|
-
|
|
6
|
-
vi.mock("../../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
7
|
-
|
|
8
|
-
const originalDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
if (originalDescriptor) Object.defineProperty(globalThis, "localStorage", originalDescriptor);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("falls back to the default tab when localStorage is blocked", () => {
|
|
15
|
-
// Chrome throws on the property read itself when site data is blocked for
|
|
16
|
-
// the document. This runs as a useState initializer, so throwing here takes
|
|
17
|
-
// Studio to the crash boundary.
|
|
18
|
-
Object.defineProperty(globalThis, "localStorage", {
|
|
19
|
-
configurable: true,
|
|
20
|
-
get() {
|
|
21
|
-
throw new Error(
|
|
22
|
-
"Failed to read the 'localStorage' property from 'Window': Access is denied for this document.",
|
|
23
|
-
);
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
expect(getPersistedTab()).toBe("compositions");
|
|
28
|
-
});
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
memo,
|
|
3
|
-
useState,
|
|
4
|
-
useCallback,
|
|
5
|
-
useImperativeHandle,
|
|
6
|
-
useMemo,
|
|
7
|
-
useRef,
|
|
8
|
-
forwardRef,
|
|
9
|
-
type ReactNode,
|
|
10
|
-
} from "react";
|
|
11
|
-
import { CompositionsTab } from "./CompositionsTab";
|
|
12
|
-
import { AssetsTab } from "./AssetsTab";
|
|
13
|
-
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
14
|
-
import { safeLocalStorage } from "../../utils/safeStorage";
|
|
15
|
-
import { resolveMasterCompositionPath } from "../../utils/studioUrlState";
|
|
16
|
-
import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
|
|
17
|
-
import { FileTree } from "../editor/FileTree";
|
|
18
|
-
import { Tooltip } from "../ui";
|
|
19
|
-
|
|
20
|
-
export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
|
|
21
|
-
|
|
22
|
-
export interface LeftSidebarHandle {
|
|
23
|
-
selectTab: (tab: SidebarTab) => void;
|
|
24
|
-
getTab: () => SidebarTab;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const STORAGE_KEY = "hf-studio-sidebar-tab";
|
|
28
|
-
|
|
29
|
-
const SIDEBAR_TABS: Array<{ id: SidebarTab; label: string; tooltip: string }> = [
|
|
30
|
-
{ id: "code", label: "Code", tooltip: "Source code editor" },
|
|
31
|
-
{ id: "compositions", label: "Comps", tooltip: "Compositions and sub-compositions" },
|
|
32
|
-
{ id: "assets", label: "Assets", tooltip: "Videos, images, audio, fonts" },
|
|
33
|
-
{ id: "blocks", label: "Catalog", tooltip: "Browse blocks and components" },
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
// Both the `localStorage` reference and `getItem` itself can throw when the
|
|
37
|
-
// browsing context is partitioned or site data is blocked — the same case
|
|
38
|
-
// telemetry/config.ts documents. This runs as a `useState` initializer, so an
|
|
39
|
-
// unguarded throw here takes the whole editor to the crash boundary rather
|
|
40
|
-
// than losing one remembered tab.
|
|
41
|
-
export function getPersistedTab(): SidebarTab {
|
|
42
|
-
let stored: string | null = null;
|
|
43
|
-
try {
|
|
44
|
-
stored = safeLocalStorage()?.getItem(STORAGE_KEY) ?? null;
|
|
45
|
-
} catch {
|
|
46
|
-
/* storage unavailable — fall back to the default tab */
|
|
47
|
-
}
|
|
48
|
-
if (stored === "assets") return "assets";
|
|
49
|
-
if (stored === "code") return "code";
|
|
50
|
-
if (stored === "blocks") return "blocks";
|
|
51
|
-
return "compositions";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface LeftSidebarProps {
|
|
55
|
-
width?: number;
|
|
56
|
-
projectId: string;
|
|
57
|
-
compositions: string[];
|
|
58
|
-
assets: string[];
|
|
59
|
-
activeComposition: string | null;
|
|
60
|
-
onSelectComposition: (comp: string) => void;
|
|
61
|
-
onImportFiles?: (files: FileList, dir?: string) => void | Promise<void>;
|
|
62
|
-
fileTree?: string[];
|
|
63
|
-
editingFile?: { path: string; content: string | null } | null;
|
|
64
|
-
onSelectFile?: (path: string) => void;
|
|
65
|
-
onCreateFile?: (path: string) => void;
|
|
66
|
-
onCreateFolder?: (path: string) => void;
|
|
67
|
-
onDeleteFile?: (path: string) => void;
|
|
68
|
-
onRenameFile?: (oldPath: string, newPath: string) => void;
|
|
69
|
-
onDuplicateFile?: (path: string) => void;
|
|
70
|
-
onMoveFile?: (oldPath: string, newPath: string) => void;
|
|
71
|
-
codeChildren?: ReactNode;
|
|
72
|
-
onRenderComposition?: (comp: string) => void;
|
|
73
|
-
isRendering?: boolean;
|
|
74
|
-
onLint?: () => void;
|
|
75
|
-
linting?: boolean;
|
|
76
|
-
lintFindingCount?: number;
|
|
77
|
-
lintHasError?: boolean;
|
|
78
|
-
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
79
|
-
onToggleCollapse?: () => void;
|
|
80
|
-
onAddBlock?: (blockName: string) => void | Promise<void>;
|
|
81
|
-
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
82
|
-
takeoverContent?: ReactNode;
|
|
83
|
-
onAddAssetToTimeline?: (path: string) => void;
|
|
84
|
-
onAddCompositionToTimeline?: (path: string) => void;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const LeftSidebar = memo(
|
|
88
|
-
forwardRef<LeftSidebarHandle, LeftSidebarProps>(function LeftSidebar(
|
|
89
|
-
{
|
|
90
|
-
width = 240,
|
|
91
|
-
projectId,
|
|
92
|
-
compositions,
|
|
93
|
-
assets,
|
|
94
|
-
activeComposition,
|
|
95
|
-
onSelectComposition,
|
|
96
|
-
onImportFiles,
|
|
97
|
-
fileTree: fileProp,
|
|
98
|
-
editingFile,
|
|
99
|
-
onSelectFile,
|
|
100
|
-
onCreateFile,
|
|
101
|
-
onCreateFolder,
|
|
102
|
-
onDeleteFile,
|
|
103
|
-
onRenameFile,
|
|
104
|
-
onDuplicateFile,
|
|
105
|
-
onMoveFile,
|
|
106
|
-
codeChildren,
|
|
107
|
-
onRenderComposition,
|
|
108
|
-
isRendering,
|
|
109
|
-
onLint,
|
|
110
|
-
linting,
|
|
111
|
-
lintFindingCount,
|
|
112
|
-
lintHasError,
|
|
113
|
-
lintFindingsByFile,
|
|
114
|
-
onToggleCollapse,
|
|
115
|
-
onAddBlock,
|
|
116
|
-
onPreviewBlock,
|
|
117
|
-
takeoverContent,
|
|
118
|
-
onAddAssetToTimeline,
|
|
119
|
-
onAddCompositionToTimeline,
|
|
120
|
-
},
|
|
121
|
-
ref,
|
|
122
|
-
) {
|
|
123
|
-
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
|
|
124
|
-
const tabRef = useRef(tab);
|
|
125
|
-
tabRef.current = tab;
|
|
126
|
-
const tablistRef = useRef<HTMLDivElement>(null);
|
|
127
|
-
const masterCompositionPath = useMemo(
|
|
128
|
-
() => resolveMasterCompositionPath(compositions),
|
|
129
|
-
[compositions],
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const selectTab = useCallback((t: SidebarTab) => {
|
|
133
|
-
setTab(t);
|
|
134
|
-
try {
|
|
135
|
-
safeLocalStorage()?.setItem(STORAGE_KEY, t);
|
|
136
|
-
} catch {
|
|
137
|
-
/* storage unavailable — the tab just won't be remembered */
|
|
138
|
-
}
|
|
139
|
-
trackStudioEvent("tab_switch", { panel: "left_sidebar", tab: t });
|
|
140
|
-
}, []);
|
|
141
|
-
|
|
142
|
-
const getTab = useCallback(() => tabRef.current, []);
|
|
143
|
-
|
|
144
|
-
useImperativeHandle(ref, () => ({ selectTab, getTab }), [selectTab, getTab]);
|
|
145
|
-
|
|
146
|
-
// APG tabs pattern: Left/Right move focus AND selection between tabs.
|
|
147
|
-
const handleTablistKeyDown = useCallback(
|
|
148
|
-
(e: React.KeyboardEvent) => {
|
|
149
|
-
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
150
|
-
e.preventDefault();
|
|
151
|
-
const ids = SIDEBAR_TABS.map((t) => t.id);
|
|
152
|
-
const idx = ids.indexOf(tabRef.current);
|
|
153
|
-
const next = ids[(idx + (e.key === "ArrowRight" ? 1 : -1) + ids.length) % ids.length];
|
|
154
|
-
selectTab(next);
|
|
155
|
-
tablistRef.current?.querySelector<HTMLButtonElement>(`[data-tab-id="${next}"]`)?.focus();
|
|
156
|
-
},
|
|
157
|
-
[selectTab],
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<div
|
|
162
|
-
className="flex flex-col h-full overflow-hidden rounded-lg border border-neutral-800/50 bg-neutral-950"
|
|
163
|
-
style={{ width }}
|
|
164
|
-
>
|
|
165
|
-
{takeoverContent ? (
|
|
166
|
-
<div className="flex min-h-0 flex-1">{takeoverContent}</div>
|
|
167
|
-
) : (
|
|
168
|
-
<>
|
|
169
|
-
{/* Tabs — Code first */}
|
|
170
|
-
<div className="border-b border-neutral-800/50 px-3 py-3 shrink-0">
|
|
171
|
-
<div className="flex items-center gap-2">
|
|
172
|
-
<div
|
|
173
|
-
ref={tablistRef}
|
|
174
|
-
role="tablist"
|
|
175
|
-
aria-label="Sidebar panels"
|
|
176
|
-
onKeyDown={handleTablistKeyDown}
|
|
177
|
-
className="grid min-w-0 flex-1 gap-0.5 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
|
178
|
-
style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}
|
|
179
|
-
>
|
|
180
|
-
{SIDEBAR_TABS.map((t) => (
|
|
181
|
-
<Tooltip key={t.id} label={t.tooltip} side="bottom">
|
|
182
|
-
<button
|
|
183
|
-
type="button"
|
|
184
|
-
role="tab"
|
|
185
|
-
data-tab-id={t.id}
|
|
186
|
-
aria-selected={tab === t.id}
|
|
187
|
-
aria-controls={`sidebar-panel-${t.id}`}
|
|
188
|
-
tabIndex={tab === t.id ? 0 : -1}
|
|
189
|
-
onClick={() => selectTab(t.id)}
|
|
190
|
-
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all active:scale-[0.97] ${
|
|
191
|
-
tab === t.id
|
|
192
|
-
? "bg-neutral-800 text-white"
|
|
193
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
194
|
-
}`}
|
|
195
|
-
>
|
|
196
|
-
{t.label}
|
|
197
|
-
</button>
|
|
198
|
-
</Tooltip>
|
|
199
|
-
))}
|
|
200
|
-
</div>
|
|
201
|
-
{onToggleCollapse && (
|
|
202
|
-
<button
|
|
203
|
-
type="button"
|
|
204
|
-
onClick={onToggleCollapse}
|
|
205
|
-
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
|
|
206
|
-
title="Hide sidebar"
|
|
207
|
-
aria-label="Hide sidebar"
|
|
208
|
-
>
|
|
209
|
-
<svg
|
|
210
|
-
width="14"
|
|
211
|
-
height="14"
|
|
212
|
-
viewBox="0 0 24 24"
|
|
213
|
-
fill="none"
|
|
214
|
-
stroke="currentColor"
|
|
215
|
-
strokeWidth="1.5"
|
|
216
|
-
strokeLinecap="round"
|
|
217
|
-
strokeLinejoin="round"
|
|
218
|
-
aria-hidden="true"
|
|
219
|
-
>
|
|
220
|
-
<path d="m14 7-5 5 5 5" />
|
|
221
|
-
<path d="M19 4v16" />
|
|
222
|
-
</svg>
|
|
223
|
-
</button>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
|
|
228
|
-
{/* Tab content */}
|
|
229
|
-
{tab === "compositions" && (
|
|
230
|
-
<div
|
|
231
|
-
id="sidebar-panel-compositions"
|
|
232
|
-
role="tabpanel"
|
|
233
|
-
className="flex flex-col flex-1 min-h-0"
|
|
234
|
-
>
|
|
235
|
-
<CompositionsTab
|
|
236
|
-
projectId={projectId}
|
|
237
|
-
compositions={compositions}
|
|
238
|
-
activeComposition={activeComposition}
|
|
239
|
-
masterCompositionPath={masterCompositionPath}
|
|
240
|
-
onSelect={onSelectComposition}
|
|
241
|
-
onAddToTimeline={onAddCompositionToTimeline}
|
|
242
|
-
onRenderComposition={onRenderComposition}
|
|
243
|
-
isRendering={isRendering}
|
|
244
|
-
lintFindingsByFile={lintFindingsByFile}
|
|
245
|
-
/>
|
|
246
|
-
</div>
|
|
247
|
-
)}
|
|
248
|
-
{tab === "assets" && (
|
|
249
|
-
<div
|
|
250
|
-
id="sidebar-panel-assets"
|
|
251
|
-
role="tabpanel"
|
|
252
|
-
className="flex flex-col flex-1 min-h-0"
|
|
253
|
-
>
|
|
254
|
-
<AssetsTab
|
|
255
|
-
projectId={projectId}
|
|
256
|
-
assets={assets}
|
|
257
|
-
onImport={onImportFiles}
|
|
258
|
-
onDelete={onDeleteFile}
|
|
259
|
-
onRename={onRenameFile}
|
|
260
|
-
onAddAssetToTimeline={onAddAssetToTimeline}
|
|
261
|
-
/>
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
{tab === "code" && (
|
|
265
|
-
<div id="sidebar-panel-code" role="tabpanel" className="flex flex-1 min-h-0">
|
|
266
|
-
{(fileProp?.length ?? 0) > 0 && (
|
|
267
|
-
<div className="w-[160px] shrink-0 border-r border-neutral-800 overflow-y-auto">
|
|
268
|
-
<FileTree
|
|
269
|
-
files={fileProp ?? []}
|
|
270
|
-
activeFile={editingFile?.path ?? null}
|
|
271
|
-
onSelectFile={onSelectFile ?? (() => {})}
|
|
272
|
-
onCreateFile={onCreateFile}
|
|
273
|
-
onCreateFolder={onCreateFolder}
|
|
274
|
-
onDeleteFile={onDeleteFile}
|
|
275
|
-
onRenameFile={onRenameFile}
|
|
276
|
-
onDuplicateFile={onDuplicateFile}
|
|
277
|
-
onMoveFile={onMoveFile}
|
|
278
|
-
onImportFiles={onImportFiles}
|
|
279
|
-
lintFindingsByFile={lintFindingsByFile}
|
|
280
|
-
/>
|
|
281
|
-
</div>
|
|
282
|
-
)}
|
|
283
|
-
<div className="flex-1 overflow-hidden min-w-0">
|
|
284
|
-
{codeChildren ?? (
|
|
285
|
-
<div className="flex items-center justify-center h-full text-neutral-600 text-sm">
|
|
286
|
-
Select a file to edit
|
|
287
|
-
</div>
|
|
288
|
-
)}
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
)}
|
|
292
|
-
|
|
293
|
-
{tab === "blocks" && (
|
|
294
|
-
<div
|
|
295
|
-
id="sidebar-panel-blocks"
|
|
296
|
-
role="tabpanel"
|
|
297
|
-
className="flex flex-col flex-1 min-h-0"
|
|
298
|
-
>
|
|
299
|
-
<BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
|
|
300
|
-
</div>
|
|
301
|
-
)}
|
|
302
|
-
|
|
303
|
-
{/* Lint button pinned at the bottom */}
|
|
304
|
-
{onLint && (
|
|
305
|
-
<div className="border-t border-neutral-800 p-2 shrink-0">
|
|
306
|
-
<button
|
|
307
|
-
onClick={onLint}
|
|
308
|
-
disabled={linting}
|
|
309
|
-
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 enabled:hover:text-amber-300 enabled:hover:bg-neutral-800 enabled:active:scale-[0.98] transition-colors disabled:opacity-40"
|
|
310
|
-
>
|
|
311
|
-
<svg
|
|
312
|
-
width="12"
|
|
313
|
-
height="12"
|
|
314
|
-
viewBox="0 0 24 24"
|
|
315
|
-
fill="none"
|
|
316
|
-
stroke="currentColor"
|
|
317
|
-
strokeWidth="2"
|
|
318
|
-
>
|
|
319
|
-
<path d="M9 11l3 3L22 4" />
|
|
320
|
-
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
|
|
321
|
-
</svg>
|
|
322
|
-
{linting ? "Linting…" : "Lint"}
|
|
323
|
-
{!linting && lintFindingCount != null && lintFindingCount > 0 && (
|
|
324
|
-
<span
|
|
325
|
-
data-lint-badge={lintHasError ? "error" : "warning"}
|
|
326
|
-
className={
|
|
327
|
-
lintHasError
|
|
328
|
-
? "ml-1 min-w-[16px] rounded-full bg-panel-danger/25 px-1 text-[9px] font-bold text-panel-danger animate-pulse motion-reduce:animate-none"
|
|
329
|
-
: "ml-1 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[9px] font-bold text-amber-400"
|
|
330
|
-
}
|
|
331
|
-
>
|
|
332
|
-
{lintFindingCount}
|
|
333
|
-
<span className="sr-only">
|
|
334
|
-
{lintHasError
|
|
335
|
-
? " lint findings, including errors"
|
|
336
|
-
: " lint findings, warnings only"}
|
|
337
|
-
</span>
|
|
338
|
-
</span>
|
|
339
|
-
)}
|
|
340
|
-
</button>
|
|
341
|
-
</div>
|
|
342
|
-
)}
|
|
343
|
-
</>
|
|
344
|
-
)}
|
|
345
|
-
</div>
|
|
346
|
-
);
|
|
347
|
-
}),
|
|
348
|
-
);
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
|
2
|
-
|
|
3
|
-
const MIN_INSPECTOR_SPLIT_PERCENT = 20;
|
|
4
|
-
const MAX_INSPECTOR_SPLIT_PERCENT = 75;
|
|
5
|
-
|
|
6
|
-
export function useInspectorSplitResize() {
|
|
7
|
-
const [layersPanePercent, setLayersPanePercent] = useState(40);
|
|
8
|
-
const splitContainerRef = useRef<HTMLDivElement>(null);
|
|
9
|
-
const splitDragRef = useRef<{
|
|
10
|
-
startY: number;
|
|
11
|
-
startPercent: number;
|
|
12
|
-
height: number;
|
|
13
|
-
} | null>(null);
|
|
14
|
-
|
|
15
|
-
const handleInspectorSplitResizeStart = useCallback(
|
|
16
|
-
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
17
|
-
event.preventDefault();
|
|
18
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
19
|
-
const height = splitContainerRef.current?.getBoundingClientRect().height ?? 0;
|
|
20
|
-
splitDragRef.current = {
|
|
21
|
-
startY: event.clientY,
|
|
22
|
-
startPercent: layersPanePercent,
|
|
23
|
-
height,
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
[layersPanePercent],
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const handleInspectorSplitResizeMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
|
30
|
-
const drag = splitDragRef.current;
|
|
31
|
-
if (!drag || drag.height <= 0) return;
|
|
32
|
-
const deltaPercent = ((event.clientY - drag.startY) / drag.height) * 100;
|
|
33
|
-
const next = Math.min(
|
|
34
|
-
MAX_INSPECTOR_SPLIT_PERCENT,
|
|
35
|
-
Math.max(MIN_INSPECTOR_SPLIT_PERCENT, drag.startPercent + deltaPercent),
|
|
36
|
-
);
|
|
37
|
-
setLayersPanePercent(next);
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
const handleInspectorSplitResizeEnd = useCallback(() => {
|
|
41
|
-
splitDragRef.current = null;
|
|
42
|
-
}, []);
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
layersPanePercent,
|
|
46
|
-
splitContainerRef,
|
|
47
|
-
handleInspectorSplitResizeStart,
|
|
48
|
-
handleInspectorSplitResizeMove,
|
|
49
|
-
handleInspectorSplitResizeEnd,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
MIN_PREVIEW_H,
|
|
4
|
-
MIN_PREVIEW_W,
|
|
5
|
-
MIN_TIMELINE_H,
|
|
6
|
-
RAIL_W,
|
|
7
|
-
defaultPanelWidths,
|
|
8
|
-
fitPanelWidths,
|
|
9
|
-
fitTimelineHeight,
|
|
10
|
-
} from "./fitPanels";
|
|
11
|
-
|
|
12
|
-
function fitAt(viewportWidth: number) {
|
|
13
|
-
return fitPanelWidths(viewportWidth, defaultPanelWidths(viewportWidth));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("defaultPanelWidths", () => {
|
|
17
|
-
// Frozen literals, deliberately NOT compared against the live hook: the hook
|
|
18
|
-
// now delegates here, so asserting against it would be circular and would
|
|
19
|
-
// silently stop guarding the "wide windows are untouched" promise.
|
|
20
|
-
it.each([
|
|
21
|
-
[1680, 384, 424],
|
|
22
|
-
[1512, 384, 424],
|
|
23
|
-
[1280, 329, 364],
|
|
24
|
-
])("keeps %ipx identical to the pre-change defaults", (vw, left, right) => {
|
|
25
|
-
expect(defaultPanelWidths(vw)).toEqual({ left, right });
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("fitPanelWidths", () => {
|
|
30
|
-
it.each([1680, 1512, 1280])("leaves %ipx untouched by reconciliation", (vw) => {
|
|
31
|
-
const preferred = defaultPanelWidths(vw);
|
|
32
|
-
const fitted = fitPanelWidths(vw, preferred);
|
|
33
|
-
expect(fitted.left).toBe(preferred.left);
|
|
34
|
-
expect(fitted.right).toBe(preferred.right);
|
|
35
|
-
expect(fitted.autoCollapseLeft).toBe(false);
|
|
36
|
-
expect(fitted.autoCollapseRight).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it.each([1680, 1512, 1280, 1100, 960, 860, 760, 640, 560, 480])(
|
|
40
|
-
"never starves the preview at %ipx",
|
|
41
|
-
(vw) => {
|
|
42
|
-
expect(fitAt(vw).preview).toBeGreaterThanOrEqual(MIN_PREVIEW_W);
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
it("matches the projected preview widths from the plan", () => {
|
|
47
|
-
const projected: Array<[number, number]> = [
|
|
48
|
-
[1680, 866],
|
|
49
|
-
[1512, 698],
|
|
50
|
-
[1280, 581],
|
|
51
|
-
[1100, 499],
|
|
52
|
-
[960, 427],
|
|
53
|
-
[860, 360],
|
|
54
|
-
[760, 432],
|
|
55
|
-
[640, 592],
|
|
56
|
-
[560, 512],
|
|
57
|
-
[480, 432],
|
|
58
|
-
];
|
|
59
|
-
for (const [vw, preview] of projected) {
|
|
60
|
-
expect({ vw, preview: fitAt(vw).preview }).toEqual({ vw, preview });
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("fits everything at its minimum at exactly 860, the derived threshold", () => {
|
|
65
|
-
const fitted = fitAt(860);
|
|
66
|
-
expect(fitted).toMatchObject({
|
|
67
|
-
left: 214,
|
|
68
|
-
right: 280,
|
|
69
|
-
preview: MIN_PREVIEW_W,
|
|
70
|
-
autoCollapseLeft: false,
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("rails the sidebar one pixel below the threshold", () => {
|
|
75
|
-
expect(fitAt(860).autoCollapseLeft).toBe(false);
|
|
76
|
-
expect(fitAt(859).autoCollapseLeft).toBe(true);
|
|
77
|
-
expect(fitAt(859).left).toBe(RAIL_W);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("hides the inspector one pixel below its own threshold", () => {
|
|
81
|
-
expect(fitAt(700).autoCollapseRight).toBe(false);
|
|
82
|
-
expect(fitAt(700).right).toBeGreaterThan(0);
|
|
83
|
-
expect(fitAt(699).autoCollapseRight).toBe(true);
|
|
84
|
-
expect(fitAt(699).right).toBe(0);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("never drives an EXPANDED sidebar below its usable minimum", () => {
|
|
88
|
-
// Guards the leftFloor/rightFloor split: the squeeze fallback may only reach
|
|
89
|
-
// the rail width when the panel is actually railed.
|
|
90
|
-
const fitted = fitPanelWidths(900, { left: 600, right: 600 });
|
|
91
|
-
expect(fitted.left).toBeGreaterThanOrEqual(200);
|
|
92
|
-
expect(fitted.right).toBeGreaterThanOrEqual(280);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("caps a single panel at 40% of the window", () => {
|
|
96
|
-
expect(fitPanelWidths(1600, { left: 900, right: 280 }).left).toBe(640);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("lets the preview floor win when the cap alone is not enough", () => {
|
|
100
|
-
// 40% of 1000 is 400, but 400 + 280 + 6 leaves the preview at 314.
|
|
101
|
-
const fitted = fitPanelWidths(1000, { left: 900, right: 280 });
|
|
102
|
-
expect(fitted.left).toBe(354);
|
|
103
|
-
expect(fitted.preview).toBe(MIN_PREVIEW_W);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("survives a viewport narrower than the preview floor alone", () => {
|
|
107
|
-
const fitted = fitAt(300);
|
|
108
|
-
expect(fitted.preview).toBeGreaterThanOrEqual(0);
|
|
109
|
-
expect(Number.isNaN(fitted.preview)).toBe(false);
|
|
110
|
-
expect(fitted.left).toBe(RAIL_W);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("returns preferences untouched before the shell is measured", () => {
|
|
114
|
-
const preferred = { left: 320, right: 400 };
|
|
115
|
-
expect(fitPanelWidths(0, preferred)).toMatchObject({ ...preferred, preview: 0 });
|
|
116
|
-
expect(fitPanelWidths(Number.NaN, preferred)).toMatchObject(preferred);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("is idempotent, so a resize loop cannot oscillate", () => {
|
|
120
|
-
for (const vw of [1512, 1100, 860, 760, 560]) {
|
|
121
|
-
const once = fitPanelWidths(vw, defaultPanelWidths(vw));
|
|
122
|
-
const twice = fitPanelWidths(vw, once);
|
|
123
|
-
expect(twice).toEqual(once);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("fitTimelineHeight", () => {
|
|
129
|
-
it("keeps a height that fits", () => {
|
|
130
|
-
expect(fitTimelineHeight(717, 429)).toBe(429);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("reclaims height for the preview when the shell shrinks", () => {
|
|
134
|
-
// The measured bug: mounted at 760 tall, dragged to 520. Preview was 47px.
|
|
135
|
-
expect(fitTimelineHeight(477, 429)).toBe(477 - MIN_PREVIEW_H);
|
|
136
|
-
expect(477 - fitTimelineHeight(477, 429)).toBeGreaterThanOrEqual(MIN_PREVIEW_H);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("raises a too-small preference to the timeline minimum", () => {
|
|
140
|
-
expect(fitTimelineHeight(717, 10)).toBe(MIN_TIMELINE_H);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("keeps the timeline usable when the shell is shorter than both minimums", () => {
|
|
144
|
-
expect(fitTimelineHeight(120, 429)).toBe(MIN_TIMELINE_H);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("returns the preference before the shell is measured", () => {
|
|
148
|
-
expect(fitTimelineHeight(0, 429)).toBe(429);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("is idempotent", () => {
|
|
152
|
-
const once = fitTimelineHeight(477, 429);
|
|
153
|
-
expect(fitTimelineHeight(477, once)).toBe(once);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("would return the preferred height if callers kept one", () => {
|
|
157
|
-
// The helper is preference-preserving; the vertical CALLER still stores the
|
|
158
|
-
// clamped value, so a shrink-then-grow does not restore the old height the
|
|
159
|
-
// way panel widths do. Tracked as a known asymmetry, not fixed here.
|
|
160
|
-
const preferred = 429;
|
|
161
|
-
expect(fitTimelineHeight(477, preferred)).toBe(277);
|
|
162
|
-
expect(fitTimelineHeight(717, preferred)).toBe(429);
|
|
163
|
-
});
|
|
164
|
-
});
|