@hyperframes/studio 0.7.37 → 0.7.38
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-BBrKzTGd.js +459 -0
- package/dist/assets/index-BLRTwY5l.js +396 -0
- package/dist/assets/{index-BXYJbIax.js → index-ClUipc8i.js} +1 -1
- package/dist/assets/index-DJaiR8T2.css +1 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-Ykq7ihge.js} +1 -1
- package/dist/index.d.ts +35 -10
- package/dist/index.html +2 -2
- package/dist/index.js +5146 -3321
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +19 -18
- package/src/components/AskAgentModal.tsx +23 -6
- package/src/components/LintModal.tsx +40 -10
- package/src/components/MediaPreview.tsx +36 -1
- package/src/components/SaveQueuePausedBanner.tsx +7 -6
- package/src/components/StudioErrorBoundary.tsx +19 -31
- package/src/components/StudioFeedbackBar.tsx +11 -2
- package/src/components/StudioGlobalDragOverlay.tsx +1 -1
- package/src/components/StudioHeader.tsx +184 -106
- package/src/components/StudioLeftSidebar.tsx +24 -3
- package/src/components/StudioOverlays.tsx +34 -8
- package/src/components/StudioRightPanel.tsx +154 -9
- package/src/components/StudioSplash.tsx +6 -3
- package/src/components/StudioToast.tsx +12 -11
- package/src/components/TimelineToolbar.test.tsx +9 -3
- package/src/components/TimelineToolbar.tsx +63 -29
- package/src/components/editor/PropertyPanel.tsx +18 -17
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
- package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
- package/src/components/editor/propertyPanelHelpers.ts +22 -91
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelTypes.ts +111 -0
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
- package/src/components/nle/NLELayout.tsx +54 -51
- package/src/components/nle/NLEPreview.tsx +41 -5
- package/src/components/nle/TimelineResizeDivider.tsx +97 -0
- package/src/components/nle/usePreviewBlockDrop.ts +31 -7
- package/src/components/renders/RenderQueue.tsx +125 -24
- package/src/components/renders/RenderQueueItem.tsx +132 -64
- package/src/components/renders/useRenderQueue.ts +136 -24
- package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
- package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
- package/src/components/storyboard/StoryboardView.tsx +6 -0
- package/src/components/studioColorGradingScope.ts +124 -0
- package/src/components/studioMediaJobs.ts +123 -0
- package/src/components/ui/Button.tsx +20 -11
- package/src/components/ui/HyperframesLoader.tsx +9 -2
- package/src/components/ui/SearchInput.tsx +53 -0
- package/src/components/ui/Tooltip.tsx +52 -6
- package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
- package/src/components/ui/useDialogBehavior.ts +83 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +7 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useEditorSave.ts +14 -0
- package/src/hooks/useFileManager.ts +39 -25
- package/src/hooks/useFrameCapture.ts +12 -1
- package/src/hooks/usePanelLayout.ts +1 -0
- package/src/hooks/usePreviewPersistence.ts +24 -15
- package/src/hooks/useStudioContextValue.ts +5 -0
- package/src/hooks/useToast.ts +66 -13
- package/src/styles/studio.css +61 -0
- package/src/utils/sdkResolverShadow.test.ts +53 -3
- package/src/utils/sdkResolverShadow.ts +41 -1
- package/src/utils/studioPendingEdits.test.ts +43 -0
- package/src/utils/studioPendingEdits.ts +45 -0
- package/src/utils/studioUiPreferences.ts +4 -0
- package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
- package/dist/assets/index-DmkOvZns.css +0 -1
- package/dist/assets/index-SYSbQyK4.js +0 -396
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.38",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"gsap": "^3.13.0",
|
|
47
47
|
"marked": "^14.1.4",
|
|
48
48
|
"mediabunny": "^1.45.3",
|
|
49
|
-
"@hyperframes/core": "0.7.
|
|
50
|
-
"@hyperframes/parsers": "0.7.
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/
|
|
49
|
+
"@hyperframes/core": "0.7.38",
|
|
50
|
+
"@hyperframes/parsers": "0.7.38",
|
|
51
|
+
"@hyperframes/sdk": "0.7.38",
|
|
52
|
+
"@hyperframes/studio-server": "0.7.38",
|
|
53
|
+
"@hyperframes/player": "0.7.38"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "19",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.4.2",
|
|
66
66
|
"vitest": "^3.2.4",
|
|
67
67
|
"zustand": "^5.0.0",
|
|
68
|
-
"@hyperframes/producer": "0.7.
|
|
68
|
+
"@hyperframes/producer": "0.7.38"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -27,6 +27,7 @@ import { useFrameCapture } from "./hooks/useFrameCapture";
|
|
|
27
27
|
import { useLintModal } from "./hooks/useLintModal";
|
|
28
28
|
import { useCompositionDimensions } from "./hooks/useCompositionDimensions";
|
|
29
29
|
import { useToast } from "./hooks/useToast";
|
|
30
|
+
import { useCompositionContentLoader } from "./hooks/useCompositionContentLoader";
|
|
30
31
|
import { useStudioUrlState } from "./hooks/useStudioUrlState";
|
|
31
32
|
import {
|
|
32
33
|
buildStudioContextValue,
|
|
@@ -64,7 +65,6 @@ export function StudioApp() {
|
|
|
64
65
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
65
66
|
const viewModeValue = useViewModeState();
|
|
66
67
|
|
|
67
|
-
// sessionStorage-backed: fires once per tab, survives HMR remounts
|
|
68
68
|
useEffect(() => {
|
|
69
69
|
if (resolving || waitingForServer) return;
|
|
70
70
|
if (hasFiredSessionStart()) return;
|
|
@@ -133,7 +133,7 @@ export function StudioApp() {
|
|
|
133
133
|
return !v;
|
|
134
134
|
});
|
|
135
135
|
}, []);
|
|
136
|
-
const {
|
|
136
|
+
const { toasts, showToast, dismissToast } = useToast();
|
|
137
137
|
const panelLayout = usePanelLayout({
|
|
138
138
|
rightCollapsed: initialUrlStateRef.current.rightCollapsed,
|
|
139
139
|
rightPanelTab: initialUrlStateRef.current.rightPanelTab,
|
|
@@ -390,17 +390,13 @@ export function StudioApp() {
|
|
|
390
390
|
},
|
|
391
391
|
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
|
|
392
392
|
);
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
.catch(() => {});
|
|
401
|
-
},
|
|
402
|
-
[projectId, fileManager],
|
|
403
|
-
);
|
|
393
|
+
const { setEditingFile } = fileManager;
|
|
394
|
+
const handleSelectComposition = useCompositionContentLoader({
|
|
395
|
+
projectId,
|
|
396
|
+
setEditingFile,
|
|
397
|
+
setActiveCompPath,
|
|
398
|
+
showToast,
|
|
399
|
+
});
|
|
404
400
|
const {
|
|
405
401
|
designPanelActive,
|
|
406
402
|
inspectorPanelActive,
|
|
@@ -485,14 +481,20 @@ export function StudioApp() {
|
|
|
485
481
|
captureFrameFilename={frameCapture.captureFrameFilename}
|
|
486
482
|
handleCaptureFrameClick={frameCapture.handleCaptureFrameClick}
|
|
487
483
|
refreshCaptureFrameTime={frameCapture.refreshCaptureFrameTime}
|
|
484
|
+
capturing={frameCapture.capturing}
|
|
488
485
|
inspectorButtonActive={inspectorButtonActive}
|
|
489
486
|
inspectorPanelActive={inspectorPanelActive}
|
|
490
|
-
onExport={() =>
|
|
487
|
+
onExport={() => {
|
|
488
|
+
void (async () => {
|
|
489
|
+
await previewPersistence.waitForPendingDomEditSaves();
|
|
490
|
+
await renderQueue.startRender(undefined);
|
|
491
|
+
})();
|
|
492
|
+
}}
|
|
491
493
|
/>
|
|
492
494
|
{previewPersistence.domEditSaveQueuePaused && (
|
|
493
495
|
<SaveQueuePausedBanner
|
|
494
496
|
message={previewPersistence.domEditSaveQueuePaused}
|
|
495
|
-
|
|
497
|
+
onRetry={previewPersistence.resetDomEditSaveQueueBreaker}
|
|
496
498
|
/>
|
|
497
499
|
)}
|
|
498
500
|
{viewModeValue.viewMode === "storyboard" && (
|
|
@@ -501,8 +503,6 @@ export function StudioApp() {
|
|
|
501
503
|
onSelectComposition={handleSelectComposition}
|
|
502
504
|
/>
|
|
503
505
|
)}
|
|
504
|
-
{/* Timeline stage stays mounted (just hidden) in storyboard mode,
|
|
505
|
-
so preview/player/gesture/render state survives the toggle. */}
|
|
506
506
|
<div
|
|
507
507
|
className={`flex flex-1 min-h-0${
|
|
508
508
|
viewModeValue.viewMode === "storyboard" ? " hidden" : ""
|
|
@@ -576,6 +576,7 @@ export function StudioApp() {
|
|
|
576
576
|
</div>
|
|
577
577
|
<StudioOverlays
|
|
578
578
|
projectId={projectId}
|
|
579
|
+
projectDir={fileManager.projectDir}
|
|
579
580
|
lintModal={lintModal}
|
|
580
581
|
closeLintModal={closeLintModal}
|
|
581
582
|
consoleErrors={consoleErrors}
|
|
@@ -583,7 +584,7 @@ export function StudioApp() {
|
|
|
583
584
|
domEditSession={domEditSession}
|
|
584
585
|
activeCompPath={activeCompPath}
|
|
585
586
|
dragOverlayActive={dragOverlay.active}
|
|
586
|
-
|
|
587
|
+
toasts={toasts}
|
|
587
588
|
dismissToast={dismissToast}
|
|
588
589
|
/>
|
|
589
590
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, type CSSProperties } from "react";
|
|
2
2
|
import { useMountEffect } from "../hooks/useMountEffect";
|
|
3
3
|
import { type AgentModalAnchorPoint, clampNumber } from "../utils/studioHelpers";
|
|
4
|
+
import { useDialogBehavior } from "./ui/useDialogBehavior";
|
|
4
5
|
|
|
5
6
|
function getAgentModalPositionStyle(
|
|
6
7
|
anchorPoint: AgentModalAnchorPoint | null,
|
|
@@ -39,7 +40,16 @@ export function AskAgentModal({
|
|
|
39
40
|
}) {
|
|
40
41
|
const [value, setValue] = useState("");
|
|
41
42
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
43
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
42
44
|
const modalPositionStyle = getAgentModalPositionStyle(anchorPoint);
|
|
45
|
+
// A dirty draft vetoes Escape/backdrop closes — a stray click must not
|
|
46
|
+
// discard typed instructions. The X button and Copy still close directly.
|
|
47
|
+
const { requestClose } = useDialogBehavior({
|
|
48
|
+
open: true,
|
|
49
|
+
onClose,
|
|
50
|
+
containerRef,
|
|
51
|
+
canClose: () => !value.trim(),
|
|
52
|
+
});
|
|
43
53
|
|
|
44
54
|
useMountEffect(() => {
|
|
45
55
|
requestAnimationFrame(() => inputRef.current?.focus());
|
|
@@ -54,13 +64,18 @@ export function AskAgentModal({
|
|
|
54
64
|
<div
|
|
55
65
|
className={
|
|
56
66
|
anchorPoint
|
|
57
|
-
? "fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm"
|
|
58
|
-
: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
67
|
+
? "hf-backdrop-in fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm"
|
|
68
|
+
: "hf-backdrop-in fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
59
69
|
}
|
|
60
|
-
onClick={
|
|
70
|
+
onClick={requestClose}
|
|
61
71
|
>
|
|
62
72
|
<div
|
|
63
|
-
|
|
73
|
+
ref={containerRef}
|
|
74
|
+
role="dialog"
|
|
75
|
+
aria-modal="true"
|
|
76
|
+
aria-label="Copy prompt to AI agent"
|
|
77
|
+
tabIndex={-1}
|
|
78
|
+
className={`w-[480px] rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl outline-none ${
|
|
64
79
|
anchorPoint ? "fixed" : ""
|
|
65
80
|
}`}
|
|
66
81
|
style={modalPositionStyle}
|
|
@@ -74,8 +89,9 @@ export function AskAgentModal({
|
|
|
74
89
|
</p>
|
|
75
90
|
</div>
|
|
76
91
|
<button
|
|
77
|
-
className="p-1 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50"
|
|
92
|
+
className="p-1 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50 active:scale-[0.98]"
|
|
78
93
|
onClick={onClose}
|
|
94
|
+
aria-label="Close"
|
|
79
95
|
>
|
|
80
96
|
<svg
|
|
81
97
|
width="14"
|
|
@@ -100,7 +116,8 @@ export function AskAgentModal({
|
|
|
100
116
|
onChange={(e) => setValue(e.target.value)}
|
|
101
117
|
onKeyDown={(e) => {
|
|
102
118
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit();
|
|
103
|
-
|
|
119
|
+
// Escape is handled at the document level by useDialogBehavior,
|
|
120
|
+
// guarded against discarding a dirty draft.
|
|
104
121
|
}}
|
|
105
122
|
/>
|
|
106
123
|
{contextPreview && (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
2
|
import { XIcon, WarningIcon, CheckCircleIcon, CaretRightIcon } from "@phosphor-icons/react";
|
|
3
3
|
import { copyTextToClipboard } from "../utils/clipboard";
|
|
4
|
+
import { useDialogBehavior } from "./ui/useDialogBehavior";
|
|
4
5
|
|
|
5
6
|
export interface LintFinding {
|
|
6
7
|
severity: "error" | "warning";
|
|
@@ -12,16 +13,28 @@ export interface LintFinding {
|
|
|
12
13
|
export function LintModal({
|
|
13
14
|
findings,
|
|
14
15
|
projectId,
|
|
16
|
+
projectDir,
|
|
17
|
+
title = "HyperFrame Lint Results",
|
|
18
|
+
promptIntro = "Fix these HyperFrames lint issues",
|
|
15
19
|
onClose,
|
|
16
20
|
}: {
|
|
17
21
|
findings: LintFinding[];
|
|
18
22
|
projectId: string;
|
|
23
|
+
/** Real on-disk project directory for the agent prompt (not the browser URL). */
|
|
24
|
+
projectDir?: string | null;
|
|
25
|
+
/** Header subtitle — parameterize so console errors don't masquerade as lint results. */
|
|
26
|
+
title?: string;
|
|
27
|
+
/** First line of the copied agent prompt. */
|
|
28
|
+
promptIntro?: string;
|
|
19
29
|
onClose: () => void;
|
|
20
30
|
}) {
|
|
21
31
|
const errors = findings.filter((f) => f.severity === "error");
|
|
22
32
|
const warnings = findings.filter((f) => f.severity === "warning");
|
|
23
33
|
const hasIssues = findings.length > 0;
|
|
24
34
|
const [copied, setCopied] = useState(false);
|
|
35
|
+
const [copyFailed, setCopyFailed] = useState(false);
|
|
36
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
const { requestClose } = useDialogBehavior({ open: true, onClose, containerRef });
|
|
25
38
|
|
|
26
39
|
const handleCopyToAgent = async () => {
|
|
27
40
|
const lines = findings.map((f) => {
|
|
@@ -30,21 +43,31 @@ export function LintModal({
|
|
|
30
43
|
if (f.fixHint) line += `\n Fix: ${f.fixHint}`;
|
|
31
44
|
return line;
|
|
32
45
|
});
|
|
33
|
-
const
|
|
46
|
+
const pathLine = projectDir ? `Project path: ${projectDir}\n\n` : "";
|
|
47
|
+
const text = `${promptIntro} for project "${projectId}":\n\n${pathLine}${lines.join("\n\n")}`;
|
|
34
48
|
const copiedText = await copyTextToClipboard(text);
|
|
35
49
|
if (copiedText) {
|
|
36
50
|
setCopied(true);
|
|
51
|
+
setCopyFailed(false);
|
|
37
52
|
setTimeout(() => setCopied(false), 2000);
|
|
53
|
+
} else {
|
|
54
|
+
setCopyFailed(true);
|
|
55
|
+
setTimeout(() => setCopyFailed(false), 3000);
|
|
38
56
|
}
|
|
39
57
|
};
|
|
40
58
|
|
|
41
59
|
return (
|
|
42
60
|
<div
|
|
43
|
-
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
44
|
-
onClick={
|
|
61
|
+
className="hf-backdrop-in fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
62
|
+
onClick={requestClose}
|
|
45
63
|
>
|
|
46
64
|
<div
|
|
47
|
-
|
|
65
|
+
ref={containerRef}
|
|
66
|
+
role="dialog"
|
|
67
|
+
aria-modal="true"
|
|
68
|
+
aria-label={title}
|
|
69
|
+
tabIndex={-1}
|
|
70
|
+
className="bg-neutral-950 border border-neutral-800 rounded-xl shadow-2xl w-full max-w-xl max-h-[80vh] flex flex-col overflow-hidden outline-none"
|
|
48
71
|
onClick={(e) => e.stopPropagation()}
|
|
49
72
|
>
|
|
50
73
|
{/* Header */}
|
|
@@ -65,12 +88,13 @@ export function LintModal({
|
|
|
65
88
|
? `${errors.length} error${errors.length !== 1 ? "s" : ""}, ${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`
|
|
66
89
|
: "All checks passed"}
|
|
67
90
|
</h2>
|
|
68
|
-
<p className="text-xs text-neutral-500">
|
|
91
|
+
<p className="text-xs text-neutral-500">{title}</p>
|
|
69
92
|
</div>
|
|
70
93
|
</div>
|
|
71
94
|
<button
|
|
72
95
|
onClick={onClose}
|
|
73
|
-
|
|
96
|
+
aria-label="Close"
|
|
97
|
+
className="p-1.5 rounded-lg text-neutral-500 hover:text-neutral-200 hover:bg-neutral-800 transition-colors active:scale-[0.98]"
|
|
74
98
|
>
|
|
75
99
|
<XIcon size={16} />
|
|
76
100
|
</button>
|
|
@@ -81,13 +105,19 @@ export function LintModal({
|
|
|
81
105
|
<div className="flex items-center justify-end px-5 py-2 border-b border-neutral-800/50">
|
|
82
106
|
<button
|
|
83
107
|
onClick={handleCopyToAgent}
|
|
84
|
-
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors ${
|
|
108
|
+
className={`px-3 py-1 text-xs font-medium rounded-lg transition-colors active:scale-[0.98] ${
|
|
85
109
|
copied
|
|
86
110
|
? "bg-green-600 text-white"
|
|
87
|
-
:
|
|
111
|
+
: copyFailed
|
|
112
|
+
? "bg-red-600 text-white"
|
|
113
|
+
: "bg-studio-accent hover:bg-studio-accent/80 text-white"
|
|
88
114
|
}`}
|
|
89
115
|
>
|
|
90
|
-
{copied
|
|
116
|
+
{copied
|
|
117
|
+
? "Copied!"
|
|
118
|
+
: copyFailed
|
|
119
|
+
? "Copy failed — check permissions"
|
|
120
|
+
: "Copy to Agent"}
|
|
91
121
|
</button>
|
|
92
122
|
</div>
|
|
93
123
|
)}
|
|
@@ -1,8 +1,41 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { IMAGE_EXT, VIDEO_EXT, AUDIO_EXT } from "../utils/mediaTypes";
|
|
2
3
|
|
|
4
|
+
function MediaErrorPanel({ name, filePath }: { name: string; filePath: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex flex-col items-center justify-center h-full p-4 bg-neutral-950 gap-2">
|
|
7
|
+
<svg
|
|
8
|
+
width="40"
|
|
9
|
+
height="40"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth="1.5"
|
|
14
|
+
className="text-neutral-600"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
>
|
|
17
|
+
<circle cx="12" cy="12" r="10" />
|
|
18
|
+
<line x1="12" y1="8" x2="12" y2="12" strokeLinecap="round" />
|
|
19
|
+
<line x1="12" y1="16" x2="12.01" y2="16" strokeLinecap="round" />
|
|
20
|
+
</svg>
|
|
21
|
+
<span className="text-sm text-neutral-400 font-medium">{name}</span>
|
|
22
|
+
<span className="text-[11px] text-neutral-600 font-mono">{filePath}</span>
|
|
23
|
+
<span className="text-[10px] text-neutral-500">
|
|
24
|
+
Couldn't load this file — it may be missing or corrupt
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
export function MediaPreview({ projectId, filePath }: { projectId: string; filePath: string }) {
|
|
4
31
|
const serveUrl = `/api/projects/${projectId}/preview/${filePath}`;
|
|
5
32
|
const name = filePath.split("/").pop() ?? filePath;
|
|
33
|
+
// Keyed by path so switching to another file clears a previous failure.
|
|
34
|
+
const [failedPath, setFailedPath] = useState<string | null>(null);
|
|
35
|
+
const failed = failedPath === filePath;
|
|
36
|
+
const setFailed = () => setFailedPath(filePath);
|
|
37
|
+
|
|
38
|
+
if (failed) return <MediaErrorPanel name={name} filePath={filePath} />;
|
|
6
39
|
|
|
7
40
|
if (IMAGE_EXT.test(filePath)) {
|
|
8
41
|
return (
|
|
@@ -10,6 +43,7 @@ export function MediaPreview({ projectId, filePath }: { projectId: string; fileP
|
|
|
10
43
|
<img
|
|
11
44
|
src={serveUrl}
|
|
12
45
|
alt={name}
|
|
46
|
+
onError={setFailed}
|
|
13
47
|
className="max-w-full max-h-[70%] object-contain rounded border border-neutral-800"
|
|
14
48
|
/>
|
|
15
49
|
<span className="mt-3 text-[11px] text-neutral-500 font-mono">{filePath}</span>
|
|
@@ -23,6 +57,7 @@ export function MediaPreview({ projectId, filePath }: { projectId: string; fileP
|
|
|
23
57
|
<video
|
|
24
58
|
src={serveUrl}
|
|
25
59
|
controls
|
|
60
|
+
onError={setFailed}
|
|
26
61
|
className="max-w-full max-h-[70%] rounded border border-neutral-800"
|
|
27
62
|
/>
|
|
28
63
|
<span className="mt-3 text-[11px] text-neutral-500 font-mono">{filePath}</span>
|
|
@@ -46,7 +81,7 @@ export function MediaPreview({ projectId, filePath }: { projectId: string; fileP
|
|
|
46
81
|
<circle cx="6" cy="18" r="3" />
|
|
47
82
|
<circle cx="18" cy="16" r="3" />
|
|
48
83
|
</svg>
|
|
49
|
-
<audio src={serveUrl} controls className="w-full max-w-[280px]" />
|
|
84
|
+
<audio src={serveUrl} controls onError={setFailed} className="w-full max-w-[280px]" />
|
|
50
85
|
<span className="text-[11px] text-neutral-500 font-mono">{filePath}</span>
|
|
51
86
|
</div>
|
|
52
87
|
);
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
interface SaveQueuePausedBannerProps {
|
|
2
2
|
message: string;
|
|
3
|
-
|
|
3
|
+
/** Resets the save-queue circuit breaker so persistence resumes. */
|
|
4
|
+
onRetry: () => void;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
/** Alert shown when the DOM-edit save queue circuit breaker pauses persistence. */
|
|
7
|
-
export function SaveQueuePausedBanner({ message,
|
|
8
|
+
export function SaveQueuePausedBanner({ message, onRetry }: SaveQueuePausedBannerProps) {
|
|
8
9
|
return (
|
|
9
10
|
<div
|
|
10
|
-
className="absolute left-1/2 top-14 z-[92] flex max-w-[calc(100vw-32px)] -translate-x-1/2 items-center gap-3 rounded-md border border-red-500/30 bg-red-950/85 px-4 py-2 text-[12px] font-medium text-red-100 shadow-lg shadow-black/30"
|
|
11
|
+
className="hf-backdrop-in absolute left-1/2 top-14 z-[92] flex max-w-[calc(100vw-32px)] -translate-x-1/2 items-center gap-3 rounded-md border border-red-500/30 bg-red-950/85 px-4 py-2 text-[12px] font-medium text-red-100 shadow-lg shadow-black/30"
|
|
11
12
|
role="alert"
|
|
12
13
|
>
|
|
13
14
|
<span>{message}</span>
|
|
14
15
|
<button
|
|
15
16
|
type="button"
|
|
16
|
-
onClick={
|
|
17
|
-
className="rounded border border-red-300/20 px-2 py-1 text-[11px] text-red-100 transition-colors hover:bg-red-400/10"
|
|
17
|
+
onClick={onRetry}
|
|
18
|
+
className="rounded border border-red-300/20 px-2 py-1 text-[11px] text-red-100 transition-colors hover:bg-red-400/10 active:scale-[0.98]"
|
|
18
19
|
>
|
|
19
|
-
|
|
20
|
+
Retry saving
|
|
20
21
|
</button>
|
|
21
22
|
</div>
|
|
22
23
|
);
|
|
@@ -30,39 +30,27 @@ export class StudioErrorBoundary extends Component<Props, State> {
|
|
|
30
30
|
if (!this.state.error) return this.props.children;
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
|
-
<div
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
inset: 0,
|
|
37
|
-
display: "flex",
|
|
38
|
-
flexDirection: "column",
|
|
39
|
-
alignItems: "center",
|
|
40
|
-
justifyContent: "center",
|
|
41
|
-
background: "#0a0a0a",
|
|
42
|
-
color: "#e5e5e5",
|
|
43
|
-
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
44
|
-
gap: 16,
|
|
45
|
-
}}
|
|
46
|
-
>
|
|
47
|
-
<div style={{ fontSize: 18, fontWeight: 600 }}>Something went wrong</div>
|
|
48
|
-
<div style={{ fontSize: 13, color: "#888", maxWidth: 480, textAlign: "center" }}>
|
|
33
|
+
<div className="fixed inset-0 flex flex-col items-center justify-center gap-4 bg-neutral-950 font-sans text-neutral-200">
|
|
34
|
+
<div className="text-lg font-semibold">Something went wrong</div>
|
|
35
|
+
<div className="max-w-[480px] text-center text-[13px] text-neutral-500">
|
|
49
36
|
{this.state.error.message}
|
|
50
37
|
</div>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
38
|
+
<div className="mt-2 flex items-center gap-2">
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => this.setState({ error: null })}
|
|
41
|
+
className="rounded-md bg-studio-accent px-5 py-2 text-sm font-medium text-neutral-950 transition-[filter] hover:brightness-110 active:scale-[0.98]"
|
|
42
|
+
>
|
|
43
|
+
Try again
|
|
44
|
+
</button>
|
|
45
|
+
{/* If the error recurs immediately, "Try again" loops — a full reload
|
|
46
|
+
is the recovery path that always works. */}
|
|
47
|
+
<button
|
|
48
|
+
onClick={() => window.location.reload()}
|
|
49
|
+
className="rounded-md border border-neutral-700 px-5 py-2 text-sm font-medium text-neutral-300 transition-colors hover:bg-neutral-800 active:scale-[0.98]"
|
|
50
|
+
>
|
|
51
|
+
Reload Studio
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
66
54
|
</div>
|
|
67
55
|
);
|
|
68
56
|
}
|
|
@@ -69,6 +69,7 @@ function markPrompted(): void {
|
|
|
69
69
|
// fallow-ignore-next-line complexity
|
|
70
70
|
export const StudioFeedbackBar = memo(function StudioFeedbackBar() {
|
|
71
71
|
const [visible, setVisible] = useState(false);
|
|
72
|
+
const [entered, setEntered] = useState(false);
|
|
72
73
|
const [rating, setRating] = useState<number | null>(null);
|
|
73
74
|
const [comment, setComment] = useState("");
|
|
74
75
|
const [submitted, setSubmitted] = useState(false);
|
|
@@ -88,6 +89,14 @@ export const StudioFeedbackBar = memo(function StudioFeedbackBar() {
|
|
|
88
89
|
return () => clearTimeout(showTimer);
|
|
89
90
|
}, []);
|
|
90
91
|
|
|
92
|
+
// Animate height in on entrance — appearing 3s after load, an instant 32px
|
|
93
|
+
// bar shoves the whole preview stack up mid-task.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!visible) return;
|
|
96
|
+
const raf = requestAnimationFrame(() => setEntered(true));
|
|
97
|
+
return () => cancelAnimationFrame(raf);
|
|
98
|
+
}, [visible]);
|
|
99
|
+
|
|
91
100
|
// Auto-dismiss timer — reset when user interacts (sets rating)
|
|
92
101
|
useEffect(() => {
|
|
93
102
|
if (!visible || rating !== null || submitted) return;
|
|
@@ -141,8 +150,8 @@ export const StudioFeedbackBar = memo(function StudioFeedbackBar() {
|
|
|
141
150
|
return (
|
|
142
151
|
<div
|
|
143
152
|
className={[
|
|
144
|
-
"flex items-center gap-3 px-4
|
|
145
|
-
exiting ? "
|
|
153
|
+
"flex items-center gap-3 px-4 overflow-hidden border-t border-neutral-800/50 bg-neutral-900/80 text-[11px] transition-all duration-300 motion-reduce:transition-none",
|
|
154
|
+
entered && !exiting ? "h-8 opacity-100" : "h-0 opacity-0 border-t-transparent",
|
|
146
155
|
].join(" ")}
|
|
147
156
|
>
|
|
148
157
|
{submitted ? (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function StudioGlobalDragOverlay() {
|
|
2
2
|
return (
|
|
3
|
-
<div className="absolute inset-0 z-[90] flex items-center justify-center bg-black/50 backdrop-blur-sm pointer-events-none">
|
|
3
|
+
<div className="hf-backdrop-in absolute inset-0 z-[90] flex items-center justify-center bg-black/50 backdrop-blur-sm pointer-events-none">
|
|
4
4
|
<div className="flex flex-col items-center gap-3 px-8 py-6 rounded-xl border-2 border-dashed border-studio-accent/60 bg-studio-accent/[0.06]">
|
|
5
5
|
<svg
|
|
6
6
|
width="32"
|