@dailies-studio/shot-editor 0.1.0 → 0.2.0
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/.vite/manifest.json +382 -0
- package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
- package/dist/assets/AgentPanel-DJEphah0.css +1 -0
- package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
- package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
- package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
- package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
- package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
- package/dist/assets/CurvePane-BjFVabKX.css +1 -0
- package/dist/assets/CurvePane-R22WzllJ.js +1 -0
- package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
- package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
- package/dist/assets/FontPicker-v0IOIArM.css +1 -0
- package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
- package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
- package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
- package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
- package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
- package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
- package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
- package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
- package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
- package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
- package/dist/assets/SoundMode-BmcZru3i.js +3 -0
- package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
- package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
- package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
- package/dist/assets/board-B88EPuLO.css +1 -0
- package/dist/assets/board-CL-he7SL.js +3 -0
- package/dist/assets/components-0oOSPN0l.js +1 -0
- package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
- package/dist/assets/googleFonts-BNon6M7X.js +1 -0
- package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
- package/dist/assets/index-4F5NQmI8.css +1 -0
- package/dist/assets/index-BGHQGV5z.js +117 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/assets/libraryModel-CpIwfFha.js +1 -0
- package/dist/assets/lucide-6iK64tTB.js +9319 -0
- package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
- package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
- package/dist/assets/webmcp-BsB9gxWC.js +2 -0
- package/dist/index.html +34 -3
- package/package.json +12 -9
- package/src/addFamilies.ts +66 -0
- package/src/api.ts +1631 -55
- package/src/app/App.tsx +977 -198
- package/src/app/lazyPanel.tsx +137 -0
- package/src/app/lazySurfaces.tsx +203 -0
- package/src/audio/index.tsx +41 -10
- package/src/audio/meter.ts +485 -0
- package/src/audio/meterMath.ts +273 -0
- package/src/audio/meterStore.ts +155 -0
- package/src/audio/offlineMix.ts +289 -0
- package/src/audio/preview.ts +928 -118
- package/src/bridge.ts +15 -5
- package/src/bridgeTypes.ts +1611 -36
- package/src/channels.ts +17 -32
- package/src/fonts/googleFonts.ts +4 -0
- package/src/hostApi.ts +174 -0
- package/src/icons.tsx +770 -0
- package/src/layerWords.ts +61 -0
- package/src/main.tsx +56 -4
- package/src/panels/ResizeHandle.tsx +33 -43
- package/src/panels/agent/AgentDots.tsx +36 -0
- package/src/panels/agent/AgentPanel.tsx +119 -43
- package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
- package/src/panels/agent/agent.css +113 -97
- package/src/panels/agent/agentGate.ts +35 -0
- package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
- package/src/panels/code/CodeStudio.tsx +373 -0
- package/src/panels/code/FileTree.tsx +265 -0
- package/src/panels/code/codeBufferGuard.ts +52 -0
- package/src/panels/code/codeParts.tsx +321 -0
- package/src/panels/comments/CommentComposer.tsx +239 -0
- package/src/panels/comments/CommentThread.tsx +346 -0
- package/src/panels/comments/CommentsPanel.tsx +336 -0
- package/src/panels/comments/InboxButton.tsx +131 -0
- package/src/panels/comments/MentionMenu.tsx +98 -0
- package/src/panels/comments/parts.tsx +171 -0
- package/src/panels/export/ExportDialog.tsx +919 -96
- package/src/panels/export/ExportScrim.tsx +57 -0
- package/src/panels/files/AssetsTab.tsx +656 -0
- package/src/panels/files/FilePanel.tsx +1466 -216
- package/src/panels/files/LayerMarks.tsx +40 -0
- package/src/panels/files/LeftColumn.tsx +37 -0
- package/src/panels/files/McpSection.tsx +690 -47
- package/src/panels/files/MissingAssetsNotice.tsx +60 -0
- package/src/panels/files/assetDrag.ts +79 -0
- package/src/panels/files/resourceParts.tsx +255 -0
- package/src/panels/fonts/FontPicker.tsx +762 -0
- package/src/panels/fonts/faceCache.ts +207 -0
- package/src/panels/fonts/fontPicker.css +271 -0
- package/src/panels/fonts/pickerModel.ts +265 -0
- package/src/panels/fonts/pickerRequest.ts +46 -0
- package/src/panels/header/AccountAvatar.tsx +76 -0
- package/src/panels/header/AgentChip.tsx +77 -0
- package/src/panels/header/AppMenu.tsx +213 -0
- package/src/panels/header/Header.tsx +564 -84
- package/src/panels/header/HistoryControls.tsx +31 -5
- package/src/panels/header/PresenceStack.tsx +329 -0
- package/src/panels/header/ProjectTitle.tsx +232 -0
- package/src/panels/header/ShareButton.tsx +59 -0
- package/src/panels/header/StatusBadges.tsx +358 -27
- package/src/panels/header/SyncDeletedDialog.tsx +88 -0
- package/src/panels/header/ZoomControls.tsx +46 -15
- package/src/panels/header/agentHandoff.ts +52 -0
- package/src/panels/header/leave.ts +26 -0
- package/src/panels/help/HelpSection.tsx +189 -163
- package/src/panels/insert/InsertDrawer.tsx +723 -0
- package/src/panels/insert/iconSet.ts +109 -0
- package/src/panels/inspector/ExposeDot.tsx +117 -0
- package/src/panels/inspector/FramingRows.tsx +314 -44
- package/src/panels/inspector/InspectorFooter.tsx +91 -0
- package/src/panels/inspector/InspectorHead.tsx +425 -0
- package/src/panels/inspector/InspectorPanel.tsx +408 -124
- package/src/panels/inspector/KeyframeDot.tsx +373 -0
- package/src/panels/inspector/KeyframeSection.tsx +257 -0
- package/src/panels/inspector/MarkerSection.tsx +55 -11
- package/src/panels/inspector/ParamRow.tsx +84 -53
- package/src/panels/inspector/RightColumn.tsx +152 -0
- package/src/panels/inspector/TimeSection.tsx +866 -0
- package/src/panels/inspector/animate/EaseControl.tsx +244 -0
- package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
- package/src/panels/inspector/animate/ShotSection.tsx +562 -48
- package/src/panels/inspector/design/AlignRow.tsx +95 -31
- package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
- package/src/panels/inspector/design/ClonerSection.tsx +323 -62
- package/src/panels/inspector/design/ComponentSection.tsx +50 -43
- package/src/panels/inspector/design/CornerSection.tsx +45 -14
- package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
- package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
- package/src/panels/inspector/design/DeviceSection.tsx +210 -0
- package/src/panels/inspector/design/FillSection.tsx +83 -51
- package/src/panels/inspector/design/FontField.tsx +196 -0
- package/src/panels/inspector/design/HtmlSection.tsx +23 -12
- package/src/panels/inspector/design/InstanceSection.tsx +373 -154
- package/src/panels/inspector/design/MaskSection.tsx +84 -0
- package/src/panels/inspector/design/MaterialSection.tsx +177 -0
- package/src/panels/inspector/design/MediaSection.tsx +273 -120
- package/src/panels/inspector/design/MeshSection.tsx +910 -82
- package/src/panels/inspector/design/PaintRows.tsx +550 -0
- package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
- package/src/panels/inspector/design/ShadowSection.tsx +221 -48
- package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
- package/src/panels/inspector/design/StrokeSection.tsx +228 -43
- package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
- package/src/panels/inspector/design/TransformSection.tsx +244 -50
- package/src/panels/inspector/design/TypographySection.tsx +679 -110
- package/src/panels/inspector/layout.ts +295 -0
- package/src/panels/inspector/propValue.ts +199 -0
- package/src/panels/inspector/scene/CameraSection.tsx +232 -22
- package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
- package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
- package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
- package/src/panels/inspector/scene/SceneSection.tsx +219 -44
- package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
- package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
- package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
- package/src/panels/inspector/timeModel.ts +190 -0
- package/src/panels/inspector/useDocWrite.ts +438 -146
- package/src/panels/library/LibraryBrowser.tsx +435 -0
- package/src/panels/library/LibraryGrid.tsx +190 -0
- package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
- package/src/panels/library/LibraryRail.tsx +84 -0
- package/src/panels/library/LibrarySoundPanel.tsx +293 -0
- package/src/panels/library/SoundHistory.tsx +245 -0
- package/src/panels/library/StockSounds.tsx +390 -0
- package/src/panels/library/libraryModel.ts +147 -0
- package/src/panels/library/stockSearch.ts +77 -0
- package/src/panels/maskMarks.tsx +117 -0
- package/src/panels/material/MaterialCommentNode.tsx +54 -0
- package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
- package/src/panels/material/MaterialGraphMenu.tsx +45 -0
- package/src/panels/material/MaterialGroupNode.tsx +56 -0
- package/src/panels/material/MaterialLibrary.tsx +253 -0
- package/src/panels/material/MaterialNode.tsx +453 -0
- package/src/panels/material/MaterialNodePalette.tsx +349 -0
- package/src/panels/material/MaterialParameterControls.tsx +525 -0
- package/src/panels/material/MaterialRerouteNode.tsx +42 -0
- package/src/panels/material/MaterialWorkspace.tsx +501 -0
- package/src/panels/material/NodePreview.tsx +70 -0
- package/src/panels/material/flowModel.ts +441 -0
- package/src/panels/material/parameterValues.ts +231 -0
- package/src/panels/material/previewScheduler.ts +235 -0
- package/src/panels/material/reactFlow.css +6 -0
- package/src/panels/material/socketGlyph.ts +85 -0
- package/src/panels/material/viewportMemory.ts +58 -0
- package/src/panels/preferences/PreferencesDialog.tsx +215 -0
- package/src/panels/share/ShareComponentDialog.tsx +247 -0
- package/src/panels/stage/StageMenu.tsx +35 -5
- package/src/panels/stage/StagePanel.tsx +338 -23
- package/src/panels/stage/cellSlots.tsx +70 -0
- package/src/panels/storyboard/BoardCanvas.tsx +739 -0
- package/src/panels/storyboard/BoardCursors.tsx +158 -0
- package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
- package/src/panels/storyboard/BoardPlan.tsx +42 -0
- package/src/panels/storyboard/ComponentSheet.tsx +187 -0
- package/src/panels/storyboard/ComponentStage.tsx +235 -0
- package/src/panels/storyboard/MiniSketch.tsx +173 -0
- package/src/panels/storyboard/PartPicker.tsx +64 -0
- package/src/panels/storyboard/PlanPanel.tsx +617 -0
- package/src/panels/storyboard/ReferencePreview.tsx +217 -0
- package/src/panels/storyboard/ShotBrief.tsx +360 -0
- package/src/panels/storyboard/ShotCard.tsx +251 -0
- package/src/panels/storyboard/StagePreview.tsx +348 -0
- package/src/panels/storyboard/StillThumbs.tsx +162 -0
- package/src/panels/storyboard/StoryboardView.tsx +79 -0
- package/src/panels/storyboard/board.css +1065 -0
- package/src/panels/storyboard/board.ts +12 -0
- package/src/panels/storyboard/boardClipboard.ts +111 -0
- package/src/panels/storyboard/boardOrder.ts +68 -0
- package/src/panels/storyboard/boardRows.tsx +425 -0
- package/src/panels/storyboard/liveCap.ts +90 -0
- package/src/panels/storyboard/plan.css +295 -0
- package/src/panels/storyboard/posterCache.ts +133 -0
- package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
- package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
- package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
- package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
- package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
- package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
- package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
- package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
- package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
- package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
- package/src/panels/storyboard/shotboard/sketch.css +1392 -0
- package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
- package/src/panels/storyboard/storyboardDrop.ts +164 -0
- package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
- package/src/panels/studio/ComponentSidePanel.tsx +355 -0
- package/src/panels/studio/ComponentStudio.tsx +243 -0
- package/src/panels/studio/ComponentsHome.tsx +318 -0
- package/src/panels/studio/ComponentsPreview.tsx +179 -0
- package/src/panels/studio/EveryUse.tsx +244 -0
- package/src/panels/studio/ThisUsePanel.tsx +679 -0
- package/src/panels/studio/componentParts.tsx +341 -0
- package/src/panels/studio/components.ts +6 -0
- package/src/panels/studio/newCodeComponent.ts +66 -0
- package/src/panels/timeline/AudioFxButton.tsx +290 -0
- package/src/panels/timeline/AudioLane.tsx +674 -85
- package/src/panels/timeline/BlockRow.tsx +154 -85
- package/src/panels/timeline/ChannelRow.tsx +378 -129
- package/src/panels/timeline/CurvePane.tsx +508 -0
- package/src/panels/timeline/Filmstrip.tsx +361 -0
- package/src/panels/timeline/GenerateDrawer.tsx +407 -0
- package/src/panels/timeline/KeyStretch.tsx +115 -0
- package/src/panels/timeline/LifespanRow.tsx +730 -47
- package/src/panels/timeline/MarkerLane.tsx +46 -8
- package/src/panels/timeline/PlayButton.tsx +91 -0
- package/src/panels/timeline/PropertyPicker.tsx +254 -0
- package/src/panels/timeline/ShotRow.tsx +400 -0
- package/src/panels/timeline/SoundMode.tsx +1406 -0
- package/src/panels/timeline/TimelineDock.tsx +2338 -272
- package/src/panels/timeline/TransportVolume.tsx +163 -0
- package/src/panels/timeline/beatRuler.ts +74 -0
- package/src/panels/timeline/curveModel.ts +500 -0
- package/src/panels/timeline/duckUnderVoice.ts +68 -0
- package/src/panels/timeline/keyframeMenu.ts +138 -0
- package/src/panels/timeline/labelWidth.ts +49 -0
- package/src/panels/timeline/marquee.ts +155 -0
- package/src/panels/timeline/model.ts +1860 -199
- package/src/panels/timeline/snap.ts +125 -43
- package/src/panels/timeline/staggerPreview.tsx +92 -0
- package/src/panels/timeline/takeMeasure.ts +72 -0
- package/src/panels/timeline/useLive.ts +48 -0
- package/src/panels/timeline/viewport.ts +234 -0
- package/src/panels/toolbar/BackChip.tsx +116 -0
- package/src/panels/toolbar/CameraViewBar.tsx +118 -49
- package/src/panels/toolbar/CanvasBar.tsx +117 -0
- package/src/panels/toolbar/InsertButton.tsx +38 -0
- package/src/panels/toolbar/IsolationTrail.tsx +156 -0
- package/src/panels/toolbar/PanelToggles.tsx +86 -0
- package/src/panels/toolbar/ShadingBar.tsx +119 -26
- package/src/panels/toolbar/ToolDockBar.tsx +329 -80
- package/src/panels/toolbar/ToolFooter.tsx +102 -0
- package/src/panels/toolbar/ViewModeBar.tsx +247 -38
- package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
- package/src/panels/toolbar/useAddRows.tsx +326 -0
- package/src/panels/toolbar/useDismiss.ts +122 -0
- package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
- package/src/panels/versions/versionModel.ts +103 -0
- package/src/panels/virtualWindow.ts +271 -0
- package/src/shotTokens.css +151 -60
- package/src/stage/FrustumGizmo.tsx +504 -84
- package/src/stage/GridPlane.tsx +225 -16
- package/src/stage/NodePreviews.tsx +628 -0
- package/src/stage/PerfHud.tsx +437 -47
- package/src/stage/SplitRenderer.tsx +816 -169
- package/src/stage/StageApp.tsx +4274 -486
- package/src/stage/StageRootBoundary.tsx +114 -0
- package/src/stage/StageTools.tsx +651 -151
- package/src/stage/TransformGizmo.tsx +696 -0
- package/src/stage/adaptivePresentation.ts +145 -0
- package/src/stage/bridgeClient.ts +2522 -41
- package/src/stage/exportWorker.ts +9 -0
- package/src/stage/feed.ts +305 -14
- package/src/stage/fontPreview.ts +176 -0
- package/src/stage/frustumGeometry.ts +319 -1
- package/src/stage/furnitureTokens.ts +33 -0
- package/src/stage/gizmoMath.ts +508 -16
- package/src/stage/gpuTimer.ts +276 -0
- package/src/stage/instanceModules.ts +194 -33
- package/src/stage/interactions/move.ts +24 -5
- package/src/stage/isolation.ts +291 -33
- package/src/stage/jsSelfProfiler.ts +325 -0
- package/src/stage/main.tsx +116 -16
- package/src/stage/modalState.ts +222 -23
- package/src/stage/moduleEpoch.ts +60 -0
- package/src/stage/motionPathStage.ts +279 -0
- package/src/stage/motionPreview.ts +74 -0
- package/src/stage/overlays/Overlays.tsx +1112 -159
- package/src/stage/overlays/Rulers.tsx +495 -0
- package/src/stage/panes.ts +159 -10
- package/src/stage/project.ts +68 -3
- package/src/stage/renderScaleController.ts +730 -0
- package/src/stage/rulers.ts +129 -0
- package/src/stage/sceneExtent.ts +75 -0
- package/src/stage/shading.ts +20 -0
- package/src/stage/splineStage.ts +632 -0
- package/src/stage/stagePerformanceTelemetry.ts +717 -0
- package/src/stage/stageTheme.ts +36 -0
- package/src/stage/stageWarmup.ts +11 -0
- package/src/stage/textAutoSize.ts +124 -0
- package/src/stage/tier1Export.ts +641 -0
- package/src/stage/tokenColor.ts +14 -9
- package/src/stage/view.ts +56 -9
- package/src/stage/workingView.ts +277 -33
- package/src/state/accountPrefs.ts +261 -0
- package/src/state/agentActions.ts +173 -15
- package/src/state/agentActivity.ts +198 -0
- package/src/state/agentQuestion.ts +239 -0
- package/src/state/bootOpen.ts +42 -0
- package/src/state/cameraVerbs.ts +139 -0
- package/src/state/clientId.ts +6 -1
- package/src/state/comments.ts +1099 -0
- package/src/state/context.tsx +19 -1
- package/src/state/editorPrefs.ts +220 -0
- package/src/state/errorReports.ts +121 -0
- package/src/state/fieldWrite.ts +161 -0
- package/src/state/fontRecents.ts +70 -0
- package/src/state/fonts.ts +656 -0
- package/src/state/graphClipboard.ts +414 -0
- package/src/state/imageThumbs.ts +216 -0
- package/src/state/insert.ts +444 -67
- package/src/state/isolationTrail.ts +92 -0
- package/src/state/keyMoves.ts +134 -0
- package/src/state/keyframeClipboard.ts +177 -0
- package/src/state/keymap.ts +1153 -117
- package/src/state/layoutPrefs.ts +272 -0
- package/src/state/layouts.ts +205 -0
- package/src/state/measureMedia.ts +167 -0
- package/src/state/menu.ts +2341 -28
- package/src/state/motionStamp.ts +7 -0
- package/src/state/panelSize.ts +375 -5
- package/src/state/pendingChanges.ts +119 -0
- package/src/state/playHold.ts +78 -0
- package/src/state/playbackPreparation.ts +562 -0
- package/src/state/posterCapture.ts +242 -0
- package/src/state/presence.ts +254 -0
- package/src/state/projectComponents.ts +160 -0
- package/src/state/renderEvents.ts +298 -18
- package/src/state/runtimeErrors.ts +46 -0
- package/src/state/selection.ts +12 -0
- package/src/state/shortcuts.ts +1606 -0
- package/src/state/sketchGuide.ts +51 -0
- package/src/state/splineRefs.ts +87 -0
- package/src/state/stageHealth.ts +259 -0
- package/src/state/stageWatchdog.ts +47 -0
- package/src/state/stillStage.ts +330 -0
- package/src/state/stillThumbs.ts +474 -0
- package/src/state/store.ts +23591 -2982
- package/src/state/storyboardModel.ts +528 -0
- package/src/state/sync.ts +1257 -0
- package/src/state/syncCache.ts +159 -0
- package/src/state/triangles.ts +13 -0
- package/src/state/types.ts +1698 -55
- package/src/state/unsavedGuard.ts +49 -0
- package/src/state/viewMode.ts +200 -0
- package/src/state/webmcp.ts +1128 -0
- package/src/state/workspace.ts +1147 -0
- package/src/styles/assets-tab.css +304 -0
- package/src/styles/canvas.css +545 -0
- package/src/styles/comments.css +1435 -0
- package/src/styles/controls.css +162 -0
- package/src/styles/dialogs.css +390 -0
- package/src/styles/insert-drawer.css +586 -0
- package/src/styles/inspector-board.css +207 -0
- package/src/styles/inspector-object.css +486 -0
- package/src/styles/inspector-shot.css +252 -0
- package/src/styles/inspector.css +360 -0
- package/src/styles/left-column.css +1628 -0
- package/src/styles/library-preview.css +575 -0
- package/src/styles/library.css +512 -0
- package/src/styles/modes/code.css +569 -0
- package/src/styles/modes/component.css +1561 -0
- package/src/styles/modes/material.css +1201 -0
- package/src/styles/modes/storyboard.css +518 -0
- package/src/styles/share-component.css +49 -0
- package/src/styles/shell.css +653 -0
- package/src/styles/shot-brief.css +360 -0
- package/src/styles/states.css +76 -0
- package/src/styles/sync.css +380 -0
- package/src/styles/timeline-graph.css +384 -0
- package/src/styles/timeline-meters.css +89 -0
- package/src/styles/timeline-rows.css +1959 -0
- package/src/styles/timeline-sound.css +1003 -0
- package/src/styles/timeline.css +754 -0
- package/src/styles/tool-footer.css +525 -0
- package/src/styles/topbar.css +886 -0
- package/src/styles/versions.css +262 -0
- package/src/styles.css +72 -2921
- package/src/tools.ts +108 -23
- package/dist/assets/index-B7notge7.css +0 -1
- package/dist/assets/index-BBE6Ta9L.js +0 -52
- package/src/panels/files/icons.tsx +0 -118
- package/src/panels/header/DocSwitcher.tsx +0 -85
- package/src/panels/header/TransportBar.tsx +0 -86
- package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
- package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
- package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
- package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
- package/src/panels/inspector/animate/useBlocks.ts +0 -43
- package/src/panels/material/MaterialPicker.tsx +0 -172
- package/src/panels/timeline/SceneStrip.tsx +0 -145
- package/src/panels/toolbar/AddMenu.tsx +0 -230
- package/src/stage/MoveGizmo.tsx +0 -411
package/src/app/App.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ── THE ONE STRUCTURAL RULE ──────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// `<StagePanel>` is the UNCONDITIONAL, UNKEYED FIRST CHILD of the shell root,
|
|
6
|
-
// and every panel
|
|
6
|
+
// and every panel is docked over it. Nothing this component renders may reorder,
|
|
7
7
|
// reparent, key or conditionally wrap it, because a remount costs a full iframe
|
|
8
8
|
// document load, a fresh WebGPU adapter request, every layer raster redrawn and
|
|
9
9
|
// a dead stage for at least one connect poll. `test/stageIdentity.test.tsx`
|
|
@@ -15,71 +15,222 @@
|
|
|
15
15
|
// element a containing block for fixed-position descendants, and the iframe is
|
|
16
16
|
// `position: fixed; inset: 0`; the failure is the stage silently sizing itself
|
|
17
17
|
// to a panel instead of the viewport.
|
|
18
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import { Activity, useCallback, useEffect, useRef, useState } from "react";
|
|
19
|
+
import { PanelBoundary, useToasts } from "@dailies-studio/ui";
|
|
20
|
+
import type { ToastHandle } from "@dailies-studio/ui";
|
|
21
|
+
import { requestEditAccess } from "../api.js";
|
|
22
|
+
import { LeftColumn } from "../panels/files/LeftColumn.js";
|
|
23
|
+
import { MissingAssetsNotice } from "../panels/files/MissingAssetsNotice.js";
|
|
21
24
|
import { ResizeHandle } from "../panels/ResizeHandle.js";
|
|
22
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
CANVAS_MIN,
|
|
27
|
+
INSPECTOR_WIDTH,
|
|
28
|
+
RAIL_WIDTH,
|
|
29
|
+
boardColumns,
|
|
30
|
+
fitShell,
|
|
31
|
+
layoutFor,
|
|
32
|
+
toolFooterShown,
|
|
33
|
+
} from "../state/panelSize.js";
|
|
34
|
+
import { CellSlot, CellSlotsProvider } from "../panels/stage/cellSlots.js";
|
|
35
|
+
import { VIEW_ONLY, boardRightPanel, componentEditOpen, componentsHideTimeline, workspaceOf } from "../state/workspace.js";
|
|
36
|
+
import { splitRatioAt, splitX } from "../stage/panes.js";
|
|
23
37
|
import { Header } from "../panels/header/Header.js";
|
|
24
|
-
import {
|
|
38
|
+
import { SyncQuotaBanner } from "../panels/header/StatusBadges.js";
|
|
39
|
+
import { SyncDeletedDialog } from "../panels/header/SyncDeletedDialog.js";
|
|
40
|
+
import { RightColumn } from "../panels/inspector/RightColumn.js";
|
|
25
41
|
import { StagePanel } from "../panels/stage/StagePanel.js";
|
|
26
42
|
import { TimelineDock } from "../panels/timeline/TimelineDock.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { ShadingBar } from "../panels/toolbar/ShadingBar.js";
|
|
30
|
-
import { CameraViewBar } from "../panels/toolbar/CameraViewBar.js";
|
|
31
|
-
import { ExportDialog } from "../panels/export/ExportDialog.js";
|
|
43
|
+
import { ToolFooter } from "../panels/toolbar/ToolFooter.js";
|
|
44
|
+
import { CanvasBar } from "../panels/toolbar/CanvasBar.js";
|
|
32
45
|
import { StageMenu } from "../panels/stage/StageMenu.js";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
46
|
+
import { StillThumbsProvider, stillThumbsActive } from "../panels/storyboard/StillThumbs.js";
|
|
47
|
+
import {
|
|
48
|
+
CodeStudio,
|
|
49
|
+
ComponentStudio,
|
|
50
|
+
ComponentsPreview,
|
|
51
|
+
ExportDialog,
|
|
52
|
+
LibraryBrowser,
|
|
53
|
+
AgentQuestionDialog,
|
|
54
|
+
MaterialWorkspace,
|
|
55
|
+
StoryboardView,
|
|
56
|
+
prefetchStoryboard,
|
|
57
|
+
prefetchInspectorSections,
|
|
58
|
+
} from "./lazySurfaces.js";
|
|
59
|
+
import { whenIdle } from "./lazyPanel.js";
|
|
35
60
|
import { AudioPreviewBridge } from "../audio/index.js";
|
|
61
|
+
import { ExportScrim } from "../panels/export/ExportScrim.js";
|
|
36
62
|
import { subscribeStudioEvents } from "../state/renderEvents.js";
|
|
63
|
+
import { reportStageFailures } from "../state/errorReports.js";
|
|
64
|
+
import { startPosterCapture } from "../state/posterCapture.js";
|
|
65
|
+
import { bootOpenFor, wantedDocOf } from "../state/bootOpen.js";
|
|
37
66
|
import { useShotEditor, useStore } from "../state/context.js";
|
|
67
|
+
import { useUnsavedWorkGuard } from "../state/unsavedGuard.js";
|
|
68
|
+
import { useAccountPrefs } from "../state/accountPrefs.js";
|
|
69
|
+
import { shownLayoutMode } from "../state/viewMode.js";
|
|
70
|
+
import { CURRENT_SHOT_SCHEMA_VERSION } from "@dailies-studio/shot/doc";
|
|
38
71
|
import type { StageInset } from "../bridgeTypes.js";
|
|
72
|
+
import type { ShotEditorState } from "../state/types.js";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The two halves disagreeing about the schema, said in a sentence.
|
|
76
|
+
*
|
|
77
|
+
* DERIVED FROM THE OPEN DOCUMENT, never stored, and that is the whole point.
|
|
78
|
+
* It began as one `set({ error })` at load, which is how every other message
|
|
79
|
+
* in this shell works — and it was wrong for this one, because `state.error`
|
|
80
|
+
* is transient by design: `beginScrub` and `openMaterialWorkspace` both clear
|
|
81
|
+
* it, so the warning was gone by the author's second gesture and stayed gone
|
|
82
|
+
* while every save the session would attempt was still doomed.
|
|
83
|
+
*
|
|
84
|
+
* A condition that lasts until a process is restarted has to be said for as
|
|
85
|
+
* long as it lasts. Derived, it cannot be dismissed, it cannot go stale, and
|
|
86
|
+
* it disappears by itself the moment a document at the right version loads.
|
|
87
|
+
*
|
|
88
|
+
* ── WHY IT IS A WARNING AND NOT A LOCKED DOOR ──────────────────────────
|
|
89
|
+
*
|
|
90
|
+
* The document is readable and everything in it is real; the editor simply
|
|
91
|
+
* cannot save what it is about to let the author build. Refusing to open it
|
|
92
|
+
* would take away the one thing that still works. So: keep working, and know.
|
|
93
|
+
*/
|
|
94
|
+
export function schemaMismatchNotice(version: number | null): string | null {
|
|
95
|
+
if (version === null || version === CURRENT_SHOT_SCHEMA_VERSION) return null;
|
|
96
|
+
return version < CURRENT_SHOT_SCHEMA_VERSION
|
|
97
|
+
? `The dev server is older than this editor: it migrated this document to schema ${version}, and the editor is at ${CURRENT_SHOT_SCHEMA_VERSION}. Saves will be refused (and new node kinds rejected) until you restart it: stop it and run \`dailies dev\` again.`
|
|
98
|
+
: `This editor is older than the dev server: the document is at schema ${version} and the editor is at ${CURRENT_SHOT_SCHEMA_VERSION}. Rebuild it: \`pnpm --filter @dailies-studio/shot-editor build\`.`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Components with nothing in the right column: the home, unless Agent or
|
|
102
|
+
* Comments is open (`boardRightPanel`). The column is drawn 0 wide and its
|
|
103
|
+
* resize handle is not mounted. The Version history takes the panel width. */
|
|
104
|
+
function componentsColumnEmpty(s: ShotEditorState): boolean {
|
|
105
|
+
return workspaceOf(s) === "components" && !s.versionsOpen && boardRightPanel(s) === null;
|
|
106
|
+
}
|
|
39
107
|
|
|
40
108
|
export function App() {
|
|
41
109
|
const store = useStore();
|
|
42
110
|
const [stageError, setStageError] = useState<string | null>(null);
|
|
43
|
-
const onConnectError = useCallback(
|
|
111
|
+
const onConnectError = useCallback(
|
|
112
|
+
(message: string | null) => setStageError(message),
|
|
113
|
+
[],
|
|
114
|
+
);
|
|
44
115
|
|
|
45
116
|
const name = useShotEditor((s) => s.name);
|
|
46
117
|
const error = useShotEditor((s) => s.error);
|
|
47
118
|
const reportedStageError = useShotEditor((s) => s.stageError);
|
|
48
119
|
const exportOpen = useShotEditor((s) => s.exportOpen);
|
|
49
|
-
const
|
|
120
|
+
const materialWorkspace = useShotEditor((s) => s.materialWorkspace);
|
|
121
|
+
// An open definition is always the Components workspace. Its Edit tab is
|
|
122
|
+
// the editor (mock C6): the layers outline in the left column, the tool
|
|
123
|
+
// footer and the def timeline (`componentEditOpen`).
|
|
124
|
+
const componentStudio = useShotEditor((s) => s.editingDef !== null);
|
|
125
|
+
const componentEdit = useShotEditor(componentEditOpen);
|
|
126
|
+
// The OTHER family's studio. Mutually exclusive with the one above by
|
|
127
|
+
// construction: each entry clears the other's subject, so the two
|
|
128
|
+
// `.stage-window` holes can never both be mounted and `measureInset` can
|
|
129
|
+
// never have to choose.
|
|
130
|
+
const codeStudio = useShotEditor((s) => s.codeComponent !== null);
|
|
131
|
+
// The fourth mode, the storyboard. Its preview is a hole in the bottom row.
|
|
132
|
+
const storyboardOpen = useShotEditor((s) => s.storyboardOpen);
|
|
133
|
+
const stillsActive = useShotEditor(stillThumbsActive);
|
|
134
|
+
// WHICH WORKSPACE, for the one class every mode rule keys on (`is-mode`):
|
|
135
|
+
// the four legacy classes above stay, because tests and the CLI's e2e read
|
|
136
|
+
// them, and each mode's own rules are about that mode alone.
|
|
137
|
+
const workspace = useShotEditor(workspaceOf);
|
|
138
|
+
// Components without the timeline: nothing open, the Preview tab (its
|
|
139
|
+
// transport is in the canvas cell), or a code component (its Edit tab has
|
|
140
|
+
// its own footer). Only a Studio-made component's Edit tab keeps it.
|
|
141
|
+
const componentsEmpty = useShotEditor(componentsHideTimeline);
|
|
142
|
+
// And the right column with nothing in it there (the home): 0 wide, no seam.
|
|
143
|
+
const columnEmpty = useShotEditor(componentsColumnEmpty);
|
|
144
|
+
// THE TOOL FOOTER'S ROW, opened by a class in the same commit that mounts
|
|
145
|
+
// the footer (see `.shell.has-tool-footer` in styles/shell.css). One
|
|
146
|
+
// predicate for the class, the mount, the inset's edge and the dock's ceiling.
|
|
147
|
+
const footerShown = useShotEditor(toolFooterShown);
|
|
148
|
+
// Preferences > Canvas > Toolbar position: the same row, moved above the
|
|
149
|
+
// stage (`.shell.tool-footer-top` in styles/shell.css).
|
|
150
|
+
const footerTop = useShotEditor((s) => s.toolbarPosition === "top");
|
|
151
|
+
// The VERSION alone and not the document: this re-renders the shell, and
|
|
152
|
+
// subscribing to `doc` here would do it on every keystroke.
|
|
153
|
+
const schemaNotice = schemaMismatchNotice(
|
|
154
|
+
useShotEditor((s) => s.doc?.schemaVersion ?? null),
|
|
155
|
+
);
|
|
156
|
+
const saveConflict = useShotEditor((s) => s.saveConflict);
|
|
50
157
|
|
|
51
158
|
useOpenFromUrl();
|
|
52
159
|
useKeymap();
|
|
53
160
|
useExternalChangeRelay();
|
|
161
|
+
useWindowDropGuard();
|
|
162
|
+
// Hosted only: the editor prefs on the account (boot merge, then saves).
|
|
163
|
+
useAccountPrefs();
|
|
54
164
|
|
|
55
165
|
// Subscribed for the WHOLE session, not while the dialog is open: closing the
|
|
56
166
|
// dialog must not stop the render or lose its progress, and the header badge
|
|
57
167
|
// reads the same store bit.
|
|
58
168
|
useEffect(() => subscribeStudioEvents(store), [store]);
|
|
169
|
+
// Hosted only: what breaks on the stage reaches the admin panel.
|
|
170
|
+
useEffect(() => reportStageFailures(store), [store]);
|
|
171
|
+
|
|
172
|
+
// The browser's own close confirmation, while anything is not on disk. See
|
|
173
|
+
// unsavedGuard.ts: a failed save deliberately does not retry, so the
|
|
174
|
+
// in-memory document IS the only copy until the author acts on it.
|
|
175
|
+
useUnsavedWorkGuard(
|
|
176
|
+
useShotEditor((s) => s.dirty),
|
|
177
|
+
useShotEditor((s) => s.saveState),
|
|
178
|
+
useShotEditor((s) => s.syncUnstored),
|
|
179
|
+
);
|
|
59
180
|
|
|
60
|
-
|
|
181
|
+
|
|
182
|
+
// A debounced hosted-lane poster attempt after a save settles: the stage
|
|
183
|
+
// captures the poster frame through the Tier-1 exact capture and it is
|
|
184
|
+
// uploaded for the dashboard card. Owed until it lands (see posterCapture.ts);
|
|
185
|
+
// every failure stays silent and the card keeps its gradient.
|
|
186
|
+
useEffect(() => startPosterCapture({ store }), [store]);
|
|
187
|
+
|
|
188
|
+
// THE OTHER MAIN WORKSPACE, FETCHED AT IDLE. Studio is in the entry; the
|
|
189
|
+
// Storyboard is a chunk (lazySurfaces.tsx). Fetched once the first paint is
|
|
190
|
+
// done, so switching to it is as quick as it was when it shipped in the
|
|
191
|
+
// entry, and never on the boot's critical path. A failure is silent: the
|
|
192
|
+
// board fetches again when it opens, and says so there if it cannot. The
|
|
193
|
+
// inspector's two template sections come at the same idle moment, so the
|
|
194
|
+
// first text, image or video layer selected draws its Template field.
|
|
195
|
+
useEffect(
|
|
196
|
+
() =>
|
|
197
|
+
whenIdle(() => {
|
|
198
|
+
if (workspaceOf(store.getState()) !== "storyboard") prefetchStoryboard().catch(() => {});
|
|
199
|
+
prefetchInspectorSections().catch(() => {});
|
|
200
|
+
}),
|
|
201
|
+
[store],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// THE INSERT DRAWER SHOWS THE RAIL: it docks in the left column, so a
|
|
205
|
+
// collapsed rail draws open while it is up, without writing the author's
|
|
206
|
+
// collapse (it comes back when the drawer closes).
|
|
207
|
+
const railCollapsed = useShotEditor((s) => s.railCollapsed && (s.insertDrawer === null || s.readOnly));
|
|
61
208
|
const inspectorCollapsed = useShotEditor((s) => s.inspectorCollapsed);
|
|
62
209
|
// The MODE alone, not the whole layout: the divider's own drag writes the
|
|
63
|
-
// ratio dozens of times a second, and subscribing to that here would
|
|
210
|
+
// split ratio dozens of times a second, and subscribing to that here would
|
|
64
211
|
// re-render the entire shell on every frame of it. `--split-x` carries the
|
|
65
212
|
// position, written imperatively beside `--rail-w`.
|
|
66
|
-
|
|
213
|
+
//
|
|
214
|
+
// WHAT IS ON SCREEN, not what is chosen: Video draws the frame over the
|
|
215
|
+
// author's split without writing it, so the seam handle would otherwise be
|
|
216
|
+
// left lying across a picture with no seam under it.
|
|
217
|
+
const stageLayout = { mode: useShotEditor(shownLayoutMode) };
|
|
67
218
|
|
|
68
|
-
// The dragged sizes reach CSS as variables on the shell,
|
|
69
|
-
// the
|
|
70
|
-
// without any of the three knowing about the others.
|
|
219
|
+
// The dragged sizes reach CSS as variables on the shell, and the variables
|
|
220
|
+
// ARE the grid's tracks: `--rail-w`, `--inspector-w` and `--dock-row-h`.
|
|
71
221
|
//
|
|
72
|
-
// WRITTEN IMPERATIVELY, OUTSIDE REACT. The values exist only to become
|
|
222
|
+
// WRITTEN IMPERATIVELY, OUTSIDE REACT. The values exist only to become
|
|
73
223
|
// custom properties, and subscribing them here re-rendered the entire shell
|
|
74
224
|
// — every panel, every dock row — on every pointermove of a resize drag,
|
|
75
|
-
// to do a style write. A raw store subscription writes the same
|
|
76
|
-
//
|
|
225
|
+
// to do a style write. A raw store subscription writes the same variables
|
|
226
|
+
// and renders nothing.
|
|
77
227
|
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
228
|
+
// THROUGH `fitShell`, which is display only: a window too small for the
|
|
229
|
+
// stored sizes shows smaller tracks, and the stored ones come back when the
|
|
230
|
+
// window can hold them. A collapsed column keeps its track (the canvas spans
|
|
231
|
+
// over it in CSS), so these are written unconditionally.
|
|
82
232
|
const shellRef = useRef<HTMLDivElement | null>(null);
|
|
233
|
+
const shownShell = () => fitShell(window.innerWidth, window.innerHeight, store.getState());
|
|
83
234
|
useEffect(() => {
|
|
84
235
|
const el = shellRef.current;
|
|
85
236
|
if (el === null) return;
|
|
@@ -91,31 +242,106 @@ export function App() {
|
|
|
91
242
|
// is a FORCED LAYOUT, and this is a raw store subscription — so it ran once
|
|
92
243
|
// per playback frame, for four numbers that a seek cannot possibly change.
|
|
93
244
|
//
|
|
94
|
-
// These
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// ResizeObserver instead, which forces the measure outright.
|
|
245
|
+
// These fields are every piece of state that moves an edge. The window's
|
|
246
|
+
// own resize comes through the ResizeObserver instead, which forces the
|
|
247
|
+
// measure outright.
|
|
98
248
|
let lastEdges = "";
|
|
99
249
|
const edgesOf = (s: ReturnType<typeof store.getState>): string =>
|
|
100
250
|
[
|
|
101
251
|
s.railCollapsed,
|
|
252
|
+
s.insertDrawer === null,
|
|
102
253
|
s.inspectorCollapsed,
|
|
103
254
|
s.dockCollapsed,
|
|
104
255
|
s.railWidth,
|
|
105
256
|
s.dockHeight,
|
|
106
257
|
s.agentOpen,
|
|
258
|
+
s.inspectorWidth,
|
|
259
|
+
// The tool footer opens and closes its own grid row, and its top is
|
|
260
|
+
// the inset's bottom edge. Its own predicate, plus the two fields that
|
|
261
|
+
// can move an edge while that answer stays the same.
|
|
262
|
+
toolFooterShown(s),
|
|
263
|
+
// And which edge it eats: the toolbar position pref moves it.
|
|
264
|
+
s.toolbarPosition,
|
|
265
|
+
s.enteredScene ?? "",
|
|
107
266
|
s.name,
|
|
267
|
+
s.materialWorkspace?.layerId ?? "",
|
|
268
|
+
s.materialColumnWidth,
|
|
269
|
+
s.codeLiveWidth,
|
|
270
|
+
// The modes can MOVE the hole without mounting or unmounting it: a
|
|
271
|
+
// subject change re-lays the canvas cell around one continuous
|
|
272
|
+
// `.stage-window`.
|
|
273
|
+
// The querySelector bit below only says whether A hole exists, so
|
|
274
|
+
// without these keys the measure was skipped and the stage kept
|
|
275
|
+
// framing the subject into the old rect.
|
|
276
|
+
s.codeComponent ?? "",
|
|
277
|
+
s.editingDef ?? "",
|
|
278
|
+
// Components' Preview | Edit swaps the hole for the studio's.
|
|
279
|
+
s.componentsTab,
|
|
280
|
+
// The board's right cell opens, closes and changes width with its
|
|
281
|
+
// occupant (`boardColumns`).
|
|
282
|
+
boardRightPanel(s) ?? "",
|
|
283
|
+
s.versionsOpen,
|
|
284
|
+
// A workspace swap moves content between cells without mounting or
|
|
285
|
+
// unmounting a shell child, so the child-list observer below cannot
|
|
286
|
+
// see it: the workspace itself is an edge.
|
|
287
|
+
workspaceOf(s),
|
|
108
288
|
].join("|");
|
|
109
289
|
const write = (force: boolean): void => {
|
|
110
290
|
raf = 0;
|
|
111
291
|
const s = store.getState();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
292
|
+
const fit = fitShell(window.innerWidth, window.innerHeight, s);
|
|
293
|
+
const ws = workspaceOf(s);
|
|
294
|
+
// THE BOARD DRAWS ITS COLUMNS AT DISPLAY WIDTHS (`boardColumns`): no
|
|
295
|
+
// rail, and the right cell only while it has an occupant, at 320, or 400
|
|
296
|
+
// for the component sheet. None of it is
|
|
297
|
+
// written back to `inspectorWidth` or `railWidth`, so Studio keeps the
|
|
298
|
+
// author's own sizes. The Version history takes the panel width.
|
|
299
|
+
const board = ws === "storyboard" ? boardColumns(window.innerWidth, s.versionsOpen ? "inspector" : boardRightPanel(s)) : null;
|
|
300
|
+
el.style.setProperty("--rail-w", `${board?.railWidth ?? fit.railWidth}px`);
|
|
301
|
+
// The Components home has no right cell either: the card grid runs to
|
|
302
|
+
// the window's edge.
|
|
303
|
+
const inspectorW = board?.inspectorWidth ?? (componentsColumnEmpty(s) ? 0 : fit.inspectorWidth);
|
|
304
|
+
el.style.setProperty("--inspector-w", `${inspectorW}px`);
|
|
305
|
+
// No document, no timeline (TimelineDock renders nothing): the bottom
|
|
306
|
+
// row closes rather than showing the stage through an empty cell. The
|
|
307
|
+
// board is one canvas to the window's foot, so it closes there too.
|
|
308
|
+
// Components closes it too, except on a Studio-made component's Edit
|
|
309
|
+
// tab (`componentsHideTimeline`).
|
|
310
|
+
const row = s.doc === null || ws === "storyboard" || componentsHideTimeline(s) ? 0 : fit.dockRow;
|
|
311
|
+
el.style.setProperty("--dock-row-h", `${row}px`);
|
|
312
|
+
// The material workspace's object: the stored width of its hole at the
|
|
313
|
+
// right of the canvas cell (material.css caps it at half the cell).
|
|
314
|
+
el.style.setProperty("--material-col-w", `${s.materialColumnWidth}px`);
|
|
315
|
+
// A code component's live pane, beside the code (code.css).
|
|
316
|
+
el.style.setProperty("--code-live-w", `${s.codeLiveWidth}px`);
|
|
115
317
|
// Where the stage's own divider is, so the handle can sit on it without
|
|
116
318
|
// the shell re-rendering per frame of the drag.
|
|
117
|
-
|
|
118
|
-
|
|
319
|
+
//
|
|
320
|
+
// THROUGH `splitX`, WITH THE INSET, which is the same call the renderer's
|
|
321
|
+
// scissor makes — the seam divides the canvas the author can SEE rather
|
|
322
|
+
// than the window. Doing the arithmetic here as well is how the handle
|
|
323
|
+
// and the seam it claims to sit on drift a column apart.
|
|
324
|
+
el.style.setProperty(
|
|
325
|
+
"--split-x",
|
|
326
|
+
`${splitX(s.stageLayout.splitRatio, window.innerWidth, s.stageInset)}px`,
|
|
327
|
+
);
|
|
328
|
+
// ── AND ONE BIT THAT IS NOT STATE ────────────────────────────────
|
|
329
|
+
//
|
|
330
|
+
// Whether the stage's WINDOW is mounted. In the material mode the stage
|
|
331
|
+
// is visible through exactly one element and nowhere else, and that
|
|
332
|
+
// element appears the moment the mesh gets a graph — a document write,
|
|
333
|
+
// with every field above it unchanged. So the edge list alone said
|
|
334
|
+
// "nothing moved", the measure was skipped, and the object rendered
|
|
335
|
+
// somewhere behind the mode instead of into the hole left for it.
|
|
336
|
+
//
|
|
337
|
+
// A `querySelector` and not a store field, because the honest question is
|
|
338
|
+
// about the DOM: no bit of state means "the hole is up" without deriving
|
|
339
|
+
// it a second way and being wrong when the two disagree. It costs one
|
|
340
|
+
// selector match per store notification and forces no layout — the
|
|
341
|
+
// rect is only read past the early return below.
|
|
342
|
+
// The dock row as DRAWN, too: an untouched dock fits its folded content
|
|
343
|
+
// (`fitShell`), so a document write can move the bottom edge alone.
|
|
344
|
+
const edges = `${edgesOf(s)}|${fit.dockRow}|${el.querySelector(STAGE_WINDOW) === null ? 0 : 1}`;
|
|
119
345
|
if (!force && edges === lastEdges) return;
|
|
120
346
|
lastEdges = edges;
|
|
121
347
|
// ── AND THE STAGE IS TOLD WHERE IT IS COVERED ────────────────────
|
|
@@ -124,15 +350,28 @@ export function App() {
|
|
|
124
350
|
// panes are the whole window and it cannot see any of this. It used to
|
|
125
351
|
// carry a hardcoded 116 for the top edge and nothing at all for the
|
|
126
352
|
// other three, which put two mode bars behind the opaque header and the
|
|
127
|
-
// G / R / S
|
|
128
|
-
// been visible in the default layout.
|
|
353
|
+
// stage's G / R / S readout behind the 232px left rail, where it had
|
|
354
|
+
// never once been visible in the default layout.
|
|
129
355
|
//
|
|
130
|
-
// MEASURED, not derived from the
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
store.stage()
|
|
356
|
+
// MEASURED, not derived from the track sizes: the modes swap their own
|
|
357
|
+
// holes in, the control strip wraps, and reading the boxes is both
|
|
358
|
+
// shorter than reproducing the layout and correct when someone changes
|
|
359
|
+
// it.
|
|
360
|
+
// THROUGH THE STORE, not straight down the bridge. This runs at mount,
|
|
361
|
+
// where `store.stage()` is still null because the iframe has not
|
|
362
|
+
// connected — so the push was simply dropped, and nothing re-sent it
|
|
363
|
+
// until a panel edge happened to move. The store remembers it and
|
|
364
|
+
// re-pushes it on every attach with the rest of the workspace.
|
|
365
|
+
const inset = measureInset(el, store.getState().toolbarPosition);
|
|
366
|
+
store.setStageInset(inset);
|
|
367
|
+
// The scrims cover exactly what the author can see of the stage, which
|
|
368
|
+
// is the inset: every box around the picture is docked now, the tool
|
|
369
|
+
// footer included, so what frames the picture and what covers it are
|
|
370
|
+
// one rect.
|
|
371
|
+
el.style.setProperty("--window-t", `${inset.top}px`);
|
|
372
|
+
el.style.setProperty("--window-r", `${inset.right}px`);
|
|
373
|
+
el.style.setProperty("--window-b", `${inset.bottom}px`);
|
|
374
|
+
el.style.setProperty("--window-l", `${inset.left}px`);
|
|
136
375
|
};
|
|
137
376
|
// COALESCED TO THE FRAME. This is a raw store subscription so it fires on
|
|
138
377
|
// every pointermove of a panel resize, and `getBoundingClientRect` is a
|
|
@@ -148,156 +387,609 @@ export function App() {
|
|
|
148
387
|
});
|
|
149
388
|
};
|
|
150
389
|
write(true);
|
|
151
|
-
|
|
390
|
+
// The COLD channel: every edge above is a cold field, so a transport
|
|
391
|
+
// tick can move none of them, and the raw channel ran this measure (a
|
|
392
|
+
// state read, a join and a `querySelector`) sixty times a second during
|
|
393
|
+
// playback to find that out. The observers below cover what the store
|
|
394
|
+
// cannot see.
|
|
395
|
+
const unsubscribe = store.subscribeCold(() => schedule(false));
|
|
396
|
+
// ── AND A WINDOW THAT CHANGES SIZE GETS THE LAYOUT FOR ITS NEW SIZE ──
|
|
397
|
+
//
|
|
398
|
+
// `bootLayout` fits the three panels to the window once, at startup. A
|
|
399
|
+
// window can change size afterwards — a laptop docked to a monitor, a
|
|
400
|
+
// split-screen tile — and leaving the boot sizes there would keep a 1366
|
|
401
|
+
// laptop's 200px rail on a 2560 display for the rest of the session.
|
|
402
|
+
//
|
|
403
|
+
// ONLY WHAT THE AUTHOR HAS NOT TOUCHED. The test is whether a panel still
|
|
404
|
+
// holds exactly the number `layoutFor` last gave it: a dragged panel is
|
|
405
|
+
// some other number and is left alone forever after, which is the whole
|
|
406
|
+
// rule and needs no flag to record it. A drag that happens to land on the
|
|
407
|
+
// fitted value to the pixel is indistinguishable from no drag, and being
|
|
408
|
+
// re-fitted is the right answer for it anyway.
|
|
409
|
+
let fitted = layoutFor(window.innerWidth, window.innerHeight);
|
|
410
|
+
const refit = (): void => {
|
|
411
|
+
const next = layoutFor(window.innerWidth, window.innerHeight);
|
|
412
|
+
const s = store.getState();
|
|
413
|
+
if (s.railWidth === fitted.railWidth) store.setRailWidth(next.railWidth);
|
|
414
|
+
if (s.inspectorWidth === fitted.inspectorWidth)
|
|
415
|
+
store.setInspectorWidth(next.inspectorWidth);
|
|
416
|
+
if (s.dockHeight === fitted.dockHeight)
|
|
417
|
+
store.setDockHeight(next.dockHeight);
|
|
418
|
+
fitted = next;
|
|
419
|
+
};
|
|
152
420
|
// The store cannot see a WINDOW resize, and that is exactly when the
|
|
153
421
|
// control strip wraps and the top edge moves — so its tick FORCES the
|
|
154
422
|
// measure rather than asking the store whether anything changed.
|
|
155
|
-
const observer = new ResizeObserver(() =>
|
|
423
|
+
const observer = new ResizeObserver(() => {
|
|
424
|
+
refit();
|
|
425
|
+
schedule(true);
|
|
426
|
+
});
|
|
156
427
|
observer.observe(el);
|
|
428
|
+
// ── AND A PANEL THAT ARRIVES AFTER THE MEASURE WAS SCHEDULED ─────────
|
|
429
|
+
//
|
|
430
|
+
// A RACE, and the losing side is unrecoverable. Everything above schedules
|
|
431
|
+
// the measure from STATE: the store notifies, `edgesOf` says an edge could
|
|
432
|
+
// have moved, and a frame is booked. But the panels themselves arrive when
|
|
433
|
+
// REACT COMMITS, and nothing orders that commit against the booked frame —
|
|
434
|
+
// opening a document sets `name` synchronously, and whether the rail, the
|
|
435
|
+
// inspector and the dock are in the DOM by the time the callback runs is a
|
|
436
|
+
// question about React's scheduler, not about this code.
|
|
437
|
+
//
|
|
438
|
+
// Losing that race is permanent, which is what makes it worth closing.
|
|
439
|
+
// `edgesOf` is a list of state, and no state changes when a panel finishes
|
|
440
|
+
// mounting, so a measure taken one frame early is never retried: the stage
|
|
441
|
+
// spends the session believing the inspector and the dock are not there,
|
|
442
|
+
// and everything derived from the inset — the overlay's furniture, the
|
|
443
|
+
// head-on framing, the split seam — is quietly wrong by the width of a
|
|
444
|
+
// panel. It reads as "the framing is a bit off", never as a bug.
|
|
445
|
+
//
|
|
446
|
+
// The commit IS the signal: a panel appearing or disappearing is a change
|
|
447
|
+
// to the shell's own child list, so this fires after it, whichever way the
|
|
448
|
+
// race went. DIRECT CHILDREN ONLY, and that is what makes it affordable —
|
|
449
|
+
// every panel the inset is measured from is a child of the shell, while a
|
|
450
|
+
// `subtree` observer would fire on every timeline row and every inspector
|
|
451
|
+
// keystroke and force a layout read for each, sixty times a second during
|
|
452
|
+
// playback, for four numbers a seek cannot move.
|
|
453
|
+
const panels = new MutationObserver(() => schedule(true));
|
|
454
|
+
panels.observe(el, { childList: true });
|
|
157
455
|
return () => {
|
|
158
456
|
if (raf !== 0) cancelAnimationFrame(raf);
|
|
159
457
|
unsubscribe();
|
|
160
458
|
observer.disconnect();
|
|
459
|
+
panels.disconnect();
|
|
161
460
|
};
|
|
162
461
|
}, [store]);
|
|
163
462
|
|
|
164
463
|
return (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
label="
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
464
|
+
// THE BOARD'S STILL QUEUE wraps the cells, so the strip (a cell portal)
|
|
465
|
+
// and the inspector's Move section (a shell child) share one queue. It is
|
|
466
|
+
// a stable wrapper, so the shell under it never remounts; `active` makes
|
|
467
|
+
// the queue only while the board is open or the Brief shows its sketches.
|
|
468
|
+
<StillThumbsProvider active={stillsActive}>
|
|
469
|
+
<CellSlotsProvider>
|
|
470
|
+
<div
|
|
471
|
+
ref={shellRef}
|
|
472
|
+
className={`shell${railCollapsed ? " is-rail-collapsed" : ""}${
|
|
473
|
+
inspectorCollapsed ? " is-inspector-collapsed" : ""
|
|
474
|
+
}${
|
|
475
|
+
materialWorkspace === null ? "" : " is-material-workspace"
|
|
476
|
+
}${componentStudio ? " is-component-studio" : ""}${componentEdit ? " is-component-edit" : ""}${
|
|
477
|
+
codeStudio ? " is-code-studio" : ""
|
|
478
|
+
}${componentsEmpty ? " is-components-empty" : ""}${storyboardOpen ? " is-storyboard" : ""}${workspace === "stage" ? "" : " is-mode"}${
|
|
479
|
+
footerShown ? " has-tool-footer" : ""
|
|
480
|
+
}${footerTop ? " tool-footer-top" : ""}`}
|
|
481
|
+
>
|
|
482
|
+
{/* FIRST, unconditional, unkeyed. See the header. */}
|
|
483
|
+
<StagePanel onConnectError={onConnectError} />
|
|
484
|
+
|
|
485
|
+
<Header />
|
|
486
|
+
{/* A BOUNDARY EACH, and OUTSIDE the mode rather than inside it. The
|
|
487
|
+
placement rule in ErrorBoundary.tsx says a boundary belongs inside
|
|
488
|
+
the panel container; these four ARE their own container, and a
|
|
489
|
+
boundary inside one cannot catch a throw in that component's own
|
|
490
|
+
body — which is exactly the bug worth catching here (a malformed
|
|
491
|
+
`storyboard.parts`, a reference row of an unexpected shape). The
|
|
492
|
+
rule's actual concern is the stage, and the stage is an earlier
|
|
493
|
+
sibling that a catch here never touches. `.shell > .panel-crash` in
|
|
494
|
+
styles/shell.css puts the fallback in the centre column on the
|
|
495
|
+
overlay tier, so a crash card is never painted underneath the stage
|
|
496
|
+
iframe.
|
|
497
|
+
|
|
498
|
+
Each mode below is its eager shell (lazySurfaces.tsx): null until
|
|
499
|
+
the mode is first wanted, then its chunk, behind the mode's own
|
|
500
|
+
skeleton. A chunk that fails to load lands in the same boundary. */}
|
|
501
|
+
<PanelBoundary label="Material workspace">
|
|
502
|
+
<MaterialWorkspace />
|
|
503
|
+
</PanelBoundary>
|
|
504
|
+
{/* The Component Studio. A SIBLING of the stage like every panel, and
|
|
505
|
+
it renders nothing until a definition is open on the Studio face,
|
|
506
|
+
so it is unconditional here for the reason `StageMenu` is. Like
|
|
507
|
+
every mode it shows its parts in the docked cells (cellSlots.tsx). */}
|
|
508
|
+
<PanelBoundary label="Component studio">
|
|
509
|
+
<ComponentStudio />
|
|
510
|
+
</PanelBoundary>
|
|
511
|
+
{/* And the third, for the OTHER component family: one code component's
|
|
512
|
+
source beside its own render. Unconditional here for the same reason,
|
|
513
|
+
and it renders nothing until `codeComponent` is set. */}
|
|
514
|
+
<PanelBoundary label="Code studio">
|
|
515
|
+
<CodeStudio />
|
|
516
|
+
</PanelBoundary>
|
|
517
|
+
{/* Components' Preview tab and its empty state (the two studios
|
|
518
|
+
above are its Edit tab). Unconditional for the same reason. */}
|
|
519
|
+
<PanelBoundary label="Components">
|
|
520
|
+
<ComponentsPreview />
|
|
521
|
+
</PanelBoundary>
|
|
522
|
+
{/* And the fourth: the Project's planning board. Unconditional here for
|
|
523
|
+
the same reason, and it renders nothing until `storyboardOpen`. */}
|
|
524
|
+
<PanelBoundary label="Storyboard">
|
|
525
|
+
<StoryboardView />
|
|
526
|
+
</PanelBoundary>
|
|
527
|
+
<LeftColumn />
|
|
528
|
+
{/* A SIBLING of the rail, not a child: a grid item on the rail's seam,
|
|
529
|
+
on the same --rail-w track the rail's width comes from, so the two
|
|
530
|
+
cannot disagree. It runs down through the timeline, because the
|
|
531
|
+
timeline's name column is that same track.
|
|
532
|
+
|
|
533
|
+
AND IT SERVES EVERY WORKSPACE BUT THE BOARD. A mode shows its own
|
|
534
|
+
left column in this same cell (the material library, the
|
|
535
|
+
component library, the file tree), so the seam drags there too.
|
|
536
|
+
The board has no left column: it is one full-width canvas.
|
|
537
|
+
|
|
538
|
+
FROM THE SHOWN WIDTH, AND NO WIDER THAN THE CANVAS ALLOWS. A drag
|
|
539
|
+
starts from what the author sees, not from a stored size the window
|
|
540
|
+
is too small for, and a drag that would squeeze the canvas under its
|
|
541
|
+
minimum stops where the track stops. */}
|
|
542
|
+
{railCollapsed || workspace === "storyboard" ? null : (
|
|
543
|
+
<ResizeHandle
|
|
544
|
+
axis="x"
|
|
545
|
+
getValue={() => shownShell().railWidth}
|
|
546
|
+
sign={1}
|
|
547
|
+
onChange={(w) =>
|
|
548
|
+
store.setRailWidth(
|
|
549
|
+
Math.min(w, window.innerWidth - CANVAS_MIN.width - shownShell().inspectorWidth),
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
onReset={() => store.setRailWidth(RAIL_WIDTH.default)}
|
|
553
|
+
className="rail-resize"
|
|
554
|
+
label="Resize the left panel"
|
|
555
|
+
/>
|
|
556
|
+
)}
|
|
557
|
+
{/* ── THE SPLIT DIVIDER DRAGS ────────────────────────────────────────
|
|
558
|
+
`splitRatio` has been in the schema, clamped twice and read by the
|
|
559
|
+
scissor since the split landed, and NOTHING ever moved it: the stage
|
|
560
|
+
drew a 1px seam and `panes.ts`'s header claimed the chrome laid out a
|
|
561
|
+
handle from the same rects. It did not.
|
|
562
|
+
|
|
563
|
+
CHROME-SIDE, because a stage-side handle needs a new stage-to-chrome
|
|
564
|
+
bridge message AND `pointerEvents: auto` inside an overlay whose
|
|
565
|
+
`pointerEvents: none` is a stated invariant — without it every drag
|
|
566
|
+
would end the moment the cursor crossed the divider.
|
|
567
|
+
|
|
568
|
+
Against `window.innerWidth`, which IS the pane width: the stage iframe
|
|
569
|
+
is pinned `inset: 0`, so its viewport is the whole window (panes.ts).
|
|
570
|
+
`clampSplitRatio` and `setStageLayout` already bound the value twice,
|
|
571
|
+
so there is no third validation here. */}
|
|
572
|
+
{/* The stage's own panes are on screen only in the stage workspace. In
|
|
573
|
+
a mode the stage frames one subject into a hole, and a divider at
|
|
574
|
+
the split's x would lie across the hole and take its drags. */}
|
|
575
|
+
{stageLayout.mode !== "split" || workspace !== "stage" ? null : (
|
|
576
|
+
<ResizeHandle
|
|
577
|
+
axis="x"
|
|
578
|
+
getValue={() =>
|
|
579
|
+
splitX(
|
|
580
|
+
store.getState().stageLayout.splitRatio,
|
|
581
|
+
window.innerWidth,
|
|
582
|
+
store.getState().stageInset,
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
sign={1}
|
|
586
|
+
onChange={(px) =>
|
|
587
|
+
store.setStageLayout({
|
|
588
|
+
...store.getState().stageLayout,
|
|
589
|
+
splitRatio: splitRatioAt(
|
|
590
|
+
px,
|
|
591
|
+
window.innerWidth,
|
|
592
|
+
store.getState().stageInset,
|
|
593
|
+
),
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
onReset={() =>
|
|
597
|
+
store.setStageLayout({
|
|
598
|
+
...store.getState().stageLayout,
|
|
599
|
+
splitRatio: 0.5,
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
className="split-resize"
|
|
603
|
+
label="Resize the split"
|
|
604
|
+
/>
|
|
605
|
+
)}
|
|
606
|
+
{/* THE CANVAS BAR, docked over the canvas cell: where am I, which panes
|
|
607
|
+
are up and how each is drawn, then the badges and the zoom. It names
|
|
608
|
+
the PLACE; the tools name the GESTURE and live in the footer under the
|
|
609
|
+
canvas. See CanvasBar.tsx. */}
|
|
610
|
+
<div className="control-strip">
|
|
611
|
+
{/* INSIDE the positioned strip, per PanelBoundary's placement rule.
|
|
612
|
+
Hidden on the board, which has no canvas bar: see the timeline
|
|
613
|
+
dock below for why an `<Activity>`. */}
|
|
614
|
+
<Activity mode={workspace === "storyboard" ? "hidden" : "visible"}>
|
|
615
|
+
<PanelBoundary label="Toolbar">
|
|
616
|
+
<CanvasBar />
|
|
617
|
+
</PanelBoundary>
|
|
618
|
+
</Activity>
|
|
619
|
+
</div>
|
|
620
|
+
{/* THE TOOL FOOTER, docked under the canvas cell: a grid row of its own
|
|
621
|
+
(`foot-t / main-b`), so it follows every dock drag and collapse with
|
|
622
|
+
no arithmetic and nothing floats over the picture. See `.tool-footer`
|
|
623
|
+
in styles/tool-footer.css.
|
|
624
|
+
|
|
625
|
+
MOUNTED AT EVERY LEVEL OF THE STAGE (`toolFooterShown`): a loaded
|
|
626
|
+
document in the stage workspace, the Project context included. In
|
|
627
|
+
every mode the row closes and the canvas takes its height back,
|
|
628
|
+
rather than drawing a bar about a picture that is not on screen.
|
|
629
|
+
|
|
630
|
+
ToolFooter holds the tools (or, in the Project context, the shot
|
|
631
|
+
under the playhead) on the left and the view track on the right.
|
|
632
|
+
The boundary is INSIDE the positioned box, per PanelBoundary's
|
|
633
|
+
placement rule.
|
|
634
|
+
|
|
635
|
+
RIGHT AFTER THE CANVAS BAR in the DOM, though the grid draws it
|
|
636
|
+
further down: the grid places every cell explicitly, so source
|
|
637
|
+
order is only the TAB order, and the tools belong with the canvas
|
|
638
|
+
they act on, before the inspector and the timeline. */}
|
|
639
|
+
{footerShown ? (
|
|
640
|
+
<div className="tool-footer">
|
|
641
|
+
<PanelBoundary label="Tools">
|
|
642
|
+
<ToolFooter />
|
|
643
|
+
</PanelBoundary>
|
|
644
|
+
</div>
|
|
645
|
+
) : null}
|
|
646
|
+
{/* ── AND THE RIGHT RAIL DRAGS TOO ──────────────────────────────────
|
|
647
|
+
The inspector was a CSS literal with no writer, and the panel had
|
|
648
|
+
been worked around rather than widened: a comment in SceneDocSection
|
|
649
|
+
records a label being SHORTENED because `.insp-label` is capped at
|
|
650
|
+
60% of a 288px pill. A width the author can set is the honest fix.
|
|
651
|
+
|
|
652
|
+
A SIBLING of the column, never a child: a grid item on its seam, the
|
|
653
|
+
same reason `.rail-resize` sits out here, and it runs down through
|
|
654
|
+
the timeline's meters cell, which is the same track.
|
|
655
|
+
|
|
656
|
+
`sign={-1}` because this rail is on the RIGHT: moving the pointer
|
|
657
|
+
along +x makes it narrower.
|
|
658
|
+
|
|
659
|
+
Hidden while collapsed, and on the Components home, whose column
|
|
660
|
+
has no occupant and is drawn at 0: a handle for a hidden column
|
|
661
|
+
is an 8px strip of dead canvas, and its drag would size a column
|
|
662
|
+
nobody can see.
|
|
663
|
+
|
|
664
|
+
NOT ON THE BOARD, whose right panel is drawn at fixed display
|
|
665
|
+
widths (`boardColumns`) that a drag would never move. */}
|
|
666
|
+
{inspectorCollapsed || workspace === "storyboard" || columnEmpty ? null : (
|
|
667
|
+
<ResizeHandle
|
|
668
|
+
axis="x"
|
|
669
|
+
getValue={() => shownShell().inspectorWidth}
|
|
670
|
+
sign={-1}
|
|
671
|
+
onChange={(w) =>
|
|
672
|
+
store.setInspectorWidth(
|
|
673
|
+
Math.min(w, window.innerWidth - CANVAS_MIN.width - shownShell().railWidth),
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
onReset={() => store.setInspectorWidth(INSPECTOR_WIDTH.default)}
|
|
677
|
+
className="inspector-resize"
|
|
678
|
+
label="Resize the right panel"
|
|
679
|
+
/>
|
|
680
|
+
)}
|
|
681
|
+
<RightColumn />
|
|
682
|
+
{/* STUDIO'S PANELS SLEEP ON THE BOARD. The board shows none of them
|
|
683
|
+
(storyboard.css hides their cells), but a hidden cell still
|
|
684
|
+
re-rendered on every store change the board made. Inside a hidden
|
|
685
|
+
`<Activity>` they keep their state (the dock's scroll, its open
|
|
686
|
+
rows) and stop rendering and running effects until Studio is back.
|
|
687
|
+
The stage is never inside one: it is the shell's first child, and
|
|
688
|
+
stageIdentity.test.tsx holds it there. The left column's Files
|
|
689
|
+
panel sleeps the same way (LeftColumn.tsx); the tool footer is
|
|
690
|
+
not mounted on the board at all (`toolFooterShown`). */}
|
|
691
|
+
<Activity mode={workspace === "storyboard" ? "hidden" : "visible"}>
|
|
692
|
+
<TimelineDock />
|
|
693
|
+
</Activity>
|
|
694
|
+
{/* THE MODES' CELLS (panels/stage/cellSlots.tsx). The canvas cell holds
|
|
695
|
+
nothing in the stage workspace, so the picture shows through it; a
|
|
696
|
+
mode portals its centre here. The bottom cell stands in for the
|
|
697
|
+
timeline in the two modes whose strip replaces it. */}
|
|
698
|
+
<CellSlot name="centre" className="canvas-cell" />
|
|
699
|
+
<CellSlot name="bottom" className="bottom-cell" />
|
|
700
|
+
{/* Renders nothing. Mounted HERE and not in the stage: audio is not part
|
|
701
|
+
of the document, and the stage is a GL iframe that must never
|
|
702
|
+
remount. Unconditional, so it survives every shell state. */}
|
|
703
|
+
<AudioPreviewBridge />
|
|
704
|
+
{/* A SIBLING of the stage panel, never a wrapper: the iframe must not
|
|
705
|
+
remount, and anything inside it would be captured by the export it
|
|
706
|
+
describes. Renders nothing unless an in-browser export is running. */}
|
|
707
|
+
<ExportScrim />
|
|
708
|
+
|
|
709
|
+
{exportOpen && name !== null && (
|
|
710
|
+
<ExportDialog onClose={() => store.setExportOpen(false)} />
|
|
711
|
+
)}
|
|
712
|
+
|
|
713
|
+
{/* Portals out of the layout, so it hangs here rather than inside a
|
|
714
|
+
panel: the stage menu opens over whichever half was right-clicked.
|
|
715
|
+
Unconditional, because it renders nothing until its state field is set. */}
|
|
716
|
+
<StageMenu />
|
|
717
|
+
{/* The library browser hangs here for the same reason. It is a `Dialog`
|
|
718
|
+
and not a workspace: it portals to `document.body` at 1100, so it
|
|
719
|
+
opens over the board and both studios rather than refusing while
|
|
720
|
+
one of them is up. */}
|
|
721
|
+
<LibraryBrowser />
|
|
722
|
+
{/* An MCP agent's question to the person ("The storyboard is
|
|
723
|
+
ready"): a Dialog over whatever is on screen, its chunk loaded
|
|
724
|
+
the first time one opens. */}
|
|
725
|
+
<AgentQuestionDialog />
|
|
726
|
+
|
|
727
|
+
{name === null ? (
|
|
728
|
+
<div className="empty-state">
|
|
729
|
+
<h1>No Project open</h1>
|
|
730
|
+
<p>Open a Project to start.</p>
|
|
731
|
+
{/* A plain link: with no Project there is nothing to flush first. */}
|
|
732
|
+
<a className="btn is-primary" href="/projects/" data-testid="empty-go-projects">
|
|
733
|
+
Go to Projects
|
|
734
|
+
</a>
|
|
735
|
+
</div>
|
|
736
|
+
) : null}
|
|
737
|
+
|
|
738
|
+
{/* Top of the work area (Paper page 21): editing is held until it clears. */}
|
|
739
|
+
<SyncQuotaBanner />
|
|
740
|
+
{/* A delete under live sync, with changes still waiting (R10). */}
|
|
741
|
+
<SyncDeletedDialog />
|
|
742
|
+
|
|
743
|
+
<div className="overlay-strip" role="status" aria-live="polite">
|
|
744
|
+
{/* FIRST, and with no dismiss: it outlives every message under it and
|
|
745
|
+
is the reason some of them are about to appear. */}
|
|
746
|
+
{schemaNotice === null ? null : (
|
|
747
|
+
<div
|
|
748
|
+
className="overlay-message is-sticky"
|
|
749
|
+
data-testid="schema-mismatch"
|
|
750
|
+
>
|
|
751
|
+
<span>{schemaNotice}</span>
|
|
752
|
+
</div>
|
|
753
|
+
)}
|
|
754
|
+
{/* Sticky too: autosave is stopped until the author acts on it. */}
|
|
755
|
+
{saveConflict === null ? null : (
|
|
756
|
+
<div className="overlay-message is-sticky" data-testid="save-conflict">
|
|
757
|
+
{saveConflict === "deleted" ? (
|
|
758
|
+
<>
|
|
759
|
+
<span>This Project was deleted or renamed somewhere else, so your edits are not saving.</span>
|
|
760
|
+
<a className="overlay-action" href="/projects/">
|
|
761
|
+
Back to Projects
|
|
762
|
+
</a>
|
|
763
|
+
</>
|
|
764
|
+
) : (
|
|
765
|
+
<>
|
|
766
|
+
<span>
|
|
767
|
+
This Project changed in another tab or by an agent, so your edits are not saving. Reload
|
|
768
|
+
takes the latest version, and Undo then brings yours back.
|
|
769
|
+
</span>
|
|
770
|
+
<button type="button" className="overlay-action" onClick={() => void store.reloadLatest()}>
|
|
771
|
+
Reload
|
|
772
|
+
</button>
|
|
773
|
+
</>
|
|
774
|
+
)}
|
|
775
|
+
</div>
|
|
776
|
+
)}
|
|
777
|
+
{/* Sticky: it goes when the file is relinked or removed. */}
|
|
778
|
+
<MissingAssetsNotice />
|
|
779
|
+
<ViewOnlyToast />
|
|
780
|
+
{/* Each row clears its own message: a stage error is not `error`.
|
|
781
|
+
VIEW_ONLY is not a row: ViewOnlyToast says it, with a way on. */}
|
|
782
|
+
{(
|
|
783
|
+
[
|
|
784
|
+
[stageError, () => setStageError(null)],
|
|
785
|
+
[reportedStageError, () => store.dismissStageError()],
|
|
786
|
+
[error === VIEW_ONLY ? null : error, () => store.dismissError()],
|
|
787
|
+
] as const
|
|
788
|
+
)
|
|
789
|
+
.filter((row): row is readonly [string, () => void] => row[0] !== null)
|
|
790
|
+
.map(([message, dismiss]) => (
|
|
791
|
+
<div key={message} className="overlay-message">
|
|
792
|
+
<span>{message}</span>
|
|
793
|
+
<button
|
|
794
|
+
type="button"
|
|
795
|
+
onClick={dismiss}
|
|
796
|
+
title="Dismiss"
|
|
797
|
+
>
|
|
798
|
+
×
|
|
799
|
+
</button>
|
|
800
|
+
</div>
|
|
801
|
+
))}
|
|
270
802
|
</div>
|
|
271
|
-
|
|
272
|
-
</
|
|
273
|
-
</
|
|
803
|
+
</div>
|
|
804
|
+
</CellSlotsProvider>
|
|
805
|
+
</StillThumbsProvider>
|
|
274
806
|
);
|
|
275
807
|
}
|
|
276
808
|
|
|
277
|
-
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
809
|
+
/** A Viewer's edit attempt (Paper page 21, "View only"): a toast with the way
|
|
810
|
+
* on, "Request edit access", in place of the error row. The request is
|
|
811
|
+
* always 202 (packages/worker/src/requests.ts), so "sent" is all it can say.
|
|
812
|
+
* One at a time: a second attempt replaces the first. */
|
|
813
|
+
function ViewOnlyToast() {
|
|
814
|
+
const store = useStore();
|
|
815
|
+
const error = useShotEditor((s) => s.error);
|
|
816
|
+
const { toast, node } = useToasts();
|
|
817
|
+
const shown = useRef<ToastHandle | null>(null);
|
|
818
|
+
useEffect(() => {
|
|
819
|
+
if (error !== VIEW_ONLY) return;
|
|
820
|
+
store.dismissError();
|
|
821
|
+
shown.current?.dismiss();
|
|
822
|
+
shown.current = toast({
|
|
823
|
+
message: VIEW_ONLY,
|
|
824
|
+
action: {
|
|
825
|
+
label: "Request edit access",
|
|
826
|
+
run: () => {
|
|
827
|
+
const projectId = store.getState().doc?.id;
|
|
828
|
+
if (projectId === undefined) return;
|
|
829
|
+
requestEditAccess(projectId).then(
|
|
830
|
+
() => toast({ message: "Request sent. A Project admin will see it.", severity: "success" }),
|
|
831
|
+
(e: unknown) => toast({ message: e instanceof Error ? e.message : String(e), severity: "error" }),
|
|
832
|
+
);
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
}, [error, store, toast]);
|
|
837
|
+
return node;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** `?scene=<stable-slug>` opens exactly one Project. A bare editor never picks
|
|
841
|
+
* one on the author's behalf; Project Home owns that choice. `?shot=` remains
|
|
842
|
+
* the original document-key spelling for old deep links. */
|
|
286
843
|
function useOpenFromUrl(): void {
|
|
287
844
|
const store = useStore();
|
|
288
845
|
useEffect(() => {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
846
|
+
const params = new URLSearchParams(window.location.search);
|
|
847
|
+
// ── `?scene=` IS THE DOCUMENT AND `?shot=` IS THE SEGMENT ─────────────
|
|
848
|
+
//
|
|
849
|
+
// The words moved when the chrome's vocabulary did, and `?shot=<docName>`
|
|
850
|
+
// is the ORIGINAL spelling of the first — old links carry it and must keep
|
|
851
|
+
// working. So one precedence rule rather than a guess: `scene=` names the
|
|
852
|
+
// document and wins, and `shot=` is read AS A DOCUMENT NAME only when
|
|
853
|
+
// `scene=` is absent.
|
|
854
|
+
//
|
|
855
|
+
// A URL carrying BOTH is the ordinary shape this chrome now writes (see
|
|
856
|
+
// `mirrorUrl`), not a stale bookmark: `scene=` is the document and `shot=`
|
|
857
|
+
// is the shot inside it, read below once that document is loaded.
|
|
858
|
+
const wanted = wantedDocOf(params);
|
|
859
|
+
// ?def= REOPENS A COMPONENT CONTEXT. It rides the open rather than being a
|
|
860
|
+
// second effect because it addresses INTO the document: a definition id
|
|
861
|
+
// means nothing until the document holding it is loaded, and `editComponent`
|
|
862
|
+
// called against an empty store would be validated away on the first write.
|
|
863
|
+
// A stale id (the definition was deleted since the link was made) opens the
|
|
864
|
+
// shot and nothing else, which is the same degradation `?focus=` takes.
|
|
865
|
+
const reopenDef = (): void => {
|
|
866
|
+
// ?shot=<sc_…> REOPENS THE SHOT CONTEXT, and it rides the open for the
|
|
867
|
+
// same reason `?def=` does: a shot id means nothing until the document
|
|
868
|
+
// holding it is loaded. It is read ONLY when `scene=` supplied the
|
|
869
|
+
// document — otherwise `shot=` is the original spelling of the document
|
|
870
|
+
// name and `wanted` has already consumed it, so treating it as a shot id
|
|
871
|
+
// would refuse a link that used to work.
|
|
872
|
+
const shot = params.get("shot");
|
|
873
|
+
// NON-EMPTY `scene=`, not merely present. `?scene=&shot=sc_x` is a URL a
|
|
874
|
+
// hand edit or a truncating share sheet produces, and `wanted` already
|
|
875
|
+
// rejects it as a document name — so a bare presence check here would
|
|
876
|
+
// enter a shot id against whatever document the "open the first one"
|
|
877
|
+
// fallback happened to load.
|
|
878
|
+
const named = params.get("scene");
|
|
879
|
+
if (
|
|
880
|
+
named !== null &&
|
|
881
|
+
named.length > 0 &&
|
|
882
|
+
shot !== null &&
|
|
883
|
+
shot.length > 0
|
|
884
|
+
) {
|
|
885
|
+
// Membership in `doc.scenes`, never `scenesById[shot] !== undefined`:
|
|
886
|
+
// that map is a plain object and `?shot=constructor` is a string a link
|
|
887
|
+
// can carry, so the presence guard would pass on `Object`'s own
|
|
888
|
+
// constructor. `enterScene` refuses it too; this is the outer half.
|
|
889
|
+
if (store.getState().doc?.scenes.includes(shot) === true)
|
|
890
|
+
store.enterScene(shot);
|
|
891
|
+
}
|
|
892
|
+
const def = params.get("def");
|
|
893
|
+
if (
|
|
894
|
+
def !== null &&
|
|
895
|
+
def.length > 0 &&
|
|
896
|
+
store.getState().doc?.components?.[def] !== undefined
|
|
897
|
+
) {
|
|
898
|
+
// `?def=` reopens the definition in Components. An old `&edit=1` face
|
|
899
|
+
// flag means nothing now: there is one place.
|
|
900
|
+
store.editComponent(def);
|
|
299
901
|
}
|
|
300
|
-
|
|
902
|
+
// `?code=` REOPENS THE OTHER FAMILY'S SUBJECT. Validated by the REGISTRY
|
|
903
|
+
// rather than by the document, which is the whole difference between the
|
|
904
|
+
// two: a code component is a file in the repo, so whether a link to it
|
|
905
|
+
// still works has nothing to do with which shot is open. `openCodeComponent`
|
|
906
|
+
// does that lookup itself and reports an id the registry has lost, so
|
|
907
|
+
// there is nothing to check here.
|
|
908
|
+
const code = params.get("code");
|
|
909
|
+
if (code !== null && code.length > 0) {
|
|
910
|
+
// `?file=` IS THE BUFFER, and it rides BEHIND the subject rather than
|
|
911
|
+
// beside it: it names a file to read inside the code studio, and there
|
|
912
|
+
// is no code studio until `code=` has opened one. Awaited for that
|
|
913
|
+
// reason — opening the file first would land a buffer in a mode that
|
|
914
|
+
// is about to replace the whole buffer set.
|
|
915
|
+
//
|
|
916
|
+
// GATED ON THE SUBJECT ACTUALLY OPENING, not on the promise settling:
|
|
917
|
+
// `openCodeComponent` refuses by RETURNING (an id the registry lost, a
|
|
918
|
+
// builtin, an unreadable file), so a resolved promise says nothing
|
|
919
|
+
// about whether the mode is on screen. `openCodeFile` refuses the same
|
|
920
|
+
// case itself; this check keeps the reason legible at the call site.
|
|
921
|
+
//
|
|
922
|
+
// The path is otherwise unvalidated here, like `?material=`:
|
|
923
|
+
// `openCodeFile` reads through `/api/files`, which is the only thing
|
|
924
|
+
// that knows whether a path is editable, and it reports its own refusal.
|
|
925
|
+
const file = params.get("file");
|
|
926
|
+
void store.openCodeComponent(code).then(() => {
|
|
927
|
+
if (file === null || file.length === 0) return;
|
|
928
|
+
if (store.getState().codeComponent === null) return;
|
|
929
|
+
void store.openCodeFile(file);
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
reopenMaterial();
|
|
933
|
+
// ?storyboard=1 REOPENS THE BOARD, after the material reopen and only
|
|
934
|
+
// over the stage: opening hands over from a mode, but a link carrying
|
|
935
|
+
// both is a link the mirror never wrote, and the board yields.
|
|
936
|
+
if (params.get("storyboard") === "1" && workspaceOf(store.getState()) === "stage") store.setStoryboardOpen(true);
|
|
937
|
+
// ?plan=<partId>&sketch=<id> REOPENS THE SHOT BOARD on the board just
|
|
938
|
+
// opened, and only on it: a plan that is gone opens the whole board.
|
|
939
|
+
// `openShotBoard` takes a Shot id too, so it is checked as a part here.
|
|
940
|
+
const plan = params.get("plan");
|
|
941
|
+
if (
|
|
942
|
+
plan !== null &&
|
|
943
|
+
store.getState().storyboardOpen &&
|
|
944
|
+
store.getState().doc?.storyboard?.parts.some((p) => p.id === plan) === true
|
|
945
|
+
) {
|
|
946
|
+
store.openShotBoard(plan);
|
|
947
|
+
const sketch = params.get("sketch");
|
|
948
|
+
if (sketch !== null && sketch.length > 0) store.selectSketch(sketch);
|
|
949
|
+
}
|
|
950
|
+
// ?thread=<id> OPENS A COMMENT THREAD (an inbox item, an email link)
|
|
951
|
+
// where it sits (`openThread`), and in the Comments tab. A DOOR, NOT A
|
|
952
|
+
// PLACE: the mirror never writes it, so it leaves the URL once read.
|
|
953
|
+
// Left in, every reload would open the thread again over the mode the
|
|
954
|
+
// author has moved to since, and a card thread hands over to the board.
|
|
955
|
+
const thread = params.get("thread");
|
|
956
|
+
if (thread !== null && thread !== "") {
|
|
957
|
+
const url = new URL(window.location.href);
|
|
958
|
+
url.searchParams.delete("thread");
|
|
959
|
+
window.history.replaceState(null, "", url);
|
|
960
|
+
void store.openThread(thread);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
// ?material=<layerId>&slot=<index> REOPENS THE SHADER, and it rides the
|
|
964
|
+
// open behind the shot and the definition for the same reason both of them
|
|
965
|
+
// do: a layer id means nothing until the document holding it is loaded, and
|
|
966
|
+
// `openMaterialWorkspace` resolves the layer through whichever container
|
|
967
|
+
// the chrome is currently in — so entering the shot has to have happened
|
|
968
|
+
// first or the mesh is not reachable and the call refuses.
|
|
969
|
+
//
|
|
970
|
+
// The SLOT is passed through unchecked on purpose. The slot list is pushed
|
|
971
|
+
// up by the stage when the model finishes parsing, which is well after
|
|
972
|
+
// boot, so validating here would reject every deep link into a submesh.
|
|
973
|
+
// `openMaterialWorkspace` already drops a slot on a primitive, and
|
|
974
|
+
// `onHistoryChange` drops one the loaded model turns out not to have.
|
|
975
|
+
const reopenMaterial = (): void => {
|
|
976
|
+
const material = params.get("material");
|
|
977
|
+
if (material === null || material.length === 0) return;
|
|
978
|
+
const raw = params.get("slot");
|
|
979
|
+
const slot = raw === null || raw === "" ? null : Number(raw);
|
|
980
|
+
if (slot !== null && !Number.isInteger(slot)) return;
|
|
981
|
+
store.openMaterialWorkspace(material, { slot });
|
|
982
|
+
};
|
|
983
|
+
if (wanted !== null) {
|
|
984
|
+
// ADOPT the boot open when main.tsx already started it (see bootOpen.ts:
|
|
985
|
+
// the fetch overlaps the mount instead of following it); initiate only
|
|
986
|
+
// when there is none — a test's own store, or a future second mount.
|
|
987
|
+
// The continuation still runs after the doc lands and against live store
|
|
988
|
+
// state, exactly as before.
|
|
989
|
+
void (bootOpenFor(store, wanted) ?? store.openShot(wanted)).then(
|
|
990
|
+
reopenDef,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
301
993
|
}, [store]);
|
|
302
994
|
}
|
|
303
995
|
|
|
@@ -309,32 +1001,81 @@ function useOpenFromUrl(): void {
|
|
|
309
1001
|
* panel and a stage is a keymap where "what does ⇧A do" has three answers and
|
|
310
1002
|
* a reader has to find all three to be sure.
|
|
311
1003
|
*/
|
|
312
|
-
/** The
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
1004
|
+
/** The docked cells around the canvas, per edge they eat into. Hidden ones
|
|
1005
|
+
* are skipped, which is what makes a collapsed rail give the stage its
|
|
1006
|
+
* corner back.
|
|
1007
|
+
*
|
|
1008
|
+
* The tool footer is a docked cell like the rest: the picture is framed away
|
|
1009
|
+
* from it and the scrims stop at it. WHICH edge is the toolbar position pref:
|
|
1010
|
+
* under the stage it sits above the dock, so it is the larger of the bottom
|
|
1011
|
+
* edges whenever it is mounted; on top it sits under the canvas bar, so it is
|
|
1012
|
+
* the larger of the top edges. Built from the pref, never a special case in
|
|
1013
|
+
* the measure. */
|
|
1014
|
+
function insetPanels(toolbar: "bottom" | "top") {
|
|
1015
|
+
const footer = ".tool-footer";
|
|
1016
|
+
return {
|
|
1017
|
+
top: [".shell-header", ".control-strip", ...(toolbar === "top" ? [footer] : [])],
|
|
1018
|
+
left: [".left-rail"],
|
|
1019
|
+
right: [".inspector-column"],
|
|
1020
|
+
bottom: [".timeline-dock", ".bottom-cell", ...(toolbar === "bottom" ? [footer] : [])],
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* The one element that is a WINDOW onto the stage rather than a lid over it.
|
|
1026
|
+
*
|
|
1027
|
+
* A mode paints its own parts in the cells around this one, the canvas cell
|
|
1028
|
+
* included, so the stage's whole visible area IS this rect. That makes the
|
|
1029
|
+
* per-side max below the wrong sum: it knows only the docked cells, so it
|
|
1030
|
+
* would frame the subject into the whole canvas cell, under the mode's parts.
|
|
1031
|
+
*
|
|
1032
|
+
* A SHARED CLASS and not one selector per mode. There are four (the material
|
|
1033
|
+
* workspace's object, the Component Studio's subject, Code Studio's render and
|
|
1034
|
+
* the board's preview); a second selector here is a third place the rule lives,
|
|
1035
|
+
* and the failure when someone forgets it is silent — the mode paints, the
|
|
1036
|
+
* measure falls through to the per-panel maxes, and the stage frames the
|
|
1037
|
+
* subject into a rect it can see none of.
|
|
1038
|
+
*/
|
|
1039
|
+
const STAGE_WINDOW = ".stage-window";
|
|
321
1040
|
|
|
322
1041
|
/**
|
|
323
1042
|
* How far the chrome's furniture reaches into the stage, per side, in CSS px.
|
|
324
1043
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* `shell.right - 0` is the whole window
|
|
328
|
-
* the stage its entire width was covered and every
|
|
329
|
-
* screen.
|
|
1044
|
+
* A PANEL NOBODY CAN SEE COVERS NOTHING, and there are two ways to be one.
|
|
1045
|
+
* Zero-sized is the older test and it is not an optimisation: for the RIGHT
|
|
1046
|
+
* edge `shell.right - 0` is the whole window, so a collapsed inspector that
|
|
1047
|
+
* measured zero would tell the stage its entire width was covered and every
|
|
1048
|
+
* badge would render off screen. Hidden is the second, and it is why the two
|
|
1049
|
+
* are asked separately now. See `edge`.
|
|
330
1050
|
*/
|
|
331
|
-
function measureInset(shell: HTMLElement): StageInset {
|
|
1051
|
+
export function measureInset(shell: HTMLElement, toolbar: "bottom" | "top"): StageInset {
|
|
1052
|
+
const panels = insetPanels(toolbar);
|
|
332
1053
|
const box = shell.getBoundingClientRect();
|
|
333
|
-
|
|
1054
|
+
// A WINDOW WINS OUTRIGHT. Not merged with the panels below: when one is on
|
|
1055
|
+
// screen the stage is visible THERE and nowhere else, so the four numbers are
|
|
1056
|
+
// that rect's four distances and no panel can add to them.
|
|
1057
|
+
const port = shell.querySelector(STAGE_WINDOW)?.getBoundingClientRect();
|
|
1058
|
+
if (port !== undefined && port.width > 0 && port.height > 0) {
|
|
1059
|
+
const hole = {
|
|
1060
|
+
top: port.top - box.top,
|
|
1061
|
+
left: port.left - box.left,
|
|
1062
|
+
right: box.right - port.right,
|
|
1063
|
+
bottom: box.bottom - port.bottom,
|
|
1064
|
+
};
|
|
1065
|
+
return hole;
|
|
1066
|
+
}
|
|
1067
|
+
const edge = (
|
|
1068
|
+
selectors: readonly string[],
|
|
1069
|
+
of: (r: DOMRect) => number,
|
|
1070
|
+
): number => {
|
|
334
1071
|
let most = 0;
|
|
335
1072
|
for (const selector of selectors) {
|
|
336
1073
|
const el = shell.querySelector(selector);
|
|
337
1074
|
if (el === null) continue;
|
|
1075
|
+
// AND A HIDDEN ONE, which is not the same question. A collapsed column is
|
|
1076
|
+
// `visibility: hidden` and keeps its whole track, so it measures full
|
|
1077
|
+
// width while covering nothing: the canvas spans over it.
|
|
1078
|
+
if (window.getComputedStyle(el).visibility === "hidden") continue;
|
|
338
1079
|
const rect = el.getBoundingClientRect();
|
|
339
1080
|
if (rect.width === 0 || rect.height === 0) continue;
|
|
340
1081
|
most = Math.max(most, of(rect));
|
|
@@ -342,18 +1083,44 @@ function measureInset(shell: HTMLElement): StageInset {
|
|
|
342
1083
|
return most;
|
|
343
1084
|
};
|
|
344
1085
|
return {
|
|
345
|
-
top: edge(
|
|
346
|
-
left: edge(
|
|
347
|
-
right: edge(
|
|
348
|
-
bottom: edge(
|
|
1086
|
+
top: edge(panels.top, (r) => r.bottom - box.top),
|
|
1087
|
+
left: edge(panels.left, (r) => r.right - box.left),
|
|
1088
|
+
right: edge(panels.right, (r) => box.right - r.left),
|
|
1089
|
+
bottom: edge(panels.bottom, (r) => box.bottom - r.top),
|
|
349
1090
|
};
|
|
350
1091
|
}
|
|
351
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* A file dropped on the chrome must not navigate the tab away from the
|
|
1095
|
+
* editor. The panels that take drops prevent the default themselves, so this
|
|
1096
|
+
* only answers for everywhere else, and it leaves text fields their own drop.
|
|
1097
|
+
* The stage iframe is its own document and guards itself (StagePanel.tsx).
|
|
1098
|
+
*/
|
|
1099
|
+
function useWindowDropGuard(): void {
|
|
1100
|
+
useEffect(() => {
|
|
1101
|
+
const guard = (e: DragEvent): void => {
|
|
1102
|
+
if (e.defaultPrevented) return;
|
|
1103
|
+
const t = e.target;
|
|
1104
|
+
if (t instanceof HTMLElement && (t.isContentEditable || t.closest("input, textarea") !== null))
|
|
1105
|
+
return;
|
|
1106
|
+
e.preventDefault();
|
|
1107
|
+
if (e.dataTransfer !== null) e.dataTransfer.dropEffect = "none";
|
|
1108
|
+
};
|
|
1109
|
+
window.addEventListener("dragover", guard);
|
|
1110
|
+
window.addEventListener("drop", guard);
|
|
1111
|
+
return () => {
|
|
1112
|
+
window.removeEventListener("dragover", guard);
|
|
1113
|
+
window.removeEventListener("drop", guard);
|
|
1114
|
+
};
|
|
1115
|
+
}, []);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
352
1118
|
function useKeymap(): void {
|
|
353
1119
|
const store = useStore();
|
|
354
1120
|
useEffect(() => {
|
|
355
1121
|
const onKeyDown = (e: KeyboardEvent): void => {
|
|
356
|
-
if (store.handleKey({ ...keyOf(e), target: e.target }))
|
|
1122
|
+
if (store.handleKey({ ...keyOf(e), target: e.target }))
|
|
1123
|
+
e.preventDefault();
|
|
357
1124
|
};
|
|
358
1125
|
window.addEventListener("keydown", onKeyDown);
|
|
359
1126
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
@@ -370,6 +1137,8 @@ function keyOf(e: KeyboardEvent) {
|
|
|
370
1137
|
ctrlKey: e.ctrlKey,
|
|
371
1138
|
metaKey: e.metaKey,
|
|
372
1139
|
code: e.code,
|
|
1140
|
+
// A held key's OS repeats: the play toggle ignores them (`handleKey`).
|
|
1141
|
+
repeat: e.repeat,
|
|
373
1142
|
};
|
|
374
1143
|
}
|
|
375
1144
|
|
|
@@ -385,10 +1154,20 @@ function useExternalChangeRelay(): void {
|
|
|
385
1154
|
if (e.origin !== window.location.origin) return;
|
|
386
1155
|
const data = e.data as { type?: unknown; data?: unknown } | null;
|
|
387
1156
|
if (data?.type !== "studio:shot-doc-changed") return;
|
|
388
|
-
|
|
1157
|
+
// THE MAIN STAGE ONLY. The inspector's component preview is a second
|
|
1158
|
+
// stage page (ComponentStage), and it relays the same push: two relays
|
|
1159
|
+
// meant two reloads of one write.
|
|
1160
|
+
const main = document.querySelector<HTMLIFrameElement>("iframe.stage-frame")?.contentWindow;
|
|
1161
|
+
if (main !== undefined && main !== null && e.source !== main) return;
|
|
1162
|
+
store.adoptExternalChange(
|
|
1163
|
+
(data.data ?? {}) as {
|
|
1164
|
+
name?: string;
|
|
1165
|
+
origin?: string;
|
|
1166
|
+
clientId?: string;
|
|
1167
|
+
},
|
|
1168
|
+
);
|
|
389
1169
|
};
|
|
390
1170
|
window.addEventListener("message", onMessage);
|
|
391
1171
|
return () => window.removeEventListener("message", onMessage);
|
|
392
1172
|
}, [store]);
|
|
393
1173
|
}
|
|
394
|
-
|