@hyperframes/studio 0.8.50 → 0.8.52
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-CxxDxHTW.js → hyperframes-player-I5YRzB02.js} +3 -3
- package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
- package/dist/assets/index-CxAu_Okg.css +1 -0
- package/dist/assets/index-DocJlW2v.js +376 -0
- package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
- package/dist/index.d.ts +419 -20
- package/dist/index.html +2 -2
- package/dist/index.js +7496 -8288
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +13 -12
- package/src/App.tsx +131 -143
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/captions/components/CaptionOverlayUtils.ts +2 -3
- package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
- package/src/captions/components/CaptionTimeline.tsx +1 -1
- package/src/captions/components/shared.tsx +2 -2
- package/src/components/AskAgentModal.tsx +5 -5
- package/src/components/EditorShell.tsx +8 -7
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.tsx +110 -210
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +1 -1
- package/src/components/StudioRightPanel.tsx +51 -39
- package/src/components/StudioToast.tsx +2 -2
- 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 +8 -12
- package/src/components/dock/Dock.test.tsx +260 -0
- package/src/components/dock/Dock.tsx +281 -0
- package/src/components/dock/dock.css +28 -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 +105 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.ts +91 -0
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/dock/panelRegistry.ts +67 -0
- package/src/components/editor/AnimationCard.tsx +4 -4
- package/src/components/editor/AnimationCardParts.tsx +6 -6
- package/src/components/editor/BlockParamsPanel.tsx +4 -4
- package/src/components/editor/BorderRadiusEditor.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +1 -1
- package/src/components/editor/EaseParamFields.tsx +5 -5
- package/src/components/editor/FileTree.tsx +4 -4
- package/src/components/editor/FileTreeIcons.tsx +1 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -13
- package/src/components/editor/InlineTextToolbar.tsx +1 -1
- package/src/components/editor/InspectorHeaderActions.tsx +3 -3
- package/src/components/editor/KeyframeDiamond.tsx +1 -1
- package/src/components/editor/KeyframeEaseList.tsx +1 -1
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +7 -5
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +2 -2
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +7 -5
- package/src/components/editor/Transform3DCube.tsx +2 -2
- package/src/components/editor/audioFxRevealTarget.ts +1 -0
- package/src/components/editor/domEditing.test.ts +1 -1
- package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
- package/src/components/editor/domEditingAgentPrompt.ts +4 -3
- package/src/components/editor/propertyPanelColor.tsx +11 -11
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
- package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
- package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +4 -4
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
- package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
- package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +5 -5
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +6 -6
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
- package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
- package/src/components/editor/propertyPanelSections.tsx +8 -8
- 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/feedback/StudioFeedbackCard.tsx +7 -7
- package/src/components/feedback/feedbackTrigger.ts +0 -1
- package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
- package/src/components/nle/NLEPreview.tsx +3 -3
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/TimelinePane.tsx +3 -3
- package/src/components/nle/TimelineResizeDivider.tsx +2 -2
- package/src/components/panels/SlideshowPanel.tsx +2 -2
- package/src/components/panels/SlideshowSubPanels.tsx +20 -17
- package/src/components/panels/VariablesBindElement.tsx +2 -2
- package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +7 -7
- package/src/components/renders/RenderQueueItem.tsx +6 -6
- package/src/components/sidebar/AssetCard.tsx +8 -8
- package/src/components/sidebar/AssetContextMenu.tsx +8 -8
- package/src/components/sidebar/AssetsTab.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +6 -6
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
- package/src/components/sidebar/LeftSidebar.tsx +19 -5
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +252 -0
- package/src/components/ui/Button.tsx +89 -126
- 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/SearchInput.tsx +2 -2
- 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/hooks/useAppHotkeys.ts +21 -80
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/index.ts +51 -0
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +3 -3
- package/src/player/components/ClipContextMenu.tsx +3 -3
- package/src/player/components/CompositionThumbnail.test.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +3 -4
- package/src/player/components/EditModal.tsx +1 -1
- package/src/player/components/ImageThumbnail.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
- package/src/player/components/LayerDisclosureRow.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +1 -1
- package/src/player/components/ShortcutsPanel.tsx +9 -9
- package/src/player/components/SpeedMenu.tsx +3 -5
- package/src/player/components/Timeline.tsx +2 -1
- package/src/player/components/TimelineCanvas.tsx +21 -14
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
- package/src/player/components/TimelineEmptyState.tsx +5 -5
- package/src/player/components/TimelineFxButton.tsx +6 -6
- package/src/player/components/TimelineGroupHeader.tsx +5 -5
- package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +32 -0
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineRuler.tsx +1 -1
- package/src/player/components/TimelineShortcutHint.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
- package/src/player/components/TimelineTypes.ts +2 -1
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/VideoThumbnail.tsx +4 -2
- package/src/player/components/VolumeControl.tsx +2 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +61 -1
- package/src/player/components/timelineDragDrop.ts +57 -15
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineSnapping.test.ts +27 -0
- package/src/player/components/timelineSnapping.ts +14 -1
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/lib/describeClips.ts +3 -54
- package/src/player/lib/playbackShortcuts.ts +2 -0
- 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/iconTokens.ts +20 -0
- package/src/styles/palette-reset.css +6 -0
- package/src/styles/studio.css +24 -21
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +243 -0
- package/src/styles/theme.test.ts +183 -0
- 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 -1
- package/src/utils/studioUiPreferences.ts +7 -0
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-n2ml4tUn.js +0 -442
- package/dist/assets/index-pO6p0H27.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
- package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
- package/src/components/storyboard/FramePoster.test.tsx +0 -61
- package/src/components/storyboard/FramePoster.tsx +0 -86
- package/src/components/storyboard/StoryboardDirection.tsx +0 -45
- package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
- package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
- package/src/components/storyboard/StoryboardGrid.tsx +0 -55
- package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
- package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
- package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
- package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
- package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
- package/src/components/storyboard/StoryboardView.tsx +0 -169
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
- package/src/components/storyboard/frameComments.test.ts +0 -95
- package/src/components/storyboard/frameComments.ts +0 -115
- package/src/components/storyboard/frameStatus.ts +0 -36
- package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
- package/src/components/storyboard/storyboardReviewStage.ts +0 -45
- package/src/components/storyboard/useFrameComments.ts +0 -98
- package/src/contexts/ViewModeContext.tsx +0 -142
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The header on the shared primitives: Capture and Inspector grouped, Export separate,
|
|
5
|
+
* hotkey filters unchanged (KTD13). Contexts are mocked, not provided.
|
|
6
|
+
*/
|
|
7
|
+
import React, { act } from "react";
|
|
8
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
9
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
10
|
+
import { buttonSizes, buttonVariants } from "./ui";
|
|
11
|
+
import { isTypingTarget } from "../utils/typingTarget";
|
|
12
|
+
import { shouldIgnorePlaybackShortcutTarget } from "../player/lib/playbackShortcuts";
|
|
13
|
+
|
|
14
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
const editHistory = {
|
|
17
|
+
canUndo: false,
|
|
18
|
+
canRedo: false,
|
|
19
|
+
undoLabel: undefined as string | undefined,
|
|
20
|
+
redoLabel: undefined as string | undefined,
|
|
21
|
+
};
|
|
22
|
+
const renderQueue = { isRendering: false, ffmpegMissing: false };
|
|
23
|
+
|
|
24
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
25
|
+
useStudioShellContext: () => ({
|
|
26
|
+
projectId: "demo",
|
|
27
|
+
editHistory,
|
|
28
|
+
handleUndo: vi.fn(),
|
|
29
|
+
handleRedo: vi.fn(),
|
|
30
|
+
renderQueue,
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../contexts/PanelLayoutContext", () => ({
|
|
35
|
+
usePanelLayoutContext: () => ({
|
|
36
|
+
effectiveRightCollapsed: false,
|
|
37
|
+
setRightCollapsed: vi.fn(),
|
|
38
|
+
setRightPanelTab: vi.fn(),
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
43
|
+
|
|
44
|
+
const { StudioHeader } = await import("./StudioHeader");
|
|
45
|
+
|
|
46
|
+
let mounted: { root: Root; host: HTMLElement } | null = null;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
editHistory.canUndo = false;
|
|
50
|
+
editHistory.canRedo = false;
|
|
51
|
+
editHistory.undoLabel = undefined;
|
|
52
|
+
editHistory.redoLabel = undefined;
|
|
53
|
+
renderQueue.isRendering = false;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (!mounted) return;
|
|
58
|
+
const { root, host } = mounted;
|
|
59
|
+
mounted = null;
|
|
60
|
+
act(() => root.unmount());
|
|
61
|
+
host.remove();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function mount(props: { inspectorButtonActive?: boolean } = {}): HTMLElement {
|
|
65
|
+
const host = document.createElement("div");
|
|
66
|
+
document.body.append(host);
|
|
67
|
+
const root = createRoot(host);
|
|
68
|
+
mounted = { root, host };
|
|
69
|
+
act(() =>
|
|
70
|
+
root.render(
|
|
71
|
+
<StudioHeader
|
|
72
|
+
captureFrameHref="blob:frame"
|
|
73
|
+
captureFrameFilename="frame.png"
|
|
74
|
+
handleCaptureFrameClick={vi.fn()}
|
|
75
|
+
refreshCaptureFrameTime={vi.fn()}
|
|
76
|
+
inspectorButtonActive={props.inspectorButtonActive ?? false}
|
|
77
|
+
inspectorPanelActive={false}
|
|
78
|
+
/>,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
return host;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function query(host: HTMLElement, selector: string): HTMLElement {
|
|
85
|
+
const el = host.querySelector<HTMLElement>(selector);
|
|
86
|
+
if (!el) throw new Error(`not rendered: ${selector}`);
|
|
87
|
+
return el;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Every class the recipe asks for, on the element the header rendered. */
|
|
91
|
+
function expectRecipe(el: HTMLElement, ...recipes: string[]): void {
|
|
92
|
+
const applied = new Set(el.className.split(/\s+/));
|
|
93
|
+
for (const recipe of recipes) {
|
|
94
|
+
for (const token of recipe.split(/\s+/)) {
|
|
95
|
+
expect(applied, `${token} missing from: ${el.className}`).toContain(token);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it("renders Export as the shared primary Button at the medium size", () => {
|
|
101
|
+
const host = mount();
|
|
102
|
+
|
|
103
|
+
expectRecipe(
|
|
104
|
+
query(host, '[data-testid="header-export"]'),
|
|
105
|
+
buttonVariants.primary,
|
|
106
|
+
buttonSizes.md,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("no longer renders Undo or Redo in the header", () => {
|
|
111
|
+
const host = mount();
|
|
112
|
+
|
|
113
|
+
expect(host.querySelector('[aria-label="Undo"]')).toBeNull();
|
|
114
|
+
expect(host.querySelector('[aria-label="Redo"]')).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("groups Capture and Inspector in one bordered segment, Export outside it", () => {
|
|
118
|
+
const host = mount();
|
|
119
|
+
const group = query(host, '[aria-label="Capture current frame"]').closest(".divide-x");
|
|
120
|
+
|
|
121
|
+
expect(group?.contains(query(host, '[aria-label="Inspector"]'))).toBe(true);
|
|
122
|
+
expect(group?.contains(query(host, '[data-testid="header-export"]'))).toBe(false);
|
|
123
|
+
expect(group).not.toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("drops the Capture label below 1000px but keeps its accessible name", () => {
|
|
127
|
+
const host = mount();
|
|
128
|
+
const capture = query(host, '[aria-label="Capture current frame"]');
|
|
129
|
+
|
|
130
|
+
expect(query(capture, "span").className).toContain("max-[1000px]:hidden");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/** The bare token, not a `hover:`/`data-[…]:`-prefixed variant of it. */
|
|
134
|
+
function hasToken(className: string, token: string): boolean {
|
|
135
|
+
return className.split(/\s+/).includes(token);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
it("shows Inspector pressed and filled only when on", () => {
|
|
139
|
+
const host = mount({ inspectorButtonActive: true });
|
|
140
|
+
const on = query(host, '[aria-label="Inspector"]');
|
|
141
|
+
expect(on.getAttribute("aria-pressed")).toBe("true");
|
|
142
|
+
expect(hasToken(on.className, "text-accent")).toBe(true);
|
|
143
|
+
expect(hasToken(on.className, "bg-hover")).toBe(true);
|
|
144
|
+
act(() => mounted?.root.unmount());
|
|
145
|
+
mounted?.host.remove();
|
|
146
|
+
mounted = null;
|
|
147
|
+
|
|
148
|
+
const off = query(mount(), '[aria-label="Inspector"]');
|
|
149
|
+
expect(off.getAttribute("aria-pressed")).toBe("false");
|
|
150
|
+
expect(hasToken(off.className, "text-accent")).toBe(false);
|
|
151
|
+
expect(hasToken(off.className, "bg-hover")).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("keeps Capture a real download link rather than a button", () => {
|
|
155
|
+
// `download` is what saves the frame. A Button here would render a <button>
|
|
156
|
+
// and the control would quietly stop downloading anything.
|
|
157
|
+
const host = mount();
|
|
158
|
+
const capture = query(host, '[aria-label="Capture current frame"]');
|
|
159
|
+
|
|
160
|
+
expect(capture.tagName).toBe("A");
|
|
161
|
+
expect(capture.getAttribute("download")).toBe("frame.png");
|
|
162
|
+
// h-full replaces the md height: the group's own h-ctl sets the shared control height.
|
|
163
|
+
expectRecipe(capture, "px-3", "text-step-12");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("classifies the new header controls for the hotkey filters as the old ones were (KTD13)", () => {
|
|
167
|
+
// Every one of these was a <button> or an <a href> before the sweep: never a
|
|
168
|
+
// typing target, always claimed by the playback filter. A primitive that
|
|
169
|
+
// rendered a different element would leak or swallow hotkeys in silence.
|
|
170
|
+
const host = mount();
|
|
171
|
+
const controls = [
|
|
172
|
+
query(host, '[data-testid="header-export"]'),
|
|
173
|
+
query(host, '[aria-label="Inspector"]'),
|
|
174
|
+
query(host, '[aria-label="Capture current frame"]'),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const el of controls) {
|
|
178
|
+
expect(isTypingTarget(el), el.getAttribute("aria-label") ?? el.tagName).toBe(false);
|
|
179
|
+
expect(shouldIgnorePlaybackShortcutTarget(el), el.tagName).toBe(true);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
1
|
+
import type { MouseEvent } from "react";
|
|
2
|
+
import { Camera } from "../icons/SystemIcons";
|
|
4
3
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
5
4
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
6
|
-
import { useViewMode, type StudioViewMode } from "../contexts/ViewModeContext";
|
|
7
5
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
|
-
import { Tooltip } from "./ui";
|
|
6
|
+
import { Button, buttonBase, buttonSizes, buttonVariants, cn, Tooltip } from "./ui";
|
|
9
7
|
|
|
10
8
|
export interface StudioHeaderProps {
|
|
11
9
|
captureFrameHref: string;
|
|
@@ -41,8 +39,8 @@ function HyperframesLogo() {
|
|
|
41
39
|
y2="37.482"
|
|
42
40
|
gradientUnits="userSpaceOnUse"
|
|
43
41
|
>
|
|
44
|
-
<stop stopColor="
|
|
45
|
-
<stop offset="1" stopColor="
|
|
42
|
+
<stop stopColor="var(--color-brand-gradient-from)" />
|
|
43
|
+
<stop offset="1" stopColor="var(--color-brand-gradient-to)" />
|
|
46
44
|
</linearGradient>
|
|
47
45
|
<linearGradient
|
|
48
46
|
id="hf-g1"
|
|
@@ -52,8 +50,8 @@ function HyperframesLogo() {
|
|
|
52
50
|
y2="6.303"
|
|
53
51
|
gradientUnits="userSpaceOnUse"
|
|
54
52
|
>
|
|
55
|
-
<stop stopColor="
|
|
56
|
-
<stop offset="1" stopColor="
|
|
53
|
+
<stop stopColor="var(--color-brand-gradient-from)" />
|
|
54
|
+
<stop offset="1" stopColor="var(--color-brand-gradient-to)" />
|
|
57
55
|
</linearGradient>
|
|
58
56
|
</defs>
|
|
59
57
|
{/* heygen label */}
|
|
@@ -139,63 +137,6 @@ function HyperframesLogo() {
|
|
|
139
137
|
);
|
|
140
138
|
}
|
|
141
139
|
|
|
142
|
-
const VIEW_MODE_OPTIONS: Array<{ mode: StudioViewMode; label: string }> = [
|
|
143
|
-
{ mode: "storyboard", label: "Storyboard" },
|
|
144
|
-
{ mode: "timeline", label: "Preview" },
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
/** Segmented control switching the main stage between storyboard and preview. */
|
|
148
|
-
export function ViewModeToggle() {
|
|
149
|
-
const { viewMode, setViewMode } = useViewMode();
|
|
150
|
-
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
151
|
-
|
|
152
|
-
const selectMode = (mode: StudioViewMode) => {
|
|
153
|
-
if (mode === viewMode) return;
|
|
154
|
-
if (setViewMode(mode)) trackStudioEvent("view_mode_toggle", { mode });
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Complete APG tabs pattern: roving tabIndex + arrow-key navigation.
|
|
158
|
-
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
|
|
159
|
-
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
160
|
-
e.preventDefault();
|
|
161
|
-
const dir = e.key === "ArrowLeft" ? -1 : 1;
|
|
162
|
-
const next = (index + dir + VIEW_MODE_OPTIONS.length) % VIEW_MODE_OPTIONS.length;
|
|
163
|
-
tabRefs.current[next]?.focus();
|
|
164
|
-
selectMode(VIEW_MODE_OPTIONS[next].mode);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
return (
|
|
168
|
-
<div
|
|
169
|
-
className="flex items-center gap-0.5 rounded-md bg-neutral-800 p-0.5"
|
|
170
|
-
role="tablist"
|
|
171
|
-
aria-label="Studio view"
|
|
172
|
-
>
|
|
173
|
-
{VIEW_MODE_OPTIONS.map(({ mode, label }, index) => {
|
|
174
|
-
const active = viewMode === mode;
|
|
175
|
-
return (
|
|
176
|
-
<button
|
|
177
|
-
key={mode}
|
|
178
|
-
ref={(el) => {
|
|
179
|
-
tabRefs.current[index] = el;
|
|
180
|
-
}}
|
|
181
|
-
type="button"
|
|
182
|
-
role="tab"
|
|
183
|
-
aria-selected={active}
|
|
184
|
-
tabIndex={active ? 0 : -1}
|
|
185
|
-
onClick={() => selectMode(mode)}
|
|
186
|
-
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
187
|
-
className={`rounded px-3 py-1 text-[11px] font-medium transition-colors active:scale-[0.98] outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-studio-accent ${
|
|
188
|
-
active ? "bg-neutral-200 text-neutral-900" : "text-neutral-400 hover:text-neutral-200"
|
|
189
|
-
}`}
|
|
190
|
-
>
|
|
191
|
-
{label}
|
|
192
|
-
</button>
|
|
193
|
-
);
|
|
194
|
-
})}
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
140
|
/**
|
|
200
141
|
* Does the header's Inspector button open the panel, or close it?
|
|
201
142
|
*
|
|
@@ -222,7 +163,7 @@ export function StudioHeader({
|
|
|
222
163
|
inspectorPanelActive,
|
|
223
164
|
onExport,
|
|
224
165
|
}: StudioHeaderProps) {
|
|
225
|
-
const { projectId,
|
|
166
|
+
const { projectId, renderQueue } = useStudioShellContext();
|
|
226
167
|
// effectiveRightCollapsed, not the raw intent: in the auto-railed state the
|
|
227
168
|
// intent is still "open" while the panel is hidden, so branching on intent
|
|
228
169
|
// made this button write rightCollapsed=true — and that value is synced into
|
|
@@ -232,155 +173,114 @@ export function StudioHeader({
|
|
|
232
173
|
const ffmpegMissing = renderQueue.ffmpegMissing;
|
|
233
174
|
|
|
234
175
|
return (
|
|
235
|
-
<div className="flex items-center justify-between h-10 px-3 bg-
|
|
176
|
+
<div className="flex items-center justify-between h-10 px-3 bg-surface border-b border-border-strong shrink-0">
|
|
236
177
|
{/* Left: logo + project name */}
|
|
237
178
|
<div className="flex items-center gap-3">
|
|
238
179
|
<HyperframesLogo />
|
|
239
|
-
<span className="text-
|
|
180
|
+
<span className="text-text-5 select-none" aria-hidden="true">
|
|
240
181
|
|
|
|
241
182
|
</span>
|
|
242
|
-
<span className="text-
|
|
183
|
+
<span className="text-step-11 font-medium text-text-1">{projectId}</span>
|
|
243
184
|
</div>
|
|
244
|
-
{/* Center: storyboard / preview toggle */}
|
|
245
|
-
<ViewModeToggle />
|
|
246
185
|
{/* Right: toolbar buttons */}
|
|
247
|
-
<div className="flex items-center gap-
|
|
248
|
-
<
|
|
249
|
-
label={
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
<svg
|
|
322
|
-
className="animate-spin motion-reduce:animate-none h-3.5 w-3.5"
|
|
323
|
-
viewBox="0 0 24 24"
|
|
324
|
-
fill="none"
|
|
325
|
-
aria-hidden="true"
|
|
326
|
-
>
|
|
327
|
-
<circle
|
|
328
|
-
className="opacity-25"
|
|
329
|
-
cx="12"
|
|
330
|
-
cy="12"
|
|
331
|
-
r="10"
|
|
186
|
+
<div className="flex items-center gap-3">
|
|
187
|
+
<div className="flex h-ctl items-center divide-x divide-border-strong overflow-hidden rounded-md border border-border-strong bg-bg-2">
|
|
188
|
+
<Tooltip label={capturing ? "Capturing frame…" : "Capture current frame"} side="bottom">
|
|
189
|
+
{/* A real download link, so it wears Button's recipe rather than being
|
|
190
|
+
one: `download` on an <a> is what saves the frame, and no <button>
|
|
191
|
+
can do that. `enabled:` never matches a link, so the ghost
|
|
192
|
+
variant's hover look is repeated unprefixed here. */}
|
|
193
|
+
<a
|
|
194
|
+
href={captureFrameHref}
|
|
195
|
+
download={captureFrameFilename}
|
|
196
|
+
onClick={(e) => {
|
|
197
|
+
if (capturing) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
trackStudioEvent("toolbar_action", { action: "capture_frame" });
|
|
202
|
+
handleCaptureFrameClick(e);
|
|
203
|
+
}}
|
|
204
|
+
onFocus={refreshCaptureFrameTime}
|
|
205
|
+
onPointerDown={refreshCaptureFrameTime}
|
|
206
|
+
aria-disabled={capturing || undefined}
|
|
207
|
+
className={cn(
|
|
208
|
+
buttonBase,
|
|
209
|
+
buttonVariants.ghost,
|
|
210
|
+
buttonSizes.md,
|
|
211
|
+
"h-full rounded-none max-[1000px]:px-2",
|
|
212
|
+
capturing
|
|
213
|
+
? "text-text-4 cursor-default"
|
|
214
|
+
: "hover:bg-hover hover:text-text-0 active:scale-[0.98]",
|
|
215
|
+
)}
|
|
216
|
+
aria-label={capturing ? "Capturing frame" : "Capture current frame"}
|
|
217
|
+
>
|
|
218
|
+
{capturing ? (
|
|
219
|
+
<svg
|
|
220
|
+
className="animate-spin motion-reduce:animate-none h-3.5 w-3.5"
|
|
221
|
+
viewBox="0 0 24 24"
|
|
222
|
+
fill="none"
|
|
223
|
+
aria-hidden="true"
|
|
224
|
+
>
|
|
225
|
+
<circle
|
|
226
|
+
className="opacity-25"
|
|
227
|
+
cx="12"
|
|
228
|
+
cy="12"
|
|
229
|
+
r="10"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
strokeWidth="4"
|
|
232
|
+
/>
|
|
233
|
+
<path
|
|
234
|
+
className="opacity-75"
|
|
235
|
+
fill="currentColor"
|
|
236
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
237
|
+
/>
|
|
238
|
+
</svg>
|
|
239
|
+
) : (
|
|
240
|
+
<Camera size={14} />
|
|
241
|
+
)}
|
|
242
|
+
<span className="max-[1000px]:hidden">{capturing ? "Capturing…" : "Capture"}</span>
|
|
243
|
+
</a>
|
|
244
|
+
</Tooltip>
|
|
245
|
+
<Tooltip label="Inspector" side="bottom">
|
|
246
|
+
<Button
|
|
247
|
+
variant="ghost"
|
|
248
|
+
aria-label="Inspector"
|
|
249
|
+
aria-pressed={inspectorButtonActive}
|
|
250
|
+
className={cn(
|
|
251
|
+
"h-full rounded-none",
|
|
252
|
+
inspectorButtonActive && "bg-hover text-accent enabled:hover:text-accent",
|
|
253
|
+
)}
|
|
254
|
+
icon={
|
|
255
|
+
<svg
|
|
256
|
+
width="12"
|
|
257
|
+
height="12"
|
|
258
|
+
viewBox="0 0 24 24"
|
|
259
|
+
fill="none"
|
|
332
260
|
stroke="currentColor"
|
|
333
|
-
strokeWidth="
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
339
|
-
/>
|
|
340
|
-
</svg>
|
|
341
|
-
) : (
|
|
342
|
-
<Camera size={14} />
|
|
343
|
-
)}
|
|
344
|
-
<span>{capturing ? "Capturing…" : "Capture"}</span>
|
|
345
|
-
</a>
|
|
346
|
-
</Tooltip>
|
|
347
|
-
<Tooltip label="Inspector" side="bottom">
|
|
348
|
-
<button
|
|
349
|
-
type="button"
|
|
350
|
-
onClick={() => {
|
|
351
|
-
if (shouldOpenInspector(effectiveRightCollapsed, inspectorPanelActive)) {
|
|
352
|
-
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
353
|
-
setRightPanelTab("design");
|
|
354
|
-
setRightCollapsed(false);
|
|
355
|
-
return;
|
|
261
|
+
strokeWidth="2"
|
|
262
|
+
>
|
|
263
|
+
<circle cx="12" cy="12" r="10" />
|
|
264
|
+
<polygon points="10 8 16 12 10 16" fill="currentColor" stroke="none" />
|
|
265
|
+
</svg>
|
|
356
266
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
>
|
|
370
|
-
<svg
|
|
371
|
-
width="12"
|
|
372
|
-
height="12"
|
|
373
|
-
viewBox="0 0 24 24"
|
|
374
|
-
fill="none"
|
|
375
|
-
stroke="currentColor"
|
|
376
|
-
strokeWidth="2"
|
|
267
|
+
onClick={() => {
|
|
268
|
+
if (shouldOpenInspector(effectiveRightCollapsed, inspectorPanelActive)) {
|
|
269
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
270
|
+
setRightPanelTab("design");
|
|
271
|
+
setRightCollapsed(false);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: true });
|
|
275
|
+
// Keep the current selection when collapsing the Inspector — closing
|
|
276
|
+
// the panel shouldn't deselect the element.
|
|
277
|
+
setRightCollapsed(true);
|
|
278
|
+
}}
|
|
377
279
|
>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
</button>
|
|
383
|
-
</Tooltip>
|
|
280
|
+
Inspector
|
|
281
|
+
</Button>
|
|
282
|
+
</Tooltip>
|
|
283
|
+
</div>
|
|
384
284
|
<Tooltip
|
|
385
285
|
label={
|
|
386
286
|
ffmpegMissing
|
|
@@ -391,8 +291,9 @@ export function StudioHeader({
|
|
|
391
291
|
}
|
|
392
292
|
side="bottom"
|
|
393
293
|
>
|
|
394
|
-
<
|
|
395
|
-
|
|
294
|
+
<Button
|
|
295
|
+
variant="primary"
|
|
296
|
+
data-testid="header-export"
|
|
396
297
|
disabled={isRendering}
|
|
397
298
|
onClick={() => {
|
|
398
299
|
if (isRendering) return;
|
|
@@ -406,10 +307,9 @@ export function StudioHeader({
|
|
|
406
307
|
if (ffmpegMissing) return;
|
|
407
308
|
onExport?.();
|
|
408
309
|
}}
|
|
409
|
-
className="h-7 flex items-center gap-1.5 px-3 rounded-md text-[11px] font-semibold bg-studio-accent text-[#09090B] enabled:hover:brightness-110 transition-[filter,transform] enabled:active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
410
310
|
>
|
|
411
311
|
{isRendering ? "Rendering…" : "Export"}
|
|
412
|
-
</
|
|
312
|
+
</Button>
|
|
413
313
|
</Tooltip>
|
|
414
314
|
</div>
|
|
415
315
|
</div>
|
|
@@ -17,6 +17,7 @@ export interface StudioLeftSidebarProps {
|
|
|
17
17
|
onLint: () => void;
|
|
18
18
|
linting: boolean;
|
|
19
19
|
lintFindingCount?: number;
|
|
20
|
+
lintHasError?: boolean;
|
|
20
21
|
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
21
22
|
onAddAssetToTimeline?: (path: string) => void;
|
|
22
23
|
onAddCompositionToTimeline?: (path: string) => void;
|
|
@@ -31,6 +32,7 @@ export function StudioLeftSidebar({
|
|
|
31
32
|
onLint,
|
|
32
33
|
linting,
|
|
33
34
|
lintFindingCount,
|
|
35
|
+
lintHasError,
|
|
34
36
|
lintFindingsByFile,
|
|
35
37
|
onAddAssetToTimeline,
|
|
36
38
|
onAddCompositionToTimeline,
|
|
@@ -86,7 +88,7 @@ export function StudioLeftSidebar({
|
|
|
86
88
|
|
|
87
89
|
if (effectiveLeftCollapsed) {
|
|
88
90
|
return (
|
|
89
|
-
<div className="mr-0.5 flex w-10
|
|
91
|
+
<div className="mr-0.5 flex w-10 shrink-0 flex-col items-center rounded-lg border border-neutral-800/50 bg-neutral-950 pt-1">
|
|
90
92
|
<button
|
|
91
93
|
type="button"
|
|
92
94
|
onClick={toggleLeftSidebar}
|
|
@@ -160,6 +162,7 @@ export function StudioLeftSidebar({
|
|
|
160
162
|
onLint={onLint}
|
|
161
163
|
linting={linting}
|
|
162
164
|
lintFindingCount={lintFindingCount}
|
|
165
|
+
lintHasError={lintHasError}
|
|
163
166
|
lintFindingsByFile={lintFindingsByFile}
|
|
164
167
|
onToggleCollapse={toggleLeftSidebar}
|
|
165
168
|
onAddBlock={onAddBlock}
|
|
@@ -169,14 +172,14 @@ export function StudioLeftSidebar({
|
|
|
169
172
|
/>
|
|
170
173
|
{/* Vertical resize divider: 3px visible seam, 13px pointer-capture zone via
|
|
171
174
|
the absolutely-positioned inner hit area. The outer element is w-[3px] so
|
|
172
|
-
it contributes only 3px of gap in the flex row; the inner
|
|
175
|
+
it contributes only 3px of gap in the flex row; the inner left-[-2px]
|
|
173
176
|
element widens the hit area without affecting layout. */}
|
|
174
177
|
<div
|
|
175
178
|
role="separator"
|
|
176
179
|
aria-label="Resize sidebar"
|
|
177
180
|
aria-orientation="vertical"
|
|
178
181
|
tabIndex={0}
|
|
179
|
-
className="group relative w-[3px]
|
|
182
|
+
className="group relative w-[3px] shrink-0 cursor-col-resize outline-hidden focus-visible:bg-studio-accent/20"
|
|
180
183
|
style={{ touchAction: "none" }}
|
|
181
184
|
onPointerDown={(e) => handlePanelResizeStart("left", e)}
|
|
182
185
|
onPointerMove={handlePanelResizeMove}
|
|
@@ -195,7 +198,7 @@ export function StudioLeftSidebar({
|
|
|
195
198
|
24px WCAG 2.2 (2.5.8) target because the next pixel on either side is
|
|
196
199
|
live: the sidebar's scrolling tab content on the left, the preview
|
|
197
200
|
stage on the right. Silently stealing their clicks is the worse bug. */}
|
|
198
|
-
<div className="absolute inset-y-0
|
|
201
|
+
<div className="absolute inset-y-0 left-[-2px] w-[13px]" />
|
|
199
202
|
{/* Visible hairline */}
|
|
200
203
|
<div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
|
201
204
|
</div>
|
|
@@ -77,7 +77,7 @@ export function StudioOverlays({
|
|
|
77
77
|
)}
|
|
78
78
|
{/* One bottom-right stack so the feedback card and toasts queue instead
|
|
79
79
|
of covering each other. Empty when nothing is showing. */}
|
|
80
|
-
<div className="absolute bottom-6 right-6 z-
|
|
80
|
+
<div className="absolute bottom-6 right-6 z-91 flex flex-col items-end gap-2">
|
|
81
81
|
{toasts.map((toast) => (
|
|
82
82
|
<StudioToast
|
|
83
83
|
key={toast.id}
|