@volter/editor-core 0.5.57

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.
Files changed (719) hide show
  1. package/BUNDLED_NOTICES +1527 -0
  2. package/LICENSE +686 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/NOTICE +18 -0
  5. package/README.md +24 -0
  6. package/dist/build/chunk-JS4D4XZ3.js +175 -0
  7. package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
  8. package/dist/build/vite-plugin-product-contributions.js +100 -0
  9. package/dist/build/vite-plugin-product-contributions.js.map +7 -0
  10. package/dist/build/vite-plugin-shared-react.js +25 -0
  11. package/dist/build/vite-plugin-shared-react.js.map +7 -0
  12. package/dist/build/vite-plugin-shared-three.js +86 -0
  13. package/dist/build/vite-plugin-shared-three.js.map +7 -0
  14. package/dist/server/chunk-KMJC7AGO.js +98 -0
  15. package/dist/server/chunk-KMJC7AGO.js.map +7 -0
  16. package/dist/server/chunk-PSILUMGS.js +46 -0
  17. package/dist/server/chunk-PSILUMGS.js.map +7 -0
  18. package/dist/server/frame-proxy.js +255 -0
  19. package/dist/server/frame-proxy.js.map +7 -0
  20. package/dist/server/open-browser.js +53 -0
  21. package/dist/server/open-browser.js.map +7 -0
  22. package/dist/server/process-shutdown.js +127 -0
  23. package/dist/server/process-shutdown.js.map +7 -0
  24. package/dist/server/session-registry.js +141 -0
  25. package/dist/server/session-registry.js.map +7 -0
  26. package/dist/server/spawn-opener.js +11 -0
  27. package/dist/server/spawn-opener.js.map +7 -0
  28. package/dist/server/worktree-identity.js +11 -0
  29. package/dist/server/worktree-identity.js.map +7 -0
  30. package/dist-server/packaged.mjs +88816 -0
  31. package/package.json +118 -0
  32. package/scripts/build-plugins.mjs +14 -0
  33. package/scripts/build-server.mjs +26 -0
  34. package/scripts/build-session.mjs +9 -0
  35. package/server/account-credentials.ts +60 -0
  36. package/server/account-service.ts +1434 -0
  37. package/server/adapter-region-includes.ts +47 -0
  38. package/server/asset-catalog-v2.ts +175 -0
  39. package/server/asset-history-snapshots.ts +196 -0
  40. package/server/asset-ledger-store.ts +195 -0
  41. package/server/asset-library-routes.ts +1595 -0
  42. package/server/blender-wasm-artifact.ts +430 -0
  43. package/server/boot-timings.ts +106 -0
  44. package/server/canonical-path.ts +50 -0
  45. package/server/checkout-workspace-preflight.ts +349 -0
  46. package/server/cloud-asset-catalog.ts +158 -0
  47. package/server/coding-inference-launch.ts +237 -0
  48. package/server/collaboration-account-client.ts +202 -0
  49. package/server/collaboration-attribution.ts +122 -0
  50. package/server/collaboration-session.ts +934 -0
  51. package/server/comfyui-bridge.ts +248 -0
  52. package/server/console-ledger.ts +499 -0
  53. package/server/creation-site-transform.ts +304 -0
  54. package/server/creation-site-write.ts +648 -0
  55. package/server/data-file-serialize.ts +158 -0
  56. package/server/editor-brand-html.ts +72 -0
  57. package/server/editor-control-socket.ts +366 -0
  58. package/server/editor-server-options.ts +190 -0
  59. package/server/editor-server.ts +1483 -0
  60. package/server/editor-sse.ts +352 -0
  61. package/server/engine-provenance.ts +169 -0
  62. package/server/engine-source-restart.ts +117 -0
  63. package/server/frame-bridge.ts +115 -0
  64. package/server/frame-proxy.ts +490 -0
  65. package/server/frame-workbench.ts +259 -0
  66. package/server/frontend-handoff.ts +229 -0
  67. package/server/game-globals-shadow.ts +113 -0
  68. package/server/gameplay-session-retention.ts +149 -0
  69. package/server/gameplay-sessions.ts +123 -0
  70. package/server/generation-jobs.ts +201 -0
  71. package/server/generation-reconciler.ts +137 -0
  72. package/server/generative-execution-context.ts +59 -0
  73. package/server/git-workflow.ts +612 -0
  74. package/server/harness-chat-caller.ts +36 -0
  75. package/server/harness-chat-service.ts +1681 -0
  76. package/server/idle-shutdown.ts +170 -0
  77. package/server/js-profiling-policy.ts +52 -0
  78. package/server/launcher-settings.ts +84 -0
  79. package/server/local-asset-catalog.ts +410 -0
  80. package/server/managed-account-defaults.ts +22 -0
  81. package/server/mock-control-plane.ts +540 -0
  82. package/server/model-import-conversion.ts +345 -0
  83. package/server/native-credential-store.ts +162 -0
  84. package/server/open-browser.ts +118 -0
  85. package/server/packaged.ts +1192 -0
  86. package/server/play-stall.ts +126 -0
  87. package/server/process-shutdown.ts +218 -0
  88. package/server/product-presets.ts +55 -0
  89. package/server/project-build-artifact.ts +95 -0
  90. package/server/project-components.ts +100 -0
  91. package/server/project-dependency-invalidation.ts +114 -0
  92. package/server/project-file-scan.ts +235 -0
  93. package/server/project-hmr-files.ts +215 -0
  94. package/server/project-install-roots.ts +210 -0
  95. package/server/project-kinds.ts +71 -0
  96. package/server/project-mcp-servers.ts +82 -0
  97. package/server/project-module-freshness.ts +109 -0
  98. package/server/project-module-instance.ts +375 -0
  99. package/server/project-optimize-deps-entries.ts +827 -0
  100. package/server/project-output-writer.ts +679 -0
  101. package/server/project-package-origin.ts +63 -0
  102. package/server/project-root-surface.ts +831 -0
  103. package/server/project-scratch-path.ts +47 -0
  104. package/server/project-script-hmr.ts +619 -0
  105. package/server/project-serving-plugins.ts +236 -0
  106. package/server/project-tools.ts +653 -0
  107. package/server/project-validation.ts +250 -0
  108. package/server/project-verbs.ts +127 -0
  109. package/server/project-view.ts +52 -0
  110. package/server/project-watch.ts +1134 -0
  111. package/server/project-work-coordinator.ts +288 -0
  112. package/server/provider-credentials.ts +331 -0
  113. package/server/recent-projects-store.ts +98 -0
  114. package/server/redact-secrets.ts +71 -0
  115. package/server/repository-presence.ts +267 -0
  116. package/server/routes/account.ts +357 -0
  117. package/server/routes/agents.ts +60 -0
  118. package/server/routes/assets.ts +154 -0
  119. package/server/routes/build.ts +13 -0
  120. package/server/routes/collaboration.ts +595 -0
  121. package/server/routes/configurations.ts +419 -0
  122. package/server/routes/context.ts +264 -0
  123. package/server/routes/control-plane.ts +1811 -0
  124. package/server/routes/logs.ts +272 -0
  125. package/server/routes/project-identity.ts +302 -0
  126. package/server/routes/project-open.ts +429 -0
  127. package/server/routes/project-source.ts +686 -0
  128. package/server/routes/project-state.ts +383 -0
  129. package/server/routes/relay.ts +1029 -0
  130. package/server/routes/route-helpers.ts +29 -0
  131. package/server/routes/served-modules.ts +138 -0
  132. package/server/routes/session-tabs.ts +196 -0
  133. package/server/routes/settings.ts +121 -0
  134. package/server/routes/share-control.ts +486 -0
  135. package/server/routes/themes.ts +116 -0
  136. package/server/routes/tools.ts +230 -0
  137. package/server/routes/worktrees.ts +595 -0
  138. package/server/scoped-game-css.ts +461 -0
  139. package/server/server-utils.ts +1194 -0
  140. package/server/session-product.ts +112 -0
  141. package/server/session-registry.ts +230 -0
  142. package/server/share-claims.ts +138 -0
  143. package/server/share-host.ts +526 -0
  144. package/server/share-session-gateway.ts +1362 -0
  145. package/server/share-tunnel.ts +123 -0
  146. package/server/spawn-opener.ts +74 -0
  147. package/server/support/play/log-format.ts +76 -0
  148. package/server/support/play/session-record.ts +199 -0
  149. package/server/support/project/build-discipline.ts +718 -0
  150. package/server/support/project/inspection-node.ts +119 -0
  151. package/server/support/project/log-naming.ts +54 -0
  152. package/server/support/project/provenance.ts +290 -0
  153. package/server/support/project/run-name.ts +18 -0
  154. package/server/support/project/session-journal.ts +830 -0
  155. package/server/support/project/shared.ts +327 -0
  156. package/server/tab-bootstrap.ts +62 -0
  157. package/server/tab-heartbeat.ts +477 -0
  158. package/server/tab-lifecycle.ts +550 -0
  159. package/server/tab-presence.ts +1508 -0
  160. package/server/team-agent-mirror.ts +95 -0
  161. package/server/twin-auth.ts +138 -0
  162. package/server/vendored-lock-recorder.ts +968 -0
  163. package/server/worktree-identity.ts +121 -0
  164. package/server/worktree-management.ts +398 -0
  165. package/server/worktree-retention.d.mts +13 -0
  166. package/server/worktree-retention.mjs +205 -0
  167. package/src/EditorContext.tsx +169 -0
  168. package/src/account.ts +333 -0
  169. package/src/action-registry.ts +532 -0
  170. package/src/active-product.ts +74 -0
  171. package/src/active-project.ts +155 -0
  172. package/src/adapter-editor-config.ts +25 -0
  173. package/src/adapter-observation.ts +49 -0
  174. package/src/animation/stage-transport.ts +379 -0
  175. package/src/animation/three-clips-subject.ts +172 -0
  176. package/src/api/asset-library-wire.ts +45 -0
  177. package/src/api/assets.ts +365 -0
  178. package/src/api/base.ts +10 -0
  179. package/src/api/build.ts +99 -0
  180. package/src/api/git-wire.ts +56 -0
  181. package/src/api/logs.ts +92 -0
  182. package/src/api/project-identity.ts +74 -0
  183. package/src/api/project-open.ts +355 -0
  184. package/src/api/project-source.ts +162 -0
  185. package/src/api/project-state.ts +107 -0
  186. package/src/api/relay.ts +268 -0
  187. package/src/api/settings.ts +36 -0
  188. package/src/api/themes.ts +45 -0
  189. package/src/api/worktrees.ts +257 -0
  190. package/src/asset-compare-core.ts +171 -0
  191. package/src/asset-compare.ts +294 -0
  192. package/src/asset-editor-context.tsx +100 -0
  193. package/src/asset-events.ts +97 -0
  194. package/src/asset-inspector-actions.ts +87 -0
  195. package/src/asset-preview-framing.ts +357 -0
  196. package/src/asset-preview.ts +2802 -0
  197. package/src/asset-selection-viewer-registry.ts +113 -0
  198. package/src/asset-selection.ts +133 -0
  199. package/src/asset-workflow/asset-capabilities.ts +344 -0
  200. package/src/asset-workflow/asset-import-jobs.ts +106 -0
  201. package/src/asset-workflow/asset-ledger-backend.ts +126 -0
  202. package/src/asset-workflow/asset-ledger.ts +156 -0
  203. package/src/asset-workflow/asset-materialization-report.ts +140 -0
  204. package/src/asset-workflow/asset-pack-manifest.ts +320 -0
  205. package/src/asset-workflow/asset-types.ts +142 -0
  206. package/src/asset-workflow/asset-workflow-quality.ts +61 -0
  207. package/src/asset-workflow/audio-preview-player.ts +193 -0
  208. package/src/asset-workflow/audio-waveform.ts +22 -0
  209. package/src/asset-workflow/cloud-asset-client.ts +263 -0
  210. package/src/asset-workflow/folder-preview.ts +345 -0
  211. package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
  212. package/src/asset-workflow/image-view-scale.ts +32 -0
  213. package/src/asset-workflow/import-contract.ts +124 -0
  214. package/src/asset-workflow/ledger-write-lock.ts +244 -0
  215. package/src/asset-workflow/model-inspection.ts +818 -0
  216. package/src/asset-workflow/pixi-spritesheet.ts +197 -0
  217. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  218. package/src/asset-workflow/project-asset-commands.ts +23 -0
  219. package/src/asset-workflow/project-asset-health.ts +139 -0
  220. package/src/asset-workflow/project-asset-operations.ts +154 -0
  221. package/src/asset-workflow/project-asset-roots.ts +68 -0
  222. package/src/asset-workflow/project-content.ts +288 -0
  223. package/src/asset-workflow/project-source-index.ts +550 -0
  224. package/src/asset-workflow/thumbnail-system.ts +256 -0
  225. package/src/authoring/active-adapter.ts +199 -0
  226. package/src/authoring/active-systems.ts +422 -0
  227. package/src/authoring/adapter-key.ts +18 -0
  228. package/src/authoring/authoring-asset-url.ts +27 -0
  229. package/src/authoring/bootstrap-state.ts +49 -0
  230. package/src/authoring/boundary-authoring-adapter.ts +184 -0
  231. package/src/authoring/breakpoint-state.ts +43 -0
  232. package/src/authoring/canvas-scene-guides.ts +76 -0
  233. package/src/authoring/component-instance-root.ts +171 -0
  234. package/src/authoring/composite-authoring-adapter.ts +2110 -0
  235. package/src/authoring/consumer-actions.ts +520 -0
  236. package/src/authoring/css-numeric-style.ts +97 -0
  237. package/src/authoring/design-time-layers.ts +849 -0
  238. package/src/authoring/design-time-mount-registry.ts +235 -0
  239. package/src/authoring/design-time-settle.ts +343 -0
  240. package/src/authoring/edit-mode-authoring.ts +619 -0
  241. package/src/authoring/eyedropper-session.ts +60 -0
  242. package/src/authoring/instance-source-menu-register.ts +13 -0
  243. package/src/authoring/instance-source-menu.ts +135 -0
  244. package/src/authoring/layered-pick.ts +183 -0
  245. package/src/authoring/live-gesture-lock.ts +50 -0
  246. package/src/authoring/live-object-transform.ts +62 -0
  247. package/src/authoring/mount-failure-report.ts +154 -0
  248. package/src/authoring/mounted-root-subjects.ts +144 -0
  249. package/src/authoring/no-authoring-adapter.ts +55 -0
  250. package/src/authoring/null-inspection-subjects.tsx +75 -0
  251. package/src/authoring/object3d-document-persistence.ts +106 -0
  252. package/src/authoring/object3d-document-session-registry.ts +120 -0
  253. package/src/authoring/object3d-document-session.ts +1326 -0
  254. package/src/authoring/object3d-gesture-controller.ts +113 -0
  255. package/src/authoring/panel-authoring.ts +121 -0
  256. package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
  257. package/src/authoring/prefab-instance-section-model.ts +55 -0
  258. package/src/authoring/project-authoring-session.ts +105 -0
  259. package/src/authoring/provenance.ts +99 -0
  260. package/src/authoring/quarks-particle-systems.ts +19 -0
  261. package/src/authoring/react-canvas-navigation.ts +255 -0
  262. package/src/authoring/react-design-canvas-style.ts +20 -0
  263. package/src/authoring/react-story-board.ts +937 -0
  264. package/src/authoring/selection-scope.ts +195 -0
  265. package/src/authoring/shell-document-ops.ts +169 -0
  266. package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
  267. package/src/authoring/shell-viewport-policy.ts +48 -0
  268. package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
  269. package/src/authoring/stories-scope.ts +35 -0
  270. package/src/authoring/story-board-chrome-fit.ts +107 -0
  271. package/src/authoring/story-board-presentation.ts +111 -0
  272. package/src/authoring/three-projection-core.ts +226 -0
  273. package/src/authoring/viewport-pick-context.ts +40 -0
  274. package/src/authoring/viewport-raycast.ts +240 -0
  275. package/src/authoring/viewport-tool-context.ts +73 -0
  276. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  277. package/src/authoring/world-hidden-viewport.ts +152 -0
  278. package/src/authoring/world-pan-state.ts +198 -0
  279. package/src/authoring/world-session-state.ts +101 -0
  280. package/src/authoring/write-pipe.ts +173 -0
  281. package/src/availability-tick.ts +66 -0
  282. package/src/bitmap-label.ts +120 -0
  283. package/src/blender-tab-metrics.ts +161 -0
  284. package/src/board-open-actions.ts +20 -0
  285. package/src/boot-routing.ts +391 -0
  286. package/src/build-identity.ts +15 -0
  287. package/src/bytes-codec.ts +62 -0
  288. package/src/camera-authoring.ts +175 -0
  289. package/src/cancellation-reason.ts +58 -0
  290. package/src/canvas-preview-frames.ts +482 -0
  291. package/src/capture-camera-pose.ts +77 -0
  292. package/src/chrome-registry.ts +159 -0
  293. package/src/chrome-slot-registry.ts +91 -0
  294. package/src/collaboration-client.ts +264 -0
  295. package/src/collaboration-presence.ts +41 -0
  296. package/src/command-dispatch.ts +19 -0
  297. package/src/command-listener.ts +2743 -0
  298. package/src/command-registry.ts +70 -0
  299. package/src/component-board-registry.ts +205 -0
  300. package/src/component-states-registry.ts +200 -0
  301. package/src/components/AgentPresentationNotice.tsx +75 -0
  302. package/src/components/AlignToolbar.tsx +204 -0
  303. package/src/components/AppRoot.tsx +394 -0
  304. package/src/components/ApplicationMenus.tsx +415 -0
  305. package/src/components/AssetBrowser.tsx +2843 -0
  306. package/src/components/AssetEditorShell.tsx +216 -0
  307. package/src/components/AssetInspectorToolSection.tsx +124 -0
  308. package/src/components/BoardRulers.tsx +354 -0
  309. package/src/components/CameraInfo.tsx +81 -0
  310. package/src/components/CanvasSceneViewport.tsx +556 -0
  311. package/src/components/CapabilityCoverageSection.tsx +185 -0
  312. package/src/components/CenterDocuments.tsx +22 -0
  313. package/src/components/ChromeSlot.tsx +20 -0
  314. package/src/components/CodeView.tsx +470 -0
  315. package/src/components/CollaborationHeaderControl.css +74 -0
  316. package/src/components/CompactInspectorCard.tsx +190 -0
  317. package/src/components/CompactInspectorShell.tsx +39 -0
  318. package/src/components/ConsolePanel.css +55 -0
  319. package/src/components/ConsolePanel.tsx +270 -0
  320. package/src/components/DefaultEditorLayout.tsx +106 -0
  321. package/src/components/DocumentHeaderStrip.tsx +96 -0
  322. package/src/components/DocumentShelfRail.tsx +45 -0
  323. package/src/components/DocumentThumbnail.tsx +345 -0
  324. package/src/components/EditorLeaseGuard.tsx +372 -0
  325. package/src/components/ErrorBanner.tsx +123 -0
  326. package/src/components/FolderPreviewTile.tsx +159 -0
  327. package/src/components/GameHierarchy.tsx +3842 -0
  328. package/src/components/GameplaySessionTimeline.tsx +295 -0
  329. package/src/components/InspectionProjection.tsx +968 -0
  330. package/src/components/Inspector.tsx +233 -0
  331. package/src/components/InspectorCanvasPreview.tsx +35 -0
  332. package/src/components/InspectorFieldsSection.tsx +290 -0
  333. package/src/components/InspectorObjectPreview.tsx +57 -0
  334. package/src/components/InspectorStoriesSection.tsx +92 -0
  335. package/src/components/InspectorToolSection.tsx +96 -0
  336. package/src/components/InspectorTransformSection.tsx +245 -0
  337. package/src/components/LightExplorerPanel.tsx +433 -0
  338. package/src/components/MediaProperties.tsx +145 -0
  339. package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
  340. package/src/components/Object3DDocumentToolbar.css +100 -0
  341. package/src/components/Object3DDocumentToolbar.tsx +454 -0
  342. package/src/components/Object3DDocumentViewport.tsx +58 -0
  343. package/src/components/ProjectHeader.css +617 -0
  344. package/src/components/ProjectHeader.tsx +328 -0
  345. package/src/components/ProjectLayout.tsx +249 -0
  346. package/src/components/ReactCanvasControls.tsx +284 -0
  347. package/src/components/RootSelectionOverlay.tsx +3162 -0
  348. package/src/components/RootTextEditor.tsx +79 -0
  349. package/src/components/SaveStatus.tsx +70 -0
  350. package/src/components/StageHost.tsx +1954 -0
  351. package/src/components/StageOverlays.tsx +21 -0
  352. package/src/components/StartupErrorScreen.tsx +76 -0
  353. package/src/components/StartupLoadingScreen.tsx +65 -0
  354. package/src/components/StatsOverlay.tsx +78 -0
  355. package/src/components/SurfaceStateOverlay.tsx +24 -0
  356. package/src/components/ToolContributionSurfaces.tsx +65 -0
  357. package/src/components/ToolHost.tsx +370 -0
  358. package/src/components/ToolObject3DPreview.tsx +39 -0
  359. package/src/components/Toolbar.tsx +595 -0
  360. package/src/components/TransientHint.tsx +44 -0
  361. package/src/components/TransportStrip.tsx +174 -0
  362. package/src/components/VersionControlSection.tsx +470 -0
  363. package/src/components/VgaiLogo.css +83 -0
  364. package/src/components/VgaiLogo.tsx +35 -0
  365. package/src/components/ViewportControlsHint.tsx +60 -0
  366. package/src/components/ViewportFurniture.tsx +401 -0
  367. package/src/components/ViewportOverlay.tsx +145 -0
  368. package/src/components/ViewportOverlaysMenu.tsx +153 -0
  369. package/src/components/ViewportShadingMenu.tsx +300 -0
  370. package/src/components/ViewportViewMenu.tsx +101 -0
  371. package/src/components/WorkspaceDocumentSurface.tsx +204 -0
  372. package/src/components/WorkspaceUtilitySurface.tsx +22 -0
  373. package/src/components/WorktreeSwitcher.css +239 -0
  374. package/src/components/WorktreeSwitcher.tsx +890 -0
  375. package/src/components/account-documents.tsx +1162 -0
  376. package/src/components/asset-documents.tsx +812 -0
  377. package/src/components/asset-editor-persistence.ts +216 -0
  378. package/src/components/asset-selection-section.tsx +535 -0
  379. package/src/components/asset-thumbnails.tsx +312 -0
  380. package/src/components/asset-viewers/AudioViewer.tsx +201 -0
  381. package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
  382. package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  383. package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  384. package/src/components/asset-viewers/ImageViewer.tsx +300 -0
  385. package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
  386. package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
  387. package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
  388. package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  389. package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
  390. package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
  391. package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
  392. package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
  393. package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  394. package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
  395. package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  396. package/src/components/asset-viewers/VideoViewer.tsx +101 -0
  397. package/src/components/asset-viewers/shader-source.ts +144 -0
  398. package/src/components/asset-workflow.css +731 -0
  399. package/src/components/board-guides.ts +150 -0
  400. package/src/components/compact-inspector.css +539 -0
  401. package/src/components/core-utilities.tsx +90 -0
  402. package/src/components/editor-notifications.css +40 -0
  403. package/src/components/engine-workspace.css +274 -0
  404. package/src/components/inspector-preview-section.tsx +238 -0
  405. package/src/components/inspector-property-grouping.ts +64 -0
  406. package/src/components/inspector-revert-label.ts +20 -0
  407. package/src/components/inspector-selection.ts +42 -0
  408. package/src/components/inspector-stories-gating.ts +171 -0
  409. package/src/components/inspector-transform-subject.ts +11 -0
  410. package/src/components/inspector-transform.ts +75 -0
  411. package/src/components/kind-documents.tsx +500 -0
  412. package/src/components/palette-action-publisher.tsx +140 -0
  413. package/src/components/primitives/DraftColorInput.tsx +74 -0
  414. package/src/components/product-shell.css +1829 -0
  415. package/src/components/project-tool-documents.tsx +410 -0
  416. package/src/components/scene-documents.tsx +223 -0
  417. package/src/components/stage-keyboard.tsx +37 -0
  418. package/src/components/stage-overlay-set.tsx +107 -0
  419. package/src/components/stage-presence-markers.ts +478 -0
  420. package/src/components/standard-viewport-dressing.ts +417 -0
  421. package/src/components/status-contributions.tsx +403 -0
  422. package/src/components/tool-documents.tsx +309 -0
  423. package/src/components/tool-schema-form.tsx +262 -0
  424. package/src/components/use-after-paint.ts +41 -0
  425. package/src/components/use-project-image-assets.ts +86 -0
  426. package/src/components/viewport-header-controls.css +90 -0
  427. package/src/components/viewport-surface-status.tsx +55 -0
  428. package/src/components/workspace-history.ts +32 -0
  429. package/src/components/workspace-static-panel-registry.tsx +152 -0
  430. package/src/components/workspace-surfaces.css +621 -0
  431. package/src/components/world-documents.tsx +586 -0
  432. package/src/components/world-overlay-gestures.ts +1694 -0
  433. package/src/composite-screenshot.ts +1641 -0
  434. package/src/console-sync.ts +131 -0
  435. package/src/constraint-helper.ts +338 -0
  436. package/src/content-entry-source-registry.ts +184 -0
  437. package/src/coverage/authoring-seam-evidence.ts +300 -0
  438. package/src/coverage/canvas-reveal.ts +192 -0
  439. package/src/coverage/design-time-surfaces.ts +101 -0
  440. package/src/coverage/live-seam-evidence.ts +11 -0
  441. package/src/coverage/ontology-invariants.ts +466 -0
  442. package/src/coverage/session-vitals.ts +501 -0
  443. package/src/coverage/system-seam-evidence.ts +72 -0
  444. package/src/crash-null-boundary.ts +36 -0
  445. package/src/creation-site-edit.ts +1479 -0
  446. package/src/creation-site-registry.ts +160 -0
  447. package/src/delegate-harness-registry.ts +143 -0
  448. package/src/document-context-registry.ts +177 -0
  449. package/src/document-open-registry.ts +200 -0
  450. package/src/document-preview-source.ts +20 -0
  451. package/src/document-renderer-session.ts +138 -0
  452. package/src/editor-api.ts +46 -0
  453. package/src/editor-chrome-capture.ts +139 -0
  454. package/src/editor-commands.ts +164 -0
  455. package/src/editor-console.ts +563 -0
  456. package/src/editor-current-view.ts +82 -0
  457. package/src/editor-document-probe.ts +881 -0
  458. package/src/editor-git-client.ts +115 -0
  459. package/src/editor-host-door.ts +462 -0
  460. package/src/editor-hotkeys.ts +842 -0
  461. package/src/editor-lease-view.ts +39 -0
  462. package/src/editor-lease.ts +415 -0
  463. package/src/editor-mode.ts +19 -0
  464. package/src/editor-notifications.ts +140 -0
  465. package/src/editor-presence.ts +563 -0
  466. package/src/editor-presentation-activity.ts +50 -0
  467. package/src/editor-presentation-notice.ts +42 -0
  468. package/src/editor-runtime.tsx +145 -0
  469. package/src/editor-server-response.ts +86 -0
  470. package/src/editor-session-attribution.ts +75 -0
  471. package/src/editor-session-mode.ts +54 -0
  472. package/src/editor-shell-store.ts +1304 -0
  473. package/src/editor-state-facets.ts +74 -0
  474. package/src/editor-styles.css +33 -0
  475. package/src/editor-view-presentation.ts +810 -0
  476. package/src/editor-viewport.ts +5302 -0
  477. package/src/entity-lod.ts +31 -0
  478. package/src/entity-object.ts +91 -0
  479. package/src/files/file-provider.ts +62 -0
  480. package/src/files/project-files.ts +264 -0
  481. package/src/finders/index.ts +136 -0
  482. package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
  483. package/src/frame/bridge.tsx +1408 -0
  484. package/src/frame/product.ts +77 -0
  485. package/src/gameplay-dom-recording.ts +319 -0
  486. package/src/gameplay-export-state.ts +14 -0
  487. package/src/gameplay-replay.ts +417 -0
  488. package/src/gameplay-session-time.ts +9 -0
  489. package/src/gameplay-sessions.ts +204 -0
  490. package/src/harness-chat-types.ts +253 -0
  491. package/src/hierarchy-component-marks.ts +298 -0
  492. package/src/hierarchy-drop.ts +91 -0
  493. package/src/hierarchy-expansion-state.ts +80 -0
  494. package/src/hierarchy-header-slot.ts +52 -0
  495. package/src/hierarchy-internals.ts +197 -0
  496. package/src/hierarchy-kind-icon.ts +217 -0
  497. package/src/hierarchy-mark-reader.ts +73 -0
  498. package/src/hierarchy-menu-registry.ts +67 -0
  499. package/src/hierarchy-node-rows.ts +307 -0
  500. package/src/hierarchy-panel-view.ts +280 -0
  501. package/src/hierarchy-projection.ts +76 -0
  502. package/src/hierarchy-row-cache.ts +243 -0
  503. package/src/hierarchy-row-model.ts +308 -0
  504. package/src/hierarchy-rows.ts +11 -0
  505. package/src/hierarchy-walk.ts +86 -0
  506. package/src/history/editor-session.ts +25 -0
  507. package/src/history/history-commands.ts +147 -0
  508. package/src/history/history-delegate.ts +163 -0
  509. package/src/history/history-limit-notices.ts +43 -0
  510. package/src/history/history-service.ts +1173 -0
  511. package/src/history/persistence-coordinator.ts +35 -0
  512. package/src/history/project-file-history.ts +386 -0
  513. package/src/history/project-root-history-backends.ts +139 -0
  514. package/src/history/resource-registry.ts +209 -0
  515. package/src/history/snapshot-store.ts +103 -0
  516. package/src/history/source-history-backend.ts +546 -0
  517. package/src/history/types.ts +124 -0
  518. package/src/hmr-registration-group.ts +67 -0
  519. package/src/hmr-stable-react-context.ts +23 -0
  520. package/src/hotkeys.ts +188 -0
  521. package/src/inference-diagnostics.ts +69 -0
  522. package/src/initial-project.ts +80 -0
  523. package/src/inspection/active-subject.ts +578 -0
  524. package/src/inspection/active-surface.ts +142 -0
  525. package/src/inspection/compose.ts +1064 -0
  526. package/src/inspection/display.ts +170 -0
  527. package/src/inspection/document-subject.ts +109 -0
  528. package/src/inspection/game-subject.ts +85 -0
  529. package/src/inspection/model.ts +542 -0
  530. package/src/inspection/null-subject.ts +115 -0
  531. package/src/inspection/serialize.ts +357 -0
  532. package/src/inspection/use-active-inspection.ts +180 -0
  533. package/src/inspector-presentation.ts +201 -0
  534. package/src/inspector-section-registry.ts +221 -0
  535. package/src/instance-source-actions.ts +163 -0
  536. package/src/instanced-presentation.ts +164 -0
  537. package/src/js-heap.ts +71 -0
  538. package/src/key-actions.ts +91 -0
  539. package/src/keymap-presets.ts +401 -0
  540. package/src/layout-policy.ts +31 -0
  541. package/src/learn-links.ts +73 -0
  542. package/src/light-explorer-model.ts +134 -0
  543. package/src/live-canvas-frame.ts +55 -0
  544. package/src/live-document.ts +279 -0
  545. package/src/live-module-source.ts +230 -0
  546. package/src/live-session-registry.ts +220 -0
  547. package/src/live-transition.ts +633 -0
  548. package/src/manifest-project.ts +107 -0
  549. package/src/model-thumbnail.ts +523 -0
  550. package/src/native-selection-style.ts +202 -0
  551. package/src/object3d-document-write-policy.ts +137 -0
  552. package/src/packaged-runtime.ts +108 -0
  553. package/src/palettes/maya.palette.json +57 -0
  554. package/src/palettes/substance.palette.json +57 -0
  555. package/src/pasteboard-module.ts +81 -0
  556. package/src/performance-profiler.ts +367 -0
  557. package/src/performance-sources.ts +69 -0
  558. package/src/play-boot-phase.ts +145 -0
  559. package/src/presentation-surface.ts +248 -0
  560. package/src/project-adapter.ts +1143 -0
  561. package/src/project-asset-refresh.ts +26 -0
  562. package/src/project-declaration-refresh.ts +31 -0
  563. package/src/project-local-state.ts +118 -0
  564. package/src/project-manager.ts +243 -0
  565. package/src/project-module-changes.ts +163 -0
  566. package/src/project-module-split.ts +266 -0
  567. package/src/project-provenance.ts +115 -0
  568. package/src/project-ready.ts +42 -0
  569. package/src/project-session-reset.ts +67 -0
  570. package/src/project-shape.ts +68 -0
  571. package/src/project-tool-discovery.ts +66 -0
  572. package/src/project-tools.ts +107 -0
  573. package/src/project-work-types.ts +149 -0
  574. package/src/projection/three.ts +898 -0
  575. package/src/projection/types.ts +44 -0
  576. package/src/readiness.ts +113 -0
  577. package/src/reflection-probe-helper.ts +142 -0
  578. package/src/reported-play-state.ts +90 -0
  579. package/src/resolve-relative-specifier.ts +33 -0
  580. package/src/scene-document-plan.ts +315 -0
  581. package/src/scene-framing.ts +315 -0
  582. package/src/scene-live-open.ts +210 -0
  583. package/src/scene-view-fog.ts +89 -0
  584. package/src/scoped-game-css.ts +152 -0
  585. package/src/session-orphan-record.ts +193 -0
  586. package/src/session-tombstone.ts +126 -0
  587. package/src/settings/settings-provider.ts +82 -0
  588. package/src/settings-store.ts +345 -0
  589. package/src/shared-view-restore.ts +42 -0
  590. package/src/shell-store-door.ts +45 -0
  591. package/src/source-conflict.ts +122 -0
  592. package/src/spatial-handle-visuals.ts +332 -0
  593. package/src/stage-context.ts +341 -0
  594. package/src/stage-store-registry.ts +63 -0
  595. package/src/stale-chunk-recovery.ts +34 -0
  596. package/src/state-report-deferral.ts +73 -0
  597. package/src/storage/host-files-storage.ts +97 -0
  598. package/src/storage/http-storage.ts +174 -0
  599. package/src/storage/index.ts +75 -0
  600. package/src/storage/mem-storage.ts +149 -0
  601. package/src/storage/path-lock.ts +44 -0
  602. package/src/storage/paths.ts +26 -0
  603. package/src/storage/types.ts +116 -0
  604. package/src/stories/StoryComponentThumbnail.tsx +184 -0
  605. package/src/stories/StoryPreviewMount.tsx +306 -0
  606. package/src/stories/component-content-source.tsx +106 -0
  607. package/src/stories/component-states-source.ts +78 -0
  608. package/src/stories/compose-project-stories.ts +255 -0
  609. package/src/stories/pixi-story-model.ts +30 -0
  610. package/src/stories/prefabs-finder.ts +54 -0
  611. package/src/stories/prefabs-from-stories.ts +182 -0
  612. package/src/stories/project-story-discovery.ts +78 -0
  613. package/src/stories/project-story-regions.ts +24 -0
  614. package/src/stories/story-actions.ts +54 -0
  615. package/src/stories/story-capture-command.ts +124 -0
  616. package/src/stories/story-capture.ts +584 -0
  617. package/src/stories/story-declared-medium.ts +126 -0
  618. package/src/stories/story-discovery.ts +176 -0
  619. package/src/stories/story-document-openers.ts +36 -0
  620. package/src/stories/story-dom-runtime.ts +78 -0
  621. package/src/stories/story-grouping.ts +111 -0
  622. package/src/stories/story-lane.ts +88 -0
  623. package/src/stories/story-mount-turn.ts +27 -0
  624. package/src/stories/story-opener.ts +260 -0
  625. package/src/stories/story-pixi-preview.ts +408 -0
  626. package/src/stories/story-presentation.ts +215 -0
  627. package/src/stories/story-registry.ts +517 -0
  628. package/src/stories/story-three-preview.ts +807 -0
  629. package/src/stories/three-story-model.ts +96 -0
  630. package/src/story-three-preview-runtime.ts +56 -0
  631. package/src/surface-keyboard.ts +112 -0
  632. package/src/surface-state.ts +124 -0
  633. package/src/tab-bootstrap.js +628 -0
  634. package/src/tab-census.ts +198 -0
  635. package/src/tab-lifecycle-client.ts +196 -0
  636. package/src/theme-library.ts +844 -0
  637. package/src/theme-preference.ts +378 -0
  638. package/src/theme.css +3437 -0
  639. package/src/three-viewport/bone-selection-highlight.ts +119 -0
  640. package/src/three-viewport/camera-fit.ts +41 -0
  641. package/src/three-viewport/interactive-renderer.ts +132 -0
  642. package/src/three-viewport/selection-brackets.ts +310 -0
  643. package/src/three-viewport/selection-outline.ts +183 -0
  644. package/src/three-viewport/skeleton-helper.ts +61 -0
  645. package/src/three-viewport/source-color.ts +167 -0
  646. package/src/three-viewport/studio-environment.ts +96 -0
  647. package/src/three-viewport-presentation.ts +23 -0
  648. package/src/tool-contribution-play.ts +74 -0
  649. package/src/tool-loader.ts +1798 -0
  650. package/src/transform-mode-request.ts +57 -0
  651. package/src/transient-hint.ts +78 -0
  652. package/src/trigger-volume-helper.ts +116 -0
  653. package/src/ui-source/adapter-region-includes.ts +241 -0
  654. package/src/ui-source/ensure-import.ts +132 -0
  655. package/src/ui-source/file-region-resolver.ts +302 -0
  656. package/src/ui-source/inspect.ts +775 -0
  657. package/src/ui-source/oid-transform.ts +1845 -0
  658. package/src/ui-source/plan-csf-story.ts +196 -0
  659. package/src/ui-source/plan-extract-component.ts +421 -0
  660. package/src/ui-source/plan-fork-component.ts +621 -0
  661. package/src/ui-source/plan-named-style.ts +95 -0
  662. package/src/ui-source/plan-source-edit.ts +353 -0
  663. package/src/ui-source/r3f-contract-resolver.ts +327 -0
  664. package/src/ui-source/r3f-diagnostic-index.ts +165 -0
  665. package/src/ui-source/r3f-environment-binding.ts +133 -0
  666. package/src/ui-source/r3f-joint-binding.ts +251 -0
  667. package/src/ui-source/r3f-lod-binding.ts +91 -0
  668. package/src/ui-source/r3f-particle-binding.ts +235 -0
  669. package/src/ui-source/r3f-physics-binding.ts +483 -0
  670. package/src/ui-source/r3f-project-contracts.ts +444 -0
  671. package/src/ui-source/relative-import-specifier.ts +34 -0
  672. package/src/ui-source/reparent-guard.ts +544 -0
  673. package/src/ui-source/source-edit-request.ts +97 -0
  674. package/src/ui-source/source-write-backend.ts +618 -0
  675. package/src/ui-source/syntactic-prop-specs.ts +296 -0
  676. package/src/ui-source/tier-source-write-backend.ts +279 -0
  677. package/src/ui-source/ts-ast.ts +171 -0
  678. package/src/ui-source/utility-class-support.ts +169 -0
  679. package/src/ui-source/write-component-default.ts +133 -0
  680. package/src/ui-source/writer.ts +1989 -0
  681. package/src/viewport-activation-timings.ts +840 -0
  682. package/src/viewport-authoring-policy.ts +186 -0
  683. package/src/viewport-controls-hint.ts +57 -0
  684. package/src/viewport-door.ts +237 -0
  685. package/src/viewport-shading-boundary.ts +12 -0
  686. package/src/vite-error-surface.ts +141 -0
  687. package/src/wait-until.ts +37 -0
  688. package/src/workspace-areas.ts +156 -0
  689. package/src/workspace-aux-commands.ts +11 -0
  690. package/src/workspace-available-documents.ts +127 -0
  691. package/src/workspace-core-utilities.ts +31 -0
  692. package/src/workspace-document-ids.ts +59 -0
  693. package/src/workspace-document-registry.ts +612 -0
  694. package/src/workspace-document-restore.ts +145 -0
  695. package/src/workspace-host-commands.ts +141 -0
  696. package/src/workspace-persistence-gate.ts +40 -0
  697. package/src/workspace-play-utilities.ts +44 -0
  698. package/src/workspace-presets.ts +425 -0
  699. package/src/workspace-regions.ts +294 -0
  700. package/src/workspace-state-persistence.ts +536 -0
  701. package/src/workspace-static-panels.ts +73 -0
  702. package/src/workspace-status-registry.ts +121 -0
  703. package/src/workspace-style.ts +239 -0
  704. package/src/workspace-utility-commands.ts +74 -0
  705. package/src/workspace-utility-registry.ts +263 -0
  706. package/src/workspace-viewport-rect.ts +97 -0
  707. package/src/world-adoption.ts +115 -0
  708. package/src/world-document-routing.ts +104 -0
  709. package/vite-plugin-creation-site-write.ts +184 -0
  710. package/vite-plugin-creation-site.ts +80 -0
  711. package/vite-plugin-game-static.ts +303 -0
  712. package/vite-plugin-module-doorways.ts +336 -0
  713. package/vite-plugin-product-contributions.ts +197 -0
  714. package/vite-plugin-project-game-static.ts +125 -0
  715. package/vite-plugin-project-jsx-js.ts +65 -0
  716. package/vite-plugin-project-root-absolute-assets.ts +359 -0
  717. package/vite-plugin-shared-react.ts +420 -0
  718. package/vite-plugin-shared-three.ts +203 -0
  719. package/vite-plugin-ui-oid.ts +2106 -0
@@ -0,0 +1,844 @@
1
+ import type { EditorPalette } from '@volter/editor-sdk/widgets';
2
+ import {
3
+ type EditorThemeId,
4
+ editorThemes,
5
+ graphiteDarkEditorTheme,
6
+ isEditorThemeId,
7
+ } from '@volter/editor-sdk/widgets';
8
+ import { activeProjectKey, getCurrentProject, onProjectChange } from './active-project';
9
+ import type { SettingsLayer } from './api/settings';
10
+ import {
11
+ deleteThemeDocument,
12
+ listThemeDocuments,
13
+ saveThemeDocument,
14
+ type ThemeLayerRead,
15
+ } from './api/themes';
16
+ import { editorConsole } from './editor-console';
17
+ import mayaPalette from './palettes/maya.palette.json';
18
+ import substancePalette from './palettes/substance.palette.json';
19
+
20
+ // Custom themes are PALETTE DOCUMENTS, one file per theme, named by id:
21
+ // `~/.vgai/themes/<id>.json` (the user layer, where the Theme Manager
22
+ // writes) and `<project>/.vgai/themes/<id>.json` (the project layer, a
23
+ // theme a project ships to everyone who opens it; on an id collision the
24
+ // project's wins). `api/themes.ts` is the wire; a file that fails the
25
+ // contract below is reported by path and skipped, never silently dropped.
26
+ // v3 makes the file's real role explicit: it is a PALETTE document. Material
27
+ // treatment, shape, and elevation are resolved from the independent material
28
+ // axis and can no longer leak through an imported color theme. v3 is the ONLY
29
+ // accepted version: the v2 read path (which projected a Glass-era theme's
30
+ // `treatment.contentOnBright` onto `color.content.onBright.*` and swapped
31
+ // translucent surfaces for the opaque Classic ramp) was REMOVED — a v2 file
32
+ // is now rejected loudly, naming the removal.
33
+ export const CUSTOM_EDITOR_THEME_SCHEMA_VERSION = 3 as const;
34
+
35
+ export interface CustomEditorThemeDocument {
36
+ readonly schemaVersion: typeof CUSTOM_EDITOR_THEME_SCHEMA_VERSION;
37
+ readonly name: string;
38
+ readonly theme: EditorPalette;
39
+ }
40
+
41
+ export interface EditorThemeChoice {
42
+ readonly id: string;
43
+ readonly name: string;
44
+ readonly kind: 'built-in' | 'custom';
45
+ readonly theme: EditorPalette;
46
+ }
47
+
48
+ /** Built-in PALETTES. Glass/Classic is a separate material axis. */
49
+ export const BUILT_IN_EDITOR_PALETTE_IDS = [
50
+ 'graphite-dark',
51
+ 'graphite-neutral',
52
+ 'midnight-high-contrast',
53
+ ] as const satisfies readonly EditorThemeId[];
54
+
55
+ const BUILT_IN_NAMES: Record<(typeof BUILT_IN_EDITOR_PALETTE_IDS)[number], string> = {
56
+ 'graphite-dark': 'Classic Graphite',
57
+ 'graphite-neutral': 'Graphite Neutral',
58
+ 'midnight-high-contrast': 'Midnight Blue',
59
+ };
60
+
61
+ /**
62
+ * SHIPPED PALETTE DOCUMENTS — the v3 files under `palettes/`, byte-for-byte
63
+ * what a user's own import is, read through the same parser. Data, no code:
64
+ * a palette that names another tool's look (Blender's greys and widget blue,
65
+ * Maya's mid-grey ground, Substance's charcoal) is a document, and the style
66
+ * bundles (`workspace-style.ts`) are what pair each with a material,
67
+ * composition and region set. They list as built-in: not editable in place,
68
+ * duplicable like any other.
69
+ */
70
+ const SHIPPED_PALETTE_DOCUMENTS: readonly unknown[] = [mayaPalette, substancePalette];
71
+ let cachedShipped: readonly CustomEditorThemeDocument[] | null = null;
72
+ /** Palettes a package's `workspace.style` contribution carries (Blender's
73
+ * ships with `@volter/editor-blender`): parsed through the same v3 reader as a person's
74
+ * import, listed beside the shipped documents for as long as the
75
+ * contribution is registered. */
76
+ const contributedPaletteDocuments: CustomEditorThemeDocument[] = [];
77
+ function shippedPalettes(): readonly CustomEditorThemeDocument[] {
78
+ cachedShipped ??= [
79
+ ...SHIPPED_PALETTE_DOCUMENTS.map((document) => parseCustomEditorThemeDocument(document)),
80
+ ...contributedPaletteDocuments,
81
+ ];
82
+ return cachedShipped;
83
+ }
84
+
85
+ /**
86
+ * Register a palette document a contribution carries. Parsed strictly (a bad
87
+ * document throws here, at registration, with the reader's own message).
88
+ * Returns the unregister.
89
+ */
90
+ export function registerContributedPalette(document: unknown): () => void {
91
+ const parsed = parseCustomEditorThemeDocument(document);
92
+ if (shippedPalettes().some((entry) => entry.theme.id === parsed.theme.id))
93
+ throw new Error(
94
+ `registerContributedPalette: palette "${parsed.theme.id}" is already registered.`,
95
+ );
96
+ contributedPaletteDocuments.push(parsed);
97
+ cachedShipped = null;
98
+ emit();
99
+ return () => {
100
+ const index = contributedPaletteDocuments.indexOf(parsed);
101
+ if (index === -1) return;
102
+ contributedPaletteDocuments.splice(index, 1);
103
+ cachedShipped = null;
104
+ emit();
105
+ };
106
+ }
107
+
108
+ /** A palette the editor ships as a document — see {@link SHIPPED_PALETTE_DOCUMENTS}. */
109
+ export function isShippedPaletteId(id: string): boolean {
110
+ return shippedPalettes().some((document) => document.theme.id === id);
111
+ }
112
+
113
+ const THEME_STRING_PATHS = [
114
+ 'id',
115
+ 'color.surface.shell',
116
+ 'color.surface.panel',
117
+ 'color.surface.chrome',
118
+ 'color.surface.raised',
119
+ 'color.surface.inset',
120
+ 'color.surface.overlay',
121
+ 'color.boundary.default',
122
+ 'color.boundary.strong',
123
+ 'color.boundary.area',
124
+ 'color.boundary.indent',
125
+ 'color.boundary.divider',
126
+ 'color.content.primary',
127
+ 'color.content.muted',
128
+ 'color.content.dim',
129
+ 'color.content.onAccent',
130
+ 'color.content.menu',
131
+ 'color.content.status',
132
+ 'color.content.placeholder',
133
+ 'color.content.active',
134
+ 'color.content.selected',
135
+ 'color.content.onBright.primary',
136
+ 'color.content.onBright.muted',
137
+ 'color.content.onBright.dim',
138
+ 'color.accent.default',
139
+ 'color.accent.muted',
140
+ 'color.semantic.danger',
141
+ 'color.semantic.dangerMuted',
142
+ 'color.semantic.dangerFaint',
143
+ 'color.semantic.warning',
144
+ 'color.semantic.warningMuted',
145
+ 'color.semantic.success',
146
+ 'color.semantic.successMuted',
147
+ 'color.semantic.dynamic',
148
+ 'color.semantic.dynamicMuted',
149
+ 'color.semantic.instance',
150
+ 'color.neutralOverlay.hover',
151
+ 'color.neutralOverlay.active',
152
+ 'color.scrim',
153
+ 'color.viewport.background',
154
+ 'color.viewport.grid',
155
+ 'color.viewport.axisX',
156
+ 'color.viewport.axisY',
157
+ 'color.viewport.selection',
158
+ 'color.viewport.active',
159
+ 'color.widget.regular',
160
+ 'color.widget.menu',
161
+ 'color.widget.field',
162
+ 'color.widget.emboss',
163
+ 'color.category.object',
164
+ 'color.category.modifier',
165
+ 'color.category.material',
166
+ 'color.category.tool',
167
+ 'color.category.operator',
168
+ 'color.category.data',
169
+ 'color.region.outliner',
170
+ 'color.region.properties',
171
+ 'typography.sans',
172
+ 'typography.mono',
173
+ ] as const;
174
+
175
+ /**
176
+ * Palette fields introduced AFTER schema v3 shipped. A document written
177
+ * before the field existed stays valid and projects onto the graphite
178
+ * default; rejecting it would silently drop a user's saved palette from the
179
+ * library (`recordsFrom` names and skips a file whose parse throws). This is
180
+ * FORWARD compatibility for fields added after v3 — not a second schema
181
+ * version. A value that IS present is still validated as a CSS color.
182
+ */
183
+ const POST_V3_OPTIONAL_STRING_PATHS: ReadonlySet<string> = new Set([
184
+ 'color.semantic.instance',
185
+ 'color.content.menu',
186
+ 'color.content.status',
187
+ 'color.content.placeholder',
188
+ 'color.content.active',
189
+ 'color.content.selected',
190
+ 'color.boundary.area',
191
+ 'color.boundary.indent',
192
+ 'color.boundary.divider',
193
+ 'color.viewport.background',
194
+ 'color.viewport.grid',
195
+ 'color.viewport.axisX',
196
+ 'color.viewport.axisY',
197
+ 'color.viewport.selection',
198
+ 'color.viewport.active',
199
+ 'color.widget.regular',
200
+ 'color.widget.menu',
201
+ 'color.widget.field',
202
+ 'color.widget.emboss',
203
+ 'color.category.object',
204
+ 'color.category.modifier',
205
+ 'color.category.material',
206
+ 'color.category.tool',
207
+ 'color.category.operator',
208
+ 'color.category.data',
209
+ 'color.region.outliner',
210
+ 'color.region.properties',
211
+ ]);
212
+
213
+ function cssPropertyForThemePath(path: string): string | null {
214
+ if (path.startsWith('color.')) return 'color';
215
+ if (path.startsWith('typography.')) return 'font-family';
216
+ return null;
217
+ }
218
+
219
+ function validCssValue(property: string, value: string): boolean {
220
+ if (typeof CSS !== 'undefined' && typeof CSS.supports === 'function') {
221
+ return CSS.supports(property, value);
222
+ }
223
+ if (typeof document !== 'undefined') {
224
+ const element = document.createElement('div');
225
+ element.style.setProperty(property, value);
226
+ return element.style.getPropertyValue(property) !== '';
227
+ }
228
+ // Import-safe server fallback: the browser performs the authoritative CSS
229
+ // validation before a user can save or apply the theme.
230
+ return true;
231
+ }
232
+
233
+ let cachedChoices: readonly EditorThemeChoice[] | null = null;
234
+ const listeners = new Set<() => void>();
235
+
236
+ interface ThemeRecord {
237
+ readonly layer: SettingsLayer;
238
+ readonly document: CustomEditorThemeDocument;
239
+ }
240
+ let userRecords: readonly ThemeRecord[] = [];
241
+ let projectRecords: readonly ThemeRecord[] = [];
242
+ let userLoaded = false;
243
+ let projectLoadedFor: string | null = null;
244
+ let userLoading: Promise<void> | null = null;
245
+ let projectLoading: Promise<void> | null = null;
246
+
247
+ function valueAtPath(value: unknown, path: string): unknown {
248
+ let current = value;
249
+ for (const key of path.split('.')) {
250
+ if (!current || typeof current !== 'object') return undefined;
251
+ current = (current as Record<string, unknown>)[key];
252
+ }
253
+ return current;
254
+ }
255
+
256
+ function validateStringPaths(theme: unknown, paths: readonly string[]): void {
257
+ for (const path of paths) {
258
+ const field = valueAtPath(theme, path);
259
+ if (field === undefined && POST_V3_OPTIONAL_STRING_PATHS.has(path)) continue;
260
+ if (typeof field !== 'string' || !field.trim()) {
261
+ throw new Error(`Theme field “${path}” must be a non-empty string.`);
262
+ }
263
+ const property = cssPropertyForThemePath(path);
264
+ if (property && !validCssValue(property, field)) {
265
+ throw new Error(`Theme field “${path}” is not a valid CSS ${property} value.`);
266
+ }
267
+ }
268
+ }
269
+
270
+ export function parseCustomEditorThemeDocument(value: unknown): CustomEditorThemeDocument {
271
+ if (!value || typeof value !== 'object') throw new Error('Theme file must contain an object.');
272
+ const record = value as Record<string, unknown>;
273
+ const schemaVersion = record['schemaVersion'];
274
+ if (schemaVersion !== CUSTOM_EDITOR_THEME_SCHEMA_VERSION) {
275
+ throw new Error(
276
+ `Unsupported theme schema version ${JSON.stringify(schemaVersion)}. Expected ` +
277
+ `${CUSTOM_EDITOR_THEME_SCHEMA_VERSION}. Schema v2 (the Glass-era theme document, with ` +
278
+ '`treatment`/`shape`/`elevation` and no `color.content.onBright.*`) was REMOVED along ' +
279
+ 'with the read-time projection that accepted it — re-export the palette from the ' +
280
+ 'editor to get a v3 file.',
281
+ );
282
+ }
283
+ if (typeof record['name'] !== 'string' || !record['name'].trim()) {
284
+ throw new Error('Theme name must be a non-empty string.');
285
+ }
286
+ if (record['name'].trim().length > 120) {
287
+ throw new Error('Theme name must be 120 characters or fewer.');
288
+ }
289
+ validateStringPaths(record['theme'], THEME_STRING_PATHS);
290
+ validateOpaqueClassicSurfaces(record['theme']);
291
+ return {
292
+ schemaVersion: CUSTOM_EDITOR_THEME_SCHEMA_VERSION,
293
+ name: record['name'].trim(),
294
+ theme: reconstructEditorPalette(record['theme']),
295
+ };
296
+ }
297
+
298
+ const CLASSIC_OPAQUE_SURFACE_PATHS = [
299
+ 'color.surface.shell',
300
+ 'color.surface.panel',
301
+ 'color.surface.chrome',
302
+ 'color.surface.raised',
303
+ 'color.surface.inset',
304
+ ] as const;
305
+
306
+ function isDefinitelyOpaqueColor(value: string): boolean {
307
+ const trimmed = value.trim();
308
+ if (/^#[0-9a-f]{3}$/i.test(trimmed)) return true;
309
+ const shortHexWithAlpha = /^#[0-9a-f]{3}([0-9a-f])$/i.exec(trimmed);
310
+ if (shortHexWithAlpha) return shortHexWithAlpha[1]?.toLowerCase() === 'f';
311
+ const hexWithAlpha = /^#[0-9a-f]{6}([0-9a-f]{2})$/i.exec(trimmed);
312
+ if (hexWithAlpha) return hexWithAlpha[1]?.toLowerCase() === 'ff';
313
+ const rgb = parseRgb(trimmed);
314
+ if (rgb) return rgb.a === 1;
315
+ if (/^hsl\(/i.test(trimmed) && !trimmed.includes('/')) return true;
316
+ return /^[a-z]+$/i.test(trimmed) && trimmed.toLowerCase() !== 'transparent';
317
+ }
318
+
319
+ function validateOpaqueClassicSurfaces(value: unknown): void {
320
+ for (const path of CLASSIC_OPAQUE_SURFACE_PATHS) {
321
+ const field = valueAtPath(value, path) as string;
322
+ if (!isDefinitelyOpaqueColor(field)) {
323
+ throw new Error(
324
+ `Palette field “${path}” must be an opaque color because it paints the Classic material.`,
325
+ );
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Reconstruct exactly the palette contract — every field is read from a known
332
+ * path, so no material treatment, shape or elevation an input object happens
333
+ * to carry is ever retained.
334
+ */
335
+ function reconstructEditorPalette(value: unknown): EditorPalette {
336
+ const get = (path: string): string => valueAtPath(value, path) as string;
337
+ const onBright = {
338
+ primary: get('color.content.onBright.primary'),
339
+ muted: get('color.content.onBright.muted'),
340
+ dim: get('color.content.onBright.dim'),
341
+ };
342
+ // The post-v3 VIEWPORT group. It is reconstructed here or it does not
343
+ // survive at all: this function rebuilds the palette field by field, so a
344
+ // group missing from it is dropped from every palette that arrives as a
345
+ // DOCUMENT — which is every contributed palette. Measured live: the Blender
346
+ // palette's `color.viewport` reached the library and vanished here, so
347
+ // `--vgai-viewport-*` emitted empty, `nativeViewportLook` read all-null, and
348
+ // the 3D viewport kept its dressing gradient, its 0x999999 grid and no axis
349
+ // lines under a palette that names all three.
350
+ const viewportKeys = ['background', 'grid', 'axisX', 'axisY', 'selection', 'active'] as const;
351
+ const viewportValues = viewportKeys.map(
352
+ (key) => valueAtPath(value, `color.viewport.${key}`) as string | undefined,
353
+ );
354
+ const viewportPresent = viewportValues.filter((entry) => entry !== undefined).length;
355
+ if (viewportPresent !== 0 && viewportPresent !== viewportKeys.length) {
356
+ throw new Error(
357
+ `A palette's color.viewport group is all-or-nothing: ${viewportKeys
358
+ .filter((_, index) => viewportValues[index] === undefined)
359
+ .join(', ')} missing. Omit the group to keep the editor's own viewport colours.`,
360
+ );
361
+ }
362
+ const viewport =
363
+ viewportPresent === viewportKeys.length
364
+ ? (Object.fromEntries(
365
+ viewportKeys.map((key, index) => [key, viewportValues[index] as string]),
366
+ ) as unknown as NonNullable<EditorPalette['color']['viewport']>)
367
+ : undefined;
368
+ // The post-v3 WIDGET group (`EditorTheme.color.widget`). Same reason as
369
+ // `viewport` above: a group this function does not rebuild is dropped from
370
+ // every palette that arrives as a document. Unlike `viewport` it is NOT
371
+ // all-or-nothing — each class falls back to the surface its call sites read
372
+ // before the group existed, so naming one (Blender names the menu well
373
+ // without needing to restate the pushbutton) is a complete, valid palette.
374
+ // `emboss` is the line UNDER a widget rather than a fill, and it is in this
375
+ // list for exactly the same reason as the three fills: a member the rebuild
376
+ // does not name is dropped.
377
+ const widgetKeys = ['regular', 'menu', 'field', 'emboss'] as const;
378
+ const widgetEntries = widgetKeys
379
+ .map((key) => [key, valueAtPath(value, `color.widget.${key}`) as string | undefined] as const)
380
+ .filter(
381
+ (entry): entry is readonly [(typeof widgetKeys)[number], string] => entry[1] !== undefined,
382
+ );
383
+ const widget =
384
+ widgetEntries.length === 0
385
+ ? undefined
386
+ : (Object.fromEntries(widgetEntries) as NonNullable<EditorPalette['color']['widget']>);
387
+ // The post-v3 CATEGORY group (`EditorTheme.color.category`) — the glyph
388
+ // inks. Rebuilt here for the same reason as the two groups above: this
389
+ // function reconstructs field by field, so a group it does not name is
390
+ // dropped from every palette that arrives as a document, and the Blender
391
+ // style's whole colour channel is contributed as a document. Per-member
392
+ // optional like `widget`, never all-or-nothing: each token falls back to
393
+ // `currentColor`, so a palette naming one category and no other is
394
+ // complete and paints the rest exactly as a monochrome set does.
395
+ const categoryKeys = ['object', 'modifier', 'material', 'tool', 'operator', 'data'] as const;
396
+ const categoryEntries = categoryKeys
397
+ .map((key) => [key, valueAtPath(value, `color.category.${key}`) as string | undefined] as const)
398
+ .filter(
399
+ (entry): entry is readonly [(typeof categoryKeys)[number], string] => entry[1] !== undefined,
400
+ );
401
+ const category =
402
+ categoryEntries.length === 0
403
+ ? undefined
404
+ : (Object.fromEntries(categoryEntries) as NonNullable<EditorPalette['color']['category']>);
405
+ // The post-v3 REGION group (`EditorTheme.color.region`) — the per-EDITOR-AREA
406
+ // fills. Rebuilt here for the same reason as the three groups above, and
407
+ // per-member optional the same way: each region falls back to the surfaces
408
+ // its call sites already read, so a palette naming one area and no other is
409
+ // complete and paints every other group exactly as before.
410
+ const regionKeys = ['outliner', 'properties'] as const;
411
+ const regionEntries = regionKeys
412
+ .map((key) => [key, valueAtPath(value, `color.region.${key}`) as string | undefined] as const)
413
+ .filter(
414
+ (entry): entry is readonly [(typeof regionKeys)[number], string] => entry[1] !== undefined,
415
+ );
416
+ const region =
417
+ regionEntries.length === 0
418
+ ? undefined
419
+ : (Object.fromEntries(regionEntries) as NonNullable<EditorPalette['color']['region']>);
420
+ const surface = Object.fromEntries(
421
+ ['shell', 'panel', 'chrome', 'raised', 'inset', 'overlay'].map((key) => [
422
+ key,
423
+ get(`color.surface.${key}`),
424
+ ]),
425
+ ) as unknown as EditorPalette['color']['surface'];
426
+
427
+ return {
428
+ id: get('id'),
429
+ color: {
430
+ surface,
431
+ boundary: {
432
+ default: get('color.boundary.default'),
433
+ strong: get('color.boundary.strong'),
434
+ // The post-v3 AREA seam. Rebuilt here for the same reason as the
435
+ // three optional GROUPS below: this function reconstructs field by
436
+ // field, so a member it does not name is dropped from every palette
437
+ // that arrives as a document — which is every contributed palette,
438
+ // the Blender style's included. Spread rather than assigned so a
439
+ // palette without it carries no `area` key at all, and the emitter's
440
+ // `?? boundary.default` fallback is what every other skin reads.
441
+ ...(typeof valueAtPath(value, 'color.boundary.area') === 'string'
442
+ ? { area: valueAtPath(value, 'color.boundary.area') as string }
443
+ : {}),
444
+ // The tree's INDENT GUIDE, spread for the identical reason: a member
445
+ // this rebuild does not name is dropped from every palette that
446
+ // arrives as a document, and the Blender style's palette is one.
447
+ // Absent here means the emitted token is `transparent` and no tree in
448
+ // the editor draws a guide — see `theme.ts`'s `boundary.indent`.
449
+ ...(typeof valueAtPath(value, 'color.boundary.indent') === 'string'
450
+ ? { indent: valueAtPath(value, 'color.boundary.indent') as string }
451
+ : {}),
452
+ // The DIVIDER rule, spread for the identical reason: a member this
453
+ // rebuild does not name is dropped from every palette that arrives as
454
+ // a document, the Blender style's included. Absent here means the
455
+ // emitted token is `boundary.default` and the rule paints what it
456
+ // always painted — see `theme.ts`'s `boundary.divider`.
457
+ ...(typeof valueAtPath(value, 'color.boundary.divider') === 'string'
458
+ ? { divider: valueAtPath(value, 'color.boundary.divider') as string }
459
+ : {}),
460
+ },
461
+ content: {
462
+ primary: get('color.content.primary'),
463
+ muted: get('color.content.muted'),
464
+ dim: get('color.content.dim'),
465
+ onAccent: get('color.content.onAccent'),
466
+ // The post-v3 MENU-WORD ink, spread for the reason every optional
467
+ // member below is: this function rebuilds field by field, so a member
468
+ // it does not name is silently dropped from every palette that arrives
469
+ // as a DOCUMENT — which is every contributed palette, the Blender
470
+ // style's included. Absent here means the emitter's
471
+ // `?? content.primary` fallback is what the menu words read.
472
+ ...(typeof valueAtPath(value, 'color.content.menu') === 'string'
473
+ ? { menu: valueAtPath(value, 'color.content.menu') as string }
474
+ : {}),
475
+ // The STATUS band's ink and the PLACEHOLDER's, spread for the same
476
+ // reason — a member this rebuild does not name is dropped from every
477
+ // contributed palette, and the Blender style's is one.
478
+ ...(typeof valueAtPath(value, 'color.content.status') === 'string'
479
+ ? { status: valueAtPath(value, 'color.content.status') as string }
480
+ : {}),
481
+ ...(typeof valueAtPath(value, 'color.content.placeholder') === 'string'
482
+ ? { placeholder: valueAtPath(value, 'color.content.placeholder') as string }
483
+ : {}),
484
+ // The post-v3 ACTIVE ink, spread rather than assigned for the same
485
+ // reason `boundary.area` is: this function rebuilds field by field, so
486
+ // an unnamed member is dropped from every palette that arrives as a
487
+ // DOCUMENT — which is every contributed palette, the Blender style's
488
+ // included. Absent here means absent in the emitted token, and the
489
+ // hierarchy row's own fallback is what every other skin reads.
490
+ ...(typeof valueAtPath(value, 'color.content.active') === 'string'
491
+ ? { active: valueAtPath(value, 'color.content.active') as string }
492
+ : {}),
493
+ // Its other half — the SELECTED-but-not-active ink — spread the same
494
+ // way and for the same reason. Absent here means absent in the emitted
495
+ // token, and the emitter falls it back to `active`.
496
+ ...(typeof valueAtPath(value, 'color.content.selected') === 'string'
497
+ ? { selected: valueAtPath(value, 'color.content.selected') as string }
498
+ : {}),
499
+ onBright,
500
+ },
501
+ accent: {
502
+ default: get('color.accent.default'),
503
+ muted: get('color.accent.muted'),
504
+ },
505
+ semantic: {
506
+ danger: get('color.semantic.danger'),
507
+ dangerMuted: get('color.semantic.dangerMuted'),
508
+ dangerFaint: get('color.semantic.dangerFaint'),
509
+ warning: get('color.semantic.warning'),
510
+ warningMuted: get('color.semantic.warningMuted'),
511
+ success: get('color.semantic.success'),
512
+ successMuted: get('color.semantic.successMuted'),
513
+ dynamic: get('color.semantic.dynamic'),
514
+ dynamicMuted: get('color.semantic.dynamicMuted'),
515
+ // Post-v3 field: absent in documents saved before instance identity
516
+ // existed, so it projects onto the graphite default rather than
517
+ // producing `undefined` in a palette typed as all-strings.
518
+ instance:
519
+ (valueAtPath(value, 'color.semantic.instance') as string | undefined) ??
520
+ graphiteDarkEditorTheme.color.semantic.instance,
521
+ },
522
+ neutralOverlay: {
523
+ hover: get('color.neutralOverlay.hover'),
524
+ active: get('color.neutralOverlay.active'),
525
+ },
526
+ scrim: get('color.scrim'),
527
+ ...(viewport ? { viewport } : {}),
528
+ ...(widget ? { widget } : {}),
529
+ ...(category ? { category } : {}),
530
+ ...(region ? { region } : {}),
531
+ },
532
+ typography: { sans: get('typography.sans'), mono: get('typography.mono') },
533
+ };
534
+ }
535
+
536
+ interface Rgb {
537
+ readonly r: number;
538
+ readonly g: number;
539
+ readonly b: number;
540
+ readonly a: number;
541
+ }
542
+
543
+ function parseRgb(value: string): Rgb | null {
544
+ const hex = /^#([0-9a-f]{6})$/i.exec(value.trim())?.[1];
545
+ if (hex) {
546
+ return {
547
+ r: Number.parseInt(hex.slice(0, 2), 16),
548
+ g: Number.parseInt(hex.slice(2, 4), 16),
549
+ b: Number.parseInt(hex.slice(4, 6), 16),
550
+ a: 1,
551
+ };
552
+ }
553
+ const rgb =
554
+ /^rgba?\(\s*(\d+(?:\.\d+)?)\s*(?:,\s*|\s+)(\d+(?:\.\d+)?)\s*(?:,\s*|\s+)(\d+(?:\.\d+)?)(?:\s*(?:,|\/)\s*(\d+(?:\.\d+)?%?))?\s*\)$/i.exec(
555
+ value.trim(),
556
+ );
557
+ if (!rgb) return null;
558
+ const [r, g, b] = rgb.slice(1, 4).map(Number);
559
+ const alphaToken = rgb[4];
560
+ const a = alphaToken?.endsWith('%')
561
+ ? Number(alphaToken.slice(0, -1)) / 100
562
+ : Number(alphaToken ?? 1);
563
+ return r! <= 255 && g! <= 255 && b! <= 255 && a >= 0 && a <= 1
564
+ ? { r: r!, g: g!, b: b!, a }
565
+ : null;
566
+ }
567
+
568
+ function relativeLuminance(color: Rgb): number {
569
+ const channels = [color.r, color.g, color.b].map((channel) => {
570
+ const value = channel / 255;
571
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
572
+ });
573
+ return channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722;
574
+ }
575
+
576
+ function contrastRatio(foreground: string, background: string): number | null {
577
+ const fg = parseRgb(foreground);
578
+ const bg = parseRgb(background);
579
+ // A translucent pair has no single WCAG ratio until the live scene behind
580
+ // it is known. Treat it as backdrop-dependent instead of silently dropping
581
+ // alpha and reporting a precise-but-false opaque ratio in Theme Manager.
582
+ if (!fg || !bg || fg.a < 1 || bg.a < 1) return null;
583
+ const lighter = Math.max(relativeLuminance(fg), relativeLuminance(bg));
584
+ const darker = Math.min(relativeLuminance(fg), relativeLuminance(bg));
585
+ return (lighter + 0.05) / (darker + 0.05);
586
+ }
587
+
588
+ /** Human-readable warnings for essential editor-chrome WCAG AA pairs. */
589
+ export function editorThemeContrastWarnings(theme: EditorPalette): readonly string[] {
590
+ const pairs = [
591
+ ['Primary text', theme.color.content.primary, theme.color.surface.panel],
592
+ ['Muted text', theme.color.content.muted, theme.color.surface.panel],
593
+ ['Dim text', theme.color.content.dim, theme.color.surface.panel],
594
+ ['Text on accent', theme.color.content.onAccent, theme.color.accent.default],
595
+ ['Danger text', theme.color.semantic.danger, theme.color.surface.panel],
596
+ ] as const;
597
+ return pairs.flatMap(([label, foreground, background]) => {
598
+ const ratio = contrastRatio(foreground, background);
599
+ return ratio !== null && ratio < 4.5 ? [`${label} contrast is ${ratio.toFixed(2)}:1`] : [];
600
+ });
601
+ }
602
+
603
+ function recordsFrom(layer: SettingsLayer, read: ThemeLayerRead): ThemeRecord[] {
604
+ const records: ThemeRecord[] = [];
605
+ for (const issue of read.issues) {
606
+ editorConsole.warn(`${layer} theme ${issue} — skipped`, 'settings');
607
+ }
608
+ for (const entry of read.documents) {
609
+ try {
610
+ const document = parseCustomEditorThemeDocument(entry.document);
611
+ if (document.theme.id !== entry.id) {
612
+ throw new Error(`the file is named ${entry.id} but its palette id is ${document.theme.id}`);
613
+ }
614
+ if (isEditorThemeId(document.theme.id) || isShippedPaletteId(document.theme.id)) {
615
+ throw new Error('a built-in palette id cannot be redefined');
616
+ }
617
+ records.push({ layer, document });
618
+ } catch (cause) {
619
+ // One malformed file must not make every saved theme unavailable —
620
+ // and it is named, so the person who wrote it can fix it.
621
+ editorConsole.warn(
622
+ `${layer} theme ${entry.path}: ${(cause as Error).message} — skipped`,
623
+ 'settings',
624
+ );
625
+ }
626
+ }
627
+ return records;
628
+ }
629
+
630
+ function projectKey(): string {
631
+ return activeProjectKey();
632
+ }
633
+
634
+ function loadUser(): Promise<void> {
635
+ if (userLoaded) return Promise.resolve();
636
+ userLoading ??= listThemeDocuments('user')
637
+ .then((read) => {
638
+ userRecords = recordsFrom('user', read);
639
+ })
640
+ .catch(() => {
641
+ userRecords = [];
642
+ })
643
+ .finally(() => {
644
+ userLoaded = true;
645
+ userLoading = null;
646
+ });
647
+ return userLoading;
648
+ }
649
+
650
+ function loadProject(): Promise<void> {
651
+ const key = projectKey();
652
+ if (projectLoadedFor === key) return Promise.resolve();
653
+ // In flight for another project: this one loads after it, never joins it.
654
+ if (projectLoading) return projectLoading.then(() => loadProject());
655
+ if (!getCurrentProject()) {
656
+ projectRecords = [];
657
+ projectLoadedFor = key;
658
+ return Promise.resolve();
659
+ }
660
+ projectLoading = listThemeDocuments('project')
661
+ .then((read) => {
662
+ projectRecords = recordsFrom('project', read);
663
+ })
664
+ .catch(() => {
665
+ projectRecords = [];
666
+ })
667
+ .finally(() => {
668
+ projectLoadedFor = key;
669
+ projectLoading = null;
670
+ });
671
+ return projectLoading;
672
+ }
673
+
674
+ /** Load both layers' documents (memoized per layer; the project layer per
675
+ * project) and publish. The boot awaits this before the first paint so a
676
+ * chosen custom palette resolves on the first frame. */
677
+ export function preloadEditorThemeLibrary(): Promise<void> {
678
+ return Promise.all([loadUser(), loadProject()]).then(emit);
679
+ }
680
+
681
+ /** Custom documents, project over user on an id collision. */
682
+ function customThemes(): readonly CustomEditorThemeDocument[] {
683
+ const byId = new Map<string, CustomEditorThemeDocument>();
684
+ for (const record of userRecords) byId.set(record.document.theme.id, record.document);
685
+ for (const record of projectRecords) byId.set(record.document.theme.id, record.document);
686
+ return [...byId.values()];
687
+ }
688
+
689
+ function emit(): void {
690
+ cachedChoices = null;
691
+ for (const listener of listeners) listener();
692
+ }
693
+
694
+ function layerHolding(id: string): SettingsLayer | null {
695
+ if (projectRecords.some((record) => record.document.theme.id === id)) return 'project';
696
+ if (userRecords.some((record) => record.document.theme.id === id)) return 'user';
697
+ return null;
698
+ }
699
+
700
+ /** Write one document to the layer that holds it (a new theme goes to the
701
+ * USER layer — a project theme is placed by hand, the way a `.vscode` file
702
+ * is) and publish. */
703
+ function persistTheme(document: CustomEditorThemeDocument): void {
704
+ const id = document.theme.id;
705
+ const layer = layerHolding(id) ?? 'user';
706
+ const record = { layer, document };
707
+ if (layer === 'project') {
708
+ projectRecords = [...projectRecords.filter((r) => r.document.theme.id !== id), record];
709
+ } else {
710
+ userRecords = [...userRecords.filter((r) => r.document.theme.id !== id), record];
711
+ }
712
+ emit();
713
+ void saveThemeDocument(layer, id, document).catch((cause: unknown) => {
714
+ editorConsole.error(
715
+ `Could not save the ${layer} theme ${id}: ${(cause as Error).message}`,
716
+ 'settings',
717
+ );
718
+ });
719
+ }
720
+
721
+ function removeTheme(id: string): boolean {
722
+ const layer = layerHolding(id);
723
+ if (!layer) return false;
724
+ if (layer === 'project') {
725
+ projectRecords = projectRecords.filter((r) => r.document.theme.id !== id);
726
+ } else {
727
+ userRecords = userRecords.filter((r) => r.document.theme.id !== id);
728
+ }
729
+ emit();
730
+ void deleteThemeDocument(layer, id).catch((cause: unknown) => {
731
+ editorConsole.error(
732
+ `Could not delete the ${layer} theme ${id}: ${(cause as Error).message}`,
733
+ 'settings',
734
+ );
735
+ });
736
+ return true;
737
+ }
738
+
739
+ function uniqueThemeId(seed = 'custom-theme'): string {
740
+ const slug =
741
+ seed
742
+ .toLowerCase()
743
+ .replace(/[^a-z0-9]+/g, '-')
744
+ .replace(/^-|-$/g, '') || 'custom-theme';
745
+ const occupied = new Set(editorThemeLibrarySnapshot().map((choice) => choice.id));
746
+ if (!occupied.has(slug)) return slug;
747
+ let suffix = 2;
748
+ while (occupied.has(`${slug}-${suffix}`)) suffix += 1;
749
+ return `${slug}-${suffix}`;
750
+ }
751
+
752
+ function paletteFromBuiltIn(id: (typeof BUILT_IN_EDITOR_PALETTE_IDS)[number]): EditorPalette {
753
+ const theme = editorThemes[id];
754
+ return { id: theme.id, color: theme.color, typography: theme.typography };
755
+ }
756
+
757
+ export function editorThemeLibrarySnapshot(): readonly EditorThemeChoice[] {
758
+ cachedChoices ??= [
759
+ ...BUILT_IN_EDITOR_PALETTE_IDS.map((id) => ({
760
+ id,
761
+ name: BUILT_IN_NAMES[id],
762
+ kind: 'built-in' as const,
763
+ theme: paletteFromBuiltIn(id),
764
+ })),
765
+ ...shippedPalettes().map((document) => ({
766
+ id: document.theme.id,
767
+ name: document.name,
768
+ kind: 'built-in' as const,
769
+ theme: document.theme,
770
+ })),
771
+ ...customThemes().map((document) => ({
772
+ id: document.theme.id,
773
+ name: document.name,
774
+ kind: 'custom' as const,
775
+ theme: document.theme,
776
+ })),
777
+ ];
778
+ return cachedChoices;
779
+ }
780
+
781
+ export function subscribeEditorThemeLibrary(listener: () => void): () => void {
782
+ listeners.add(listener);
783
+ return () => listeners.delete(listener);
784
+ }
785
+
786
+ export function resolveEditorTheme(id: string): EditorThemeChoice | undefined {
787
+ return editorThemeLibrarySnapshot().find((choice) => choice.id === id);
788
+ }
789
+
790
+ export function duplicateEditorTheme(sourceId: string, requestedName?: string): EditorThemeChoice {
791
+ const source = resolveEditorTheme(sourceId);
792
+ if (!source) throw new Error(`Unknown editor theme “${sourceId}”.`);
793
+ const name = requestedName?.trim() || `${source.name} Copy`;
794
+ const id = uniqueThemeId(name);
795
+ const document = parseCustomEditorThemeDocument({
796
+ schemaVersion: CUSTOM_EDITOR_THEME_SCHEMA_VERSION,
797
+ name,
798
+ theme: { ...structuredClone(source.theme), id },
799
+ });
800
+ persistTheme(document);
801
+ return resolveEditorTheme(id)!;
802
+ }
803
+
804
+ export function saveCustomEditorTheme(document: CustomEditorThemeDocument): EditorThemeChoice {
805
+ const parsed = parseCustomEditorThemeDocument(document);
806
+ if (isEditorThemeId(parsed.theme.id) || isShippedPaletteId(parsed.theme.id)) {
807
+ throw new Error('Built-in themes cannot be overwritten.');
808
+ }
809
+ persistTheme(parsed);
810
+ return resolveEditorTheme(parsed.theme.id)!;
811
+ }
812
+
813
+ export function importCustomEditorTheme(value: unknown): EditorThemeChoice {
814
+ let serializedLength: number;
815
+ try {
816
+ serializedLength = JSON.stringify(value).length;
817
+ } catch {
818
+ throw new Error('Theme file must be plain JSON data.');
819
+ }
820
+ if (serializedLength > 262_144) {
821
+ throw new Error('Theme file too large (256 KB limit).');
822
+ }
823
+ const parsed = parseCustomEditorThemeDocument(value);
824
+ const id = uniqueThemeId(parsed.name);
825
+ return saveCustomEditorTheme({ ...parsed, theme: { ...parsed.theme, id } });
826
+ }
827
+
828
+ export function deleteCustomEditorTheme(id: string): boolean {
829
+ return removeTheme(id);
830
+ }
831
+
832
+ export function resetEditorThemeLibraryForTests(): void {
833
+ userRecords = [];
834
+ projectRecords = [];
835
+ userLoaded = false;
836
+ projectLoadedFor = null;
837
+ userLoading = null;
838
+ projectLoading = null;
839
+ cachedChoices = null;
840
+ }
841
+
842
+ onProjectChange(() => {
843
+ void loadProject().then(emit);
844
+ });