@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,109 @@
|
|
|
1
|
+
// The token gate (R11, AE1). Tailwind silently drops a class it cannot compile, so `rounded-button`
|
|
2
|
+
// renders as no radius and nothing goes red. This compiles Studio's real entry stylesheet with every
|
|
3
|
+
// class the source claims and reports any candidate producing no selector as `file: class`.
|
|
4
|
+
// Tailwind is the only judge, so there is no allowlist; a failing class is fixed, never excused.
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { compile } from "tailwindcss";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { extractClassCandidates } from "./classCandidates";
|
|
11
|
+
import { listSourceFiles, loadStylesheet, STYLES_DIR } from "./styleSources";
|
|
12
|
+
|
|
13
|
+
// A class selector in the emitted CSS, escapes removed: `.hover\:bg-surface\/50` reads back as its candidate.
|
|
14
|
+
const CLASS_SELECTOR = /\.((?:\\.|[^\s.,{}()>+~:[\]#'"*/\\])+)/g;
|
|
15
|
+
|
|
16
|
+
// Tailwind's `group`/`peer` variant markers (`group/card`) produce no rule of their own, so no
|
|
17
|
+
// compiled selector can vouch for them. Nothing else is exempt.
|
|
18
|
+
const MARKERS = new Set(["group", "peer"]);
|
|
19
|
+
|
|
20
|
+
// `hf-` is HyperFrames' reserved prefix for semantic hooks, not utilities, so Tailwind is the
|
|
21
|
+
// wrong judge of them.
|
|
22
|
+
const HOOK_PREFIX = /^hf-/;
|
|
23
|
+
|
|
24
|
+
/** Compile Studio's stylesheet and return every class it can produce. */
|
|
25
|
+
async function resolvable(candidates: string[]): Promise<Set<string>> {
|
|
26
|
+
const compiled = await compile(readFileSync(path.join(STYLES_DIR, "studio.css"), "utf8"), {
|
|
27
|
+
base: STYLES_DIR,
|
|
28
|
+
loadStylesheet,
|
|
29
|
+
});
|
|
30
|
+
const css = compiled.build(candidates);
|
|
31
|
+
const selectors = new Set<string>();
|
|
32
|
+
for (const [, selector] of css.matchAll(CLASS_SELECTOR)) {
|
|
33
|
+
selectors.add(selector.replace(/\\(.)/g, "$1"));
|
|
34
|
+
}
|
|
35
|
+
return selectors;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `file: class` for every class the sources claim that Tailwind cannot make. Sources are passed
|
|
39
|
+
// in so the fixtures exercise the same code path as the tree.
|
|
40
|
+
async function unresolved(sources: ReadonlyMap<string, string>): Promise<string[]> {
|
|
41
|
+
const claims = new Map<string, string[]>();
|
|
42
|
+
for (const [file, source] of sources) {
|
|
43
|
+
for (const candidate of extractClassCandidates(source)) {
|
|
44
|
+
const seen = claims.get(candidate.base);
|
|
45
|
+
if (seen) seen.push(file);
|
|
46
|
+
else claims.set(candidate.base, [file]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const produced = await resolvable([...claims.keys()]);
|
|
50
|
+
const failures: string[] = [];
|
|
51
|
+
for (const [candidate, files] of claims) {
|
|
52
|
+
if (produced.has(candidate) || MARKERS.has(candidate.split("/")[0])) continue;
|
|
53
|
+
if (HOOK_PREFIX.test(candidate)) continue;
|
|
54
|
+
for (const file of files) failures.push(`${file}: ${candidate}`);
|
|
55
|
+
}
|
|
56
|
+
return failures.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Every file the gate reads, mirroring the `@source` globs in `studio.css`. Tests are excluded:
|
|
60
|
+
// a fixture in a test is not markup.
|
|
61
|
+
function studioSources(): Map<string, string> {
|
|
62
|
+
return listSourceFiles((file) => /\.tsx?$/.test(file) && !/\.test\.tsx?$/.test(file));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("token gate", () => {
|
|
66
|
+
it("names the file and the class when a class resolves to nothing", async () => {
|
|
67
|
+
// AE1: `rounded-hologram` looks like a token name but is defined nowhere. The map is named
|
|
68
|
+
// `buttonSizes`, not `sizeStyles`: the `cn()` that consumes it puts it in front of the gate.
|
|
69
|
+
const failures = await unresolved(
|
|
70
|
+
new Map([
|
|
71
|
+
[
|
|
72
|
+
"ui/Button.tsx",
|
|
73
|
+
`const buttonSizes = { md: "h-8 rounded-hologram text-mega" };
|
|
74
|
+
const Button = ({ size }) => <button className={cn(buttonSizes[size])} />;`,
|
|
75
|
+
],
|
|
76
|
+
]),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(failures).toEqual(["ui/Button.tsx: rounded-hologram", "ui/Button.tsx: text-mega"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("accepts an arbitrary value and reports it as one", async () => {
|
|
83
|
+
const source = `<div className="text-[11px]" />`;
|
|
84
|
+
|
|
85
|
+
expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
|
|
86
|
+
expect(extractClassCandidates(source).filter((c) => c.arbitrary)).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("accepts variants, static utilities and Studio's own CSS classes", async () => {
|
|
90
|
+
// `timeline-clip` is a plain rule in `studio.css`, not a utility.
|
|
91
|
+
const source = `<div className="hover:bg-surface/50 text-center border-dashed timeline-clip" />`;
|
|
92
|
+
|
|
93
|
+
expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("leaves Studio's own hook prefix and Tailwind's variant markers alone", async () => {
|
|
97
|
+
const source = `<div className="group group/card peer hf-fx-node" />`;
|
|
98
|
+
|
|
99
|
+
expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("claims nothing for a template chunk that touches an interpolation", async () => {
|
|
103
|
+
expect(await unresolved(new Map([["a.tsx", "<div className={`w-${i}`} />"]]))).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("resolves every class Studio's own source claims", async () => {
|
|
107
|
+
expect(await unresolved(studioSources())).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
|
+
historyTooltipLabel,
|
|
5
6
|
findMatchingTimelineElementId,
|
|
6
7
|
findTimelineIdByAncestor,
|
|
7
8
|
resolveDroppedAssetDimensions,
|
|
@@ -217,3 +218,20 @@ describe("resolveElementTrack", () => {
|
|
|
217
218
|
expect(resolveElementTrack({ authoredTrack: undefined, track: 3.6 })).toBe(4);
|
|
218
219
|
});
|
|
219
220
|
});
|
|
221
|
+
|
|
222
|
+
describe("historyTooltipLabel", () => {
|
|
223
|
+
// A disabled control gets no pointer events, so its tooltip cannot be opened
|
|
224
|
+
// to read. The wording is checked here instead of through the DOM.
|
|
225
|
+
it("names the action that would be undone", () => {
|
|
226
|
+
expect(historyTooltipLabel("undo", "Move layer")).toMatch(/^Undo Move layer \(.+\)$/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("names the action that would be redone", () => {
|
|
230
|
+
expect(historyTooltipLabel("redo", "Move layer")).toMatch(/^Redo Move layer \(.+\)$/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("keeps the shortcut when the history is empty", () => {
|
|
234
|
+
expect(historyTooltipLabel("undo", undefined)).toMatch(/^Undo \(.+\)$/);
|
|
235
|
+
expect(historyTooltipLabel("redo", null)).toMatch(/^Redo \(.+\)$/);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -119,13 +119,23 @@ export function shouldIgnoreHistoryShortcut(target: EventTarget | null): boolean
|
|
|
119
119
|
return isTypingTarget(target);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
function getHistoryShortcutLabel(action: "undo" | "redo"): string {
|
|
123
123
|
const isMac =
|
|
124
124
|
typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
|
125
125
|
const modifier = isMac ? "Cmd" : "Ctrl";
|
|
126
126
|
return action === "undo" ? `${modifier}+Z` : `${modifier}+Shift+Z`;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** The Undo / Redo tooltip: the shortcut always, the last action's name when there is one. */
|
|
130
|
+
export function historyTooltipLabel(
|
|
131
|
+
action: "undo" | "redo",
|
|
132
|
+
lastAction: string | null | undefined,
|
|
133
|
+
): string {
|
|
134
|
+
const shortcut = getHistoryShortcutLabel(action);
|
|
135
|
+
const verb = action === "undo" ? "Undo" : "Redo";
|
|
136
|
+
return lastAction ? `${verb} ${lastAction} (${shortcut})` : `${verb} (${shortcut})`;
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
export type ElementMatchSelection = Pick<
|
|
130
140
|
DomEditSelection,
|
|
131
141
|
| "id"
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { SerializedDockview } from "dockview-react";
|
|
2
|
+
import { parseDockLayout } from "../components/dock/dockLayoutSchema";
|
|
3
|
+
|
|
1
4
|
export interface StoredPreviewZoomState {
|
|
2
5
|
zoomPercent: number;
|
|
3
6
|
panX: number;
|
|
@@ -47,6 +50,8 @@ export interface StudioUiPreferences {
|
|
|
47
50
|
* intentionally scoped to one mount.
|
|
48
51
|
*/
|
|
49
52
|
agentToolsEnabled?: boolean;
|
|
53
|
+
/** The dock's serialized panel tree; parsed by `parseDockLayout` on read. */
|
|
54
|
+
dockLayout?: SerializedDockview;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
const STUDIO_UI_PREFERENCES_KEY = "hf-studio-ui-preferences";
|
|
@@ -169,6 +174,8 @@ function readStorage(storage: Storage | null, key: string): StudioUiPreferences
|
|
|
169
174
|
if (typeof parsed.agentToolsEnabled === "boolean") {
|
|
170
175
|
preferences.agentToolsEnabled = parsed.agentToolsEnabled;
|
|
171
176
|
}
|
|
177
|
+
const dockLayout = parseDockLayout(parsed.dockLayout);
|
|
178
|
+
if (dockLayout) preferences.dockLayout = dockLayout;
|
|
172
179
|
return preferences;
|
|
173
180
|
} catch {
|
|
174
181
|
return {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { TimelineElement } from "../player";
|
|
3
|
+
import {
|
|
4
|
+
applyTrackRenumbers,
|
|
5
|
+
planDropTrackInsert,
|
|
6
|
+
resolveDropTrack,
|
|
7
|
+
} from "./timelineDropTrackInsert";
|
|
8
|
+
|
|
9
|
+
function clip(id: string, track: number, start: number): TimelineElement {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
key: id,
|
|
13
|
+
tag: "div",
|
|
14
|
+
start,
|
|
15
|
+
duration: 2,
|
|
16
|
+
track,
|
|
17
|
+
authoredTrack: track,
|
|
18
|
+
hfId: `hf-${id}`,
|
|
19
|
+
domId: id,
|
|
20
|
+
// Main-document elements carry no sourceFile, as in production.
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trackOrder = [0, 1, 2];
|
|
25
|
+
const dropped = { id: "new", tag: "img", start: 5, duration: 3 };
|
|
26
|
+
const elements = [clip("a", 0, 0), clip("b", 1, 0), clip("c", 2, 0)];
|
|
27
|
+
const source = elements
|
|
28
|
+
.map(
|
|
29
|
+
(e) =>
|
|
30
|
+
`<div data-hf-id="${e.hfId}" id="${e.id}" data-start="0" data-track-index="${e.track}"></div>`,
|
|
31
|
+
)
|
|
32
|
+
.join("\n");
|
|
33
|
+
|
|
34
|
+
describe("planDropTrackInsert", () => {
|
|
35
|
+
it("opens a lane between two rows and pushes the lanes below down", () => {
|
|
36
|
+
const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 1, dropped });
|
|
37
|
+
expect(plan?.track).toBe(1);
|
|
38
|
+
expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([
|
|
39
|
+
["b", 2],
|
|
40
|
+
["c", 3],
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("opens a lane above the first row", () => {
|
|
45
|
+
const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 0, dropped });
|
|
46
|
+
expect(plan?.track).toBe(0);
|
|
47
|
+
expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([
|
|
48
|
+
["a", 1],
|
|
49
|
+
["b", 2],
|
|
50
|
+
["c", 3],
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("still renumbers when the peers' own sourceFile is a real path, not undefined", () => {
|
|
55
|
+
const subComp = elements.map((e) => ({ ...e, sourceFile: "sub.html" }));
|
|
56
|
+
const plan = planDropTrackInsert({ elements: subComp, trackOrder, insertRow: 1, dropped });
|
|
57
|
+
expect(plan?.track).toBe(1);
|
|
58
|
+
expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([
|
|
59
|
+
["b", 2],
|
|
60
|
+
["c", 3],
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("planDropTrackInsert against the display row order", () => {
|
|
66
|
+
it("counts an audio-group anchor row the way a clip drag does", () => {
|
|
67
|
+
// Rows shown: group anchor (-0.5), track 0, track 1. Row boundary 2 is between 0 and 1.
|
|
68
|
+
const grouped = elements.slice(0, 2).map((e) => (e.id === "a" ? { ...e, audioGroup: "g" } : e));
|
|
69
|
+
const plan = planDropTrackInsert({
|
|
70
|
+
elements: grouped,
|
|
71
|
+
trackOrder: [-0.5, 0, 1],
|
|
72
|
+
insertRow: 2,
|
|
73
|
+
dropped,
|
|
74
|
+
});
|
|
75
|
+
expect(plan?.track).toBe(1);
|
|
76
|
+
expect(plan?.renumbers.map((r) => [r.element.id, r.track])).toEqual([["b", 2]]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("applyTrackRenumbers", () => {
|
|
81
|
+
it("rewrites only the renumbered clips' data-track-index", () => {
|
|
82
|
+
const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 1, dropped });
|
|
83
|
+
const out = applyTrackRenumbers(source, plan!);
|
|
84
|
+
const tracks = [...out.matchAll(/id="(\w)"[^>]*data-track-index="(\d+)"/g)].map((m) => [
|
|
85
|
+
m[1],
|
|
86
|
+
+m[2],
|
|
87
|
+
]);
|
|
88
|
+
expect(tracks).toEqual([
|
|
89
|
+
["a", 0],
|
|
90
|
+
["b", 2],
|
|
91
|
+
["c", 3],
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("throws when a clip's opening tag is missing", () => {
|
|
96
|
+
const plan = planDropTrackInsert({ elements, trackOrder, insertRow: 1, dropped });
|
|
97
|
+
expect(() => applyTrackRenumbers("<div></div>", plan!)).toThrow(/Cannot renumber/);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("resolveDropTrack", () => {
|
|
102
|
+
it("keeps the aimed lane and the source when no insert is asked for", () => {
|
|
103
|
+
const out = resolveDropTrack({
|
|
104
|
+
source,
|
|
105
|
+
elements,
|
|
106
|
+
placement: { track: 2 },
|
|
107
|
+
dropped,
|
|
108
|
+
});
|
|
109
|
+
expect(out).toEqual({ source, track: 2 });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns the new lane and the source with the lanes below pushed down", () => {
|
|
113
|
+
const out = resolveDropTrack({
|
|
114
|
+
source,
|
|
115
|
+
elements,
|
|
116
|
+
placement: { track: 1, insertRow: 1, trackOrder },
|
|
117
|
+
dropped,
|
|
118
|
+
});
|
|
119
|
+
expect(out.track).toBe(1);
|
|
120
|
+
expect(out.source).toContain('id="b" data-start="0" data-track-index="2"');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("refuses to insert when a locked clip would have to move", () => {
|
|
124
|
+
const locked = elements.map((e) => (e.id === "b" ? { ...e, timelineLocked: true } : e));
|
|
125
|
+
expect(() =>
|
|
126
|
+
resolveDropTrack({
|
|
127
|
+
source,
|
|
128
|
+
elements: locked,
|
|
129
|
+
placement: { track: 1, insertRow: 1, trackOrder },
|
|
130
|
+
dropped,
|
|
131
|
+
}),
|
|
132
|
+
).toThrow(/locked/);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { TimelineElement } from "../player";
|
|
2
|
+
import { layoutAfterTrackInsert } from "../player/components/timelineDragLanding";
|
|
3
|
+
import { canMoveTimelineElement } from "../player/components/timelineAuthoredMoveTarget";
|
|
4
|
+
import type { TimelineDropPlacement } from "../player/components/timelineCallbacks";
|
|
5
|
+
import { applyPatchByTarget, readAttributeByTarget } from "./sourcePatcher";
|
|
6
|
+
import { buildPatchTarget } from "../hooks/timelineEditingHelpers";
|
|
7
|
+
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
8
|
+
|
|
9
|
+
export interface DropTrackInsertPlan {
|
|
10
|
+
/** Lane the dropped clip is written on. */
|
|
11
|
+
track: number;
|
|
12
|
+
/** Existing clips whose lane changes to make room. */
|
|
13
|
+
renumbers: Array<{ element: TimelineElement; track: number }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Plan a new lane at boundary `insertRow` with the renumber a clip drag into a gutter uses.
|
|
17
|
+
* Null when a locked clip would have to move. */
|
|
18
|
+
export function planDropTrackInsert(input: {
|
|
19
|
+
elements: TimelineElement[];
|
|
20
|
+
/** The row order the timeline shows (anchor rows included), which `insertRow` indexes. */
|
|
21
|
+
trackOrder: readonly number[];
|
|
22
|
+
insertRow: number;
|
|
23
|
+
dropped: Pick<TimelineElement, "id" | "tag" | "start" | "duration">;
|
|
24
|
+
}): DropTrackInsertPlan | null {
|
|
25
|
+
const { elements, trackOrder, insertRow, dropped } = input;
|
|
26
|
+
const newElement: TimelineElement = {
|
|
27
|
+
...dropped,
|
|
28
|
+
key: dropped.id,
|
|
29
|
+
// Parked on an existing lane so it adds no lane of its own to the topology.
|
|
30
|
+
track: elements[0]?.track ?? 0,
|
|
31
|
+
// sameSourceFile compares this raw field: borrow the peers' value, not the resolved path.
|
|
32
|
+
sourceFile: elements[0]?.sourceFile,
|
|
33
|
+
};
|
|
34
|
+
const layout = layoutAfterTrackInsert(newElement, dropped.start, insertRow, null, {
|
|
35
|
+
elements: [...elements, newElement],
|
|
36
|
+
trackOrder: [...trackOrder],
|
|
37
|
+
});
|
|
38
|
+
if (!layout) return null;
|
|
39
|
+
const byKey = new Map(elements.map((e) => [e.key ?? e.id, e]));
|
|
40
|
+
const renumbers: DropTrackInsertPlan["renumbers"] = [];
|
|
41
|
+
for (const norm of layout.normalized) {
|
|
42
|
+
const key = norm.key ?? norm.id;
|
|
43
|
+
if (key === dropped.id) continue;
|
|
44
|
+
const src = byKey.get(key);
|
|
45
|
+
if (!src || norm.track === (src.authoredTrack ?? src.track)) continue;
|
|
46
|
+
if (!canMoveTimelineElement(src)) return null;
|
|
47
|
+
renumbers.push({ element: src, track: norm.track });
|
|
48
|
+
}
|
|
49
|
+
const track = layout.normalized.find((n) => (n.key ?? n.id) === dropped.id)?.track;
|
|
50
|
+
return track == null ? null : { track, renumbers };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Rewrite `data-track-index` on each renumbered clip's opening tag, via the shared source patcher. */
|
|
54
|
+
export function applyTrackRenumbers(source: string, plan: DropTrackInsertPlan): string {
|
|
55
|
+
let out = source;
|
|
56
|
+
for (const { element, track } of plan.renumbers) {
|
|
57
|
+
const target = buildPatchTarget(element);
|
|
58
|
+
if (!target || readAttributeByTarget(out, target, "track-index") === undefined) {
|
|
59
|
+
throw new Error(`Cannot renumber the track of "${element.id}" in the source`);
|
|
60
|
+
}
|
|
61
|
+
out = applyPatchByTarget(out, target, {
|
|
62
|
+
type: "attribute",
|
|
63
|
+
property: "track-index",
|
|
64
|
+
value: formatTimelineAttributeNumber(track),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The lane a dropped clip is written on and the source with any lanes pushed down to make room. */
|
|
71
|
+
export function resolveDropTrack(input: {
|
|
72
|
+
source: string;
|
|
73
|
+
elements: TimelineElement[];
|
|
74
|
+
placement: TimelineDropPlacement;
|
|
75
|
+
dropped: Pick<TimelineElement, "id" | "tag" | "start" | "duration">;
|
|
76
|
+
}): { source: string; track: number } {
|
|
77
|
+
const { source, elements, placement, dropped } = input;
|
|
78
|
+
if (placement.insertRow == null) return { source, track: placement.track };
|
|
79
|
+
const { insertRow, trackOrder } = placement;
|
|
80
|
+
const plan = planDropTrackInsert({ elements, trackOrder, insertRow, dropped });
|
|
81
|
+
if (!plan) throw new Error("Cannot open a new track here: a locked clip would have to move.");
|
|
82
|
+
return { source: applyTrackRenumbers(source, plan), track: plan.track };
|
|
83
|
+
}
|
|
@@ -33,6 +33,10 @@ const TYPING_SELECTOR = [
|
|
|
33
33
|
"[role='textbox']",
|
|
34
34
|
"[role='searchbox']",
|
|
35
35
|
"[role='combobox']",
|
|
36
|
+
// A switch is a button in the DOM, but the native checkbox it replaces was an
|
|
37
|
+
// `input` and matched above. Without this row the shared Toggle would let a
|
|
38
|
+
// global shortcut claim Space instead of flipping the control (KTD13).
|
|
39
|
+
"[role='switch']",
|
|
36
40
|
".cm-editor",
|
|
37
41
|
].join(",");
|
|
38
42
|
|