@hyperframes/studio 0.6.109 → 0.6.111
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-BRjQ5a5_.js +418 -0
- package/dist/assets/index-B7S86vK1.js +370 -0
- package/dist/assets/index-DP8pPIk2.css +1 -0
- package/dist/assets/{index-SlMJloLR.js → index-_IV-vm9l.js} +1 -1
- package/dist/assets/{index-CFCe0Xvn.js → index-x0c2-zQN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -5
- package/src/App.tsx +133 -141
- package/src/components/StudioHeader.tsx +44 -0
- package/src/components/StudioOverlays.tsx +70 -0
- package/src/components/editor/SourceEditor.tsx +19 -16
- package/src/components/editor/gsapAnimationConstants.ts +24 -0
- package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
- package/src/components/editor/manualEditingAvailability.ts +25 -7
- package/src/components/storyboard/FramePoster.tsx +56 -0
- package/src/components/storyboard/StoryboardDirection.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +262 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +88 -0
- package/src/components/storyboard/StoryboardGrid.tsx +33 -0
- package/src/components/storyboard/StoryboardLoaded.tsx +136 -0
- package/src/components/storyboard/StoryboardScriptPanel.tsx +26 -0
- package/src/components/storyboard/StoryboardSourceEditor.tsx +231 -0
- package/src/components/storyboard/StoryboardStatusLegend.tsx +21 -0
- package/src/components/storyboard/StoryboardView.tsx +78 -0
- package/src/components/storyboard/frameStatus.ts +36 -0
- package/src/contexts/ViewModeContext.tsx +98 -0
- package/src/hooks/gsapScriptCommitTypes.ts +4 -7
- package/src/hooks/sdkSelfWriteRegistry.test.ts +67 -0
- package/src/hooks/sdkSelfWriteRegistry.ts +77 -0
- package/src/hooks/timelineEditingHelpers.ts +2 -0
- package/src/hooks/useAppHotkeys.ts +20 -0
- package/src/hooks/useDomEditCommits.ts +43 -14
- package/src/hooks/useDomEditSession.test.ts +41 -0
- package/src/hooks/useDomEditSession.ts +38 -6
- package/src/hooks/useDomGeometryCommits.ts +5 -9
- package/src/hooks/useElementLifecycleOps.ts +30 -0
- package/src/hooks/useGsapAnimationOps.ts +78 -51
- package/src/hooks/useGsapKeyframeOps.ts +127 -43
- package/src/hooks/useGsapPropertyDebounce.test.ts +70 -0
- package/src/hooks/useGsapPropertyDebounce.ts +201 -23
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +85 -0
- package/src/hooks/useGsapScriptCommits.ts +95 -40
- package/src/hooks/useGsapTweenCache.ts +0 -27
- package/src/hooks/useRazorSplit.ts +4 -10
- package/src/hooks/useSdkSession.test.ts +12 -0
- package/src/hooks/useSdkSession.ts +91 -25
- package/src/hooks/useStoryboard.ts +80 -0
- package/src/hooks/useTimelineEditing.ts +163 -68
- package/src/utils/gsapSoftReload.ts +14 -0
- package/src/utils/sdkCutover.gate.test.ts +61 -0
- package/src/utils/sdkCutover.test.ts +839 -0
- package/src/utils/sdkCutover.ts +465 -0
- package/src/utils/sdkOpMapping.ts +43 -0
- package/src/utils/sdkResolverShadow.test.ts +366 -0
- package/src/utils/sdkResolverShadow.ts +313 -0
- package/src/utils/timelineElementSplit.test.ts +61 -1
- package/src/utils/timelineElementSplit.ts +18 -0
- package/dist/assets/hyperframes-player-67pq7USK.js +0 -418
- package/dist/assets/index-BVqybwMG.css +0 -1
- package/dist/assets/index-ho_f4axK.js +0 -296
- package/src/utils/sdkShadow.test.ts +0 -606
- package/src/utils/sdkShadow.ts +0 -517
- package/src/utils/sdkShadowGsapFidelity.ts +0 -296
- package/src/utils/sdkShadowGsapKeyframe.test.ts +0 -265
- package/src/utils/sdkShadowGsapKeyframe.ts +0 -257
- package/src/utils/sdkShadowNumeric.ts +0 -11
|
@@ -3,11 +3,13 @@ import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons";
|
|
|
3
3
|
import {
|
|
4
4
|
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
5
5
|
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
6
|
+
STUDIO_STORYBOARD_ENABLED,
|
|
6
7
|
} from "./editor/manualEditingAvailability";
|
|
7
8
|
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
8
9
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
9
10
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
10
11
|
import { useDomEditActionsContext } from "../contexts/DomEditContext";
|
|
12
|
+
import { useViewMode, type StudioViewMode } from "../contexts/ViewModeContext";
|
|
11
13
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
12
14
|
|
|
13
15
|
export interface StudioHeaderProps {
|
|
@@ -141,6 +143,46 @@ function HyperframesLogo() {
|
|
|
141
143
|
);
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
const VIEW_MODE_OPTIONS: Array<{ mode: StudioViewMode; label: string }> = [
|
|
147
|
+
{ mode: "storyboard", label: "Storyboard" },
|
|
148
|
+
{ mode: "timeline", label: "Preview" },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/** Segmented control switching the main stage between storyboard and preview. */
|
|
152
|
+
function ViewModeToggle() {
|
|
153
|
+
const { viewMode, setViewMode } = useViewMode();
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
className="flex items-center gap-0.5 rounded-md bg-neutral-800 p-0.5"
|
|
157
|
+
role="tablist"
|
|
158
|
+
aria-label="Studio view"
|
|
159
|
+
>
|
|
160
|
+
{VIEW_MODE_OPTIONS.map(({ mode, label }) => {
|
|
161
|
+
const active = viewMode === mode;
|
|
162
|
+
return (
|
|
163
|
+
<button
|
|
164
|
+
key={mode}
|
|
165
|
+
type="button"
|
|
166
|
+
role="tab"
|
|
167
|
+
aria-selected={active}
|
|
168
|
+
onClick={() => {
|
|
169
|
+
if (active) return;
|
|
170
|
+
trackStudioEvent("view_mode_toggle", { mode });
|
|
171
|
+
setViewMode(mode);
|
|
172
|
+
}}
|
|
173
|
+
className={`rounded px-3 py-1 text-[11px] font-medium transition-colors ${
|
|
174
|
+
active ? "bg-neutral-200 text-neutral-900" : "text-neutral-400 hover:text-neutral-200"
|
|
175
|
+
}`}
|
|
176
|
+
>
|
|
177
|
+
{label}
|
|
178
|
+
</button>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// fallow-ignore-next-line complexity
|
|
144
186
|
export function StudioHeader({
|
|
145
187
|
captureFrameHref,
|
|
146
188
|
captureFrameFilename,
|
|
@@ -164,6 +206,8 @@ export function StudioHeader({
|
|
|
164
206
|
</span>
|
|
165
207
|
<span className="text-[11px] font-medium text-neutral-300">{projectId}</span>
|
|
166
208
|
</div>
|
|
209
|
+
{/* Center: storyboard / preview toggle (flag-gated) */}
|
|
210
|
+
{STUDIO_STORYBOARD_ENABLED && <ViewModeToggle />}
|
|
167
211
|
{/* Right: toolbar buttons */}
|
|
168
212
|
<div className="flex items-center gap-1.5">
|
|
169
213
|
<button
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { LintModal } from "./LintModal";
|
|
3
|
+
import { AskAgentModal } from "./AskAgentModal";
|
|
4
|
+
import { StudioGlobalDragOverlay } from "./StudioGlobalDragOverlay";
|
|
5
|
+
import { StudioToast } from "./StudioToast";
|
|
6
|
+
import { buildAgentContextPreview } from "./editor/domEditingAgentPrompt";
|
|
7
|
+
import type { useDomEditSession } from "../hooks/useDomEditSession";
|
|
8
|
+
import type { useToast } from "../hooks/useToast";
|
|
9
|
+
|
|
10
|
+
type LintFindings = ComponentProps<typeof LintModal>["findings"];
|
|
11
|
+
|
|
12
|
+
export interface StudioOverlaysProps {
|
|
13
|
+
projectId: string;
|
|
14
|
+
lintModal: LintFindings | null;
|
|
15
|
+
closeLintModal: () => void;
|
|
16
|
+
consoleErrors: LintFindings | null;
|
|
17
|
+
clearConsoleErrors: () => void;
|
|
18
|
+
domEditSession: ReturnType<typeof useDomEditSession>;
|
|
19
|
+
activeCompPath: string | null;
|
|
20
|
+
dragOverlayActive: boolean;
|
|
21
|
+
appToast: ReturnType<typeof useToast>["appToast"];
|
|
22
|
+
dismissToast: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Floating overlays for the studio shell: lint / console-error modals, the
|
|
27
|
+
* ask-agent modal, the global drag overlay, and the toast. Extracted from
|
|
28
|
+
* `App.tsx` to keep the shell within the studio's 600-line decomposition budget.
|
|
29
|
+
*/
|
|
30
|
+
// fallow-ignore-next-line complexity
|
|
31
|
+
export function StudioOverlays({
|
|
32
|
+
projectId,
|
|
33
|
+
lintModal,
|
|
34
|
+
closeLintModal,
|
|
35
|
+
consoleErrors,
|
|
36
|
+
clearConsoleErrors,
|
|
37
|
+
domEditSession,
|
|
38
|
+
activeCompPath,
|
|
39
|
+
dragOverlayActive,
|
|
40
|
+
appToast,
|
|
41
|
+
dismissToast,
|
|
42
|
+
}: StudioOverlaysProps) {
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{lintModal !== null && (
|
|
46
|
+
<LintModal findings={lintModal} projectId={projectId} onClose={closeLintModal} />
|
|
47
|
+
)}
|
|
48
|
+
{consoleErrors !== null && consoleErrors.length > 0 && (
|
|
49
|
+
<LintModal findings={consoleErrors} projectId={projectId} onClose={clearConsoleErrors} />
|
|
50
|
+
)}
|
|
51
|
+
{domEditSession.agentModalOpen && domEditSession.domEditSelection && (
|
|
52
|
+
<AskAgentModal
|
|
53
|
+
selectionLabel={domEditSession.domEditSelection.label}
|
|
54
|
+
contextPreview={buildAgentContextPreview(domEditSession.domEditSelection, activeCompPath)}
|
|
55
|
+
anchorPoint={domEditSession.agentModalAnchorPoint}
|
|
56
|
+
onSubmit={domEditSession.handleAgentModalSubmit}
|
|
57
|
+
onClose={() => {
|
|
58
|
+
domEditSession.setAgentModalOpen(false);
|
|
59
|
+
domEditSession.setAgentPromptSelectionContext(undefined);
|
|
60
|
+
domEditSession.setAgentModalAnchorPoint(null);
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
{dragOverlayActive && <StudioGlobalDragOverlay />}
|
|
65
|
+
{appToast && (
|
|
66
|
+
<StudioToast message={appToast.message} tone={appToast.tone} onDismiss={dismissToast} />
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -12,26 +12,26 @@ import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language
|
|
|
12
12
|
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
13
13
|
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
|
14
14
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
15
|
+
import type { Extension } from "@codemirror/state";
|
|
15
16
|
import { html } from "@codemirror/lang-html";
|
|
16
17
|
import { css } from "@codemirror/lang-css";
|
|
17
18
|
import { javascript } from "@codemirror/lang-javascript";
|
|
19
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return html();
|
|
34
|
-
}
|
|
21
|
+
const LANGUAGE_EXTENSIONS: Record<string, () => Extension> = {
|
|
22
|
+
html: () => html(),
|
|
23
|
+
css: () => css(),
|
|
24
|
+
markdown: () => markdown(),
|
|
25
|
+
md: () => markdown(),
|
|
26
|
+
javascript: () => javascript(),
|
|
27
|
+
js: () => javascript(),
|
|
28
|
+
typescript: () => javascript({ typescript: true }),
|
|
29
|
+
ts: () => javascript({ typescript: true }),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function getLanguageExtension(language: string): Extension {
|
|
33
|
+
const factory = LANGUAGE_EXTENSIONS[language] ?? html;
|
|
34
|
+
return factory();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function detectLanguage(filePath: string): string {
|
|
@@ -45,6 +45,8 @@ function detectLanguage(filePath: string): string {
|
|
|
45
45
|
jsx: "javascript",
|
|
46
46
|
tsx: "typescript",
|
|
47
47
|
json: "javascript",
|
|
48
|
+
md: "markdown",
|
|
49
|
+
markdown: "markdown",
|
|
48
50
|
};
|
|
49
51
|
return map[ext] ?? "html";
|
|
50
52
|
}
|
|
@@ -74,6 +76,7 @@ export const SourceEditor = memo(function SourceEditor({
|
|
|
74
76
|
const contentRef = useRef(content);
|
|
75
77
|
contentRef.current = content;
|
|
76
78
|
|
|
79
|
+
// fallow-ignore-next-line complexity
|
|
77
80
|
const mountEditor = useCallback(
|
|
78
81
|
(node: HTMLDivElement | null) => {
|
|
79
82
|
if (editorRef.current) {
|
|
@@ -20,6 +20,12 @@ export const PROP_LABELS: Record<string, string> = {
|
|
|
20
20
|
width: "Width",
|
|
21
21
|
height: "Height",
|
|
22
22
|
rotation: "Rotate",
|
|
23
|
+
z: "Move Z",
|
|
24
|
+
rotationX: "Rotate X",
|
|
25
|
+
rotationY: "Rotate Y",
|
|
26
|
+
rotationZ: "Rotate Z",
|
|
27
|
+
perspective: "Perspective",
|
|
28
|
+
transformOrigin: "Transform Origin",
|
|
23
29
|
opacity: "Opacity",
|
|
24
30
|
scale: "Scale",
|
|
25
31
|
scaleX: "Scale X",
|
|
@@ -46,6 +52,12 @@ export const PROP_UNITS: Record<string, string> = {
|
|
|
46
52
|
width: "px",
|
|
47
53
|
height: "px",
|
|
48
54
|
rotation: "°",
|
|
55
|
+
z: "px",
|
|
56
|
+
rotationX: "°",
|
|
57
|
+
rotationY: "°",
|
|
58
|
+
rotationZ: "°",
|
|
59
|
+
perspective: "px",
|
|
60
|
+
transformOrigin: "",
|
|
49
61
|
opacity: "%",
|
|
50
62
|
scale: "×",
|
|
51
63
|
scaleX: "×",
|
|
@@ -62,6 +74,13 @@ export const PROP_TOOLTIPS: Record<string, string> = {
|
|
|
62
74
|
scaleX: "Horizontal stretch (1 = normal)",
|
|
63
75
|
scaleY: "Vertical stretch (1 = normal)",
|
|
64
76
|
rotation: "Spin angle (360 = full rotation)",
|
|
77
|
+
z: "Move forward/back along the Z axis",
|
|
78
|
+
rotationX: "Rotate around the horizontal X axis",
|
|
79
|
+
rotationY: "Rotate around the vertical Y axis",
|
|
80
|
+
rotationZ: "Rotate around the screen-facing Z axis",
|
|
81
|
+
perspective:
|
|
82
|
+
"3D depth context for child elements; set it on a parent when rotating children in 3D",
|
|
83
|
+
transformOrigin: "Pivot point for transforms, for example center center or 50% 50%",
|
|
65
84
|
width: "Element width",
|
|
66
85
|
height: "Element height",
|
|
67
86
|
autoAlpha: "Like opacity but hides element completely at 0",
|
|
@@ -147,6 +166,11 @@ export const PROP_CONSTRAINTS: Record<string, { min?: number; max?: number; step
|
|
|
147
166
|
scaleX: { min: -10, max: 10, step: 0.01 },
|
|
148
167
|
scaleY: { min: -10, max: 10, step: 0.01 },
|
|
149
168
|
rotation: { step: 1 },
|
|
169
|
+
z: { step: 1 },
|
|
170
|
+
rotationX: { step: 1 },
|
|
171
|
+
rotationY: { step: 1 },
|
|
172
|
+
rotationZ: { step: 1 },
|
|
173
|
+
perspective: { min: 0, step: 1 },
|
|
150
174
|
skewX: { min: -90, max: 90, step: 1 },
|
|
151
175
|
skewY: { min: -90, max: 90, step: 1 },
|
|
152
176
|
width: { min: 0, step: 1 },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
|
|
2
3
|
import { buildTweenSummary } from "./gsapAnimationHelpers";
|
|
4
|
+
import { PROP_LABELS } from "./gsapAnimationConstants";
|
|
3
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
6
|
|
|
5
7
|
function anim(overrides: Partial<GsapAnimation>): GsapAnimation {
|
|
@@ -23,6 +25,27 @@ describe("buildTweenSummary", () => {
|
|
|
23
25
|
expect(s).toContain("move x");
|
|
24
26
|
});
|
|
25
27
|
|
|
28
|
+
it("describes 3D transform tweens with labels and units", () => {
|
|
29
|
+
const s = buildTweenSummary(
|
|
30
|
+
anim({
|
|
31
|
+
properties: {
|
|
32
|
+
z: 120,
|
|
33
|
+
rotationX: 45,
|
|
34
|
+
rotationY: -30,
|
|
35
|
+
rotationZ: 90,
|
|
36
|
+
perspective: 800,
|
|
37
|
+
transformOrigin: "50% 50%",
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
expect(s).toContain("move z to 120px");
|
|
42
|
+
expect(s).toContain("rotate x to 45°");
|
|
43
|
+
expect(s).toContain("rotate y to -30°");
|
|
44
|
+
expect(s).toContain("rotate z to 90°");
|
|
45
|
+
expect(s).toContain("perspective to 800px");
|
|
46
|
+
expect(s).toContain("transform origin to 50% 50%");
|
|
47
|
+
});
|
|
48
|
+
|
|
26
49
|
it("describes a from tween", () => {
|
|
27
50
|
const s = buildTweenSummary(anim({ method: "from", properties: { opacity: 0 } }));
|
|
28
51
|
expect(s).toContain("enters from");
|
|
@@ -65,3 +88,9 @@ describe("buildTweenSummary", () => {
|
|
|
65
88
|
expect(s).toContain("no properties yet");
|
|
66
89
|
});
|
|
67
90
|
});
|
|
91
|
+
|
|
92
|
+
describe("PROP_LABELS", () => {
|
|
93
|
+
it("provides labels for every inspector-supported GSAP property", () => {
|
|
94
|
+
expect(SUPPORTED_PROPS.filter((prop) => !PROP_LABELS[prop])).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -82,6 +82,16 @@ export const STUDIO_RAZOR_TOOL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
82
82
|
true,
|
|
83
83
|
);
|
|
84
84
|
|
|
85
|
+
// Storyboard view: a top-level, toggleable view that renders STORYBOARD.md as a
|
|
86
|
+
// contact sheet of live HTML frame tiles, replacing the timeline/preview stage.
|
|
87
|
+
// Opt-in / off by default until the experience is ready for broad exposure.
|
|
88
|
+
// VITE_STUDIO_ENABLE_STORYBOARD=1 npx hyperframes preview
|
|
89
|
+
export const STUDIO_STORYBOARD_ENABLED = resolveStudioBooleanEnvFlag(
|
|
90
|
+
env,
|
|
91
|
+
["VITE_STUDIO_ENABLE_STORYBOARD", "VITE_STUDIO_STORYBOARD_ENABLED"],
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
94
|
+
|
|
85
95
|
// When disabled (the default), drag/resize/rotate commits always take the CSS
|
|
86
96
|
// persist path instead of being intercepted into GSAP script keyframe
|
|
87
97
|
// mutations. The keyframe intercept rewrites timeline tweens from drag
|
|
@@ -94,14 +104,22 @@ export const STUDIO_GSAP_DRAG_INTERCEPT_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
94
104
|
|
|
95
105
|
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
|
|
96
106
|
|
|
97
|
-
// Stage 7 Step
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
// Stage 7 Step 3c: SDK cutover — routes inline-style ops through SDK dispatch
|
|
108
|
+
// instead of the server patch-element API. Default false; enable via
|
|
109
|
+
// VITE_STUDIO_SDK_CUTOVER_ENABLED=true. Requires SDK session to be open.
|
|
110
|
+
export const STUDIO_SDK_CUTOVER_ENABLED = resolveStudioBooleanEnvFlag(
|
|
111
|
+
env,
|
|
112
|
+
["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
|
|
113
|
+
false,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Resolver-parity tripwire (telemetry-only, decoupled from cutover).
|
|
117
|
+
// Runs the SDK resolver alongside any edit and emits sdk_resolver_shadow on
|
|
118
|
+
// divergence. Default true; disable via VITE_STUDIO_SDK_RESOLVER_SHADOW_ENABLED=false.
|
|
119
|
+
// Soak gate: retire once zero element_not_found divergences over a clean window.
|
|
120
|
+
export const STUDIO_SDK_RESOLVER_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
|
|
103
121
|
env,
|
|
104
|
-
["
|
|
122
|
+
["VITE_STUDIO_SDK_RESOLVER_SHADOW_ENABLED"],
|
|
105
123
|
true,
|
|
106
124
|
);
|
|
107
125
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { buildCompositionThumbnailUrl } from "../../player/components/CompositionThumbnail";
|
|
3
|
+
|
|
4
|
+
export interface FramePosterProps {
|
|
5
|
+
projectId: string;
|
|
6
|
+
/** Project-relative path to the frame's HTML sub-composition. */
|
|
7
|
+
src: string;
|
|
8
|
+
/** Time (seconds) to seek to for the poster. */
|
|
9
|
+
seconds: number;
|
|
10
|
+
title: string;
|
|
11
|
+
/** `cover` fills+crops (contact-sheet tile); `contain` letterboxes (focus hero). */
|
|
12
|
+
fit?: "cover" | "contain";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Server-rendered poster for a frame. The thumbnail route seeks the composition
|
|
17
|
+
* by time (at its real fps) and caches the result, so there's no live iframe,
|
|
18
|
+
* no postMessage seek, and no client-side fps assumption. Shared by the
|
|
19
|
+
* contact-sheet tile and the frame-focus view.
|
|
20
|
+
*/
|
|
21
|
+
export function FramePoster({ projectId, src, seconds, title, fit = "cover" }: FramePosterProps) {
|
|
22
|
+
const [failed, setFailed] = useState(false);
|
|
23
|
+
// The <img> is reused (no key) when a tile/hero swaps to a different frame, so a
|
|
24
|
+
// prior load error would stick. Reset when the poster target changes.
|
|
25
|
+
useEffect(() => setFailed(false), [src, seconds]);
|
|
26
|
+
if (failed) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex h-full w-full items-center justify-center text-[11px] text-neutral-600">
|
|
29
|
+
Preview unavailable
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const url = buildCompositionThumbnailUrl({
|
|
34
|
+
previewUrl: `/api/projects/${projectId}/preview/comp/${src}`,
|
|
35
|
+
seekTime: seconds,
|
|
36
|
+
duration: 0,
|
|
37
|
+
origin: window.location.origin,
|
|
38
|
+
});
|
|
39
|
+
return (
|
|
40
|
+
<img
|
|
41
|
+
src={url}
|
|
42
|
+
alt={title}
|
|
43
|
+
draggable={false}
|
|
44
|
+
loading="lazy"
|
|
45
|
+
onError={() => setFailed(true)}
|
|
46
|
+
className={`h-full w-full ${fit === "contain" ? "object-contain" : "object-cover"}`}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Time (seconds) to show a frame at — past the intro so the key moment is visible. */
|
|
52
|
+
export function posterTime(frame: { poster?: number; durationSeconds?: number }): number {
|
|
53
|
+
if (frame.poster != null) return frame.poster;
|
|
54
|
+
if (frame.durationSeconds != null) return frame.durationSeconds * 0.66;
|
|
55
|
+
return 1.5;
|
|
56
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { StoryboardGlobals } from "@hyperframes/core/storyboard";
|
|
2
|
+
|
|
3
|
+
export interface StoryboardDirectionProps {
|
|
4
|
+
globals: StoryboardGlobals;
|
|
5
|
+
frameCount: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Global direction header: the message/thesis plus arc, audience, and format.
|
|
10
|
+
* This is the storyboard's "north star" pulled from the manifest frontmatter.
|
|
11
|
+
*/
|
|
12
|
+
export function StoryboardDirection({ globals, frameCount }: StoryboardDirectionProps) {
|
|
13
|
+
const meta = [
|
|
14
|
+
{ label: "Arc", value: globals.arc },
|
|
15
|
+
{ label: "Audience", value: globals.audience },
|
|
16
|
+
{ label: "Voice", value: globals.extra.voice },
|
|
17
|
+
{ label: "Format", value: globals.format },
|
|
18
|
+
{ label: "Frames", value: String(frameCount) },
|
|
19
|
+
].filter((item): item is { label: string; value: string } => Boolean(item.value));
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<header className="border-b border-neutral-800 pb-5">
|
|
23
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-neutral-500">
|
|
24
|
+
Storyboard
|
|
25
|
+
</div>
|
|
26
|
+
{globals.message ? (
|
|
27
|
+
<h1 className="mt-1 text-2xl font-semibold leading-tight text-neutral-100">
|
|
28
|
+
{globals.message}
|
|
29
|
+
</h1>
|
|
30
|
+
) : (
|
|
31
|
+
<h1 className="mt-1 text-2xl font-semibold leading-tight text-neutral-400">
|
|
32
|
+
Untitled storyboard
|
|
33
|
+
</h1>
|
|
34
|
+
)}
|
|
35
|
+
<dl className="mt-4 flex flex-wrap gap-x-8 gap-y-2">
|
|
36
|
+
{meta.map((item) => (
|
|
37
|
+
<div key={item.label} className="flex items-baseline gap-2">
|
|
38
|
+
<dt className="text-[11px] uppercase tracking-wider text-neutral-500">{item.label}</dt>
|
|
39
|
+
<dd className="text-sm text-neutral-300">{item.value}</dd>
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</dl>
|
|
43
|
+
</header>
|
|
44
|
+
);
|
|
45
|
+
}
|