@volter/sdk 0.0.0-stage → 0.5.204

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