@hyperframes/studio 0.7.37 → 0.7.39
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-0P10SwC_.css +1 -0
- package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
- package/dist/assets/index-B9YvRJz1.js +396 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
- package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
- package/dist/chunk-RCLGSZ7C.js.map +1 -0
- package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
- package/dist/index.d.ts +48 -10
- package/dist/index.html +2 -2
- package/dist/index.js +6045 -3507
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -24
- 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/StudioPreviewArea.tsx +8 -0
- package/src/components/StudioRightPanel.tsx +160 -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/DomEditCropHandles.tsx +238 -0
- package/src/components/editor/DomEditOverlay.tsx +111 -108
- package/src/components/editor/DomEditRotateHandle.tsx +41 -0
- package/src/components/editor/PropertyPanel.test.ts +48 -0
- package/src/components/editor/PropertyPanel.tsx +27 -47
- package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
- package/src/components/editor/SnapToolbar.tsx +20 -1
- package/src/components/editor/clipPathHelpers.ts +113 -0
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
- package/src/components/editor/domEditOverlayCrop.ts +115 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayShape.ts +39 -0
- package/src/components/editor/domEditing.test.ts +2 -0
- package/src/components/editor/domEditingTypes.ts +3 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/marqueeCommit.ts +3 -2
- 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 +32 -125
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
- package/src/components/editor/propertyPanelTypes.ts +113 -0
- package/src/components/editor/snapTargetCollection.ts +2 -3
- package/src/components/editor/useDomEditCompositionRect.ts +68 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
- package/src/components/editor/useDomEditOverlayRects.ts +5 -3
- 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/sidebar/AssetsTab.test.ts +87 -0
- package/src/components/sidebar/AssetsTab.tsx +113 -15
- package/src/components/sidebar/BlocksTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
- 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/domSelectionTestHarness.ts +1 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useCropMode.ts +91 -0
- package/src/hooks/useDomEditCommits.test.tsx +2 -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/player/components/ShortcutsPanel.tsx +9 -0
- package/src/player/store/playerStore.ts +14 -0
- 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/dist/chunk-JND3XUJL.js.map +0 -1
- /package/dist/{domEditingLayers-UIQZJCOA.js.map → domEditingLayers-S6YOLVRG.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.39",
|
|
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/studio-server": "0.7.
|
|
49
|
+
"@hyperframes/core": "0.7.39",
|
|
50
|
+
"@hyperframes/parsers": "0.7.39",
|
|
51
|
+
"@hyperframes/sdk": "0.7.39",
|
|
52
|
+
"@hyperframes/player": "0.7.39",
|
|
53
|
+
"@hyperframes/studio-server": "0.7.39"
|
|
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.39"
|
|
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,
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
37
38
|
import { StudioHeader } from "./components/StudioHeader";
|
|
38
39
|
import { useGestureCommit } from "./hooks/useGestureCommit";
|
|
40
|
+
import { useCropModeProps } from "./hooks/useCropMode";
|
|
39
41
|
import { STUDIO_KEYFRAMES_ENABLED } from "./components/editor/manualEditingAvailability";
|
|
40
42
|
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
|
|
41
43
|
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
|
|
@@ -64,7 +66,6 @@ export function StudioApp() {
|
|
|
64
66
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
65
67
|
const viewModeValue = useViewModeState();
|
|
66
68
|
|
|
67
|
-
// sessionStorage-backed: fires once per tab, survives HMR remounts
|
|
68
69
|
useEffect(() => {
|
|
69
70
|
if (resolving || waitingForServer) return;
|
|
70
71
|
if (hasFiredSessionStart()) return;
|
|
@@ -82,6 +83,7 @@ export function StudioApp() {
|
|
|
82
83
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
83
84
|
const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
|
|
84
85
|
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
|
|
86
|
+
const cropModeProps = useCropModeProps();
|
|
85
87
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
86
88
|
const activeCompPathRef = useRef(activeCompPath);
|
|
87
89
|
activeCompPathRef.current = activeCompPath;
|
|
@@ -133,7 +135,7 @@ export function StudioApp() {
|
|
|
133
135
|
return !v;
|
|
134
136
|
});
|
|
135
137
|
}, []);
|
|
136
|
-
const {
|
|
138
|
+
const { toasts, showToast, dismissToast } = useToast();
|
|
137
139
|
const panelLayout = usePanelLayout({
|
|
138
140
|
rightCollapsed: initialUrlStateRef.current.rightCollapsed,
|
|
139
141
|
rightPanelTab: initialUrlStateRef.current.rightPanelTab,
|
|
@@ -369,6 +371,7 @@ export function StudioApp() {
|
|
|
369
371
|
isGestureRecordingRef,
|
|
370
372
|
});
|
|
371
373
|
handleToggleRecordingRef.current = handleToggleRecording;
|
|
374
|
+
const recordingToggle = STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined;
|
|
372
375
|
const canvasRectRef = useRef<CanvasRect | null>(null);
|
|
373
376
|
useLayoutEffect(() => {
|
|
374
377
|
if (gestureState !== "recording" || !previewIframe) {
|
|
@@ -390,17 +393,13 @@ export function StudioApp() {
|
|
|
390
393
|
},
|
|
391
394
|
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
|
|
392
395
|
);
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
.catch(() => {});
|
|
401
|
-
},
|
|
402
|
-
[projectId, fileManager],
|
|
403
|
-
);
|
|
396
|
+
const { setEditingFile } = fileManager;
|
|
397
|
+
const handleSelectComposition = useCompositionContentLoader({
|
|
398
|
+
projectId,
|
|
399
|
+
setEditingFile,
|
|
400
|
+
setActiveCompPath,
|
|
401
|
+
showToast,
|
|
402
|
+
});
|
|
404
403
|
const {
|
|
405
404
|
designPanelActive,
|
|
406
405
|
inspectorPanelActive,
|
|
@@ -485,14 +484,20 @@ export function StudioApp() {
|
|
|
485
484
|
captureFrameFilename={frameCapture.captureFrameFilename}
|
|
486
485
|
handleCaptureFrameClick={frameCapture.handleCaptureFrameClick}
|
|
487
486
|
refreshCaptureFrameTime={frameCapture.refreshCaptureFrameTime}
|
|
487
|
+
capturing={frameCapture.capturing}
|
|
488
488
|
inspectorButtonActive={inspectorButtonActive}
|
|
489
489
|
inspectorPanelActive={inspectorPanelActive}
|
|
490
|
-
onExport={() =>
|
|
490
|
+
onExport={() => {
|
|
491
|
+
void (async () => {
|
|
492
|
+
await previewPersistence.waitForPendingDomEditSaves();
|
|
493
|
+
await renderQueue.startRender(undefined);
|
|
494
|
+
})();
|
|
495
|
+
}}
|
|
491
496
|
/>
|
|
492
497
|
{previewPersistence.domEditSaveQueuePaused && (
|
|
493
498
|
<SaveQueuePausedBanner
|
|
494
499
|
message={previewPersistence.domEditSaveQueuePaused}
|
|
495
|
-
|
|
500
|
+
onRetry={previewPersistence.resetDomEditSaveQueueBreaker}
|
|
496
501
|
/>
|
|
497
502
|
)}
|
|
498
503
|
{viewModeValue.viewMode === "storyboard" && (
|
|
@@ -501,8 +506,6 @@ export function StudioApp() {
|
|
|
501
506
|
onSelectComposition={handleSelectComposition}
|
|
502
507
|
/>
|
|
503
508
|
)}
|
|
504
|
-
{/* Timeline stage stays mounted (just hidden) in storyboard mode,
|
|
505
|
-
so preview/player/gesture/render state survives the toggle. */}
|
|
506
509
|
<div
|
|
507
510
|
className={`flex flex-1 min-h-0${
|
|
508
511
|
viewModeValue.viewMode === "storyboard" ? " hidden" : ""
|
|
@@ -537,10 +540,9 @@ export function StudioApp() {
|
|
|
537
540
|
shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
|
|
538
541
|
isGestureRecording={gestureState === "recording"}
|
|
539
542
|
recordingState={gestureState}
|
|
540
|
-
onToggleRecording={
|
|
541
|
-
STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined
|
|
542
|
-
}
|
|
543
|
+
onToggleRecording={recordingToggle}
|
|
543
544
|
blockPreview={blockPreview}
|
|
545
|
+
{...cropModeProps}
|
|
544
546
|
gestureOverlay={
|
|
545
547
|
gestureState === "recording" && previewIframe ? (
|
|
546
548
|
<GestureTrailOverlay
|
|
@@ -564,18 +566,18 @@ export function StudioApp() {
|
|
|
564
566
|
}}
|
|
565
567
|
recordingState={gestureState}
|
|
566
568
|
recordingDuration={gestureRecording.recordingDuration}
|
|
567
|
-
onToggleRecording={
|
|
568
|
-
STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined
|
|
569
|
-
}
|
|
569
|
+
onToggleRecording={recordingToggle}
|
|
570
570
|
sdkSession={sdkHandle.session}
|
|
571
571
|
reloadPreview={reloadPreview}
|
|
572
572
|
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
573
573
|
recordEdit={editHistory.recordEdit}
|
|
574
|
+
{...cropModeProps}
|
|
574
575
|
/>
|
|
575
576
|
)}
|
|
576
577
|
</div>
|
|
577
578
|
<StudioOverlays
|
|
578
579
|
projectId={projectId}
|
|
580
|
+
projectDir={fileManager.projectDir}
|
|
579
581
|
lintModal={lintModal}
|
|
580
582
|
closeLintModal={closeLintModal}
|
|
581
583
|
consoleErrors={consoleErrors}
|
|
@@ -583,7 +585,7 @@ export function StudioApp() {
|
|
|
583
585
|
domEditSession={domEditSession}
|
|
584
586
|
activeCompPath={activeCompPath}
|
|
585
587
|
dragOverlayActive={dragOverlay.active}
|
|
586
|
-
|
|
588
|
+
toasts={toasts}
|
|
587
589
|
dismissToast={dismissToast}
|
|
588
590
|
/>
|
|
589
591
|
</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"
|