@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
|
@@ -1,37 +1,197 @@
|
|
|
1
1
|
// Only for TEXT layers, and absent otherwise — a typography section over a
|
|
2
|
-
// rectangle is four controls that write nothing.
|
|
2
|
+
// rectangle is four controls that write nothing. That gate is `layout.ts`'s
|
|
3
|
+
// table now, not this file's: both sections here used to re-decide it, and a
|
|
4
|
+
// second opinion in the component is the duplication the table removes. The
|
|
5
|
+
// `layer.kind !== "text"` guards on the WRITE helpers below stay, because a
|
|
6
|
+
// mixed selection reaches them by the union rule and a rect must be left alone.
|
|
3
7
|
//
|
|
4
8
|
// `verticalAlign` is offered here and is HONOURED, unlike in the 2D lane where
|
|
5
9
|
// it is declared-and-inert: this lane owns the text box, so top/middle/bottom
|
|
6
10
|
// is one offset in the rasterizer rather than a change to a layout model.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
//
|
|
12
|
+
// EVERY READ IS A SCALAR, one per row. This file used to read the whole
|
|
13
|
+
// `TextStyle` object once and take every field off it, and `useSelectionValue`
|
|
14
|
+
// compares with `Object.is`: the object was mixed by identity for every
|
|
15
|
+
// multi-selection, so the rows showed the FIRST layer's values as if the
|
|
16
|
+
// selection agreed on them. One read per row is also what lets each widget
|
|
17
|
+
// carry its own "Mixed".
|
|
18
|
+
//
|
|
19
|
+
// THE FONT ROWS read what the family can draw (`selectionFontFacts`, over the
|
|
20
|
+
// lazily loaded catalog and workspace list) and write only through the
|
|
21
|
+
// store's font verbs, which fetch the face first and write once it lands.
|
|
22
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
23
|
+
import type { Ref } from "react";
|
|
24
|
+
import {
|
|
25
|
+
ColorField,
|
|
26
|
+
Combobox,
|
|
27
|
+
IconItalic,
|
|
28
|
+
InspectorRow,
|
|
29
|
+
InspectorSection,
|
|
30
|
+
NumberField,
|
|
31
|
+
SegmentedControl,
|
|
32
|
+
TextField,
|
|
33
|
+
useLocked,
|
|
34
|
+
} from "@dailies-studio/ui";
|
|
35
|
+
import { ExposeDot } from "../ExposeDot.js";
|
|
36
|
+
import { KeyframeDot } from "../KeyframeDot.js";
|
|
37
|
+
import type { ScrubHandlers } from "@dailies-studio/ui";
|
|
38
|
+
import {
|
|
39
|
+
fixedTextBox,
|
|
40
|
+
glyphEngineRefusals,
|
|
41
|
+
isGenericFontFamily,
|
|
42
|
+
primaryFontFamily,
|
|
43
|
+
textStyleForRole,
|
|
44
|
+
} from "@dailies-studio/shot/doc";
|
|
45
|
+
import type { TextEngine, TextRole, TextStyle } from "@dailies-studio/shot/doc";
|
|
46
|
+
import type { Layer3D, Paint, TextLayer3D } from "@dailies-studio/shot/doc";
|
|
47
|
+
import { loadGoogleFontRanges } from "../../../fonts/googleFonts.js";
|
|
48
|
+
import { useShotEditor, useStore } from "../../../state/context.js";
|
|
49
|
+
import { selectionFontFacts } from "../../../state/fonts.js";
|
|
50
|
+
import type { LayerFontFacts } from "../../../state/fonts.js";
|
|
51
|
+
import { requestFontPicker } from "../../fonts/pickerRequest.js";
|
|
52
|
+
import { useSelectedLayers, useSelectionScrub, useSelectionValue, useSelectionWrite } from "../useDocWrite.js";
|
|
53
|
+
import { FontField } from "./FontField.js";
|
|
54
|
+
import { PaintRows, type PaintEdit } from "./PaintRows.js";
|
|
55
|
+
|
|
56
|
+
/** Shown when the selection disagrees, in the two widgets that have no `mixed`
|
|
57
|
+
* prop of their own. `SegmentedControl` renders an unmatched value as "none of
|
|
58
|
+
* these selected", which is the same statement in its own vocabulary. */
|
|
59
|
+
const MIXED_PLACEHOLDER = "Mixed";
|
|
60
|
+
const NO_SEGMENT = "";
|
|
61
|
+
const TEXT_ROLE_OPTIONS: { value: TextRole; label: string; title: string }[] = [
|
|
62
|
+
{ value: "display", label: "Disp", title: "Display" },
|
|
63
|
+
{ value: "displayMedium", label: "Mid", title: "Display medium" },
|
|
64
|
+
{ value: "body", label: "Body", title: "Body" },
|
|
65
|
+
{ value: "emphasis", label: "Emph", title: "Emphasis" },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** None / Underline / Strike. `undefined` is the stored absence, and the
|
|
69
|
+
* control needs a value it can compare, so absence reads as the empty string
|
|
70
|
+
* and writing it writes `undefined` back. Both engines draw all three.
|
|
71
|
+
*
|
|
72
|
+
* A ONE-LETTER segment says its word to a screen reader and to the pointer
|
|
73
|
+
* (`ariaLabel`, `title`): "U" announced as "U" names nothing. */
|
|
74
|
+
const DECORATION_OPTIONS = [
|
|
75
|
+
{ value: "", label: "None" },
|
|
76
|
+
{ value: "underline", label: "U", ariaLabel: "Underline", title: "Underline" },
|
|
77
|
+
{ value: "line-through", label: "S", ariaLabel: "Strikethrough", title: "Strikethrough" },
|
|
78
|
+
];
|
|
79
|
+
const VERTICAL_OPTIONS = [
|
|
80
|
+
{ value: "top", label: "T", ariaLabel: "Top", title: "Top" },
|
|
81
|
+
{ value: "middle", label: "M", ariaLabel: "Middle", title: "Middle" },
|
|
82
|
+
{ value: "bottom", label: "B", ariaLabel: "Bottom", title: "Bottom" },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
type StyleNumber = "sizePx" | "lineHeight" | "letterSpacingPx";
|
|
86
|
+
|
|
87
|
+
/** What the selected text's family can draw (state/fonts.ts). */
|
|
88
|
+
type SelectionFontFacts = LayerFontFacts | { kind: "mixed" } | null;
|
|
89
|
+
|
|
90
|
+
const WEIGHT_NAMES: Readonly<Record<number, string>> = {
|
|
91
|
+
100: "Thin",
|
|
92
|
+
200: "Extra Light",
|
|
93
|
+
300: "Light",
|
|
94
|
+
400: "Regular",
|
|
95
|
+
500: "Medium",
|
|
96
|
+
600: "Semi Bold",
|
|
97
|
+
700: "Bold",
|
|
98
|
+
800: "Extra Bold",
|
|
99
|
+
900: "Black",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** "Semi Bold 600". A weight with no name (450) reads as its number. */
|
|
103
|
+
function weightLabel(weight: number): string {
|
|
104
|
+
const name = WEIGHT_NAMES[weight];
|
|
105
|
+
return name === undefined ? String(weight) : `${name} ${weight}`;
|
|
106
|
+
}
|
|
12
107
|
|
|
13
108
|
export function TypographySection() {
|
|
109
|
+
const store = useStore();
|
|
14
110
|
const write = useSelectionWrite();
|
|
111
|
+
// kind gate removed: see layout.ts
|
|
112
|
+
const content = useSelectionValue((layer) => (layer.kind === "text" ? layer.text.content : undefined));
|
|
113
|
+
// The font rows read the TEXT layers alone, as the font verbs write them:
|
|
114
|
+
// a rect beside a text layer has no family to disagree with.
|
|
15
115
|
const layers = useSelectedLayers();
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
116
|
+
const texts = useMemo(() => layers.filter((layer): layer is TextLayer3D => layer.kind === "text"), [layers]);
|
|
117
|
+
const assets = useShotEditor((s) => s.doc?.assets);
|
|
118
|
+
const catalog = useShotEditor((s) => s.fontCatalog);
|
|
119
|
+
const workspace = useShotEditor((s) => s.workspaceFonts?.fonts ?? null);
|
|
120
|
+
const facts = useMemo(
|
|
121
|
+
() => selectionFontFacts(texts, assets ?? {}, catalog, workspace),
|
|
122
|
+
[texts, assets, catalog, workspace],
|
|
123
|
+
);
|
|
124
|
+
// The primary family, so `Inter` and `"Inter", sans-serif` are one family.
|
|
125
|
+
const families = texts.map((layer) => primaryFontFamily(layer.text.style.fontFamily));
|
|
126
|
+
const family = families[0] ?? "";
|
|
127
|
+
// LAZY, and only for text: the catalog while a layer waits on it (a
|
|
128
|
+
// catalog copy, or a machine font that Self-host may name), the workspace
|
|
129
|
+
// list once (a workspace copy reads as a plain file until it lands). A load
|
|
130
|
+
// that fails stays failed until the next mount: the effect runs on these
|
|
131
|
+
// flags, not on every render.
|
|
132
|
+
const needsCatalog = facts?.kind === "loading";
|
|
133
|
+
const needsWorkspace = texts.length > 0 && workspace === null;
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (needsCatalog) void store.loadFontCatalog();
|
|
136
|
+
}, [store, needsCatalog]);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (needsWorkspace) void store.loadWorkspaceFonts();
|
|
139
|
+
}, [store, needsWorkspace]);
|
|
140
|
+
const align = useSelectionValue((layer) => readStyle(layer)?.align);
|
|
141
|
+
// Absent MEANS "top" (types.ts), so the default is applied inside the read
|
|
142
|
+
// and a layer that stores it explicitly agrees with one that does not. The
|
|
143
|
+
// undefined that survives is the one that says "not a text layer".
|
|
144
|
+
const verticalAlign = useSelectionValue((layer) => {
|
|
145
|
+
const style = readStyle(layer);
|
|
146
|
+
return style === undefined ? undefined : (style.verticalAlign ?? "top");
|
|
147
|
+
});
|
|
148
|
+
const decoration = useSelectionValue((layer) => {
|
|
149
|
+
const style = readStyle(layer);
|
|
150
|
+
return style === undefined ? undefined : (style.decoration ?? "");
|
|
151
|
+
});
|
|
152
|
+
// Absent MEANS canvas and always will (schema v30), so the default is
|
|
153
|
+
// applied in the read: a layer that stores "canvas" and one that stores
|
|
154
|
+
// nothing must show the same segment.
|
|
155
|
+
const engine = useSelectionValue((layer) =>
|
|
156
|
+
layer.kind === "text" ? (layer.text.engine ?? "canvas") : undefined,
|
|
157
|
+
);
|
|
158
|
+
// WHY the glyph segment is unavailable, in the validator's own words. Only
|
|
159
|
+
// the first is shown: a control has one line of room, and an author fixes
|
|
160
|
+
// these one at a time anyway.
|
|
161
|
+
const glyphBlocked = useSelectionValue((layer) =>
|
|
162
|
+
layer.kind === "text" ? (glyphEngineRefusals(layer)[0]?.message ?? "") : undefined,
|
|
163
|
+
);
|
|
164
|
+
// Absent MEANS fixed (schema v38), so the default is applied in the read.
|
|
165
|
+
const box = useSelectionValue((layer) => (layer.kind === "text" ? (layer.text.box ?? "fixed") : undefined));
|
|
166
|
+
const size = useStyleNumber("sizePx");
|
|
167
|
+
const lineHeight = useStyleNumber("lineHeight");
|
|
168
|
+
const letterSpacing = useStyleNumber("letterSpacingPx");
|
|
169
|
+
const role = useSelectionValue(textRoleOf);
|
|
170
|
+
|
|
171
|
+
const patchStyle = (next: Partial<TextStyle>): void => write((layer) => withStyle(layer, next));
|
|
27
172
|
|
|
28
|
-
const s = style.value;
|
|
29
173
|
return (
|
|
30
|
-
<InspectorSection title="Text" persistKey="shot:text">
|
|
31
|
-
|
|
174
|
+
<InspectorSection className="insp-object" title="Text" persistKey="shot:text">
|
|
175
|
+
{/* THE ONE HIGH-VALUE PATH WITH NO DIAMOND, so it mounts the pin itself.
|
|
176
|
+
`text.content` is not in `TRACKABLE_PATHS` — a string has no
|
|
177
|
+
in-between and the table refuses it by name — yet it is the field a
|
|
178
|
+
reusable component publishes first: one Title definition placed five
|
|
179
|
+
times with five headlines is the whole reason exposed props exist.
|
|
180
|
+
The kind is stated rather than looked up, because there is no table
|
|
181
|
+
row here to look it up from. A GROWING box, so the row aligns to its
|
|
182
|
+
start: the label stays level with the first line and the pin sits
|
|
183
|
+
at the box's top right, not centred on a paragraph. */}
|
|
184
|
+
<InspectorRow
|
|
185
|
+
label="Content"
|
|
186
|
+
align="start"
|
|
187
|
+
trailing={<ExposeDot path="text.content" label="Content" kind="string" />}
|
|
188
|
+
>
|
|
32
189
|
<TextField
|
|
33
|
-
value={content.value ?? ""}
|
|
190
|
+
value={content.mixed ? "" : (content.value ?? "")}
|
|
191
|
+
{...(content.mixed ? { placeholder: MIXED_PLACEHOLDER } : {})}
|
|
34
192
|
ariaLabel="Text content"
|
|
193
|
+
// A textarea keeps "\n": Enter breaks the line, blur commits.
|
|
194
|
+
multiline
|
|
35
195
|
onCommit={(next) =>
|
|
36
196
|
write((layer) =>
|
|
37
197
|
layer.kind === "text" ? ({ ...layer, text: { ...layer.text, content: next } } as Layer3D) : layer,
|
|
@@ -39,132 +199,541 @@ export function TypographySection() {
|
|
|
39
199
|
}
|
|
40
200
|
/>
|
|
41
201
|
</InspectorRow>
|
|
202
|
+
{/* No typed family any more: the picker's search is how a family is
|
|
203
|
+
named, and every pick goes through the store's one apply path. */}
|
|
42
204
|
<InspectorRow label="Font">
|
|
43
|
-
<
|
|
205
|
+
<FontField value={family} mixed={families.some((f) => f !== family)} />
|
|
44
206
|
</InspectorRow>
|
|
45
|
-
<
|
|
46
|
-
|
|
207
|
+
{facts?.kind === "machine" ? <MachineFontNote family={facts.family} selfHost={facts.selfHost?.family ?? null} /> : null}
|
|
208
|
+
{/* Role writes its weight with the rest of the role in one edit, and
|
|
209
|
+
the store's `followFontWeights` then swaps the files to match. */}
|
|
210
|
+
<InspectorRow label="Role">
|
|
211
|
+
<SegmentedControl
|
|
212
|
+
ariaLabel="Text role"
|
|
213
|
+
value={role.mixed ? NO_SEGMENT : (role.value ?? NO_SEGMENT)}
|
|
214
|
+
options={TEXT_ROLE_OPTIONS}
|
|
215
|
+
onChange={(next) => write((layer) => withRole(layer, next as TextRole))}
|
|
216
|
+
/>
|
|
47
217
|
</InspectorRow>
|
|
218
|
+
{/* "Size", not the table's "Text size": the section is already called Text,
|
|
219
|
+
and a tooltip that disagreed with the pill beside it reads as a second
|
|
220
|
+
property. */}
|
|
48
221
|
<InspectorRow>
|
|
49
|
-
<NumberField
|
|
222
|
+
<NumberField
|
|
223
|
+
value={size.value ?? 16}
|
|
224
|
+
mixed={size.mixed}
|
|
225
|
+
label="Size"
|
|
226
|
+
ariaLabel="Font size"
|
|
227
|
+
unit="px"
|
|
228
|
+
min={1}
|
|
229
|
+
// 1px is the smallest type that is type at all.
|
|
230
|
+
step={1}
|
|
231
|
+
trailing={<KeyframeDot path="text.style.sizePx" label="Size" />}
|
|
232
|
+
onCommit={size.commit}
|
|
233
|
+
onScrubStart={size.scrub.start}
|
|
234
|
+
onScrub={size.scrub.move}
|
|
235
|
+
onScrubEnd={size.scrub.end}
|
|
236
|
+
/>
|
|
50
237
|
</InspectorRow>
|
|
51
|
-
<
|
|
52
|
-
|
|
238
|
+
<WeightRow texts={texts} facts={facts} />
|
|
239
|
+
{/* Auto width grows with the text and never wraps; Fixed wraps inside
|
|
240
|
+
its width. Fixed keeps the measured size, so the switch moves
|
|
241
|
+
nothing on screen. */}
|
|
242
|
+
<InspectorRow label="Box">
|
|
243
|
+
<SegmentedControl
|
|
244
|
+
ariaLabel="Text box"
|
|
245
|
+
value={box.mixed ? NO_SEGMENT : (box.value ?? "fixed")}
|
|
246
|
+
options={[
|
|
247
|
+
{ value: "auto", label: "Auto width" },
|
|
248
|
+
{ value: "fixed", label: "Fixed" },
|
|
249
|
+
]}
|
|
250
|
+
onChange={(next) =>
|
|
251
|
+
write((layer) =>
|
|
252
|
+
next === "fixed"
|
|
253
|
+
? fixedTextBox(layer)
|
|
254
|
+
: layer.kind === "text"
|
|
255
|
+
? ({ ...layer, text: { ...layer.text, box: "auto" } } as Layer3D)
|
|
256
|
+
: layer,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
/>
|
|
53
260
|
</InspectorRow>
|
|
261
|
+
{/* No colour row here: the glyph colour is the Fill section's (F20),
|
|
262
|
+
which this layer always shows, and a second row was a second
|
|
263
|
+
diamond over the one `text.style.color` track. */}
|
|
54
264
|
<InspectorRow>
|
|
55
|
-
<NumberField
|
|
265
|
+
<NumberField
|
|
266
|
+
value={lineHeight.value ?? 1.2}
|
|
267
|
+
mixed={lineHeight.mixed}
|
|
268
|
+
label="Line"
|
|
269
|
+
ariaLabel="Line height"
|
|
270
|
+
title="Line height (multiplier)"
|
|
271
|
+
step={0.05}
|
|
272
|
+
min={0.5}
|
|
273
|
+
// Below 0.5 the lines overlap into mush.
|
|
274
|
+
trailing={<KeyframeDot path="text.style.lineHeight" />}
|
|
275
|
+
onCommit={lineHeight.commit}
|
|
276
|
+
onScrubStart={lineHeight.scrub.start}
|
|
277
|
+
onScrub={lineHeight.scrub.move}
|
|
278
|
+
onScrubEnd={lineHeight.scrub.end}
|
|
279
|
+
/>
|
|
56
280
|
</InspectorRow>
|
|
57
|
-
<InspectorRow
|
|
58
|
-
<
|
|
281
|
+
<InspectorRow>
|
|
282
|
+
<NumberField
|
|
283
|
+
value={letterSpacing.value ?? 0}
|
|
284
|
+
mixed={letterSpacing.mixed}
|
|
285
|
+
label="Letter"
|
|
286
|
+
ariaLabel="Letter spacing"
|
|
287
|
+
unit="px"
|
|
288
|
+
step={0.1}
|
|
289
|
+
trailing={<KeyframeDot path="text.style.letterSpacingPx" />}
|
|
290
|
+
onCommit={letterSpacing.commit}
|
|
291
|
+
onScrubStart={letterSpacing.scrub.start}
|
|
292
|
+
onScrub={letterSpacing.scrub.move}
|
|
293
|
+
onScrubEnd={letterSpacing.scrub.end}
|
|
294
|
+
/>
|
|
59
295
|
</InspectorRow>
|
|
296
|
+
{/* JUSTIFY IS GLYPH-ONLY, and that is a capability rather than a
|
|
297
|
+
preference: the Canvas2D path has always drawn `justify` as `left`,
|
|
298
|
+
because `measureText` gives no way to distribute the slack a line
|
|
299
|
+
leaves without re-shaping it by hand. Offering the segment on a
|
|
300
|
+
canvas layer would offer a setting that does nothing. */}
|
|
60
301
|
<InspectorRow label="Align">
|
|
61
302
|
<SegmentedControl
|
|
62
303
|
ariaLabel="Text align"
|
|
63
|
-
value={
|
|
304
|
+
value={align.mixed ? NO_SEGMENT : (align.value ?? "left")}
|
|
64
305
|
options={[
|
|
65
|
-
{ value: "left", label: "L" },
|
|
66
|
-
{ value: "center", label: "C" },
|
|
67
|
-
{ value: "right", label: "R" },
|
|
306
|
+
{ value: "left", label: "L", ariaLabel: "Left", title: "Left" },
|
|
307
|
+
{ value: "center", label: "C", ariaLabel: "Centre", title: "Centre" },
|
|
308
|
+
{ value: "right", label: "R", ariaLabel: "Right", title: "Right" },
|
|
309
|
+
{
|
|
310
|
+
value: "justify",
|
|
311
|
+
label: "J",
|
|
312
|
+
ariaLabel: "Justify",
|
|
313
|
+
title:
|
|
314
|
+
engine.value === "glyph"
|
|
315
|
+
? "Justify"
|
|
316
|
+
: "Justify needs the Glyph engine: the Canvas engine draws it as Left",
|
|
317
|
+
disabled: engine.mixed || engine.value !== "glyph",
|
|
318
|
+
},
|
|
68
319
|
]}
|
|
69
320
|
onChange={(next) => patchStyle({ align: next as TextStyle["align"] })}
|
|
70
321
|
/>
|
|
71
322
|
</InspectorRow>
|
|
323
|
+
<InspectorRow label="Decoration">
|
|
324
|
+
<SegmentedControl
|
|
325
|
+
ariaLabel="Text decoration"
|
|
326
|
+
value={decoration.mixed ? NO_SEGMENT : (decoration.value ?? "")}
|
|
327
|
+
options={DECORATION_OPTIONS}
|
|
328
|
+
onChange={(next) => write((layer) => withDecoration(layer, next))}
|
|
329
|
+
/>
|
|
330
|
+
</InspectorRow>
|
|
72
331
|
<InspectorRow label="Vertical">
|
|
73
332
|
<SegmentedControl
|
|
74
333
|
ariaLabel="Vertical align"
|
|
75
|
-
value={
|
|
334
|
+
value={verticalAlign.mixed ? NO_SEGMENT : (verticalAlign.value ?? "top")}
|
|
335
|
+
options={VERTICAL_OPTIONS}
|
|
336
|
+
onChange={(next) => patchStyle({ verticalAlign: next as NonNullable<TextStyle["verticalAlign"]> })}
|
|
337
|
+
/>
|
|
338
|
+
</InspectorRow>
|
|
339
|
+
{/* LAST, because it is the rarest edit and the one with consequences:
|
|
340
|
+
switching a layer to Glyph changes which engine draws it, and the
|
|
341
|
+
disabled reason is the validator's own sentence so the control and
|
|
342
|
+
the save gate can never disagree. */}
|
|
343
|
+
<InspectorRow label="Engine">
|
|
344
|
+
<SegmentedControl
|
|
345
|
+
ariaLabel="Text engine"
|
|
346
|
+
value={engine.mixed ? NO_SEGMENT : (engine.value ?? "canvas")}
|
|
76
347
|
options={[
|
|
77
|
-
{ value: "
|
|
78
|
-
{
|
|
79
|
-
|
|
348
|
+
{ value: "canvas", label: "Canvas" },
|
|
349
|
+
{
|
|
350
|
+
value: "glyph",
|
|
351
|
+
label: "Glyph",
|
|
352
|
+
title: glyphBlocked.mixed
|
|
353
|
+
? "Some of these layers cannot use the Glyph engine"
|
|
354
|
+
: (glyphBlocked.value ?? "") === ""
|
|
355
|
+
? "Glyph: outlines from a project font, drawn on the GPU"
|
|
356
|
+
: (glyphBlocked.value ?? ""),
|
|
357
|
+
disabled: glyphBlocked.mixed || (glyphBlocked.value ?? "") !== "",
|
|
358
|
+
},
|
|
80
359
|
]}
|
|
81
|
-
onChange={(next) =>
|
|
360
|
+
onChange={(next) =>
|
|
361
|
+
write((layer) =>
|
|
362
|
+
layer.kind === "text"
|
|
363
|
+
? ({ ...layer, text: { ...layer.text, engine: next as TextEngine } } as Layer3D)
|
|
364
|
+
: layer,
|
|
365
|
+
)
|
|
366
|
+
}
|
|
82
367
|
/>
|
|
83
368
|
</InspectorRow>
|
|
84
369
|
</InspectorSection>
|
|
85
370
|
);
|
|
86
371
|
}
|
|
87
372
|
|
|
373
|
+
/** The one value the selected text layers share, or Mixed. */
|
|
374
|
+
function agreed<T>(values: readonly T[]): { value: T | undefined; mixed: boolean } {
|
|
375
|
+
const value = values[0];
|
|
376
|
+
return { value, mixed: values.some((v) => v !== value) };
|
|
377
|
+
}
|
|
378
|
+
|
|
88
379
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
380
|
+
* WEIGHT, with Italic in its box's accessory lane. ONE labelled 28px pill in
|
|
381
|
+
* every state and both FieldLabels modes, only the control inside it changing,
|
|
382
|
+
* so nothing moves when the catalog lands and Italic is never remounted:
|
|
383
|
+
* - a catalog or workspace family with named weights: a menu of exactly
|
|
384
|
+
* those ("Bold 700"), one entry when it has one;
|
|
385
|
+
* - a variable family: a number clamped to its axis, typed to 1 and stepped
|
|
386
|
+
* by 100;
|
|
387
|
+
* - anything else (an uploaded file, a machine font, loading, Mixed): 100 to
|
|
388
|
+
* 900 by 100, as before.
|
|
389
|
+
* The number field takes no `label` of its own: under "named" it would draw
|
|
390
|
+
* its name above the box, a line the menu does not have.
|
|
391
|
+
* Every commit is `setFontWeight`, which fetches the face first, so a
|
|
392
|
+
* workspace family read as a plain file while its list loads still follows.
|
|
91
393
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* the
|
|
394
|
+
* THE MENU WAITS FOR A FOCUSED NUMBER TO BLUR. A load that lands while the
|
|
395
|
+
* author types in the number (the catalog, the workspace list) would swap it
|
|
396
|
+
* for the menu, unmounting the input with its draft and dropping focus on
|
|
397
|
+
* <body>, where the next Backspace deletes the layer. The number holds its
|
|
398
|
+
* place until focus leaves it; its blur commits the draft as always, then
|
|
399
|
+
* the menu takes the slot. Focus is read on `document`, after React's own
|
|
400
|
+
* blur handler has committed.
|
|
95
401
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
402
|
+
* A DRAG previews through the inspector's scrub lane and commits nothing
|
|
403
|
+
* there. A move writes `weight` alone, which fetches nothing (the store
|
|
404
|
+
* follows files only on a commit), and that is safe to show: a variable file
|
|
405
|
+
* draws every weight on its axis, and a plain file draws whatever weight it
|
|
406
|
+
* is given. The release cancels the scrub and hands the last value to
|
|
407
|
+
* `setFontWeight`, the one undo step.
|
|
99
408
|
*/
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
409
|
+
function WeightRow(props: { texts: readonly TextLayer3D[]; facts: SelectionFontFacts }) {
|
|
410
|
+
const store = useStore();
|
|
411
|
+
const { facts } = props;
|
|
412
|
+
const weight = agreed(props.texts.map((layer) => layer.text.style.weight));
|
|
413
|
+
const scrub = useSelectionScrub((layer, raw) => withStyle(layer, { weight: Number(raw) }));
|
|
414
|
+
const dragged = useRef<number | null>(null);
|
|
415
|
+
const commit = (next: number): void => void store.setFontWeight(next);
|
|
416
|
+
const family = facts?.kind === "catalog" || facts?.kind === "workspace" ? facts : null;
|
|
417
|
+
const axis = family?.axis ?? null;
|
|
418
|
+
// A variable Google family's commit plans its faces with the subset ranges,
|
|
419
|
+
// a chunk nothing loads before a pick. Cold, the release that cancels the
|
|
420
|
+
// scrub would show the old weight for that round trip. A failure here is
|
|
421
|
+
// the commit's to retry and report.
|
|
422
|
+
const warmRanges = family?.kind === "catalog" && axis !== null;
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (warmRanges) loadGoogleFontRanges().catch(() => {});
|
|
425
|
+
}, [warmRanges]);
|
|
426
|
+
// The Italic key is the row's one element this file holds; the row's one
|
|
427
|
+
// input is the number's (the menu's search is portaled away).
|
|
428
|
+
const italic = useRef<HTMLButtonElement>(null);
|
|
429
|
+
const [typing, setTyping] = useState(false);
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
const inNumber = (node: EventTarget | null): boolean => {
|
|
432
|
+
const row = italic.current?.closest(".insp-prop");
|
|
433
|
+
return row != null && node instanceof HTMLInputElement && row.contains(node);
|
|
434
|
+
};
|
|
435
|
+
const onIn = (e: FocusEvent): void => setTyping(inNumber(e.target));
|
|
436
|
+
const onOut = (e: FocusEvent): void => setTyping(inNumber(e.relatedTarget));
|
|
437
|
+
document.addEventListener("focusin", onIn);
|
|
438
|
+
document.addEventListener("focusout", onOut);
|
|
439
|
+
return () => {
|
|
440
|
+
document.removeEventListener("focusin", onIn);
|
|
441
|
+
document.removeEventListener("focusout", onOut);
|
|
442
|
+
};
|
|
443
|
+
}, []);
|
|
444
|
+
let control;
|
|
445
|
+
if (family !== null && axis === null && family.weights.length > 0 && !typing) {
|
|
446
|
+
// A stored weight the family has no file for (a Role's 500 on 400 and
|
|
447
|
+
// 700) is still what the document holds, so the trigger names it.
|
|
448
|
+
const listed = !weight.mixed && weight.value !== undefined && family.weights.includes(weight.value);
|
|
449
|
+
control = (
|
|
450
|
+
<Combobox
|
|
451
|
+
ariaLabel="Font weight"
|
|
452
|
+
options={family.weights.map((w) => ({ value: String(w), label: weightLabel(w) }))}
|
|
453
|
+
value={listed ? String(weight.value) : ""}
|
|
454
|
+
placeholder={weight.mixed ? MIXED_PLACEHOLDER : weightLabel(weight.value ?? 400)}
|
|
455
|
+
onChange={(next) => commit(Number(next))}
|
|
456
|
+
/>
|
|
117
457
|
);
|
|
458
|
+
} else {
|
|
459
|
+
control = (
|
|
460
|
+
<NumberField
|
|
461
|
+
value={weight.value ?? 400}
|
|
462
|
+
mixed={weight.mixed}
|
|
463
|
+
ariaLabel="Font weight"
|
|
464
|
+
min={axis?.[0] ?? 100}
|
|
465
|
+
max={axis?.[1] ?? 900}
|
|
466
|
+
{...(axis === null ? { step: 100 } : { step: 1, nudge: 100 })}
|
|
467
|
+
onCommit={commit}
|
|
468
|
+
onScrubStart={() => {
|
|
469
|
+
dragged.current = null;
|
|
470
|
+
scrub.start();
|
|
471
|
+
}}
|
|
472
|
+
onScrub={(next) => {
|
|
473
|
+
dragged.current = next;
|
|
474
|
+
scrub.move(next);
|
|
475
|
+
}}
|
|
476
|
+
onScrubEnd={(ok) => {
|
|
477
|
+
scrub.end(false);
|
|
478
|
+
const last = dragged.current;
|
|
479
|
+
dragged.current = null;
|
|
480
|
+
if (ok && last !== null) commit(last);
|
|
481
|
+
}}
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return (
|
|
486
|
+
<InspectorRow label="Weight" trailing={<ItalicKey ref={italic} texts={props.texts} facts={facts} />}>
|
|
487
|
+
{control}
|
|
488
|
+
</InspectorRow>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
118
491
|
|
|
119
|
-
|
|
492
|
+
/**
|
|
493
|
+
* ITALIC, a verb in the Weight box's lane: pressed while the text is italic,
|
|
494
|
+
* Mixed while the layers disagree (a press makes every one italic, the house
|
|
495
|
+
* switch policy), and refused with the reason when the family has no italic
|
|
496
|
+
* (`aria-disabled`, still pressable so the press can say why). Disabled under
|
|
497
|
+
* `LockedControls`. `setFontStyle` fetches the face first.
|
|
498
|
+
*/
|
|
499
|
+
function ItalicKey(props: { ref: Ref<HTMLButtonElement>; texts: readonly TextLayer3D[]; facts: SelectionFontFacts }) {
|
|
500
|
+
const store = useStore();
|
|
501
|
+
const locked = useLocked();
|
|
502
|
+
const { facts } = props;
|
|
503
|
+
const italic = agreed(props.texts.map((layer) => layer.text.style.fontStyle === "italic"));
|
|
504
|
+
const pressed = italic.mixed ? "mixed" : italic.value === true;
|
|
505
|
+
const missing =
|
|
506
|
+
pressed !== true && (facts?.kind === "catalog" || facts?.kind === "workspace") && !facts.italic
|
|
507
|
+
? `${facts.family} has no italic style.`
|
|
508
|
+
: null;
|
|
120
509
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
510
|
+
<button
|
|
511
|
+
ref={props.ref}
|
|
512
|
+
type="button"
|
|
513
|
+
className="insp-key is-toggle"
|
|
514
|
+
aria-label="Italic"
|
|
515
|
+
aria-pressed={pressed}
|
|
516
|
+
title={missing ?? "Italic"}
|
|
517
|
+
{...(missing === null ? {} : { "aria-disabled": true })}
|
|
518
|
+
disabled={locked}
|
|
519
|
+
onClick={() => {
|
|
520
|
+
if (missing !== null) store.reportError(missing);
|
|
521
|
+
else void store.setFontStyle(pressed === true ? "normal" : "italic");
|
|
522
|
+
}}
|
|
523
|
+
>
|
|
524
|
+
<IconItalic />
|
|
525
|
+
</button>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A canvas text layer with no font file draws whatever the computer has
|
|
531
|
+
* (`drawsMachineFont`), so the note says so under the Font row, with the one
|
|
532
|
+
* way out: the catalog's copy when Google Fonts has the family, else the
|
|
533
|
+
* picker's upload view. A generic keyword (`sans-serif`) opens the picker's
|
|
534
|
+
* list instead, since no file is that family.
|
|
535
|
+
*
|
|
536
|
+
* SELF-HOST REMOVES THE NOTE: once the copy lands the layer has a file. So a
|
|
537
|
+
* press made from the verb hands focus to the Font row's trigger, the field
|
|
538
|
+
* the note sits under (its previous sibling), unless focus moved on while the
|
|
539
|
+
* file downloaded.
|
|
540
|
+
*/
|
|
541
|
+
function MachineFontNote(props: { family: string; selfHost: string | null }) {
|
|
542
|
+
const store = useStore();
|
|
543
|
+
const locked = useLocked();
|
|
544
|
+
// A generic keyword (`sans-serif`) names no font a file could be: every
|
|
545
|
+
// computer has its own, so the way out is choosing one.
|
|
546
|
+
const generic = isGenericFontFamily(props.family);
|
|
547
|
+
const selfHost = async (verb: HTMLButtonElement): Promise<void> => {
|
|
548
|
+
const trigger = verb.closest(".insp-notice")?.previousElementSibling?.querySelector<HTMLElement>("button");
|
|
549
|
+
const held = document.activeElement === verb;
|
|
550
|
+
const outcome = await store.selfHostFont();
|
|
551
|
+
const now = document.activeElement;
|
|
552
|
+
if (outcome.status === "applied" && held && (now === verb || now === null || now === document.body)) {
|
|
553
|
+
trigger?.focus();
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
return (
|
|
557
|
+
<div className="insp-notice is-warn" data-testid="machine-font-note">
|
|
558
|
+
{generic
|
|
559
|
+
? `This text draws the computer's own ${props.family} font. Viewers, renders and exports may draw a different one.`
|
|
560
|
+
: `Only this computer has ${props.family}. Viewers, renders and exports may draw another font.`}
|
|
561
|
+
<div className="insp-buttons">
|
|
562
|
+
{generic ? (
|
|
563
|
+
<button type="button" className="btn" disabled={locked} onClick={() => requestFontPicker("list")}>
|
|
564
|
+
Choose a font
|
|
565
|
+
</button>
|
|
566
|
+
) : props.selfHost !== null ? (
|
|
567
|
+
<button type="button" className="btn" disabled={locked} onClick={(e) => void selfHost(e.currentTarget)}>
|
|
568
|
+
{`Self-host ${props.selfHost}`}
|
|
569
|
+
</button>
|
|
570
|
+
) : (
|
|
571
|
+
<button type="button" className="btn" disabled={locked} onClick={() => requestFontPicker("upload")}>
|
|
572
|
+
Upload font file
|
|
573
|
+
</button>
|
|
574
|
+
)}
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* A text layer's Fill is what its GLYPHS are painted with (F20), the way Figma
|
|
582
|
+
* and Framer read it. `paint.fill` on text is the plate behind the words, so
|
|
583
|
+
* `FillSection` titles itself "Background" there. Over a mixed selection this
|
|
584
|
+
* one says "Text fill", so the two never share a name.
|
|
585
|
+
*
|
|
586
|
+
* The layer fill's own rows (`PaintRows`), over the one paint the glyphs show:
|
|
587
|
+
* Solid is `text.style.color`, and Linear or Radial is `text.style.fill`
|
|
588
|
+
* (schema v47), written by `withTextPaint` so the document holds one or the
|
|
589
|
+
* other. Two cases keep the plain colour row, each saying why, because the
|
|
590
|
+
* save gate refuses a gradient there: a keyed colour (a `text.style.color`
|
|
591
|
+
* track beside a fill is a second writer) and the Glyph engine.
|
|
592
|
+
*/
|
|
593
|
+
export function TextFillSection() {
|
|
594
|
+
const layers = useSelectedLayers();
|
|
595
|
+
const write = useSelectionWrite();
|
|
596
|
+
const texts = layers.filter((layer): layer is TextLayer3D => layer.kind === "text");
|
|
597
|
+
// Each layer's OWN paint as painted, so a multi-selection edit changes one
|
|
598
|
+
// field per layer and never copies the lead's paint onto the rest.
|
|
599
|
+
const painted = new Map(texts.map((layer) => [layer.id, textPaintOf(layer)]));
|
|
600
|
+
const edited = (layer: Layer3D, edit: PaintEdit): Layer3D =>
|
|
601
|
+
layer.kind === "text" ? withTextPaint(layer, edit(painted.get(layer.id) ?? textPaintOf(layer))) : layer;
|
|
602
|
+
const scrub = useSelectionScrub((layer, raw) => edited(layer, raw as PaintEdit));
|
|
603
|
+
const blocked = texts.some((layer) => layer.tracks?.["text.style.color"] !== undefined)
|
|
604
|
+
? "Remove the colour keys to fill the text with a gradient."
|
|
605
|
+
: texts.some((layer) => layer.text.engine === "glyph")
|
|
606
|
+
? "Gradient text needs the Canvas engine."
|
|
607
|
+
: null;
|
|
608
|
+
const lead = texts[0];
|
|
609
|
+
return (
|
|
610
|
+
<InspectorSection
|
|
611
|
+
className="insp-object"
|
|
612
|
+
title={layers.every((layer) => layer.kind === "text") ? "Fill" : "Text fill"}
|
|
613
|
+
persistKey="shot:textFill"
|
|
614
|
+
>
|
|
615
|
+
{blocked !== null || lead === undefined ? (
|
|
616
|
+
<>
|
|
617
|
+
<TextColorRow />
|
|
618
|
+
{blocked !== null ? <p className="insp-note">{blocked}</p> : null}
|
|
619
|
+
</>
|
|
620
|
+
) : (
|
|
621
|
+
<PaintRows
|
|
622
|
+
// NO `key`, for `FillSection`'s reason: a remount mid-drag drops the
|
|
623
|
+
// picker's pointer capture.
|
|
624
|
+
value={textPaintOf(lead)}
|
|
625
|
+
all={texts.map(textPaintOf)}
|
|
626
|
+
ariaPrefix="Text"
|
|
627
|
+
label={null}
|
|
628
|
+
solidTrailing={<KeyframeDot path="text.style.color" />}
|
|
629
|
+
onScrubStart={scrub.start}
|
|
630
|
+
onScrub={(edit) => scrub.move(edit)}
|
|
631
|
+
onScrubEnd={scrub.end}
|
|
632
|
+
onCommit={(edit) => write((layer) => edited(layer, edit))}
|
|
164
633
|
/>
|
|
165
|
-
|
|
634
|
+
)}
|
|
166
635
|
</InspectorSection>
|
|
167
636
|
);
|
|
168
637
|
}
|
|
169
638
|
|
|
170
|
-
|
|
639
|
+
/** The one paint a text layer's glyphs show: its gradient fill, else its
|
|
640
|
+
* colour as a solid. */
|
|
641
|
+
function textPaintOf(layer: TextLayer3D): Paint {
|
|
642
|
+
return layer.text.style.fill ?? { type: "solid", color: layer.text.style.color };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The ONE text paint edit, so a drag's move and its release land the same
|
|
646
|
+
* document. A solid writes `color` and drops `fill`; a gradient writes
|
|
647
|
+
* `fill` and keeps `color`, which a later switch back to Solid replaces. */
|
|
648
|
+
function withTextPaint(layer: TextLayer3D, paint: Paint): Layer3D {
|
|
649
|
+
const { fill: _drop, ...style } = layer.text.style;
|
|
650
|
+
return {
|
|
651
|
+
...layer,
|
|
652
|
+
text: { ...layer.text, style: paint.type === "solid" ? { ...style, color: paint.color } : { ...style, fill: paint } },
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** `text.style.color`: the glyph colour, the Fill section's one row, its
|
|
657
|
+
* diamond inside the field. */
|
|
658
|
+
function TextColorRow() {
|
|
659
|
+
const write = useSelectionWrite();
|
|
660
|
+
const color = useSelectionValue((layer) => readStyle(layer)?.color);
|
|
661
|
+
// THE DRAG HALF: the picker's SV square, hue bar and alpha bar all report
|
|
662
|
+
// through `scrub.move` and never `commit` while the pointer is down.
|
|
663
|
+
const scrub = useSelectionScrub((layer, raw) => withStyle(layer, { color: String(raw) }));
|
|
664
|
+
return (
|
|
665
|
+
<InspectorRow>
|
|
666
|
+
<ColorField
|
|
667
|
+
value={color.value ?? "#ffffff"}
|
|
668
|
+
mixed={color.mixed}
|
|
669
|
+
ariaLabel="Text colour"
|
|
670
|
+
scrub={scrub}
|
|
671
|
+
commit={(hex) => write((layer) => withStyle(layer, { color: hex }))}
|
|
672
|
+
trailing={<KeyframeDot path="text.style.color" />}
|
|
673
|
+
/>
|
|
674
|
+
</InspectorRow>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
interface NumberRow {
|
|
679
|
+
value: number | undefined;
|
|
680
|
+
mixed: boolean;
|
|
681
|
+
commit: (value: number) => void;
|
|
682
|
+
scrub: ScrubHandlers;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** One numeric style field: the scalar it READS, the typed value it COMMITS and
|
|
686
|
+
* the label drag it SCRUBS, so a row is one call rather than three. */
|
|
687
|
+
function useStyleNumber(field: StyleNumber): NumberRow {
|
|
688
|
+
const write = useSelectionWrite();
|
|
689
|
+
const { value, mixed } = useSelectionValue((layer) => readStyle(layer)?.[field]);
|
|
690
|
+
const scrub = useSelectionScrub((layer, raw) => withStyle(layer, { [field]: Number(raw) }));
|
|
691
|
+
return { value, mixed, commit: (next) => write((layer) => withStyle(layer, { [field]: next })), scrub };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function readStyle(layer: Layer3D): TextStyle | undefined {
|
|
695
|
+
return layer.kind === "text" ? layer.text.style : undefined;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Infer a role only while every persisted metric agrees with it. An authored
|
|
699
|
+
* custom style remains intentionally unmatched rather than acquiring a label
|
|
700
|
+
* the document does not store. */
|
|
701
|
+
function textRoleOf(layer: Layer3D): TextRole | undefined {
|
|
702
|
+
const style = readStyle(layer);
|
|
703
|
+
if (style === undefined) return undefined;
|
|
704
|
+
return TEXT_ROLE_OPTIONS.find(({ value }) => {
|
|
705
|
+
const role = textStyleForRole(value, style.sizePx);
|
|
706
|
+
return (
|
|
707
|
+
style.weight === role.weight &&
|
|
708
|
+
style.lineHeight === role.lineHeight &&
|
|
709
|
+
Math.abs(style.letterSpacingPx - role.letterSpacingPx) < 1e-9
|
|
710
|
+
);
|
|
711
|
+
})?.value;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Decoration is the one style field the control can set back to ABSENT, and
|
|
715
|
+
* absent is not `undefined`: under `exactOptionalPropertyTypes` a spread of
|
|
716
|
+
* `{ decoration: undefined }` is a different document from one with no key,
|
|
717
|
+
* and the schema holds no key. So it is deleted rather than assigned. */
|
|
718
|
+
function withDecoration(layer: Layer3D, next: string): Layer3D {
|
|
719
|
+
if (layer.kind !== "text") return layer;
|
|
720
|
+
if (next === "") {
|
|
721
|
+
const { decoration: _dropped, ...style } = layer.text.style;
|
|
722
|
+
return { ...layer, text: { ...layer.text, style } } as Layer3D;
|
|
723
|
+
}
|
|
724
|
+
return withStyle(layer, { decoration: next as NonNullable<TextStyle["decoration"]> });
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** The ONE style edit, so a drag's move and its release land the same document.
|
|
728
|
+
* A non-text layer in the selection is left alone rather than grown a `text`
|
|
729
|
+
* member it has no meaning for. */
|
|
730
|
+
function withStyle(layer: Layer3D, next: Partial<TextStyle>): Layer3D {
|
|
731
|
+
if (layer.kind !== "text") return layer;
|
|
732
|
+
return { ...layer, text: { ...layer.text, style: { ...layer.text.style, ...next } } } as Layer3D;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** Apply a role as raw values and preserve each selected layer's own size. */
|
|
736
|
+
function withRole(layer: Layer3D, role: TextRole): Layer3D {
|
|
737
|
+
if (layer.kind !== "text") return layer;
|
|
738
|
+
return withStyle(layer, textStyleForRole(role, layer.text.style.sizePx));
|
|
739
|
+
}
|