@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
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
// THE PLAN OF ONE PLANNED SHOT (Paper SB1 and SB2): the right panel of the
|
|
2
|
+
// shot board, and of the board when one shot card is selected.
|
|
3
|
+
//
|
|
4
|
+
// head SB2 "Plan" and "Shot 02" on the shot board, whose bar names the
|
|
5
|
+
// shot; SB1 the number, the name and the status badge on the
|
|
6
|
+
// board (`variant`). The name renames in place (`PartName`).
|
|
7
|
+
// words What we see (`part.notes`), How it moves (`part.motion`, the
|
|
8
|
+
// numbered beats: add, edit, remove, drag or Alt+arrow to
|
|
9
|
+
// reorder, at most `STORYBOARD_BEATS_MAX`), Voice (`part.voice`).
|
|
10
|
+
// timing Length (`part.durationFrames`, typed and shown in seconds,
|
|
11
|
+
// "2.00s") and Cut to next: the Shot's transition, edited with the
|
|
12
|
+
// Studio's own rows (`TransitionRows`) once the shot has a Shot.
|
|
13
|
+
// ground Paper, Dark or Brand for the sketch the panel follows (`sketchId`,
|
|
14
|
+
// else Sketch 1). Hidden while the shot has no sketch.
|
|
15
|
+
// pinned the cards pinned to this shot: Must or Optional for a
|
|
16
|
+
// requirement, "on sketch" when the sketch draws it. Each row drags
|
|
17
|
+
// as a board card (`STORYBOARD_MIME`), so it drops on a sketch.
|
|
18
|
+
// style how many Style cards apply to every shot.
|
|
19
|
+
// footer the caller's (`footer`), else on the shot board the agent line.
|
|
20
|
+
//
|
|
21
|
+
// THE BLOCKS KEEP THEIR CAPS LABELS (`.caps` is the design system's label for
|
|
22
|
+
// a form), and every control in them is a house widget at its standard
|
|
23
|
+
// geometry: TextFields, a NumberField, the select trigger, the segmented
|
|
24
|
+
// control. EVERY WRITE IS ONE UNDO STEP through `store.edit` and the pure
|
|
25
|
+
// part and sketch ops, so this panel needs no store verb of its own, and a
|
|
26
|
+
// value written back unchanged is no step at all (the ops return the same
|
|
27
|
+
// document). A VIEWER reads it all with the writes off (`LockedControls`).
|
|
28
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
29
|
+
import type { DragEvent as ReactDragEvent, KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
|
|
30
|
+
import { framesToSeconds } from "@dailies-studio/engine";
|
|
31
|
+
import { FieldBox, IconChevronDown, IconClose, LockedControls, NumberField, SegmentedControl, TextField, useLocked } from "@dailies-studio/ui";
|
|
32
|
+
import {
|
|
33
|
+
STORYBOARD_BEATS_MAX,
|
|
34
|
+
STORYBOARD_VOICE_MAX,
|
|
35
|
+
isRequirement,
|
|
36
|
+
referenceImportanceOf,
|
|
37
|
+
updatePart,
|
|
38
|
+
updateSketch,
|
|
39
|
+
} from "@dailies-studio/shot/doc";
|
|
40
|
+
import type { Doc3D, ReferenceItem3D, StoryboardBeat3D, StoryboardPart3D, StoryboardSketch3D } from "@dailies-studio/shot/doc";
|
|
41
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
42
|
+
import {
|
|
43
|
+
SHOT_STATUS_LABEL,
|
|
44
|
+
boardShots,
|
|
45
|
+
cardKindOf,
|
|
46
|
+
cutLabel,
|
|
47
|
+
shotStatusOf,
|
|
48
|
+
storyEntries,
|
|
49
|
+
styleItemsOf,
|
|
50
|
+
} from "../../state/storyboardModel.js";
|
|
51
|
+
import type { ShotStatus } from "../../state/storyboardModel.js";
|
|
52
|
+
import { useImageThumb } from "../../state/imageThumbs.js";
|
|
53
|
+
import { brandBackdrop } from "../../bridgeTypes.js";
|
|
54
|
+
import { ADD_ICON, SPARKLES_ICON } from "../../icons.js";
|
|
55
|
+
import { TransitionRows } from "../inspector/scene/TransitionSection.js";
|
|
56
|
+
import { useScrub } from "../inspector/useDocWrite.js";
|
|
57
|
+
import { formatDuration } from "../timeline/model.js";
|
|
58
|
+
import { assetUrl, pinGlyph } from "./ReferencePreview.js";
|
|
59
|
+
import { writeDrag } from "./storyboardDrop.js";
|
|
60
|
+
import "./plan.css";
|
|
61
|
+
import { useBuildingParts } from "../agent/AgentDots.js";
|
|
62
|
+
|
|
63
|
+
/** A beat row's own drag type: a reorder inside one list, never a board card. */
|
|
64
|
+
const BEAT_MIME = "application/x-dailies-plan-beat";
|
|
65
|
+
|
|
66
|
+
/** The badge tone each status reads in (G1 `.badge`). */
|
|
67
|
+
const STATUS_TONE: Record<ShotStatus, string> = { planned: "", building: " is-building", built: " is-success" };
|
|
68
|
+
|
|
69
|
+
/** "02": a planned shot's place in playing order, or null when it has none. */
|
|
70
|
+
export function partNumberOf(doc: Doc3D, partId: string): string | null {
|
|
71
|
+
const at = storyEntries(doc).findIndex((e) => (e.kind === "shot" ? e.shot.part?.id : e.part.id) === partId);
|
|
72
|
+
return at < 0 ? null : String(at + 1).padStart(2, "0");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Typed seconds ("2", "2.5s") or frames ("48f") back to frames, or null. */
|
|
76
|
+
export function parseLength(text: string, fps: number): number | null {
|
|
77
|
+
const m = /^\s*(\d+(?:\.\d+)?|\.\d+)\s*(s|f)?\s*$/i.exec(text);
|
|
78
|
+
if (m === null) return null;
|
|
79
|
+
const n = Number(m[1]);
|
|
80
|
+
const frames = m[2]?.toLowerCase() === "f" ? n : n * fps;
|
|
81
|
+
return Number.isFinite(frames) && frames > 0 ? frames / fps : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The beats with `at` moved to `to`, both clamped. */
|
|
85
|
+
export function moveBeat(beats: readonly StoryboardBeat3D[], at: number, to: number): StoryboardBeat3D[] {
|
|
86
|
+
const next = [...beats];
|
|
87
|
+
const target = Math.max(0, Math.min(next.length - 1, to));
|
|
88
|
+
if (at < 0 || at >= next.length || target === at) return next;
|
|
89
|
+
const [beat] = next.splice(at, 1);
|
|
90
|
+
next.splice(target, 0, beat!);
|
|
91
|
+
return next;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A trimmed text field's value as a patch: null drops the key. */
|
|
95
|
+
const textPatch = (text: string, max?: number): string | null => {
|
|
96
|
+
const trimmed = text.trim();
|
|
97
|
+
if (trimmed === "") return null;
|
|
98
|
+
return max === undefined ? trimmed : trimmed.slice(0, max).trim();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A PLANNED SHOT'S NAME, AND ITS RENAME, wherever the shot is named: the head
|
|
103
|
+
* of its Plan panel on the board, and the shot board's bar. The name is a
|
|
104
|
+
* button and the rename is the store's (`storyboardRenaming`), so F2 (on the
|
|
105
|
+
* board with the shot picked, or on its shot board), a double-click on the
|
|
106
|
+
* name, and Enter or F2 on the name with the keyboard all open the one field.
|
|
107
|
+
* A single press does nothing: it is a name, read far more than renamed. A
|
|
108
|
+
* viewer cannot rename, so for them it is plain words: a focusable button
|
|
109
|
+
* that did nothing and never said why was a dead stop on every Tab.
|
|
110
|
+
*/
|
|
111
|
+
export function PartName(props: { part: StoryboardPart3D; className: string; testId?: string }) {
|
|
112
|
+
const store = useStore();
|
|
113
|
+
const readOnly = useShotEditor((s) => s.readOnly);
|
|
114
|
+
const renaming = useShotEditor((s) => s.storyboardRenaming === props.part.id);
|
|
115
|
+
const name = useRef<HTMLButtonElement>(null);
|
|
116
|
+
// A RENAME CLOSED FROM THE KEYBOARD HANDS THE FOCUS BACK TO THE NAME. Enter
|
|
117
|
+
// and Escape blur the field, which then unmounts with nothing focused, so
|
|
118
|
+
// the focus fell to the page and the next Tab started from its top. A click
|
|
119
|
+
// away keeps the focus where the click put it.
|
|
120
|
+
const refocus = useRef(false);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (renaming || !refocus.current) return;
|
|
123
|
+
refocus.current = false;
|
|
124
|
+
name.current?.focus();
|
|
125
|
+
}, [renaming]);
|
|
126
|
+
if (renaming && !readOnly) {
|
|
127
|
+
return (
|
|
128
|
+
<PartRename
|
|
129
|
+
key={props.part.id}
|
|
130
|
+
part={props.part}
|
|
131
|
+
onKeyClose={() => {
|
|
132
|
+
refocus.current = true;
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (readOnly) {
|
|
138
|
+
return (
|
|
139
|
+
<span
|
|
140
|
+
className={props.className}
|
|
141
|
+
{...(props.testId === undefined ? {} : { "data-testid": props.testId })}
|
|
142
|
+
title={props.part.label}
|
|
143
|
+
>
|
|
144
|
+
{props.part.label}
|
|
145
|
+
</span>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const open = (): void => store.beginStoryboardRename(props.part.id);
|
|
149
|
+
return (
|
|
150
|
+
<button
|
|
151
|
+
ref={name}
|
|
152
|
+
type="button"
|
|
153
|
+
className={props.className}
|
|
154
|
+
{...(props.testId === undefined ? {} : { "data-testid": props.testId })}
|
|
155
|
+
title={`${props.part.label}: double-click or F2 to rename`}
|
|
156
|
+
onDoubleClick={open}
|
|
157
|
+
// A press with no pointer behind it (`detail` 0) is assistive tech's,
|
|
158
|
+
// which has no double press to give: it opens the rename too.
|
|
159
|
+
onClick={(event) => {
|
|
160
|
+
if (event.detail === 0) open();
|
|
161
|
+
}}
|
|
162
|
+
onKeyDown={(event) => {
|
|
163
|
+
if (event.key !== "F2" && event.key !== "Enter") return;
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
open();
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{props.part.label}
|
|
170
|
+
</button>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The open rename. It commits on Enter or on a blur that changed the name,
|
|
176
|
+
* and it CLOSES on every blur and on Escape: TextField commits only a change,
|
|
177
|
+
* so the field used to stay open under a click away or an Escape, with
|
|
178
|
+
* nothing else to close it (the window's keys pass a focused field by).
|
|
179
|
+
* `onKeyClose` hears Enter and Escape in the CAPTURE phase, before the field
|
|
180
|
+
* blurs itself on them, so the name knows to take the focus back.
|
|
181
|
+
*/
|
|
182
|
+
function PartRename({ part, onKeyClose }: { part: StoryboardPart3D; onKeyClose(): void }) {
|
|
183
|
+
const store = useStore();
|
|
184
|
+
const close = (): void => {
|
|
185
|
+
if (store.getState().storyboardRenaming === part.id) store.beginStoryboardRename(null);
|
|
186
|
+
};
|
|
187
|
+
return (
|
|
188
|
+
<span
|
|
189
|
+
className="plan-rename"
|
|
190
|
+
onBlur={close}
|
|
191
|
+
onKeyDownCapture={(event) => {
|
|
192
|
+
if (event.key === "Enter" || event.key === "Escape") onKeyClose();
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<TextField
|
|
196
|
+
autoFocus
|
|
197
|
+
value={part.label}
|
|
198
|
+
ariaLabel="Shot name"
|
|
199
|
+
onCommit={(text) => {
|
|
200
|
+
const label = text.trim();
|
|
201
|
+
if (label !== "" && label !== part.label) store.updateStoryboardPart(part.id, { label });
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
</span>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function PlanPanel(props: {
|
|
209
|
+
partId: string;
|
|
210
|
+
/** "shot" is the shot board's head (SB2), "board" the board's (SB1). */
|
|
211
|
+
variant?: "shot" | "board";
|
|
212
|
+
/** The sketch Ground writes and "on sketch" reads. Default: Sketch 1. */
|
|
213
|
+
sketchId?: string;
|
|
214
|
+
/** Replaces the default footer. */
|
|
215
|
+
footer?: ReactNode;
|
|
216
|
+
}) {
|
|
217
|
+
const store = useStore();
|
|
218
|
+
const doc = useShotEditor((s) => s.doc);
|
|
219
|
+
const readOnly = useShotEditor((s) => s.readOnly);
|
|
220
|
+
const building = useBuildingParts();
|
|
221
|
+
const variant = props.variant ?? "shot";
|
|
222
|
+
const part = doc?.storyboard?.parts.find((p) => p.id === props.partId);
|
|
223
|
+
if (doc === null || part === undefined) return null;
|
|
224
|
+
|
|
225
|
+
const write = (patch: Parameters<typeof updatePart>[2]): void => {
|
|
226
|
+
store.edit((base) => updatePart(base, part.id, patch));
|
|
227
|
+
};
|
|
228
|
+
const number = partNumberOf(doc, part.id);
|
|
229
|
+
const sketch = part.sketches?.find((s) => s.id === props.sketchId) ?? part.sketches?.[0];
|
|
230
|
+
const footer =
|
|
231
|
+
props.footer !== undefined ? (
|
|
232
|
+
<footer className="panel-foot">{props.footer}</footer>
|
|
233
|
+
) : variant === "shot" ? (
|
|
234
|
+
<footer className="panel-foot is-note">
|
|
235
|
+
{SPARKLES_ICON}
|
|
236
|
+
The agent reads the plan and sketches.
|
|
237
|
+
</footer>
|
|
238
|
+
) : null;
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<section className="plan-panel" data-testid="plan-panel" aria-label="Plan">
|
|
242
|
+
{variant === "shot" ? (
|
|
243
|
+
<header className="panel-head plan-head">
|
|
244
|
+
<h3 className="caps">Plan</h3>
|
|
245
|
+
{number === null ? null : <span className="plan-head-shot">{`Shot ${number}`}</span>}
|
|
246
|
+
</header>
|
|
247
|
+
) : (
|
|
248
|
+
<header className="panel-head plan-head">
|
|
249
|
+
{number === null ? null : <span className="plan-head-number">{number}</span>}
|
|
250
|
+
<h3 className="plan-head-title">
|
|
251
|
+
<PartName part={part} className="plan-head-name" />
|
|
252
|
+
</h3>
|
|
253
|
+
<StatusBadge status={shotStatusOf(part, building)} />
|
|
254
|
+
</header>
|
|
255
|
+
)}
|
|
256
|
+
<LockedControls value={readOnly}>
|
|
257
|
+
{/* KEYED ON THE PART: another shot starts with no beat draft and the
|
|
258
|
+
cut rows closed, never the last shot's. */}
|
|
259
|
+
<div className="plan-body" key={part.id}>
|
|
260
|
+
<div className="plan-field">
|
|
261
|
+
<h4 className="caps">What we see</h4>
|
|
262
|
+
<TextField
|
|
263
|
+
multiline
|
|
264
|
+
value={part.notes ?? ""}
|
|
265
|
+
placeholder="What the viewer sees in this shot"
|
|
266
|
+
ariaLabel="What we see"
|
|
267
|
+
onCommit={(text) => write({ notes: textPatch(text) })}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
<Beats part={part} onWrite={(motion) => write({ motion })} />
|
|
271
|
+
<div className="plan-field">
|
|
272
|
+
<h4 className="caps">Voice</h4>
|
|
273
|
+
<TextField
|
|
274
|
+
value={part.voice ?? ""}
|
|
275
|
+
placeholder="The line we hear"
|
|
276
|
+
ariaLabel="Voice"
|
|
277
|
+
onCommit={(text) => write({ voice: textPatch(text, STORYBOARD_VOICE_MAX) })}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
<Timing doc={doc} part={part} onLength={(durationFrames) => write({ durationFrames })} />
|
|
281
|
+
{sketch === undefined ? null : <Ground doc={doc} sketch={sketch} />}
|
|
282
|
+
<Pinned doc={doc} part={part} sketch={sketch} />
|
|
283
|
+
<StyleRow count={styleItemsOf(doc).length} />
|
|
284
|
+
</div>
|
|
285
|
+
</LockedControls>
|
|
286
|
+
{footer}
|
|
287
|
+
</section>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function StatusBadge({ status }: { status: ShotStatus }) {
|
|
292
|
+
return (
|
|
293
|
+
<span className={`badge${STATUS_TONE[status]}`} data-testid="plan-status">
|
|
294
|
+
{SHOT_STATUS_LABEL[status]}
|
|
295
|
+
</span>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── HOW IT MOVES ────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
function Beats(props: { part: StoryboardPart3D; onWrite(motion: StoryboardBeat3D[] | null): void }) {
|
|
302
|
+
const beats = props.part.motion ?? [];
|
|
303
|
+
const readOnly = useShotEditor((s) => s.readOnly);
|
|
304
|
+
// The remove buttons write, so the panel's lock takes them with the fields.
|
|
305
|
+
const locked = useLocked();
|
|
306
|
+
const [adding, setAdding] = useState(false);
|
|
307
|
+
const dragFrom = useRef<number | null>(null);
|
|
308
|
+
const focusAt = useRef<number | null>(null);
|
|
309
|
+
const list = useRef<HTMLOListElement>(null);
|
|
310
|
+
const full = beats.length >= STORYBOARD_BEATS_MAX;
|
|
311
|
+
const write = (next: StoryboardBeat3D[]): void => props.onWrite(next.length === 0 ? null : next);
|
|
312
|
+
|
|
313
|
+
// An emptied beat is removed: a beat's text is never stored empty.
|
|
314
|
+
const setText = (at: number, text: string): void => {
|
|
315
|
+
const trimmed = text.trim();
|
|
316
|
+
if (trimmed === beats[at]?.text) return;
|
|
317
|
+
write(trimmed === "" ? beats.filter((_, i) => i !== at) : beats.map((b, i) => (i === at ? { ...b, text: trimmed } : b)));
|
|
318
|
+
};
|
|
319
|
+
const move = (at: number, to: number): void => {
|
|
320
|
+
if (to < 0 || to >= beats.length || to === at) return;
|
|
321
|
+
write(moveBeat(beats, at, to));
|
|
322
|
+
};
|
|
323
|
+
const onKey = (at: number) => (event: ReactKeyboardEvent) => {
|
|
324
|
+
if (!event.altKey || readOnly || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
const to = at + (event.key === "ArrowUp" ? -1 : 1);
|
|
327
|
+
if (to < 0 || to >= beats.length) return;
|
|
328
|
+
focusAt.current = to;
|
|
329
|
+
move(at, to);
|
|
330
|
+
};
|
|
331
|
+
// THE FOCUS FOLLOWS THE MOVED BEAT: the move re-keys its row, so a second
|
|
332
|
+
// Alt+arrow would otherwise press nothing.
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
const to = focusAt.current;
|
|
335
|
+
if (to === null) return;
|
|
336
|
+
focusAt.current = null;
|
|
337
|
+
list.current?.querySelector<HTMLElement>(`[aria-label="Beat ${to + 1}"]`)?.focus();
|
|
338
|
+
});
|
|
339
|
+
const onDrop = (at: number) => (event: ReactDragEvent) => {
|
|
340
|
+
const from = dragFrom.current;
|
|
341
|
+
dragFrom.current = null;
|
|
342
|
+
if (from === null || !event.dataTransfer.types.includes(BEAT_MIME)) return;
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
move(from, at);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<div className="plan-field">
|
|
349
|
+
<div className="plan-field-head">
|
|
350
|
+
<h4 className="caps">How it moves</h4>
|
|
351
|
+
<span className="plan-count">{beats.length === 1 ? "1 beat" : `${beats.length} beats`}</span>
|
|
352
|
+
</div>
|
|
353
|
+
<ol ref={list} className="plan-beats" aria-label="How it moves">
|
|
354
|
+
{beats.map((beat, at) => (
|
|
355
|
+
<li
|
|
356
|
+
// Text, then place: a reorder re-keys the moved rows, so a draft
|
|
357
|
+
// never follows the wrong beat.
|
|
358
|
+
key={`${at}:${beat.text}`}
|
|
359
|
+
className="plan-beat"
|
|
360
|
+
data-testid="plan-beat"
|
|
361
|
+
draggable={!readOnly}
|
|
362
|
+
title="Drag, or Alt+arrow, to reorder"
|
|
363
|
+
onDragStart={(event) => {
|
|
364
|
+
dragFrom.current = at;
|
|
365
|
+
event.dataTransfer.setData(BEAT_MIME, String(at));
|
|
366
|
+
event.dataTransfer.effectAllowed = "move";
|
|
367
|
+
}}
|
|
368
|
+
onDragOver={(event) => {
|
|
369
|
+
if (dragFrom.current === null) return;
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
event.dataTransfer.dropEffect = "move";
|
|
372
|
+
}}
|
|
373
|
+
onDrop={onDrop(at)}
|
|
374
|
+
onDragEnd={() => {
|
|
375
|
+
dragFrom.current = null;
|
|
376
|
+
}}
|
|
377
|
+
onKeyDown={onKey(at)}
|
|
378
|
+
>
|
|
379
|
+
<span className="plan-beat-number">{at + 1}</span>
|
|
380
|
+
{/* THE ROW'S VERB IS INSIDE THE BEAT'S BOX, the house accessory
|
|
381
|
+
lane (`FieldBox`): a 16px key with the remove glyph that turns
|
|
382
|
+
to the danger ink under the pointer. So every beat's box ends
|
|
383
|
+
on one right edge, the open draft's too, which has no verb. */}
|
|
384
|
+
<FieldBox
|
|
385
|
+
trailing={
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
className="insp-key is-danger"
|
|
389
|
+
aria-label={`Remove beat ${at + 1}`}
|
|
390
|
+
title="Remove beat"
|
|
391
|
+
disabled={locked}
|
|
392
|
+
onClick={() => write(beats.filter((_, i) => i !== at))}
|
|
393
|
+
>
|
|
394
|
+
<IconClose />
|
|
395
|
+
</button>
|
|
396
|
+
}
|
|
397
|
+
>
|
|
398
|
+
<TextField value={beat.text} ariaLabel={`Beat ${at + 1}`} onCommit={(text) => setText(at, text)} />
|
|
399
|
+
</FieldBox>
|
|
400
|
+
</li>
|
|
401
|
+
))}
|
|
402
|
+
{adding ? (
|
|
403
|
+
// Any blur ends the draft (Enter, Escape, a click away); the field's
|
|
404
|
+
// own blur commits first, and an empty draft writes nothing.
|
|
405
|
+
<li className="plan-beat is-draft" onBlur={() => setAdding(false)}>
|
|
406
|
+
<span className="plan-beat-number">{beats.length + 1}</span>
|
|
407
|
+
<FieldBox>
|
|
408
|
+
<TextField
|
|
409
|
+
value=""
|
|
410
|
+
autoFocus
|
|
411
|
+
placeholder="What moves next"
|
|
412
|
+
ariaLabel={`Beat ${beats.length + 1}`}
|
|
413
|
+
onCommit={(text) => {
|
|
414
|
+
const trimmed = text.trim();
|
|
415
|
+
if (trimmed !== "" && !full) write([...beats, { text: trimmed }]);
|
|
416
|
+
}}
|
|
417
|
+
/>
|
|
418
|
+
</FieldBox>
|
|
419
|
+
</li>
|
|
420
|
+
) : null}
|
|
421
|
+
</ol>
|
|
422
|
+
{adding ? null : (
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
className="plan-add-beat"
|
|
426
|
+
data-testid="plan-add-beat"
|
|
427
|
+
disabled={locked || full}
|
|
428
|
+
title={full ? `A shot holds at most ${STORYBOARD_BEATS_MAX} beats` : undefined}
|
|
429
|
+
onClick={() => setAdding(true)}
|
|
430
|
+
>
|
|
431
|
+
<span className="plan-add-beat-icon" aria-hidden="true">
|
|
432
|
+
{ADD_ICON}
|
|
433
|
+
</span>
|
|
434
|
+
Add beat
|
|
435
|
+
</button>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── LENGTH AND CUT TO NEXT ──────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
function Timing(props: { doc: Doc3D; part: StoryboardPart3D; onLength(frames: number): void }) {
|
|
444
|
+
const { doc, part } = props;
|
|
445
|
+
const [open, setOpen] = useState(false);
|
|
446
|
+
const rowsId = useId();
|
|
447
|
+
const fps = doc.fps.num / doc.fps.den;
|
|
448
|
+
const seconds = framesToSeconds(part.durationFrames, doc.fps);
|
|
449
|
+
const sceneId = part.scene !== undefined && doc.scenes.includes(part.scene) ? part.scene : null;
|
|
450
|
+
const shot = sceneId === null ? undefined : boardShots(doc).find((s) => s.sceneId === sceneId);
|
|
451
|
+
const label = shot === undefined ? "Cut" : cutLabel(doc, shot);
|
|
452
|
+
// "Wipe 0.40s": the name, then the played length in the numeric face.
|
|
453
|
+
const split = /^(.*?)\s(\d+\.\d{2}s)$/.exec(label);
|
|
454
|
+
/** Seconds on the field to whole frames, never below one. */
|
|
455
|
+
const framesOf = (next: number): number => Math.max(1, Math.round(next * fps));
|
|
456
|
+
// A DRAG ON THE LENGTH writes live, in one undo step, so the shot card's
|
|
457
|
+
// length and the board's total move with the pointer.
|
|
458
|
+
const lengthScrub = useScrub((base, raw) =>
|
|
459
|
+
typeof raw === "number" && Number.isFinite(raw) ? updatePart(base, part.id, { durationFrames: framesOf(raw) }) : base,
|
|
460
|
+
);
|
|
461
|
+
return (
|
|
462
|
+
<>
|
|
463
|
+
<div className="plan-timing">
|
|
464
|
+
<div className="plan-field plan-length">
|
|
465
|
+
<h4 className="caps">Length</h4>
|
|
466
|
+
<NumberField
|
|
467
|
+
value={seconds}
|
|
468
|
+
min={1 / fps}
|
|
469
|
+
step={1 / fps}
|
|
470
|
+
format={formatDuration}
|
|
471
|
+
parse={(text) => parseLength(text, fps)}
|
|
472
|
+
ariaLabel="Length"
|
|
473
|
+
onCommit={(next) => props.onLength(framesOf(next))}
|
|
474
|
+
onScrubStart={lengthScrub.start}
|
|
475
|
+
onScrub={lengthScrub.move}
|
|
476
|
+
onScrubEnd={lengthScrub.end}
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
<div className="plan-field plan-cut">
|
|
480
|
+
<h4 className="caps">Cut to next</h4>
|
|
481
|
+
{/* THE HOUSE SELECT TRIGGER, the Combobox's own anatomy (the value
|
|
482
|
+
ellipsized, its caret), on a button that discloses the cut's
|
|
483
|
+
rows under it rather than a list: the cut is edited with the
|
|
484
|
+
Studio's own transition rows, not picked from a menu. */}
|
|
485
|
+
<button
|
|
486
|
+
type="button"
|
|
487
|
+
className="select combobox-trigger plan-cut-select"
|
|
488
|
+
data-testid="plan-cut"
|
|
489
|
+
// NAMED BY ITS LABEL, then by what it shows: the caps label over it
|
|
490
|
+
// is a heading, so the button alone read "Wipe 0.40s".
|
|
491
|
+
aria-label={`Cut to next: ${label}`}
|
|
492
|
+
{...(sceneId === null ? {} : { "aria-expanded": open, "aria-controls": rowsId })}
|
|
493
|
+
disabled={sceneId === null}
|
|
494
|
+
title={sceneId === null ? "Set once this shot has a Shot in Studio" : "Edit the cut into the next shot"}
|
|
495
|
+
onClick={() => setOpen((was) => !was)}
|
|
496
|
+
>
|
|
497
|
+
<span className="combobox-value">{split?.[1] ?? label}</span>
|
|
498
|
+
{split === null ? null : <span className="plan-cut-length">{split[2]}</span>}
|
|
499
|
+
<IconChevronDown className="combobox-caret field-caret" />
|
|
500
|
+
</button>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
{open && sceneId !== null ? (
|
|
504
|
+
<div className="plan-cut-rows" id={rowsId} data-testid="plan-cut-rows">
|
|
505
|
+
<TransitionRows sceneId={sceneId} lengthLabel="Length" />
|
|
506
|
+
</div>
|
|
507
|
+
) : null}
|
|
508
|
+
</>
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ── GROUND ──────────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
function Ground(props: { doc: Doc3D; sketch: StoryboardSketch3D }) {
|
|
515
|
+
const store = useStore();
|
|
516
|
+
const { sketch } = props;
|
|
517
|
+
const brand = brandBackdrop(props.doc);
|
|
518
|
+
const swatch = (tone: string, colour?: string): ReactNode => (
|
|
519
|
+
<span className={`plan-swatch is-${tone}`} aria-hidden="true" style={colour === undefined ? undefined : { background: colour }} />
|
|
520
|
+
);
|
|
521
|
+
return (
|
|
522
|
+
<div className="plan-field">
|
|
523
|
+
<h4 className="caps">Ground</h4>
|
|
524
|
+
<SegmentedControl
|
|
525
|
+
ariaLabel="Ground"
|
|
526
|
+
value={sketch.ground ?? "paper"}
|
|
527
|
+
options={[
|
|
528
|
+
{ value: "paper", label: <>{swatch("paper")}Paper</> },
|
|
529
|
+
{ value: "dark", label: <>{swatch("dark")}Dark</> },
|
|
530
|
+
// The Project's brand colour when it has one (Paper Q10).
|
|
531
|
+
{ value: "brand", label: <>{swatch("brand", brand ?? undefined)}Brand</> },
|
|
532
|
+
]}
|
|
533
|
+
// Paper is the absent key; the lit chip pressed again writes nothing
|
|
534
|
+
// (`updateSketch` returns the same document).
|
|
535
|
+
onChange={(value) => store.edit((base) => updateSketch(base, sketch.id, { ground: value === "dark" || value === "brand" ? value : null }))}
|
|
536
|
+
/>
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── PINNED TO THIS SHOT ─────────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
/** A card kind as the row says it: "Image", "Component". */
|
|
544
|
+
const kindWord = (item: ReferenceItem3D, assets: Doc3D["assets"]): string => {
|
|
545
|
+
const kind = cardKindOf(item, assets);
|
|
546
|
+
return kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
function Pinned(props: { doc: Doc3D; part: StoryboardPart3D; sketch: StoryboardSketch3D | undefined }) {
|
|
550
|
+
const { doc, part, sketch } = props;
|
|
551
|
+
const readOnly = useShotEditor((s) => s.readOnly);
|
|
552
|
+
const pins = (doc.resources?.references ?? []).filter((r) => r.part === part.id);
|
|
553
|
+
const onSketch = new Set((sketch?.items ?? []).flatMap((item) => (item.kind === "ref" ? [item.ref] : [])));
|
|
554
|
+
return (
|
|
555
|
+
<div className="plan-field">
|
|
556
|
+
<div className="plan-field-head">
|
|
557
|
+
<h4 className="caps">Pinned to this shot</h4>
|
|
558
|
+
<span className="plan-count is-numeric">{pins.length}</span>
|
|
559
|
+
</div>
|
|
560
|
+
{pins.length === 0 ? (
|
|
561
|
+
<p className="plan-none">Nothing pinned yet. Drop cards on this shot.</p>
|
|
562
|
+
) : (
|
|
563
|
+
<ul className="plan-pins" aria-label="Pinned to this shot">
|
|
564
|
+
{pins.map((item) => (
|
|
565
|
+
<PinRow key={item.id} doc={doc} item={item} onSketch={onSketch.has(item.id)} readOnly={readOnly} />
|
|
566
|
+
))}
|
|
567
|
+
</ul>
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function PinRow(props: { doc: Doc3D; item: ReferenceItem3D; onSketch: boolean; readOnly: boolean }) {
|
|
574
|
+
const { doc, item } = props;
|
|
575
|
+
const asset = item.type === "asset" ? doc.assets[item.asset] : undefined;
|
|
576
|
+
const image = asset?.kind === "image" ? asset : undefined;
|
|
577
|
+
const thumb = useImageThumb(image === undefined ? "" : assetUrl(image.path), image?.hash ?? "", 20);
|
|
578
|
+
const name = item.label.trim() || "Untitled card";
|
|
579
|
+
const must = referenceImportanceOf(item) === "must";
|
|
580
|
+
return (
|
|
581
|
+
<li
|
|
582
|
+
className="plan-pin"
|
|
583
|
+
data-testid={`plan-pin-${item.id}`}
|
|
584
|
+
draggable={!props.readOnly}
|
|
585
|
+
title={props.readOnly ? undefined : "Drag onto the sketch"}
|
|
586
|
+
onDragStart={(event) => writeDrag(event.dataTransfer, { kind: "reference", id: item.id })}
|
|
587
|
+
>
|
|
588
|
+
<span className="plan-pin-thumb" aria-hidden="true">
|
|
589
|
+
{image === undefined ? pinGlyph(item) : thumb === null ? null : <img src={thumb} alt="" draggable={false} />}
|
|
590
|
+
</span>
|
|
591
|
+
<span className="plan-pin-name">{name}</span>
|
|
592
|
+
{props.onSketch ? (
|
|
593
|
+
<span className="plan-pin-on" data-testid="plan-pin-on-sketch">
|
|
594
|
+
on sketch
|
|
595
|
+
</span>
|
|
596
|
+
) : null}
|
|
597
|
+
{isRequirement(item) ? <span className={`badge${must ? " is-accent" : ""}`}>{must ? "Must" : "Optional"}</span> : null}
|
|
598
|
+
<span className="plan-pin-kind">{kindWord(item, doc.assets)}</span>
|
|
599
|
+
</li>
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── STYLE ───────────────────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
/** How many Style cards every shot reads. The Style wall sits on the board's
|
|
606
|
+
* desk itself, so the row says the count and links nowhere. */
|
|
607
|
+
function StyleRow(props: { count: number }) {
|
|
608
|
+
const { count } = props;
|
|
609
|
+
return (
|
|
610
|
+
<div className="plan-style" data-testid="plan-style">
|
|
611
|
+
<h4 className="caps">Style</h4>
|
|
612
|
+
<p className="plan-style-text">
|
|
613
|
+
{count === 0 ? "No Style cards yet" : `${count} ${count === 1 ? "item applies" : "items apply"} to every shot`}
|
|
614
|
+
</p>
|
|
615
|
+
</div>
|
|
616
|
+
);
|
|
617
|
+
}
|