@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,762 @@
|
|
|
1
|
+
// THE FONT PICKER (plans/FONT-PICKER-PLAN.md "The picker", Paper page "28 ·
|
|
2
|
+
// Font picker"): a lazy chunk (app/lazySurfaces.tsx) the Font field mounts
|
|
3
|
+
// only while it is open. This module owns its Popover, so the search input it
|
|
4
|
+
// focuses once the popover is placed exists in the same commit.
|
|
5
|
+
//
|
|
6
|
+
// - FOCUS STAYS IN THE SEARCH. It is the combobox: the arrows, Page Up/Down
|
|
7
|
+
// and Home/End move ONE active option (`aria-activedescendant`), as the
|
|
8
|
+
// pointer does, and the active option previews on the stage
|
|
9
|
+
// (`store.previewFont`, which debounces). Enter or a click applies it
|
|
10
|
+
// (`store.applyFont`), records it in Recent and closes; with nothing
|
|
11
|
+
// matching, Enter opens the upload view. Alt+Left and Alt+Right step the
|
|
12
|
+
// category chip. Escape restores and closes, Tab closes with nothing
|
|
13
|
+
// chosen. A press on anything in either view that is not a control keeps
|
|
14
|
+
// focus where it is: on <body>, the editor's keymap would take the next
|
|
15
|
+
// key (Backspace deletes the selected layer).
|
|
16
|
+
// - NO KEY LEAVES THE PICKER. The frame stops every keydown inside it, so the
|
|
17
|
+
// window keymap never reads a key pressed on the upload view's buttons (a
|
|
18
|
+
// focused button owns no Backspace, arrow or tool letter there). The
|
|
19
|
+
// Popover's Escape runs first, on the window's capture phase.
|
|
20
|
+
// - ITS HEIGHT IS FIXED AT THE ROOM IT HAS: the larger side of the field,
|
|
21
|
+
// at most min(560px, 70vh). A box sized by its content shrank as a search
|
|
22
|
+
// narrowed, so the popover re-placed it and its top (the search) moved
|
|
23
|
+
// while the author typed; one taller than either side ran off the window.
|
|
24
|
+
// - EVERY CLOSE GOES THROUGH `dismiss`, which clears the preview and tells the
|
|
25
|
+
// field where focus goes. Calling `previewFont(null)` right after an apply
|
|
26
|
+
// is safe: the store holds a preview through the apply's download.
|
|
27
|
+
// - ONE FLAT, VIRTUAL LIST (pickerModel.ts), windowed at any length, since a
|
|
28
|
+
// row on screen loads its face (the rows mounted past each edge ask for
|
|
29
|
+
// none, so they never queue ahead of the ones the author sees): section
|
|
30
|
+
// heads are items of the row height,
|
|
31
|
+
// the active option is always mounted (pinned) and scrolled to by its index
|
|
32
|
+
// times the row height. Each option says its place in the whole list
|
|
33
|
+
// (`aria-posinset` of `aria-setsize`), which the mounted ones alone do not.
|
|
34
|
+
// - THE STAGE IS ANOTHER DOCUMENT: a press there only closes the picker and
|
|
35
|
+
// Escape there closes it (`useDismiss`, its frame half), and nothing closes
|
|
36
|
+
// it on a window blur, which the OS file dialog causes. Either hands focus
|
|
37
|
+
// to that frame, where the pointer is, so the next arrow nudges the text and
|
|
38
|
+
// Space plays, as on any press on the stage.
|
|
39
|
+
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
40
|
+
import type { DragEvent, KeyboardEvent, PointerEvent, ReactNode, RefObject } from "react";
|
|
41
|
+
import { isGenericFontFamily } from "@dailies-studio/shot/doc";
|
|
42
|
+
import { IconCheck, IconChevron, IconSearch, IconUpload, Popover, SegmentedControl } from "@dailies-studio/ui";
|
|
43
|
+
import { PICKER_CATEGORIES } from "../../fonts/googleFonts.js";
|
|
44
|
+
import type { GoogleFontCategory } from "../../fonts/googleFonts.js";
|
|
45
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
46
|
+
import { fold, readFontRecents, recordFontRecent } from "../../state/fontRecents.js";
|
|
47
|
+
import { useDismiss } from "../toolbar/useDismiss.js";
|
|
48
|
+
import { pinnedWindowSpans, useFixedVirtualWindow } from "../virtualWindow.js";
|
|
49
|
+
import { faceStack, usePreviewFace } from "./faceCache.js";
|
|
50
|
+
import { pickerItems } from "./pickerModel.js";
|
|
51
|
+
import type { FontRow, FontTag, PickerItem } from "./pickerModel.js";
|
|
52
|
+
import { MANAGE_FONTS_HREF } from "./pickerRequest.js";
|
|
53
|
+
import type { FontPickerClose, FontPickerView } from "./pickerRequest.js";
|
|
54
|
+
import "./fontPicker.css";
|
|
55
|
+
|
|
56
|
+
/** Every item's height, heads included: what the window and the scroll
|
|
57
|
+
* arithmetic read. The sheet takes it from `--font-picker-row-h`. */
|
|
58
|
+
export const FONT_PICKER_ROW_H = 32;
|
|
59
|
+
/** Page Up/Down's step where the list has no measured height (jsdom). */
|
|
60
|
+
const PAGE_FALLBACK = 8;
|
|
61
|
+
/** The tallest the picker stands, and its share of the window. */
|
|
62
|
+
const PICKER_MAX_H = 560;
|
|
63
|
+
const PICKER_MAX_SHARE = 0.7;
|
|
64
|
+
/** Kept clear between the picker and the window's edge: the popover's 4px
|
|
65
|
+
* gap and 8 more. */
|
|
66
|
+
const PICKER_EDGE = 12;
|
|
67
|
+
/** The least it stands: the search, the chips, a few rows and the footer. */
|
|
68
|
+
const PICKER_MIN_H = 200;
|
|
69
|
+
|
|
70
|
+
/** The picker's height beside `anchor`: the larger side's room, capped. */
|
|
71
|
+
function pickerHeight(anchor: DOMRect, windowHeight: number): number {
|
|
72
|
+
const room = Math.max(windowHeight - anchor.bottom, anchor.top) - PICKER_EDGE;
|
|
73
|
+
return Math.floor(Math.max(PICKER_MIN_H, Math.min(PICKER_MAX_H, windowHeight * PICKER_MAX_SHARE, room)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FontPickerProps {
|
|
77
|
+
/** The field box the popover hangs from. */
|
|
78
|
+
anchor: RefObject<HTMLElement | null>;
|
|
79
|
+
view: FontPickerView;
|
|
80
|
+
onView: (view: FontPickerView) => void;
|
|
81
|
+
/** The family the selected text draws, or null when it is Mixed. */
|
|
82
|
+
current: string | null;
|
|
83
|
+
/** Some selected text names `current` with no file (`drawsMachineFont`),
|
|
84
|
+
* so picking the Project's own copy of it still writes. */
|
|
85
|
+
currentMachine: boolean;
|
|
86
|
+
onClose: (how: FontPickerClose) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function FontPicker(props: FontPickerProps) {
|
|
90
|
+
const store = useStore();
|
|
91
|
+
const search = useRef<HTMLInputElement>(null);
|
|
92
|
+
const choose = useRef<HTMLButtonElement>(null);
|
|
93
|
+
|
|
94
|
+
const dismiss = (how: FontPickerClose): void => {
|
|
95
|
+
store.previewFont(null);
|
|
96
|
+
props.onClose(how);
|
|
97
|
+
};
|
|
98
|
+
// Only the frame half, so every call is a press or Escape in a stage frame:
|
|
99
|
+
// focus goes to that frame. With no frame element to name, back to the field.
|
|
100
|
+
useDismiss(
|
|
101
|
+
true,
|
|
102
|
+
props.anchor,
|
|
103
|
+
(frame?: Window) => {
|
|
104
|
+
const into = frame?.frameElement;
|
|
105
|
+
if (!(into instanceof HTMLElement)) return dismiss("refocus");
|
|
106
|
+
dismiss("stay");
|
|
107
|
+
into.focus();
|
|
108
|
+
},
|
|
109
|
+
{ window: false, blur: false },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const focusView = (): void => (props.view === "list" ? search.current : choose.current)?.focus();
|
|
113
|
+
// Opening goes through `onPlaced` (a hidden popover takes no focus); a
|
|
114
|
+
// switch between the list and the upload view lands here.
|
|
115
|
+
useEffect(focusView, [props.view]);
|
|
116
|
+
|
|
117
|
+
// Measured before the popover first places it (the anchor is the mounted
|
|
118
|
+
// field), then again when the window or a scroller moves the field.
|
|
119
|
+
const room = (): number | null => {
|
|
120
|
+
const box = props.anchor.current?.getBoundingClientRect();
|
|
121
|
+
return box === undefined ? null : pickerHeight(box, window.innerHeight);
|
|
122
|
+
};
|
|
123
|
+
const [height, setHeight] = useState(room);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const measure = (): void => setHeight(room());
|
|
126
|
+
window.addEventListener("resize", measure);
|
|
127
|
+
window.addEventListener("scroll", measure, true);
|
|
128
|
+
return () => {
|
|
129
|
+
window.removeEventListener("resize", measure);
|
|
130
|
+
window.removeEventListener("scroll", measure, true);
|
|
131
|
+
};
|
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Popover
|
|
137
|
+
open
|
|
138
|
+
anchor={props.anchor}
|
|
139
|
+
align="right"
|
|
140
|
+
className="font-picker"
|
|
141
|
+
onPlaced={focusView}
|
|
142
|
+
onClose={(reason) => dismiss(reason === "escape" ? "refocus" : "stay")}
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
className="font-picker-frame"
|
|
146
|
+
{...(height === null ? {} : { style: { height } })}
|
|
147
|
+
// Focus stays where it is (the search, or the upload view's control)
|
|
148
|
+
// through a press on a row, a head, a note, a line, the key hints or
|
|
149
|
+
// the ground: on <body>, the editor's keymap would take the next key.
|
|
150
|
+
// A control (the search itself, a chip, a button, the link) takes the
|
|
151
|
+
// press as it would.
|
|
152
|
+
onPointerDown={(e) => {
|
|
153
|
+
if ((e.target as Element).closest("input, button, a") === null) e.preventDefault();
|
|
154
|
+
}}
|
|
155
|
+
// No key reaches the window keymap: stopped at the portal's root, so
|
|
156
|
+
// Enter, Space and Tab keep their default actions.
|
|
157
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
158
|
+
>
|
|
159
|
+
{props.view === "list" ? (
|
|
160
|
+
<FontList {...props} search={search} dismiss={dismiss} />
|
|
161
|
+
) : (
|
|
162
|
+
<UploadView choose={choose} onBack={() => props.onView("list")} onDone={() => dismiss("refocus")} />
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</Popover>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── THE LIST ──────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
function FontList(props: FontPickerProps & { search: RefObject<HTMLInputElement | null>; dismiss: (how: FontPickerClose) => void }) {
|
|
172
|
+
const store = useStore();
|
|
173
|
+
const doc = useShotEditor((s) => s.doc);
|
|
174
|
+
const catalog = useShotEditor((s) => s.fontCatalog);
|
|
175
|
+
const workspace = useShotEditor((s) => s.workspaceFonts);
|
|
176
|
+
const [catalogFailed, setCatalogFailed] = useState(false);
|
|
177
|
+
const [workspaceFailed, setWorkspaceFailed] = useState(false);
|
|
178
|
+
const [recents] = useState(readFontRecents);
|
|
179
|
+
const [query, setQuery] = useState("");
|
|
180
|
+
const [category, setCategory] = useState<GoogleFontCategory | null>(null);
|
|
181
|
+
// The option the arrows or the pointer moved to; null is the default one:
|
|
182
|
+
// the current family on an open, else the top result.
|
|
183
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
184
|
+
// The preview waits for the author's first move: the open alone shows the
|
|
185
|
+
// stage as it is.
|
|
186
|
+
const touched = useRef(false);
|
|
187
|
+
// The next active change scrolls it into view (the arrows, a search, the
|
|
188
|
+
// open); a hover does not, since the row is under the pointer.
|
|
189
|
+
const reveal = useRef(true);
|
|
190
|
+
const lastPointer = useRef<{ x: number; y: number } | null>(null);
|
|
191
|
+
const scroller = useRef<HTMLDivElement>(null);
|
|
192
|
+
const list = useRef<HTMLUListElement>(null);
|
|
193
|
+
const baseId = useId();
|
|
194
|
+
const listId = `${baseId}-list`;
|
|
195
|
+
const optionId = (index: number): string => `${baseId}-option-${index}`;
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
let live = true;
|
|
199
|
+
void store.loadFontCatalog().then((fonts) => {
|
|
200
|
+
if (live && fonts === null) setCatalogFailed(true);
|
|
201
|
+
});
|
|
202
|
+
// Read again on every open, so a font added in another tab shows.
|
|
203
|
+
void store.loadWorkspaceFonts(true).then((list) => {
|
|
204
|
+
if (live && list === null) setWorkspaceFailed(true);
|
|
205
|
+
});
|
|
206
|
+
return () => {
|
|
207
|
+
live = false;
|
|
208
|
+
// Leaving the list for the upload view takes its preview with it.
|
|
209
|
+
store.previewFont(null);
|
|
210
|
+
};
|
|
211
|
+
}, [store]);
|
|
212
|
+
|
|
213
|
+
const currentKey = props.current === null ? null : fold(props.current);
|
|
214
|
+
const items = useMemo(
|
|
215
|
+
() =>
|
|
216
|
+
doc === null
|
|
217
|
+
? []
|
|
218
|
+
: pickerItems({ doc, catalog, catalogFailed, workspace: workspace?.fonts ?? null, workspaceFailed, recents, query, category, current: currentKey }),
|
|
219
|
+
[doc, catalog, catalogFailed, workspace, workspaceFailed, recents, query, category, currentKey],
|
|
220
|
+
);
|
|
221
|
+
const fontIndexes = useMemo(() => items.flatMap((item, at) => (item.type === "font" ? [at] : [])), [items]);
|
|
222
|
+
// Each option's place among the options, from 1, by its item index.
|
|
223
|
+
const positions = useMemo(() => new Map(fontIndexes.map((at, n) => [at, n + 1])), [fontIndexes]);
|
|
224
|
+
const activeIndex = useMemo(() => {
|
|
225
|
+
if (activeId !== null) {
|
|
226
|
+
const at = items.findIndex((item) => item.id === activeId);
|
|
227
|
+
if (at >= 0) return at;
|
|
228
|
+
}
|
|
229
|
+
const current = query === "" && currentKey !== null
|
|
230
|
+
? items.findIndex((item) => item.type === "font" && item.key === currentKey)
|
|
231
|
+
: -1;
|
|
232
|
+
return current >= 0 ? current : (fontIndexes[0] ?? -1);
|
|
233
|
+
}, [activeId, items, fontIndexes, query, currentKey]);
|
|
234
|
+
const active = items[activeIndex];
|
|
235
|
+
const activeRow = active?.type === "font" ? active : null;
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (!touched.current) return;
|
|
239
|
+
// The current family previews as the stage already draws it, but for the
|
|
240
|
+
// Project's own file of it while some text draws it from the computer:
|
|
241
|
+
// that file is the change a pick makes.
|
|
242
|
+
const asDrawn = activeRow?.key === currentKey && (activeRow.machine || !props.currentMachine);
|
|
243
|
+
store.previewFont(activeRow === null || asDrawn ? null : activeRow.pick);
|
|
244
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
245
|
+
}, [activeRow?.id]);
|
|
246
|
+
|
|
247
|
+
useLayoutEffect(() => {
|
|
248
|
+
const box = scroller.current;
|
|
249
|
+
if (!reveal.current || box === null || activeIndex < 0) return;
|
|
250
|
+
reveal.current = false;
|
|
251
|
+
// A section's first row brings its head with it.
|
|
252
|
+
const top = (items[activeIndex - 1]?.type === "head" ? activeIndex - 1 : activeIndex) * FONT_PICKER_ROW_H;
|
|
253
|
+
const bottom = (activeIndex + 1) * FONT_PICKER_ROW_H;
|
|
254
|
+
if (top < box.scrollTop) box.scrollTop = top;
|
|
255
|
+
else if (bottom > box.scrollTop + box.clientHeight) box.scrollTop = bottom - box.clientHeight;
|
|
256
|
+
}, [activeIndex, items]);
|
|
257
|
+
|
|
258
|
+
const range = useFixedVirtualWindow({
|
|
259
|
+
listRef: list,
|
|
260
|
+
scrollSelector: ".font-picker-scroll",
|
|
261
|
+
count: items.length,
|
|
262
|
+
rowHeight: FONT_PICKER_ROW_H,
|
|
263
|
+
enabled: true,
|
|
264
|
+
});
|
|
265
|
+
// The rows on screen, the window less its overscan: only they ask for a
|
|
266
|
+
// face (faceCache.ts), so a stop after a fast scroll loads what it shows
|
|
267
|
+
// first and fetches nothing for the rows mounted past either edge.
|
|
268
|
+
const onScreen = useFixedVirtualWindow({
|
|
269
|
+
listRef: list,
|
|
270
|
+
scrollSelector: ".font-picker-scroll",
|
|
271
|
+
count: items.length,
|
|
272
|
+
rowHeight: FONT_PICKER_ROW_H,
|
|
273
|
+
enabled: true,
|
|
274
|
+
overscanRows: 0,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const moveTo = (index: number | undefined): void => {
|
|
278
|
+
const item = index === undefined ? undefined : items[index];
|
|
279
|
+
if (item?.type !== "font") return;
|
|
280
|
+
touched.current = true;
|
|
281
|
+
reveal.current = true;
|
|
282
|
+
setActiveId(item.id);
|
|
283
|
+
};
|
|
284
|
+
const step = (delta: number): void => {
|
|
285
|
+
const at = fontIndexes.indexOf(activeIndex);
|
|
286
|
+
moveTo(fontIndexes[at < 0 ? 0 : Math.max(0, Math.min(fontIndexes.length - 1, at + delta))]);
|
|
287
|
+
};
|
|
288
|
+
const page = (): number => {
|
|
289
|
+
const height = scroller.current?.clientHeight ?? 0;
|
|
290
|
+
return height > 0 ? Math.max(1, Math.floor(height / FONT_PICKER_ROW_H) - 1) : PAGE_FALLBACK;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const commit = (row: FontRow): void => {
|
|
294
|
+
// A generic keyword (sans-serif) is no family a file could be: nothing to
|
|
295
|
+
// apply or upload.
|
|
296
|
+
if (row.machine && row.pick === null && isGenericFontFamily(row.family)) {
|
|
297
|
+
props.dismiss("refocus");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// A machine font the catalog does not have comes in as a file.
|
|
301
|
+
if (row.machine && row.pick === null) {
|
|
302
|
+
props.onView("upload");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (row.machine && row.key === currentKey) {
|
|
306
|
+
// The selection draws this machine font: give it the catalog's file.
|
|
307
|
+
void store.selfHostFont();
|
|
308
|
+
} else if (row.pick !== null && (row.key !== currentKey || props.currentMachine)) {
|
|
309
|
+
// Another family, or the Project's own copy of the current one for the
|
|
310
|
+
// text that draws it from the computer (no fetch: the files are held).
|
|
311
|
+
void store.applyFont(row.pick);
|
|
312
|
+
}
|
|
313
|
+
if (row.recent !== null) recordFontRecent(row.recent);
|
|
314
|
+
props.dismiss("refocus");
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const pickCategory = (next: GoogleFontCategory | null): void => {
|
|
318
|
+
setCategory(next);
|
|
319
|
+
setActiveId(null);
|
|
320
|
+
touched.current = true;
|
|
321
|
+
reveal.current = true;
|
|
322
|
+
};
|
|
323
|
+
// The chips in turn, as the segmented control's own arrows go: round.
|
|
324
|
+
const stepCategory = (delta: number): void => {
|
|
325
|
+
const n = PICKER_CATEGORIES.length;
|
|
326
|
+
const at = PICKER_CATEGORIES.findIndex((c) => c.category === category);
|
|
327
|
+
pickCategory(PICKER_CATEGORIES[(((at + delta) % n) + n) % n]!.category);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const nothing = query.trim() !== "" && fontIndexes.length === 0 && (catalog !== null || catalogFailed);
|
|
331
|
+
|
|
332
|
+
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
|
|
333
|
+
if (e.nativeEvent.isComposing) return;
|
|
334
|
+
if (e.altKey && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
335
|
+
stepCategory(e.key === "ArrowRight" ? 1 : -1);
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// The keyboard's way to the upload view while fonts match: focus stays in
|
|
340
|
+
// the search and Tab closes, so the footer's button needs a key. By the
|
|
341
|
+
// key's place, not its character: Alt+U types a dead key on a Mac.
|
|
342
|
+
if (e.altKey && !e.ctrlKey && !e.metaKey && e.code === "KeyU") {
|
|
343
|
+
props.onView("upload");
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
switch (e.key) {
|
|
348
|
+
case "ArrowDown":
|
|
349
|
+
step(1);
|
|
350
|
+
break;
|
|
351
|
+
case "ArrowUp":
|
|
352
|
+
step(-1);
|
|
353
|
+
break;
|
|
354
|
+
case "PageDown":
|
|
355
|
+
step(page());
|
|
356
|
+
break;
|
|
357
|
+
case "PageUp":
|
|
358
|
+
step(-page());
|
|
359
|
+
break;
|
|
360
|
+
case "Home":
|
|
361
|
+
moveTo(fontIndexes[0]);
|
|
362
|
+
break;
|
|
363
|
+
case "End":
|
|
364
|
+
moveTo(fontIndexes[fontIndexes.length - 1]);
|
|
365
|
+
break;
|
|
366
|
+
case "Enter":
|
|
367
|
+
// Nothing matches: the one way on the empty state offers.
|
|
368
|
+
if (activeRow !== null) commit(activeRow);
|
|
369
|
+
else if (nothing) props.onView("upload");
|
|
370
|
+
break;
|
|
371
|
+
case "Tab":
|
|
372
|
+
// Not prevented: the field takes focus first, and the browser's Tab
|
|
373
|
+
// moves on from there (the popover is portaled to the end of <body>).
|
|
374
|
+
props.dismiss("tab");
|
|
375
|
+
return;
|
|
376
|
+
default:
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const hover = (index: number, e: PointerEvent): void => {
|
|
383
|
+
// A row scrolled under a still pointer is not a move: the arrows' row
|
|
384
|
+
// keeps the preview.
|
|
385
|
+
const at = { x: e.clientX, y: e.clientY };
|
|
386
|
+
const was = lastPointer.current;
|
|
387
|
+
lastPointer.current = at;
|
|
388
|
+
if ((was !== null && was.x === at.x && was.y === at.y) || index === activeIndex) return;
|
|
389
|
+
touched.current = true;
|
|
390
|
+
setActiveId(items[index]!.id);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const renderItem = (item: PickerItem, index: number): ReactNode => {
|
|
394
|
+
if (item.type === "head") {
|
|
395
|
+
return (
|
|
396
|
+
<li key={item.id} role="presentation" className="caps font-picker-head">
|
|
397
|
+
{item.label}
|
|
398
|
+
{item.count !== undefined && <span className="font-picker-num">{item.count.toLocaleString("en-US")}</span>}
|
|
399
|
+
</li>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (item.type === "note") {
|
|
403
|
+
return (
|
|
404
|
+
<li key={item.id} role="presentation" className="font-picker-note" aria-busy={item.busy}>
|
|
405
|
+
{item.busy && <span className="font-picker-spinner" aria-hidden="true" />}
|
|
406
|
+
{item.text}
|
|
407
|
+
</li>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return (
|
|
411
|
+
<FontOption
|
|
412
|
+
key={item.id}
|
|
413
|
+
id={optionId(index)}
|
|
414
|
+
row={item}
|
|
415
|
+
position={positions.get(index)!}
|
|
416
|
+
size={fontIndexes.length}
|
|
417
|
+
active={index === activeIndex}
|
|
418
|
+
onScreen={index >= onScreen.start && index < onScreen.end}
|
|
419
|
+
current={item.key === currentKey}
|
|
420
|
+
onHover={(e) => hover(index, e)}
|
|
421
|
+
onPick={() => commit(item)}
|
|
422
|
+
/>
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const rows: ReactNode[] = [];
|
|
427
|
+
let cursor = 0;
|
|
428
|
+
const gap = (from: number, to: number): void => {
|
|
429
|
+
if (to > from) {
|
|
430
|
+
rows.push(
|
|
431
|
+
<li
|
|
432
|
+
key={`gap-${from}-${to}`}
|
|
433
|
+
role="presentation"
|
|
434
|
+
aria-hidden="true"
|
|
435
|
+
className="font-picker-gap"
|
|
436
|
+
style={{ height: (to - from) * FONT_PICKER_ROW_H }}
|
|
437
|
+
/>,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
for (const span of pinnedWindowSpans(range, items.length, activeIndex >= 0 ? [activeIndex] : [])) {
|
|
442
|
+
gap(cursor, span.start);
|
|
443
|
+
for (let at = span.start; at < span.end; at++) rows.push(renderItem(items[at]!, at));
|
|
444
|
+
cursor = span.end;
|
|
445
|
+
}
|
|
446
|
+
gap(cursor, items.length);
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div className="font-picker-body" style={{ ["--font-picker-row-h" as string]: `${FONT_PICKER_ROW_H}px` }}>
|
|
450
|
+
<div className="font-picker-top">
|
|
451
|
+
<label className="search-field font-picker-search">
|
|
452
|
+
<IconSearch aria-hidden="true" />
|
|
453
|
+
<input
|
|
454
|
+
ref={props.search}
|
|
455
|
+
type="text"
|
|
456
|
+
role="combobox"
|
|
457
|
+
aria-label="Search fonts"
|
|
458
|
+
aria-expanded="true"
|
|
459
|
+
aria-controls={listId}
|
|
460
|
+
aria-autocomplete="list"
|
|
461
|
+
aria-keyshortcuts="Alt+ArrowLeft Alt+ArrowRight Alt+U"
|
|
462
|
+
{...(activeRow !== null ? { "aria-activedescendant": optionId(activeIndex) } : {})}
|
|
463
|
+
autoComplete="off"
|
|
464
|
+
spellCheck={false}
|
|
465
|
+
placeholder="Search fonts"
|
|
466
|
+
value={query}
|
|
467
|
+
onChange={(e) => {
|
|
468
|
+
setQuery(e.target.value);
|
|
469
|
+
setActiveId(null);
|
|
470
|
+
touched.current = true;
|
|
471
|
+
reveal.current = true;
|
|
472
|
+
}}
|
|
473
|
+
onKeyDown={onKeyDown}
|
|
474
|
+
/>
|
|
475
|
+
</label>
|
|
476
|
+
{/* A press on a chip leaves focus in the search. */}
|
|
477
|
+
<div className="font-picker-chips" onPointerDown={(e) => e.preventDefault()}>
|
|
478
|
+
<SegmentedControl
|
|
479
|
+
className="is-chips"
|
|
480
|
+
ariaLabel="Font category"
|
|
481
|
+
value={category ?? "all"}
|
|
482
|
+
options={PICKER_CATEGORIES.map((c) => ({ value: c.category ?? "all", label: c.label }))}
|
|
483
|
+
onChange={(next) => pickCategory(next === "all" ? null : (next as GoogleFontCategory))}
|
|
484
|
+
/>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
<div className="font-picker-scroll" ref={scroller}>
|
|
488
|
+
<ul id={listId} ref={list} role="listbox" aria-label="Fonts" className="font-picker-list">
|
|
489
|
+
{rows}
|
|
490
|
+
</ul>
|
|
491
|
+
{nothing && (
|
|
492
|
+
<div className="empty font-picker-empty">
|
|
493
|
+
<p className="empty-line">
|
|
494
|
+
<IconSearch aria-hidden="true" />
|
|
495
|
+
No fonts match “{query.trim()}”
|
|
496
|
+
</p>
|
|
497
|
+
<button type="button" className="btn" onClick={() => props.onView("upload")}>
|
|
498
|
+
Upload font file
|
|
499
|
+
</button>
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
</div>
|
|
503
|
+
<div className="font-picker-foot">
|
|
504
|
+
<span className="font-picker-keys">
|
|
505
|
+
<kbd className="kbd">↑</kbd>
|
|
506
|
+
<kbd className="kbd">↓</kbd> Move
|
|
507
|
+
<kbd className="kbd">↵</kbd> Apply
|
|
508
|
+
<kbd className="kbd">Esc</kbd> Close
|
|
509
|
+
</span>
|
|
510
|
+
<button
|
|
511
|
+
type="button"
|
|
512
|
+
className="btn is-ghost"
|
|
513
|
+
title="Upload font (Alt+U)"
|
|
514
|
+
aria-keyshortcuts="Alt+U"
|
|
515
|
+
onClick={() => props.onView("upload")}
|
|
516
|
+
>
|
|
517
|
+
<IconUpload aria-hidden="true" />
|
|
518
|
+
Upload font
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const tagWords = (tag: FontTag): string =>
|
|
526
|
+
tag.kind === "variable"
|
|
527
|
+
? "variable"
|
|
528
|
+
: tag.kind === "machine"
|
|
529
|
+
? "machine font"
|
|
530
|
+
: `${tag.count} ${tag.unit}${tag.count === 1 ? "" : "s"}`;
|
|
531
|
+
|
|
532
|
+
function FontOption(props: {
|
|
533
|
+
id: string;
|
|
534
|
+
row: FontRow;
|
|
535
|
+
/** Its place among all the options, from 1, and how many there are. */
|
|
536
|
+
position: number;
|
|
537
|
+
size: number;
|
|
538
|
+
active: boolean;
|
|
539
|
+
/** Inside the viewport, not the overscan: only then does it ask for its
|
|
540
|
+
* face. Off screen it draws in Inter, which nobody sees. */
|
|
541
|
+
onScreen: boolean;
|
|
542
|
+
current: boolean;
|
|
543
|
+
onHover: (e: PointerEvent) => void;
|
|
544
|
+
onPick: () => void;
|
|
545
|
+
}) {
|
|
546
|
+
const { row } = props;
|
|
547
|
+
const status = usePreviewFace(props.onScreen ? row.face : null);
|
|
548
|
+
const loading = status === "loading";
|
|
549
|
+
const tag = row.tag;
|
|
550
|
+
return (
|
|
551
|
+
<li
|
|
552
|
+
id={props.id}
|
|
553
|
+
role="option"
|
|
554
|
+
aria-selected={props.active}
|
|
555
|
+
aria-posinset={props.position}
|
|
556
|
+
aria-setsize={props.size}
|
|
557
|
+
aria-busy={loading}
|
|
558
|
+
aria-label={[row.family, props.current ? "current font" : null, tagWords(tag)].filter(Boolean).join(", ")}
|
|
559
|
+
className={`font-picker-row${props.active ? " is-active" : ""}${loading ? " is-loading" : ""}`}
|
|
560
|
+
onPointerMove={props.onHover}
|
|
561
|
+
onClick={props.onPick}
|
|
562
|
+
>
|
|
563
|
+
<span className="font-picker-check" aria-hidden="true">
|
|
564
|
+
{props.current && <IconCheck />}
|
|
565
|
+
</span>
|
|
566
|
+
<span
|
|
567
|
+
className="font-picker-name"
|
|
568
|
+
{...(status === "ready" && row.face !== null ? { style: { fontFamily: faceStack(row.face) } } : {})}
|
|
569
|
+
>
|
|
570
|
+
{row.family}
|
|
571
|
+
</span>
|
|
572
|
+
{loading && <span className="font-picker-spinner" aria-hidden="true" />}
|
|
573
|
+
{tag.kind === "variable" ? (
|
|
574
|
+
<span className="badge">Variable</span>
|
|
575
|
+
) : tag.kind === "machine" ? (
|
|
576
|
+
<span className="font-picker-machine">Machine font</span>
|
|
577
|
+
) : (
|
|
578
|
+
<span className="font-picker-count">
|
|
579
|
+
<span className="font-picker-num">{tag.count}</span> {tag.unit}
|
|
580
|
+
{tag.count === 1 ? "" : "s"}
|
|
581
|
+
</span>
|
|
582
|
+
)}
|
|
583
|
+
</li>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── THE UPLOAD VIEW ───────────────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
const ACCEPT = ".ttf,.otf,.woff,.woff2";
|
|
590
|
+
/** A byte limit in decimal megabytes, as the server words it: 10, or 2.5. */
|
|
591
|
+
const megabytes = (bytes: number): number => Number((bytes / 1_000_000).toFixed(1));
|
|
592
|
+
|
|
593
|
+
function UploadView(props: { choose: RefObject<HTMLButtonElement | null>; onBack: () => void; onDone: () => void }) {
|
|
594
|
+
const store = useStore();
|
|
595
|
+
const list = useShotEditor((s) => s.workspaceFonts);
|
|
596
|
+
const [over, setOver] = useState(false);
|
|
597
|
+
const [busy, setBusy] = useState(false);
|
|
598
|
+
const [refusal, setRefusal] = useState<string | null>(null);
|
|
599
|
+
// The list could not be read and none is held: said, with a way to retry.
|
|
600
|
+
const [readFailed, setReadFailed] = useState(false);
|
|
601
|
+
const input = useRef<HTMLInputElement>(null);
|
|
602
|
+
const live = useRef(true);
|
|
603
|
+
const reasonId = useId();
|
|
604
|
+
|
|
605
|
+
// The count and the limit as the server has them now.
|
|
606
|
+
const read = (): void => {
|
|
607
|
+
setReadFailed(false);
|
|
608
|
+
void store.loadWorkspaceFonts(true).then((got) => {
|
|
609
|
+
if (live.current && got === null) setReadFailed(true);
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
live.current = true;
|
|
614
|
+
read();
|
|
615
|
+
return () => {
|
|
616
|
+
live.current = false;
|
|
617
|
+
};
|
|
618
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
619
|
+
}, [store]);
|
|
620
|
+
|
|
621
|
+
const reason =
|
|
622
|
+
list === null
|
|
623
|
+
? null
|
|
624
|
+
: !list.canManage
|
|
625
|
+
? "Your role can't add workspace fonts."
|
|
626
|
+
: list.fonts.length >= list.maxFonts
|
|
627
|
+
? `This workspace has ${list.fonts.length} of ${list.maxFonts} font files. Remove one in Manage workspace fonts.`
|
|
628
|
+
: null;
|
|
629
|
+
const blocked = reason !== null || busy;
|
|
630
|
+
|
|
631
|
+
const upload = async (files: readonly File[]): Promise<void> => {
|
|
632
|
+
if (blocked || files.length === 0) return;
|
|
633
|
+
setBusy(true);
|
|
634
|
+
setRefusal(null);
|
|
635
|
+
const { outcome } = await store.uploadWorkspaceFonts(files);
|
|
636
|
+
if (!live.current) return;
|
|
637
|
+
setBusy(false);
|
|
638
|
+
// The store applied the family: the picker's work is done.
|
|
639
|
+
if (outcome.status === "applied") props.onDone();
|
|
640
|
+
else if (outcome.status !== "superseded") setRefusal(outcome.message ?? "The font file did not upload.");
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const carriesFiles = (e: DragEvent): boolean => [...e.dataTransfer.types].includes("Files");
|
|
644
|
+
const onDragOver = (e: DragEvent): void => {
|
|
645
|
+
if (!carriesFiles(e)) return;
|
|
646
|
+
e.preventDefault();
|
|
647
|
+
e.dataTransfer.dropEffect = blocked ? "none" : "copy";
|
|
648
|
+
if (!blocked) setOver(true);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// The dialog pattern: Tab and Shift+Tab cycle this view's own controls.
|
|
652
|
+
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>): void => {
|
|
653
|
+
if (e.key !== "Tab") return;
|
|
654
|
+
const all = [...e.currentTarget.querySelectorAll<HTMLElement>("button, a[href]")];
|
|
655
|
+
const first = all[0];
|
|
656
|
+
const last = all[all.length - 1];
|
|
657
|
+
if (first === undefined || last === undefined) return;
|
|
658
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
659
|
+
e.preventDefault();
|
|
660
|
+
last.focus();
|
|
661
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
662
|
+
e.preventDefault();
|
|
663
|
+
first.focus();
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
return (
|
|
668
|
+
<div className="font-picker-body font-picker-upload" onKeyDown={onKeyDown}>
|
|
669
|
+
<div className="font-picker-upload-head">
|
|
670
|
+
<button type="button" className="btn is-ghost font-picker-back" onClick={props.onBack}>
|
|
671
|
+
<IconChevron aria-hidden="true" />
|
|
672
|
+
Back
|
|
673
|
+
</button>
|
|
674
|
+
<span className="font-picker-title">Upload font</span>
|
|
675
|
+
</div>
|
|
676
|
+
<div
|
|
677
|
+
className={`drop font-picker-drop${over ? " is-over" : ""}${blocked ? " is-disabled" : ""}`}
|
|
678
|
+
aria-disabled={blocked}
|
|
679
|
+
aria-busy={busy}
|
|
680
|
+
{...(reason !== null ? { "aria-describedby": reasonId } : {})}
|
|
681
|
+
onDragEnter={onDragOver}
|
|
682
|
+
onDragOver={onDragOver}
|
|
683
|
+
onDragLeave={(e) => {
|
|
684
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setOver(false);
|
|
685
|
+
}}
|
|
686
|
+
onDrop={(e) => {
|
|
687
|
+
if (!carriesFiles(e)) return;
|
|
688
|
+
e.preventDefault();
|
|
689
|
+
setOver(false);
|
|
690
|
+
void upload([...e.dataTransfer.files]);
|
|
691
|
+
}}
|
|
692
|
+
>
|
|
693
|
+
<span>{busy ? "Uploading…" : "Drop font files here"}</span>
|
|
694
|
+
<button
|
|
695
|
+
ref={props.choose}
|
|
696
|
+
type="button"
|
|
697
|
+
className="btn"
|
|
698
|
+
aria-disabled={blocked}
|
|
699
|
+
{...(reason !== null ? { "aria-describedby": reasonId } : {})}
|
|
700
|
+
onClick={() => {
|
|
701
|
+
if (!blocked) input.current?.click();
|
|
702
|
+
}}
|
|
703
|
+
>
|
|
704
|
+
Choose file
|
|
705
|
+
</button>
|
|
706
|
+
<span className="font-picker-limits">
|
|
707
|
+
ttf · otf · woff · woff2
|
|
708
|
+
{list !== null && (
|
|
709
|
+
<>
|
|
710
|
+
{" "}· up to <span className="font-picker-num">{megabytes(list.limitBytes)}</span> MB
|
|
711
|
+
</>
|
|
712
|
+
)}
|
|
713
|
+
</span>
|
|
714
|
+
<input
|
|
715
|
+
ref={input}
|
|
716
|
+
type="file"
|
|
717
|
+
hidden
|
|
718
|
+
accept={ACCEPT}
|
|
719
|
+
multiple
|
|
720
|
+
onChange={(e) => {
|
|
721
|
+
const files = [...(e.target.files ?? [])];
|
|
722
|
+
// Cleared, so choosing the same file again still uploads.
|
|
723
|
+
e.target.value = "";
|
|
724
|
+
void upload(files);
|
|
725
|
+
}}
|
|
726
|
+
/>
|
|
727
|
+
</div>
|
|
728
|
+
{list === null && readFailed ? (
|
|
729
|
+
<>
|
|
730
|
+
<p className="font-picker-line is-warn" role="alert">
|
|
731
|
+
Workspace fonts did not load.
|
|
732
|
+
</p>
|
|
733
|
+
<button type="button" className="btn font-picker-retry" onClick={read}>
|
|
734
|
+
Try again
|
|
735
|
+
</button>
|
|
736
|
+
</>
|
|
737
|
+
) : list === null ? (
|
|
738
|
+
<p className="font-picker-line is-quiet" aria-busy="true">
|
|
739
|
+
Reading workspace fonts…
|
|
740
|
+
</p>
|
|
741
|
+
) : (
|
|
742
|
+
<p className="font-picker-line">
|
|
743
|
+
<span className="font-picker-num">{list.fonts.length}</span> of <span className="font-picker-num">{list.maxFonts}</span> font files
|
|
744
|
+
</p>
|
|
745
|
+
)}
|
|
746
|
+
{reason !== null && (
|
|
747
|
+
<p id={reasonId} className="font-picker-line">
|
|
748
|
+
{reason}
|
|
749
|
+
</p>
|
|
750
|
+
)}
|
|
751
|
+
{refusal !== null && (
|
|
752
|
+
<p className="font-picker-line is-warn" role="alert">
|
|
753
|
+
{refusal}
|
|
754
|
+
</p>
|
|
755
|
+
)}
|
|
756
|
+
<p className="font-picker-line is-quiet">Only upload fonts you have a license to use.</p>
|
|
757
|
+
<a className="font-picker-manage" href={MANAGE_FONTS_HREF} target="_blank" rel="noopener">
|
|
758
|
+
Manage workspace fonts ↗
|
|
759
|
+
</a>
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
}
|