@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,52 @@
|
|
|
1
|
+
// The question asked before Code Studio's open buffers are dropped.
|
|
2
|
+
//
|
|
3
|
+
// ITS OWN FILE, WITH NO EDITOR IN IT. The header, the Components rail, home
|
|
4
|
+
// and preview all ask it, and they load with the shell; `codeParts.tsx`
|
|
5
|
+
// carries the CodeMirror editor, which loads only when code is opened. Kept
|
|
6
|
+
// together, every visit paid for CodeMirror to ask one question.
|
|
7
|
+
import { useCallback, type ReactNode } from "react";
|
|
8
|
+
import { useConfirm } from "@dailies-studio/ui";
|
|
9
|
+
import { useStore } from "../../state/context.js";
|
|
10
|
+
import type { CodeFileState } from "../../state/types.js";
|
|
11
|
+
|
|
12
|
+
/** How many open buffers hold text that is not on disk. */
|
|
13
|
+
export function dirtyCodeFileCount(files: Readonly<Record<string, CodeFileState>>): number {
|
|
14
|
+
return Object.values(files).filter((f) => f.content !== f.savedContent).length;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The one question asked before open buffers are dropped. */
|
|
18
|
+
export function discardBuffersMessage(count: number): string {
|
|
19
|
+
return count === 1
|
|
20
|
+
? "One open file has edits that have not been written to disk. Close it anyway?"
|
|
21
|
+
: `${count} open files have edits that have not been written to disk. Close them anyway?`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Leaving the code component in Components, which drops the buffers. With
|
|
26
|
+
* nothing dirty `guard` just runs `then`; otherwise it asks first. `then` is
|
|
27
|
+
* the verb that drops them (a leave, or a definition picked in its place).
|
|
28
|
+
*/
|
|
29
|
+
export function useCodeBufferGuard(): { guard: (then: () => void) => void; dialog: ReactNode } {
|
|
30
|
+
const store = useStore();
|
|
31
|
+
const { confirm, dialog } = useConfirm();
|
|
32
|
+
const guard = useCallback(
|
|
33
|
+
(then: () => void): void => {
|
|
34
|
+
const s = store.getState();
|
|
35
|
+
const count = s.codeComponent === null ? 0 : dirtyCodeFileCount(s.codeFiles);
|
|
36
|
+
if (count === 0) {
|
|
37
|
+
then();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
void confirm({
|
|
41
|
+
title: "Unsaved changes",
|
|
42
|
+
message: discardBuffersMessage(count),
|
|
43
|
+
confirmLabel: "Discard",
|
|
44
|
+
danger: true,
|
|
45
|
+
}).then((ok) => {
|
|
46
|
+
if (ok) then();
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
[confirm, store],
|
|
50
|
+
);
|
|
51
|
+
return { guard, dialog };
|
|
52
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// The parts of Code Studio's editor: the editor itself, the tab strip, the
|
|
2
|
+
// file strips above it and the typecheck under it.
|
|
3
|
+
//
|
|
4
|
+
// ONE BUFFER PER FILE, ONE 409 PATH. Every file edits the store buffers
|
|
5
|
+
// (`codeFiles`), saves through `saveCodeFile` and meets a write made elsewhere
|
|
6
|
+
// (a second tab, an agent through MCP, an editor outside the studio) through
|
|
7
|
+
// the same conflict strip.
|
|
8
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
9
|
+
import { useConfirm } from "@dailies-studio/ui";
|
|
10
|
+
import { CodeEditor } from "@dailies-studio/ui/code";
|
|
11
|
+
import * as api from "../../api.js";
|
|
12
|
+
import { useStore, useShotEditor } from "../../state/context.js";
|
|
13
|
+
import type { CodeFileState } from "../../state/types.js";
|
|
14
|
+
import { GeneratedStrip } from "./CodeDefaultsPanel.js";
|
|
15
|
+
import { agentWriting } from "../../state/agentActivity.js";
|
|
16
|
+
import { AgentDots, useAgentActivity } from "../agent/AgentDots.js";
|
|
17
|
+
|
|
18
|
+
/** The MCP agent writing `rel` now, or null (its dots go on the file's tab). */
|
|
19
|
+
export function useAgentWriter(rel: string | null): string | null {
|
|
20
|
+
const activity = useAgentActivity();
|
|
21
|
+
return rel === null ? null : (agentWriting(activity, rel, null)?.name ?? null);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Follow mode is on for `rel` and an agent is still writing it: the buffer
|
|
25
|
+
* is the agent's until the author takes over. */
|
|
26
|
+
export function useFollowing(rel: string | null): string | null {
|
|
27
|
+
const writer = useAgentWriter(rel);
|
|
28
|
+
const follow = useShotEditor((s) => s.codeFollow);
|
|
29
|
+
return rel !== null && follow === rel ? writer : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The dots an agent's file tab shows. */
|
|
33
|
+
export function TabWriter({ rel }: { rel: string }) {
|
|
34
|
+
const writer = useAgentWriter(rel);
|
|
35
|
+
return writer === null ? null : <AgentDots label={`${writer} is writing this file`} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* FOLLOW MODE'S STRIP: the agent is writing this file, so the buffer shows
|
|
40
|
+
* each write as it lands and takes no typing. "Take over" makes it the
|
|
41
|
+
* author's again; a later agent write is then an ordinary conflict.
|
|
42
|
+
*/
|
|
43
|
+
export function FollowStrip({ rel }: { rel: string }) {
|
|
44
|
+
const store = useStore();
|
|
45
|
+
const writer = useFollowing(rel);
|
|
46
|
+
if (writer === null) return null;
|
|
47
|
+
return (
|
|
48
|
+
<div className="code-studio-strip is-follow" role="status" data-testid="code-follow">
|
|
49
|
+
<AgentDots label={`${writer} is writing this file`} />
|
|
50
|
+
<span>{writer} is writing this file. It updates as each write lands.</span>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className="btn"
|
|
54
|
+
data-testid="code-take-over"
|
|
55
|
+
title="Stop following and edit the file yourself"
|
|
56
|
+
onClick={() => store.takeOverCodeFile()}
|
|
57
|
+
>
|
|
58
|
+
Take over
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** How long the buffer sits still before it is typechecked. Long enough that
|
|
65
|
+
* ordinary typing costs no round trips, short enough that a pause gets an
|
|
66
|
+
* answer: the motion chrome's drawer has run this number for as long as it
|
|
67
|
+
* has had a lint gutter. */
|
|
68
|
+
const DIAG_DEBOUNCE_MS = 700;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The typecheck, debounced, against the BUFFER rather than against disk,
|
|
72
|
+
* which is the whole point of `/api/diagnostics` taking content: the red
|
|
73
|
+
* gutter has to describe the code the author is looking at, not the code they
|
|
74
|
+
* last saved. Responses for text that has since changed are dropped.
|
|
75
|
+
*
|
|
76
|
+
* THE ACTIVE BUFFER ONLY. Typechecking every open tab on every keystroke
|
|
77
|
+
* would cost a round trip per tab for gutters nobody is looking at; a tab
|
|
78
|
+
* re-checks when it becomes active, because `rel` is a dependency here.
|
|
79
|
+
*/
|
|
80
|
+
export function useCodeDiagnostics(rel: string | null, content: string | null): void {
|
|
81
|
+
const store = useStore();
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (rel === null || content === null) return;
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
void api
|
|
86
|
+
.fileDiagnostics(rel, content)
|
|
87
|
+
.then((res) => {
|
|
88
|
+
// The buffer this describes, addressed by rel: the author can have
|
|
89
|
+
// switched tabs while this was in flight, and painting these into
|
|
90
|
+
// whatever is on screen now would mark the wrong file's lines.
|
|
91
|
+
const now = store.getState().codeFiles[rel];
|
|
92
|
+
if (now === undefined || now.content !== content) return;
|
|
93
|
+
store.setCodeDiagnostics(rel, res.diagnostics);
|
|
94
|
+
})
|
|
95
|
+
// Silent: a typecheck that will not answer is not a reason to interrupt
|
|
96
|
+
// someone who is typing, and the next keystroke asks again.
|
|
97
|
+
.catch(() => {});
|
|
98
|
+
}, DIAG_DEBOUNCE_MS);
|
|
99
|
+
return () => clearTimeout(timer);
|
|
100
|
+
}, [rel, content, store]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The file strips: what is true of the active buffer that the editor alone
|
|
105
|
+
* cannot say. Generated (writes refused), changed on disk (the 409, asked
|
|
106
|
+
* about), and a save that failed.
|
|
107
|
+
*/
|
|
108
|
+
export function CodeFileStrips({ file }: { file: CodeFileState | null }) {
|
|
109
|
+
if (file === null) return null;
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<FollowStrip rel={file.rel} />
|
|
113
|
+
{file.generated !== null ? (
|
|
114
|
+
<GeneratedStrip
|
|
115
|
+
docName={file.generated.docName}
|
|
116
|
+
className="code-studio-strip is-generated"
|
|
117
|
+
testId="code-generated"
|
|
118
|
+
/>
|
|
119
|
+
) : null}
|
|
120
|
+
{file.status === "conflict" ? <ConflictStrip rel={file.rel} /> : null}
|
|
121
|
+
{file.status === "error" ? (
|
|
122
|
+
<div className="code-studio-strip is-error" role="alert" data-testid="code-error">
|
|
123
|
+
{file.error}
|
|
124
|
+
</div>
|
|
125
|
+
) : null}
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The editor.
|
|
132
|
+
*
|
|
133
|
+
* SPLIT OUT so the CodeMirror host does not re-mount on every unrelated state
|
|
134
|
+
* change in the surface above it, and so the Ctrl+S wall has one owner. The
|
|
135
|
+
* keystroke is stopped here rather than filtered in the keymap: the chrome's
|
|
136
|
+
* own ⌘S saves the DOCUMENT, and a save inside a text editor means the text.
|
|
137
|
+
*/
|
|
138
|
+
export function CodePane({ rel }: { rel: string }) {
|
|
139
|
+
const store = useStore();
|
|
140
|
+
// BOTH READ FROM THE SAME `rel`, so `path` and `content` always describe the
|
|
141
|
+
// same buffer in the same commit. `CodeEditor` builds a fresh state from
|
|
142
|
+
// whatever content it holds when a new path arrives, so a path that led its
|
|
143
|
+
// text by one render would seed the new tab with the old tab's file and then
|
|
144
|
+
// replace the whole document as one undoable edit.
|
|
145
|
+
const content = useShotEditor((s) => s.codeFiles[rel]?.content ?? "");
|
|
146
|
+
const diagnostics = useShotEditor((s) => s.codeFiles[rel]?.diagnostics);
|
|
147
|
+
const openPaths = useShotEditor((s) => s.codeOrder);
|
|
148
|
+
// THE VIEW REFUSES THE EDIT, not just the Save button. The strip above says
|
|
149
|
+
// this file's writes are refused; letting it accept a page of typing and then
|
|
150
|
+
// fail would tell the author that only afterwards. A viewer's Project
|
|
151
|
+
// refuses every file the same way.
|
|
152
|
+
const generatedOrViewer = useShotEditor((s) => s.readOnly || (s.codeFiles[rel]?.generated ?? null) !== null);
|
|
153
|
+
// FOLLOWING AN AGENT: its writes land in this buffer, so typing here would
|
|
154
|
+
// be thrown away by the next one. Take over (the strip) gives it back.
|
|
155
|
+
const following = useFollowing(rel);
|
|
156
|
+
const readOnly = generatedOrViewer || following !== null;
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
className="code-studio-pane"
|
|
160
|
+
onKeyDown={(e) => {
|
|
161
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
e.stopPropagation();
|
|
164
|
+
void store.saveCodeFile();
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<CodeEditor
|
|
169
|
+
path={rel}
|
|
170
|
+
content={content}
|
|
171
|
+
{...(diagnostics === undefined ? {} : { diagnostics })}
|
|
172
|
+
// THE WHOLE TAB SET, which is what makes the widget's per-path cache
|
|
173
|
+
// keep an undo history per tab and drop the ones the author closed.
|
|
174
|
+
openPaths={openPaths}
|
|
175
|
+
readOnly={readOnly}
|
|
176
|
+
onChange={(text) => store.updateCodeBuffer(text)}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The open buffers.
|
|
184
|
+
*
|
|
185
|
+
* ORDERED BY `codeOrder` AND NOT BY THE MAP, because an object's key order is a
|
|
186
|
+
* language detail and a tab strip's order is a promise: tabs that reshuffle when
|
|
187
|
+
* a buffer is replaced make the author re-find the one they were reading.
|
|
188
|
+
*
|
|
189
|
+
* Hidden at one tab, which is the state the studio opens in. A tab strip
|
|
190
|
+
* offering to close the only thing on screen is a control with nothing to do,
|
|
191
|
+
* and it costs a row of the editor's height to say so.
|
|
192
|
+
*/
|
|
193
|
+
export function CodeTabs() {
|
|
194
|
+
const store = useStore();
|
|
195
|
+
const order = useShotEditor((s) => s.codeOrder);
|
|
196
|
+
const active = useShotEditor((s) => s.codeActive);
|
|
197
|
+
const { confirm, dialog } = useConfirm();
|
|
198
|
+
|
|
199
|
+
// CLOSING A DIRTY BUFFER IS THE OTHER PLACE THAT CAN LOSE WORK, so it is the
|
|
200
|
+
// other place that asks. `closeCodeFile` deletes the buffer outright and the
|
|
201
|
+
// text was never on disk: there is no undo behind it and no strip left to
|
|
202
|
+
// show it, which is exactly the case the whole file already refuses on save
|
|
203
|
+
// and on adopt. A clean buffer closes without a word: nothing is at stake.
|
|
204
|
+
const close = useCallback(
|
|
205
|
+
(rel: string): void => {
|
|
206
|
+
const buffer = store.getState().codeFiles[rel];
|
|
207
|
+
if (buffer === undefined) return;
|
|
208
|
+
if (buffer.content === buffer.savedContent) {
|
|
209
|
+
store.closeCodeFile(rel);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
void confirm({
|
|
213
|
+
title: "Unsaved changes",
|
|
214
|
+
message: `${rel} has edits that have not been written to disk. Close it anyway?`,
|
|
215
|
+
confirmLabel: "Discard",
|
|
216
|
+
danger: true,
|
|
217
|
+
}).then((ok) => {
|
|
218
|
+
if (ok) store.closeCodeFile(rel);
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
[confirm, store],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (order.length < 2) return null;
|
|
225
|
+
return (
|
|
226
|
+
<div className="code-tabs" role="tablist" aria-label="Open files" data-testid="code-tabs">
|
|
227
|
+
{dialog}
|
|
228
|
+
{order.map((rel) => (
|
|
229
|
+
<CodeTab key={rel} rel={rel} active={rel === active} store={store} onClose={close} />
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function CodeTab({
|
|
236
|
+
rel,
|
|
237
|
+
active,
|
|
238
|
+
store,
|
|
239
|
+
onClose,
|
|
240
|
+
}: {
|
|
241
|
+
rel: string;
|
|
242
|
+
active: boolean;
|
|
243
|
+
store: ReturnType<typeof useStore>;
|
|
244
|
+
onClose: (rel: string) => void;
|
|
245
|
+
}) {
|
|
246
|
+
const buffer = useShotEditor((s) => s.codeFiles[rel]);
|
|
247
|
+
const dirty = buffer !== undefined && buffer.content !== buffer.savedContent;
|
|
248
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
249
|
+
// THE STRIP HAS NO SCROLLBAR (code.css), so the tab that becomes active (a
|
|
250
|
+
// tree click, a new file) brings itself into view, close button and all.
|
|
251
|
+
// jsdom implements no `scrollIntoView`, hence the guard.
|
|
252
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
const el = ref.current;
|
|
255
|
+
if (active && el !== null && typeof el.scrollIntoView === "function") {
|
|
256
|
+
el.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
257
|
+
}
|
|
258
|
+
}, [active]);
|
|
259
|
+
return (
|
|
260
|
+
<div ref={ref} className="code-tab" data-active={active ? "" : undefined}>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
className="code-tab-name"
|
|
264
|
+
role="tab"
|
|
265
|
+
aria-selected={active}
|
|
266
|
+
title={rel}
|
|
267
|
+
data-testid={`code-tab-${rel}`}
|
|
268
|
+
onClick={() => store.activateCodeFile(rel)}
|
|
269
|
+
>
|
|
270
|
+
{name}
|
|
271
|
+
<TabWriter rel={rel} />
|
|
272
|
+
{dirty ? <span className="code-tab-dirty" title="Unsaved changes" /> : null}
|
|
273
|
+
</button>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
className="code-tab-close"
|
|
277
|
+
aria-label={`Close ${name}`}
|
|
278
|
+
title={`Close ${name}`}
|
|
279
|
+
data-testid={`code-tab-close-${rel}`}
|
|
280
|
+
onClick={() => onClose(rel)}
|
|
281
|
+
>
|
|
282
|
+
×
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The file changed on disk under the buffer.
|
|
290
|
+
*
|
|
291
|
+
* BOTH TEXTS ARE REAL WORK, which is why this is a question and not a recovery:
|
|
292
|
+
* the author's buffer is theirs, and whatever landed on disk was landed by
|
|
293
|
+
* something (a second tab, an agent through MCP, an editor outside the studio).
|
|
294
|
+
* Choosing for them loses one of the two.
|
|
295
|
+
*/
|
|
296
|
+
export function ConflictStrip({ rel }: { rel: string }) {
|
|
297
|
+
const store = useStore();
|
|
298
|
+
return (
|
|
299
|
+
<div className="code-studio-strip is-conflict" role="alert" data-testid="code-conflict">
|
|
300
|
+
<span>This file changed on disk since you opened it.</span>
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
className="btn"
|
|
304
|
+
data-testid="code-reload"
|
|
305
|
+
title="Throw away this buffer and take what is on disk"
|
|
306
|
+
onClick={() => void store.reloadCodeFile(rel)}
|
|
307
|
+
>
|
|
308
|
+
Reload
|
|
309
|
+
</button>
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
className="btn"
|
|
313
|
+
data-testid="code-overwrite"
|
|
314
|
+
title="Write this buffer over what is on disk"
|
|
315
|
+
onClick={() => void store.saveCodeFile(true, rel)}
|
|
316
|
+
>
|
|
317
|
+
Overwrite
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// THE COMPOSER (Paper page 20): a new pin's first message, or a reply.
|
|
2
|
+
//
|
|
3
|
+
// ONE DRAFT PER THREAD. The text lives in the comments store under the thread
|
|
4
|
+
// id (or `NEW_THREAD`), so closing a thread and coming back keeps what was
|
|
5
|
+
// typed. Ctrl or Cmd+Enter posts; plain Enter is a new line.
|
|
6
|
+
//
|
|
7
|
+
// THE TEXTAREA IS THE COMBOBOX. Typing `@` opens the mention menu under it;
|
|
8
|
+
// the arrows walk it, Enter or Tab pick, Escape closes it. Escape with the
|
|
9
|
+
// menu shut cancels: the draft pin, or the thread. Every key the composer
|
|
10
|
+
// answers stops here, so the board's keymap (Escape's own ladder) never sees
|
|
11
|
+
// it twice.
|
|
12
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
13
|
+
import type { KeyboardEvent, ReactNode } from "react";
|
|
14
|
+
import type { Mentionable } from "../../api.js";
|
|
15
|
+
import { useStore } from "../../state/context.js";
|
|
16
|
+
import {
|
|
17
|
+
COMMENT_BODY_MAX,
|
|
18
|
+
encodeMentions,
|
|
19
|
+
insertMention,
|
|
20
|
+
mentionQuery,
|
|
21
|
+
personLabel,
|
|
22
|
+
} from "../../state/comments.js";
|
|
23
|
+
import { MentionMenu, mentionOptions, optionId } from "./MentionMenu.js";
|
|
24
|
+
import type { MentionOption } from "./MentionMenu.js";
|
|
25
|
+
import { useComments } from "./parts.js";
|
|
26
|
+
|
|
27
|
+
/** The counter shows once this close to the limit. */
|
|
28
|
+
const COUNTER_FROM = COMMENT_BODY_MAX - 200;
|
|
29
|
+
|
|
30
|
+
export function CommentComposer(props: {
|
|
31
|
+
/** A thread id, or `NEW_THREAD` for the draft pin. */
|
|
32
|
+
threadKey: string;
|
|
33
|
+
variant: "new" | "reply";
|
|
34
|
+
onCancel: () => void;
|
|
35
|
+
/** The Frame as the author sees it, kept with a new thread. */
|
|
36
|
+
picture?: () => Promise<Blob | null>;
|
|
37
|
+
/** The new pin's "where", above the field. */
|
|
38
|
+
head?: ReactNode;
|
|
39
|
+
}) {
|
|
40
|
+
const store = useStore();
|
|
41
|
+
const { comments } = store;
|
|
42
|
+
const text = useComments().texts[props.threadKey] ?? "";
|
|
43
|
+
const field = useRef<HTMLTextAreaElement>(null);
|
|
44
|
+
const menuId = useId();
|
|
45
|
+
const [caret, setCaret] = useState(0);
|
|
46
|
+
const [picked, setPicked] = useState<{ id: string; label: string }[]>([]);
|
|
47
|
+
const [result, setResult] = useState<Mentionable | null>(null);
|
|
48
|
+
const [active, setActive] = useState(0);
|
|
49
|
+
/** The `@` the author dismissed with Escape: the menu stays shut for it. */
|
|
50
|
+
const [dismissed, setDismissed] = useState<number | null>(null);
|
|
51
|
+
const [refusal, setRefusal] = useState<string | null>(null);
|
|
52
|
+
const [posting, setPosting] = useState(false);
|
|
53
|
+
|
|
54
|
+
const query = mentionQuery(text, caret);
|
|
55
|
+
const menuOpen = query !== null && query.start !== dismissed && result !== null;
|
|
56
|
+
const options = result === null ? [] : mentionOptions(result);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
field.current?.focus();
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
// The menu's people, asked again per query. The last answer wins.
|
|
63
|
+
const q = query === null || query.start === dismissed ? null : query.query;
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (q === null) {
|
|
66
|
+
setResult(null);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let live = true;
|
|
70
|
+
comments.mentionable(q).then(
|
|
71
|
+
(res) => {
|
|
72
|
+
if (!live) return;
|
|
73
|
+
setResult(res);
|
|
74
|
+
setActive(0);
|
|
75
|
+
},
|
|
76
|
+
() => {
|
|
77
|
+
if (live) setResult(null);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
return () => {
|
|
81
|
+
live = false;
|
|
82
|
+
};
|
|
83
|
+
}, [comments, q]);
|
|
84
|
+
|
|
85
|
+
const write = (next: string, at: number): void => {
|
|
86
|
+
comments.setText(props.threadKey, next);
|
|
87
|
+
setCaret(at);
|
|
88
|
+
setRefusal(null);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const pick = (option: MentionOption): void => {
|
|
92
|
+
if (query === null) return;
|
|
93
|
+
const label = personLabel(option.person);
|
|
94
|
+
const next = insertMention(text, query.start, caret, label);
|
|
95
|
+
write(next.text, next.caret);
|
|
96
|
+
setPicked((list) => [...list.filter((p) => p.id !== option.person.id), { id: option.person.id, label }]);
|
|
97
|
+
// SHARE AND MENTION: the Project's access dialog, with this person
|
|
98
|
+
// picked as a Viewer, in the dashboard (granting access lives there). The
|
|
99
|
+
// chip goes in now; the post names them once the share is made.
|
|
100
|
+
if (option.share) {
|
|
101
|
+
const name = store.getState().name;
|
|
102
|
+
if (name !== null) {
|
|
103
|
+
window.open(`/projects/?share=${encodeURIComponent(`shot/${name}`)}&with=${encodeURIComponent(option.person.id)}`, "_blank");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
requestAnimationFrame(() => field.current?.setSelectionRange(next.caret, next.caret));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const post = async (): Promise<void> => {
|
|
110
|
+
if (posting) return;
|
|
111
|
+
setPosting(true);
|
|
112
|
+
const said = await comments.post(props.threadKey, encodeMentions(text, picked), props.picture);
|
|
113
|
+
setPosting(false);
|
|
114
|
+
setRefusal(said);
|
|
115
|
+
if (said === null) setPicked([]);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
|
|
119
|
+
if (menuOpen && options.length > 0 && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
const step = e.key === "ArrowDown" ? 1 : -1;
|
|
122
|
+
setActive((i) => (i + step + options.length) % options.length);
|
|
123
|
+
} else if (menuOpen && options.length > 0 && (e.key === "Enter" || e.key === "Tab") && !e.ctrlKey && !e.metaKey) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
pick(options[Math.min(active, options.length - 1)]!);
|
|
126
|
+
} else if (e.key === "Escape") {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
if (menuOpen) setDismissed(query.start);
|
|
129
|
+
else props.onCancel();
|
|
130
|
+
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
void post();
|
|
133
|
+
} else {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const over = text.trim().length > COMMENT_BODY_MAX;
|
|
140
|
+
const empty = text.trim() === "";
|
|
141
|
+
const counter =
|
|
142
|
+
text.length >= COUNTER_FROM ? (
|
|
143
|
+
<span className={`comment-counter${over ? " is-over" : ""}`} data-testid="comment-counter">
|
|
144
|
+
{text.length}/{COMMENT_BODY_MAX}
|
|
145
|
+
</span>
|
|
146
|
+
) : null;
|
|
147
|
+
const atButton = (
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
className="comment-at"
|
|
151
|
+
aria-label="Mention someone"
|
|
152
|
+
title="Mention someone"
|
|
153
|
+
onPointerDown={(e) => e.preventDefault()}
|
|
154
|
+
onClick={() => {
|
|
155
|
+
const el = field.current;
|
|
156
|
+
const at = el?.selectionStart ?? text.length;
|
|
157
|
+
const lead = at > 0 && !/\s/.test(text[at - 1]!) ? " @" : "@";
|
|
158
|
+
write(text.slice(0, at) + lead + text.slice(at), at + lead.length);
|
|
159
|
+
setDismissed(null);
|
|
160
|
+
requestAnimationFrame(() => {
|
|
161
|
+
el?.focus();
|
|
162
|
+
el?.setSelectionRange(at + lead.length, at + lead.length);
|
|
163
|
+
});
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
@
|
|
167
|
+
</button>
|
|
168
|
+
);
|
|
169
|
+
const postButton = (
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
className="comment-post"
|
|
173
|
+
data-testid="comment-post"
|
|
174
|
+
disabled={empty || over || posting}
|
|
175
|
+
onClick={() => void post()}
|
|
176
|
+
>
|
|
177
|
+
Post
|
|
178
|
+
</button>
|
|
179
|
+
);
|
|
180
|
+
const textarea = (
|
|
181
|
+
<textarea
|
|
182
|
+
ref={field}
|
|
183
|
+
className="comment-field"
|
|
184
|
+
data-testid="comment-field"
|
|
185
|
+
rows={props.variant === "new" ? 2 : 1}
|
|
186
|
+
value={text}
|
|
187
|
+
placeholder={props.variant === "new" ? "Add a comment, @ to mention" : "Reply, @ to mention"}
|
|
188
|
+
aria-label={props.variant === "new" ? "Comment" : "Reply"}
|
|
189
|
+
role="combobox"
|
|
190
|
+
aria-autocomplete="list"
|
|
191
|
+
aria-expanded={menuOpen}
|
|
192
|
+
aria-controls={menuId}
|
|
193
|
+
{...(menuOpen && options.length > 0 ? { "aria-activedescendant": optionId(menuId, active) } : {})}
|
|
194
|
+
onChange={(e) => write(e.target.value, e.target.selectionStart)}
|
|
195
|
+
onSelect={(e) => setCaret(e.currentTarget.selectionStart)}
|
|
196
|
+
onKeyDown={onKeyDown}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
const menu = menuOpen ? (
|
|
200
|
+
<MentionMenu id={menuId} result={result} active={active} onPick={pick} onActive={setActive} />
|
|
201
|
+
) : null;
|
|
202
|
+
const error =
|
|
203
|
+
refusal === null ? null : (
|
|
204
|
+
<p className="comment-refusal" role="alert">
|
|
205
|
+
{refusal}
|
|
206
|
+
</p>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (props.variant === "reply") {
|
|
210
|
+
return (
|
|
211
|
+
<div className="comment-reply" data-testid="comment-composer">
|
|
212
|
+
{menu}
|
|
213
|
+
{error}
|
|
214
|
+
<div className="comment-reply-row">
|
|
215
|
+
{textarea}
|
|
216
|
+
{counter}
|
|
217
|
+
{atButton}
|
|
218
|
+
{postButton}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return (
|
|
224
|
+
<div className="comment-composer" data-testid="comment-composer">
|
|
225
|
+
{props.head}
|
|
226
|
+
<div className="comment-composer-field">{textarea}</div>
|
|
227
|
+
{error}
|
|
228
|
+
<div className="comment-composer-foot">
|
|
229
|
+
<div className="comment-composer-hints">
|
|
230
|
+
{atButton}
|
|
231
|
+
<span className="comment-hint">Ctrl Enter to post · Esc to cancel</span>
|
|
232
|
+
{counter}
|
|
233
|
+
</div>
|
|
234
|
+
{postButton}
|
|
235
|
+
</div>
|
|
236
|
+
{menu}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|