@hyperframes/studio 0.8.104 → 0.8.106
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-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/index-CMiYVGv5.js +399 -0
- package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +10391 -9836
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +9 -9
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +35 -13
- package/src/components/StudioHeader.tsx +6 -2
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +37 -5
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +36 -3
- package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
- package/src/components/editor/manualEditsDomPatches.ts +0 -30
- package/src/components/editor/manualEditsSnapshot.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +34 -3
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +71 -48
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/elementOffsetStager.ts +8 -7
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +141 -23
- package/src/hooks/useClipboard.ts +154 -123
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
- package/src/hooks/useDomGeometryCommit.ts +0 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
- package/src/hooks/useDomGeometryCommits.ts +4 -49
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +16 -8
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +4 -7
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +214 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +12 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/assets/index-De2aO4XM.js +0 -399
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/dist/chunk-HIGHVWYD.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
const read = (file: string) => readFileSync(new URL(file, import.meta.url), "utf8");
|
|
5
|
+
const source = read("./studioCodeTheme.ts");
|
|
6
|
+
const themeCss = read("../../styles/theme.css");
|
|
7
|
+
|
|
8
|
+
describe("the code editor theme", () => {
|
|
9
|
+
it("takes every colour from a token theme.css declares", () => {
|
|
10
|
+
const used = [...source.matchAll(/var\((--color-[\w-]+)\)/g)].map((match) => match[1]!);
|
|
11
|
+
expect(used.length).toBeGreaterThan(20);
|
|
12
|
+
for (const token of new Set(used)) expect(themeCss, token).toContain(`${token}:`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("paints no literal colour of its own", () => {
|
|
16
|
+
expect(source).not.toMatch(
|
|
17
|
+
/#[\da-f]{3,8}\b|\b(rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\(|"(white|black|red|orange|yellow|green|blue|purple|pink|gray|grey|silver|lime|cyan|magenta)"/i,
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import { tags as t } from "@lezer/highlight";
|
|
4
|
+
|
|
5
|
+
// One Dark's layout on Studio's tokens: every colour is a theme.css variable, so the editor
|
|
6
|
+
// follows the light or dark theme the page paints with no editor reconfiguration.
|
|
7
|
+
const editorTheme = EditorView.theme({
|
|
8
|
+
"&": { height: "100%", color: "var(--color-text-0)", backgroundColor: "var(--color-surface)" },
|
|
9
|
+
".cm-scroller": { overflow: "auto" },
|
|
10
|
+
".cm-content": { caretColor: "var(--color-text-0)" },
|
|
11
|
+
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--color-text-0)" },
|
|
12
|
+
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
|
13
|
+
{ backgroundColor: "var(--color-on)" },
|
|
14
|
+
".cm-activeLine": { backgroundColor: "var(--color-hover)" },
|
|
15
|
+
".cm-selectionMatch": {
|
|
16
|
+
backgroundColor: "transparent",
|
|
17
|
+
outline: "1px solid var(--color-text-muted)",
|
|
18
|
+
},
|
|
19
|
+
".cm-searchMatch": {
|
|
20
|
+
backgroundColor: "transparent",
|
|
21
|
+
outline: "1px solid var(--color-text-muted)",
|
|
22
|
+
},
|
|
23
|
+
".cm-searchMatch.cm-searchMatch-selected": {
|
|
24
|
+
backgroundColor: "transparent",
|
|
25
|
+
outline: "2px solid var(--color-accent-ink)",
|
|
26
|
+
},
|
|
27
|
+
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
|
|
28
|
+
backgroundColor: "var(--color-on)",
|
|
29
|
+
},
|
|
30
|
+
".cm-gutters": {
|
|
31
|
+
backgroundColor: "var(--color-surface)",
|
|
32
|
+
color: "var(--color-text-muted)",
|
|
33
|
+
border: "none",
|
|
34
|
+
},
|
|
35
|
+
".cm-activeLineGutter": { backgroundColor: "var(--color-hover)", color: "var(--color-text-2)" },
|
|
36
|
+
".cm-foldPlaceholder": {
|
|
37
|
+
backgroundColor: "transparent",
|
|
38
|
+
border: "none",
|
|
39
|
+
color: "var(--color-text-2)",
|
|
40
|
+
},
|
|
41
|
+
".cm-panels": { backgroundColor: "var(--color-raised)", color: "var(--color-text-0)" },
|
|
42
|
+
".cm-panels.cm-panels-top": { borderBottom: "1px solid var(--color-border)" },
|
|
43
|
+
".cm-panels.cm-panels-bottom": { borderTop: "1px solid var(--color-border)" },
|
|
44
|
+
".cm-textfield": {
|
|
45
|
+
backgroundColor: "var(--color-input)",
|
|
46
|
+
color: "var(--color-text-0)",
|
|
47
|
+
border: "1px solid var(--color-border-input)",
|
|
48
|
+
},
|
|
49
|
+
".cm-button": {
|
|
50
|
+
backgroundImage: "none",
|
|
51
|
+
backgroundColor: "var(--color-hover)",
|
|
52
|
+
color: "var(--color-text-0)",
|
|
53
|
+
border: "1px solid var(--color-border)",
|
|
54
|
+
},
|
|
55
|
+
"&.cm-editor .cm-button:active": {
|
|
56
|
+
backgroundImage: "none",
|
|
57
|
+
backgroundColor: "var(--color-press)",
|
|
58
|
+
},
|
|
59
|
+
".cm-specialChar": { color: "var(--color-danger-ink)" },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const highlightStyle = HighlightStyle.define([
|
|
63
|
+
{ tag: t.keyword, color: "var(--color-code-keyword)" },
|
|
64
|
+
{
|
|
65
|
+
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
|
|
66
|
+
color: "var(--color-code-name)",
|
|
67
|
+
},
|
|
68
|
+
{ tag: [t.function(t.variableName), t.labelName], color: "var(--color-code-function)" },
|
|
69
|
+
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: "var(--color-code-constant)" },
|
|
70
|
+
{ tag: [t.definition(t.name), t.separator], color: "var(--color-text-0)" },
|
|
71
|
+
{
|
|
72
|
+
tag: [
|
|
73
|
+
t.typeName,
|
|
74
|
+
t.className,
|
|
75
|
+
t.number,
|
|
76
|
+
t.changed,
|
|
77
|
+
t.annotation,
|
|
78
|
+
t.modifier,
|
|
79
|
+
t.self,
|
|
80
|
+
t.namespace,
|
|
81
|
+
],
|
|
82
|
+
color: "var(--color-code-type)",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.special(t.string)],
|
|
86
|
+
color: "var(--color-code-operator)",
|
|
87
|
+
},
|
|
88
|
+
{ tag: [t.meta, t.comment], color: "var(--color-code-comment)" },
|
|
89
|
+
{ tag: t.strong, fontWeight: "bold" },
|
|
90
|
+
{ tag: t.emphasis, fontStyle: "italic" },
|
|
91
|
+
{ tag: t.strikethrough, textDecoration: "line-through" },
|
|
92
|
+
{ tag: t.link, color: "var(--color-code-comment)", textDecoration: "underline" },
|
|
93
|
+
{ tag: t.heading, fontWeight: "bold", color: "var(--color-code-name)" },
|
|
94
|
+
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: "var(--color-code-constant)" },
|
|
95
|
+
{ tag: [t.processingInstruction, t.string, t.inserted], color: "var(--color-code-string)" },
|
|
96
|
+
{ tag: t.invalid, color: "var(--color-danger-ink)" },
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
export const studioCodeTheme = [editorTheme, syntaxHighlighting(highlightStyle)];
|
|
@@ -124,6 +124,37 @@ describe("useDomEditNudge — selection cleanup keyed on stable identity", () =>
|
|
|
124
124
|
element.remove();
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it("leaves arrows on a focused slider alone", () => {
|
|
128
|
+
const host = document.createElement("div");
|
|
129
|
+
document.body.append(host);
|
|
130
|
+
const root = createRoot(host);
|
|
131
|
+
const element = document.createElement("div");
|
|
132
|
+
element.id = "dot-a";
|
|
133
|
+
const slider = document.createElement("div");
|
|
134
|
+
slider.setAttribute("role", "slider");
|
|
135
|
+
document.body.append(element, slider);
|
|
136
|
+
const commit = vi.fn();
|
|
137
|
+
act(() => {
|
|
138
|
+
root.render(
|
|
139
|
+
React.createElement(Harness, {
|
|
140
|
+
selection: makeSelection("Dot", element),
|
|
141
|
+
onPathOffsetCommit: commit,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
act(() => {
|
|
146
|
+
slider.dispatchEvent(
|
|
147
|
+
new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true }),
|
|
148
|
+
);
|
|
149
|
+
vi.advanceTimersByTime(CANVAS_NUDGE_COMMIT_DEBOUNCE_MS + 10);
|
|
150
|
+
});
|
|
151
|
+
expect(commit).not.toHaveBeenCalled();
|
|
152
|
+
act(() => root.unmount());
|
|
153
|
+
host.remove();
|
|
154
|
+
element.remove();
|
|
155
|
+
slider.remove();
|
|
156
|
+
});
|
|
157
|
+
|
|
127
158
|
it("still flushes the burst when the selection actually changes to a different element", () => {
|
|
128
159
|
const host = document.createElement("div");
|
|
129
160
|
document.body.append(host);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useCallback, useEffect, useRef, type RefObject } from "react";
|
|
9
9
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
10
|
-
import {
|
|
10
|
+
import { ownsPlainKeys } from "../../utils/typingTarget";
|
|
11
11
|
import { acquireCanvasNudgeKeys } from "../../utils/canvasNudgeGate";
|
|
12
12
|
import type { DomEditSelection } from "./domEditing";
|
|
13
13
|
import {
|
|
@@ -132,7 +132,7 @@ function resolveSingleNudgeTarget(
|
|
|
132
132
|
function shouldIgnoreNudgeKey(p: UseDomEditNudgeParams, event: KeyboardEvent): boolean {
|
|
133
133
|
if (!p.allowCanvasMovement || event.defaultPrevented) return true;
|
|
134
134
|
if (p.gestureRef.current || p.groupGestureRef.current || p.blockedMoveRef.current) return true;
|
|
135
|
-
return
|
|
135
|
+
return ownsPlainKeys(event.target);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: () => void } {
|
|
@@ -44,7 +44,7 @@ export function useInlineTextEditing(
|
|
|
44
44
|
} {
|
|
45
45
|
const actions = useDomEditActionsContextOptional();
|
|
46
46
|
const inlineText = useInlineTextEdit({
|
|
47
|
-
onCommit: (commit) =>
|
|
47
|
+
onCommit: (commit) => actions?.handleDomRichTextCommit(commit),
|
|
48
48
|
onPause: () => usePlayerStore.getState().setIsPlaying(false),
|
|
49
49
|
});
|
|
50
50
|
const lastPressRef = useRef<PressMark | null>(null);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, useRef } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
5
|
+
import { useMotionPathData } from "./useMotionPathData";
|
|
6
|
+
import { resetOverlayFrameLoopForTests } from "./overlayFrameLoop";
|
|
7
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
8
|
+
|
|
9
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
10
|
+
|
|
11
|
+
const originalRaf = window.requestAnimationFrame;
|
|
12
|
+
let frames: Array<() => void> = [];
|
|
13
|
+
const runFrames = (count: number) => {
|
|
14
|
+
for (let i = 0; i < count; i++) {
|
|
15
|
+
const batch = frames;
|
|
16
|
+
frames = [];
|
|
17
|
+
act(() => {
|
|
18
|
+
for (const frame of batch) frame();
|
|
19
|
+
vi.advanceTimersByTime(16);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.useFakeTimers({
|
|
26
|
+
toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "performance"],
|
|
27
|
+
});
|
|
28
|
+
window.requestAnimationFrame = ((callback: FrameRequestCallback) =>
|
|
29
|
+
frames.push(() => callback(performance.now()))) as typeof window.requestAnimationFrame;
|
|
30
|
+
usePlayerStore.setState({ previewBooted: true, motionPathArmed: false });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
resetOverlayFrameLoopForTests();
|
|
35
|
+
window.requestAnimationFrame = originalRaf;
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
usePlayerStore.setState({ previewBooted: false, motionPathArmed: false });
|
|
38
|
+
document.body.innerHTML = "";
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("reads no layout per frame until there is a path or a create ring to draw", () => {
|
|
42
|
+
const iframe = document.createElement("iframe");
|
|
43
|
+
document.body.append(iframe);
|
|
44
|
+
const reads = vi.spyOn(iframe, "getBoundingClientRect");
|
|
45
|
+
function Probe() {
|
|
46
|
+
const ref = useRef(iframe);
|
|
47
|
+
useMotionPathData(ref, "#box");
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const host = document.createElement("div");
|
|
51
|
+
document.body.append(host);
|
|
52
|
+
const root = createRoot(host);
|
|
53
|
+
try {
|
|
54
|
+
act(() => root.render(<Probe />));
|
|
55
|
+
runFrames(10);
|
|
56
|
+
expect(reads).not.toHaveBeenCalled();
|
|
57
|
+
|
|
58
|
+
act(() => usePlayerStore.setState({ motionPathArmed: true }));
|
|
59
|
+
runFrames(10);
|
|
60
|
+
expect(reads).toHaveBeenCalled();
|
|
61
|
+
} finally {
|
|
62
|
+
act(() => root.unmount());
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -3,6 +3,7 @@ import { readRuntimeKeyframes } from "../../hooks/gsapRuntimeKeyframes";
|
|
|
3
3
|
import { isElementVisibleForOverlay } from "./domEditOverlayGeometry";
|
|
4
4
|
import { buildMotionPathGeometry, type MotionPathGeometry } from "./motionPathGeometry";
|
|
5
5
|
import { subscribeOverlayFrame } from "./overlayFrameLoop";
|
|
6
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
6
7
|
|
|
7
8
|
type Rect = { left: number; top: number; width: number; height: number };
|
|
8
9
|
|
|
@@ -114,6 +115,8 @@ export function useMotionPathData(
|
|
|
114
115
|
// Perspective magnification (1/m44) of the selected element — applied to the
|
|
115
116
|
// path's offset points so depth (translateZ) elements' paths track on screen.
|
|
116
117
|
const [pScale, setPScale] = useState(1);
|
|
118
|
+
const armed = usePlayerStore((s) => s.motionPathArmed);
|
|
119
|
+
const drawn = geometry !== null || armed;
|
|
117
120
|
|
|
118
121
|
useEffect(() => {
|
|
119
122
|
if (!selector) {
|
|
@@ -122,6 +125,7 @@ export function useMotionPathData(
|
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
124
127
|
setHome(null);
|
|
128
|
+
if (!drawn) return;
|
|
125
129
|
const tick = () => {
|
|
126
130
|
const el = iframeRef.current;
|
|
127
131
|
if (el) {
|
|
@@ -155,7 +159,7 @@ export function useMotionPathData(
|
|
|
155
159
|
}
|
|
156
160
|
};
|
|
157
161
|
return subscribeOverlayFrame(tick);
|
|
158
|
-
}, [selector, iframeRef]);
|
|
162
|
+
}, [selector, iframeRef, drawn]);
|
|
159
163
|
|
|
160
164
|
useEffect(() => {
|
|
161
165
|
if (!selector) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { memo, useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import { toastSurface } from "../StudioToast";
|
|
2
3
|
import {
|
|
3
4
|
trackStudioFeedback,
|
|
4
5
|
trackStudioFeedbackShown,
|
|
@@ -212,24 +213,11 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
|
|
|
212
213
|
}}
|
|
213
214
|
>
|
|
214
215
|
<div
|
|
215
|
-
className=
|
|
216
|
-
style={{
|
|
217
|
-
background: failed
|
|
218
|
-
? "linear-gradient(135deg, rgba(127,29,29,0.55), rgba(80,10,10,0.45))"
|
|
219
|
-
: "linear-gradient(135deg, rgba(38,38,38,0.55), rgba(23,23,23,0.45))",
|
|
220
|
-
backdropFilter: "blur(16px) saturate(1.6)",
|
|
221
|
-
WebkitBackdropFilter: "blur(16px) saturate(1.6)",
|
|
222
|
-
border: `1px solid ${failed ? "rgba(239,68,68,0.18)" : "rgba(255,255,255,0.08)"}`,
|
|
223
|
-
boxShadow: [
|
|
224
|
-
"0 8px 32px rgba(0,0,0,0.35)",
|
|
225
|
-
`inset 0 1px 0 ${failed ? "rgba(239,68,68,0.12)" : "rgba(255,255,255,0.06)"}`,
|
|
226
|
-
"inset 0 -1px 0 rgba(0,0,0,0.15)",
|
|
227
|
-
].join(", "),
|
|
228
|
-
}}
|
|
216
|
+
className={`flex w-[min(340px,calc(100vw-48px))] flex-col gap-2.5 overflow-hidden rounded-2xl px-3.5 py-3 ${toastSurface(failed)}`}
|
|
229
217
|
>
|
|
230
218
|
<div className="flex items-start gap-3">
|
|
231
219
|
<p
|
|
232
|
-
className={`min-w-0 flex-1 text-[12px] leading-5 ${failed ? "text-
|
|
220
|
+
className={`min-w-0 flex-1 text-[12px] leading-5 ${failed ? "text-danger-ink" : "text-text-0"}`}
|
|
233
221
|
>
|
|
234
222
|
{step === "thanks" ? "Thanks, that helps." : title}
|
|
235
223
|
</p>
|
|
@@ -243,7 +231,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
|
|
|
243
231
|
if (req) trackStudioFeedbackInterviewClick({ reason: req.reason });
|
|
244
232
|
close("sent");
|
|
245
233
|
}}
|
|
246
|
-
className="h-6 shrink-0 whitespace-nowrap rounded-full border border-
|
|
234
|
+
className="h-6 shrink-0 whitespace-nowrap rounded-full border border-border-strong bg-hover px-2.5 text-[11px] leading-6 text-neutral-100 transition-[background-color,border-color,transform] duration-150 ease-out hover:border-text-muted hover:bg-press active:scale-[0.97] motion-reduce:transition-none"
|
|
247
235
|
>
|
|
248
236
|
Talk to us, 30 min
|
|
249
237
|
</a>
|
|
@@ -251,7 +239,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
|
|
|
251
239
|
<button
|
|
252
240
|
type="button"
|
|
253
241
|
onClick={() => close("close")}
|
|
254
|
-
className="-mr-1 -mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-
|
|
242
|
+
className="-mr-1 -mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-press hover:text-neutral-200"
|
|
255
243
|
aria-label="Dismiss"
|
|
256
244
|
>
|
|
257
245
|
<svg
|
|
@@ -270,7 +258,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
|
|
|
270
258
|
{/* The failure text the user already saw, quoted back so the report
|
|
271
259
|
carries it without asking them to retype the error. */}
|
|
272
260
|
{failed && request.detail && step !== "thanks" && (
|
|
273
|
-
<p className="line-clamp-2 text-[11px] leading-4 text-
|
|
261
|
+
<p className="line-clamp-2 text-[11px] leading-4 text-danger-ink">{request.detail}</p>
|
|
274
262
|
)}
|
|
275
263
|
|
|
276
264
|
{step === "rating" && (
|
|
@@ -283,7 +271,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
|
|
|
283
271
|
key={n}
|
|
284
272
|
// A resting fill, so the row reads as eleven controls rather
|
|
285
273
|
// than as a line of text that happens to be clickable.
|
|
286
|
-
className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-
|
|
274
|
+
className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-hover text-[11px] tabular-nums text-neutral-400 transition-[background-color,color,transform] duration-150 ease-out hover:bg-press hover:text-neutral-100 active:scale-[0.97] has-checked:bg-text-0 has-checked:text-bg-0 has-focus-visible:ring-1 has-focus-visible:ring-ring motion-reduce:transition-none"
|
|
287
275
|
>
|
|
288
276
|
<input
|
|
289
277
|
type="radio"
|
|
@@ -328,8 +316,8 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
|
|
|
328
316
|
// attached to nothing.
|
|
329
317
|
className={`h-6 rounded-full border px-2.5 text-[11px] transition-[background-color,border-color,color,transform] duration-150 ease-out active:scale-[0.97] motion-reduce:transition-none ${
|
|
330
318
|
failed
|
|
331
|
-
? "border-
|
|
332
|
-
: "border-
|
|
319
|
+
? "border-danger/30 bg-danger/10 text-danger-ink hover:border-danger/60 hover:bg-danger/20"
|
|
320
|
+
: "border-border bg-hover text-neutral-300 hover:border-text-muted hover:bg-press hover:text-text-0"
|
|
333
321
|
}`}
|
|
334
322
|
>
|
|
335
323
|
{preset.label}
|
|
@@ -355,12 +343,12 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
|
|
|
355
343
|
placeholder={followUp.placeholder}
|
|
356
344
|
aria-label={followUp.prompt}
|
|
357
345
|
maxLength={500}
|
|
358
|
-
className="h-7 min-w-0 flex-1 rounded-md border border-
|
|
346
|
+
className="h-7 min-w-0 flex-1 rounded-md border border-border bg-input px-2 text-[11px] text-neutral-100 outline-hidden transition-colors duration-150 placeholder:text-neutral-500 focus:border-ring"
|
|
359
347
|
/>
|
|
360
348
|
<button
|
|
361
349
|
type="button"
|
|
362
350
|
onClick={() => submit()}
|
|
363
|
-
className="h-7 shrink-0 rounded-md bg-
|
|
351
|
+
className="h-7 shrink-0 rounded-md bg-press px-2.5 text-[11px] text-neutral-100 transition-[background-color,transform] duration-150 ease-out hover:bg-on active:scale-[0.97] motion-reduce:transition-none"
|
|
364
352
|
>
|
|
365
353
|
Send
|
|
366
354
|
</button>
|
|
@@ -142,7 +142,7 @@ export function AssetPreviewOverlay() {
|
|
|
142
142
|
>
|
|
143
143
|
{/* Close button */}
|
|
144
144
|
<button
|
|
145
|
-
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-
|
|
145
|
+
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-raised hover:bg-press text-text-2 hover:text-text-0 flex items-center justify-center transition-colors z-10"
|
|
146
146
|
onClick={(e) => {
|
|
147
147
|
e.stopPropagation();
|
|
148
148
|
clearPreviewAsset();
|
|
@@ -215,7 +215,7 @@ function useMeterLoop(
|
|
|
215
215
|
|
|
216
216
|
function Bar({ maskRef, peakRef }: { maskRef: Ref<HTMLDivElement>; peakRef: Ref<HTMLDivElement> }) {
|
|
217
217
|
return (
|
|
218
|
-
<div className="relative h-full w-[18px] overflow-hidden rounded-[2px] bg-neutral-900">
|
|
218
|
+
<div className="scheme-dark relative h-full w-[18px] overflow-hidden rounded-[2px] bg-neutral-900">
|
|
219
219
|
<div
|
|
220
220
|
className="absolute inset-x-0 bottom-0 bg-green-500"
|
|
221
221
|
style={{ height: `${AMBER_AT * 100}%` }}
|
|
@@ -237,7 +237,7 @@ function Bar({ maskRef, peakRef }: { maskRef: Ref<HTMLDivElement>; peakRef: Ref<
|
|
|
237
237
|
<div
|
|
238
238
|
ref={peakRef}
|
|
239
239
|
data-testid="meter-peak"
|
|
240
|
-
className="absolute inset-x-0 bottom-0 h-px bg-
|
|
240
|
+
className="absolute inset-x-0 bottom-0 h-px bg-text-0"
|
|
241
241
|
/>
|
|
242
242
|
</div>
|
|
243
243
|
);
|
|
@@ -413,13 +413,14 @@ describe("NLEPreview", () => {
|
|
|
413
413
|
describe("a missing poster", () => {
|
|
414
414
|
const renderUrl =
|
|
415
415
|
"/api/projects/timeline-edit-playground/thumbnail/index.html?t=0&output=source";
|
|
416
|
-
const fetchSpy = vi.fn(() => Promise.resolve(new Response()));
|
|
416
|
+
const fetchSpy = vi.fn((_url: string, _init?: RequestInit) => Promise.resolve(new Response()));
|
|
417
417
|
beforeEach(() => {
|
|
418
418
|
fetchSpy.mockClear();
|
|
419
419
|
vi.stubGlobal("fetch", fetchSpy);
|
|
420
420
|
});
|
|
421
421
|
afterEach(() => vi.unstubAllGlobals());
|
|
422
422
|
|
|
423
|
+
const renders = () => fetchSpy.mock.calls.map(([url]) => url);
|
|
423
424
|
const settle = (
|
|
424
425
|
view: ReturnType<typeof renderPreview>,
|
|
425
426
|
steps: Array<"ready" | "missing" | "loaded">,
|
|
@@ -437,7 +438,10 @@ describe("NLEPreview", () => {
|
|
|
437
438
|
it("is rendered for the next open when the live frame is ready first", () => {
|
|
438
439
|
const view = renderPreview();
|
|
439
440
|
settle(view, ["ready", "missing"]);
|
|
440
|
-
expect(
|
|
441
|
+
expect(renders()).toEqual([renderUrl]);
|
|
442
|
+
expect(fetchSpy.mock.calls[0]?.[1]?.signal, "rendered as scheduler work").toBeInstanceOf(
|
|
443
|
+
AbortSignal,
|
|
444
|
+
);
|
|
441
445
|
expect(view.stage.querySelector('[data-testid="preview-poster"]')).toBeNull();
|
|
442
446
|
view.cleanup();
|
|
443
447
|
});
|
|
@@ -447,7 +451,7 @@ describe("NLEPreview", () => {
|
|
|
447
451
|
settle(view, ["missing"]);
|
|
448
452
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
449
453
|
settle(view, ["ready", "ready"]);
|
|
450
|
-
expect(
|
|
454
|
+
expect(renders()).toEqual([renderUrl]);
|
|
451
455
|
view.cleanup();
|
|
452
456
|
});
|
|
453
457
|
|
|
@@ -458,7 +462,7 @@ describe("NLEPreview", () => {
|
|
|
458
462
|
settle(view, ["ready"]);
|
|
459
463
|
view.render();
|
|
460
464
|
settle(view, ["ready", "missing"]);
|
|
461
|
-
expect(
|
|
465
|
+
expect(renders()).toEqual([renderUrl]);
|
|
462
466
|
view.cleanup();
|
|
463
467
|
});
|
|
464
468
|
});
|
|
@@ -152,7 +152,7 @@ export function PreviewOverlays({
|
|
|
152
152
|
<button
|
|
153
153
|
type="button"
|
|
154
154
|
onClick={enterCaptionMode}
|
|
155
|
-
className="pointer-events-auto absolute top-2 left-1/2 -translate-x-1/2 z-60 rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-
|
|
155
|
+
className="pointer-events-auto absolute top-2 left-1/2 -translate-x-1/2 z-60 rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-accent-ink focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-studio-accent"
|
|
156
156
|
>
|
|
157
157
|
Edit captions
|
|
158
158
|
</button>
|
|
@@ -116,9 +116,9 @@ export function PreviewPane({
|
|
|
116
116
|
<div
|
|
117
117
|
ref={containerRef}
|
|
118
118
|
// A recessed well inset in its dock card; dropped in fullscreen so the
|
|
119
|
-
// preview fills the screen edge-to-edge.
|
|
120
|
-
className={`flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
|
|
121
|
-
isFullscreen ? "" : "m-3 rounded-md border border-
|
|
119
|
+
// preview fills the screen edge-to-edge. The player stays dark in both themes.
|
|
120
|
+
className={`scheme-dark flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
|
|
121
|
+
isFullscreen ? "" : "m-3 rounded-md border border-border"
|
|
122
122
|
}`}
|
|
123
123
|
data-studio-fullscreen-target=""
|
|
124
124
|
>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from "react";
|
|
2
2
|
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
3
|
+
import {
|
|
4
|
+
createThumbnailKey,
|
|
5
|
+
thumbnailScheduler,
|
|
6
|
+
type ThumbnailRequest,
|
|
7
|
+
} from "../../player/lib/thumbnailScheduler";
|
|
3
8
|
import type { PreviewCompositionSize } from "../../utils/previewCompositionSize";
|
|
4
9
|
|
|
5
10
|
/** Frame 0 as the thumbnail route last rendered it; `cachedOnly` never starts a render. */
|
|
@@ -65,7 +70,7 @@ export function usePreviewPoster(
|
|
|
65
70
|
const renderMissingPoster = useCallback(() => {
|
|
66
71
|
if (missingForRef.current !== visit || liveReadyForRef.current !== visit) return;
|
|
67
72
|
missingForRef.current = null;
|
|
68
|
-
|
|
73
|
+
renderPosterForNextOpen(projectId);
|
|
69
74
|
}, [visit, projectId]);
|
|
70
75
|
const onLiveReadyToShowChange = useCallback(
|
|
71
76
|
(ready: boolean) => {
|
|
@@ -91,6 +96,29 @@ export function usePreviewPoster(
|
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
/** Renders frame 0 under the thumbnail cap; the lease ends once the render settles. */
|
|
100
|
+
export function renderPosterForNextOpen(projectId: string): void {
|
|
101
|
+
const url = previewPosterUrl(projectId, false);
|
|
102
|
+
const request: ThumbnailRequest = {
|
|
103
|
+
key: createThumbnailKey({ kind: "poster-render", url }),
|
|
104
|
+
projectId,
|
|
105
|
+
sessionEpoch: 0,
|
|
106
|
+
kind: "composition",
|
|
107
|
+
priority: "overscan",
|
|
108
|
+
discardWhenReleased: true,
|
|
109
|
+
load: async (signal) => {
|
|
110
|
+
await (await fetch(url, { signal })).arrayBuffer();
|
|
111
|
+
return { value: { kind: "image", url, aspect: 16 / 9 }, weight: 0 };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const settled = () => ["ready", "error"].includes(thumbnailScheduler.getSnapshot(request).status);
|
|
115
|
+
let lease: { release(): void } | null = null;
|
|
116
|
+
lease = thumbnailScheduler.acquire(request, () => {
|
|
117
|
+
if (settled()) lease?.release();
|
|
118
|
+
});
|
|
119
|
+
if (settled()) lease.release();
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
function useVisitNumber(key: string): number {
|
|
95
123
|
const [seen, setSeen] = useState({ key, visit: 0 });
|
|
96
124
|
if (seen.key === key) return seen.visit;
|
|
@@ -85,7 +85,7 @@ export function PreviewZoomOverlay({
|
|
|
85
85
|
</span>
|
|
86
86
|
<button
|
|
87
87
|
type="button"
|
|
88
|
-
className="rounded px-1.5 py-0.5 font-medium text-
|
|
88
|
+
className="rounded px-1.5 py-0.5 font-medium text-accent-ink hover:bg-white/10 transition-colors"
|
|
89
89
|
onClick={onFit}
|
|
90
90
|
aria-label="Fit the whole frame in view"
|
|
91
91
|
data-testid="preview-zoom-fit"
|
|
@@ -413,7 +413,7 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
|
|
|
413
413
|
|
|
414
414
|
return (
|
|
415
415
|
<div
|
|
416
|
-
className="flex flex-col h-full overflow-y-auto text-
|
|
416
|
+
className="flex flex-col h-full overflow-y-auto text-text-0"
|
|
417
417
|
onKeyDown={(e) => {
|
|
418
418
|
// In-panel undo — scoped so it never fights the app-level file undo.
|
|
419
419
|
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z") {
|
|
@@ -428,14 +428,14 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
|
|
|
428
428
|
{persistError && (
|
|
429
429
|
<div
|
|
430
430
|
role="alert"
|
|
431
|
-
className="flex items-center justify-between gap-2 px-3 py-2 bg-
|
|
431
|
+
className="flex items-center justify-between gap-2 px-3 py-2 bg-danger/15 border-b border-red-500/40"
|
|
432
432
|
>
|
|
433
|
-
<span className="text-[11px] text-
|
|
433
|
+
<span className="text-[11px] text-danger-ink">Changes not saved</span>
|
|
434
434
|
<button
|
|
435
435
|
type="button"
|
|
436
436
|
disabled={retrying}
|
|
437
437
|
onClick={handleRetryPersist}
|
|
438
|
-
className="px-2 py-0.5 text-[10px] rounded-sm bg-
|
|
438
|
+
className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger enabled:hover:brightness-95 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
|
|
439
439
|
>
|
|
440
440
|
{retrying ? "Retrying…" : "Retry"}
|
|
441
441
|
</button>
|