@vgai/editor 0.2.0 → 0.4.0-canary.20260715.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (430) hide show
  1. package/README.md +24 -0
  2. package/dist/assets/axe-C7YBtOuM.js +30 -0
  3. package/dist/assets/browserAll-DDtGGE48.js +1 -0
  4. package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
  5. package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
  6. package/dist/assets/game-BdTHR2_X.js +1 -0
  7. package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
  8. package/dist/assets/game-CcHfYxjP.js +1 -0
  9. package/dist/assets/game-Cqzj_lpD.js +1 -0
  10. package/dist/assets/game-DSZdeCHu.js +1 -0
  11. package/dist/assets/game-Dch9sAPc.js +1 -0
  12. package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
  13. package/dist/assets/game-loop-DMIgSMV-.js +1 -0
  14. package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
  15. package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
  16. package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
  17. package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
  18. package/dist/assets/index-CQEfi217.js +15412 -0
  19. package/dist/assets/index-Dvi-V0NE.css +1 -0
  20. package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
  21. package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
  22. package/dist/assets/react-18-C9eHn8x1.js +1 -0
  23. package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
  24. package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
  25. package/dist/index.html +15 -13
  26. package/dist/ingest/racing-game/vgai.game.json +5 -4
  27. package/dist/ingest/simcity/vgai.game.json +5 -4
  28. package/dist/ingest/tanks/vgai.game.json +5 -4
  29. package/dist/project-scripts.bundle.js +3917 -0
  30. package/dist/scenes/untitled.vscn.json +2 -0
  31. package/dist-server/packaged.mjs +7697 -3435
  32. package/package.json +31 -4
  33. package/scripts/probe-reporter.ts +349 -0
  34. package/scripts/run-probe.ts +2260 -0
  35. package/server/asset-catalog-audit.ts +268 -0
  36. package/server/asset-catalog-v2.ts +146 -0
  37. package/server/asset-library-routes.ts +276 -58
  38. package/server/canonical-path.ts +1 -1
  39. package/server/dev.ts +148 -44
  40. package/server/editor-server.ts +888 -61
  41. package/server/editor-sse.ts +115 -4
  42. package/server/harness-chat-service.ts +796 -0
  43. package/server/local-asset-catalog.ts +157 -22
  44. package/server/packaged.ts +51 -21
  45. package/server/process-shutdown.ts +136 -0
  46. package/server/prod.ts +9 -6
  47. package/server/project-hmr-files.ts +41 -0
  48. package/server/project-operations.ts +343 -0
  49. package/server/project-optimize-deps-entries.ts +4 -4
  50. package/server/project-output-writer.ts +156 -0
  51. package/server/project-validation.ts +140 -0
  52. package/server/project-view.ts +8 -50
  53. package/server/server-utils.ts +75 -10
  54. package/server/session-registry.ts +12 -0
  55. package/src/EditorContext.tsx +107 -3
  56. package/src/action-registry.ts +115 -17
  57. package/src/adapter-resolver.ts +213 -478
  58. package/src/asset-events.ts +64 -1
  59. package/src/asset-preview.ts +906 -0
  60. package/src/asset-selection.ts +121 -0
  61. package/src/asset-workflow/asset-capabilities.ts +259 -0
  62. package/src/asset-workflow/asset-editor-errors.ts +46 -0
  63. package/src/asset-workflow/asset-import-pipeline.ts +176 -0
  64. package/src/asset-workflow/asset-library-query.ts +107 -0
  65. package/src/asset-workflow/asset-types.ts +192 -0
  66. package/src/asset-workflow/asset-workflow-quality.ts +65 -0
  67. package/src/asset-workflow/import-contract.ts +113 -0
  68. package/src/asset-workflow/model-conversion.ts +126 -0
  69. package/src/asset-workflow/model-inspection.ts +185 -0
  70. package/src/asset-workflow/project-asset-commands.ts +23 -0
  71. package/src/asset-workflow/project-asset-health.ts +153 -0
  72. package/src/asset-workflow/project-asset-operations.ts +119 -0
  73. package/src/asset-workflow/thumbnail-system.ts +172 -0
  74. package/src/authoring/active-systems.ts +15 -0
  75. package/src/authoring/boundary-authoring-adapter.ts +16 -5
  76. package/src/authoring/composite-authoring-adapter.ts +305 -124
  77. package/src/authoring/design-time-layers.ts +300 -117
  78. package/src/authoring/edit-mode-authoring.ts +278 -293
  79. package/src/authoring/embed-only-adapter.ts +4 -2
  80. package/src/authoring/first-party-hierarchy-menu.ts +7 -2
  81. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
  82. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
  83. package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
  84. package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
  85. package/src/authoring/first-party-inspector/register.tsx +13 -2
  86. package/src/authoring/ingest-authoring-adapter.ts +142 -45
  87. package/src/authoring/mount-failure-report.ts +5 -5
  88. package/src/authoring/provenance.ts +41 -0
  89. package/src/authoring/react-dom-authoring-adapter.ts +92 -92
  90. package/src/authoring/react-inspector-section.tsx +489 -51
  91. package/src/authoring/react-world-authoring-adapter.ts +541 -306
  92. package/src/authoring/story-overlay.ts +1 -1
  93. package/src/authoring/story-types.ts +11 -0
  94. package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
  95. package/src/authoring/viewport-tool-context.ts +78 -0
  96. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  97. package/src/authoring/world-hidden-viewport.ts +186 -0
  98. package/src/authoring/world-pan-state.ts +33 -17
  99. package/src/authoring/world-session-state.ts +1 -1
  100. package/src/authoring/world2d-authoring-adapter.ts +109 -52
  101. package/src/availability-tick.ts +56 -0
  102. package/src/browser-play-entry.ts +5 -5
  103. package/src/browser-project-scripts.ts +1 -0
  104. package/src/build-identity.ts +15 -0
  105. package/src/command-listener.ts +893 -19
  106. package/src/components/AlignToolbar.tsx +74 -63
  107. package/src/components/AppRoot.tsx +193 -26
  108. package/src/components/ApplicationMenus.tsx +272 -0
  109. package/src/components/AssetBrowser.tsx +1450 -293
  110. package/src/components/AssetEditorShell.tsx +141 -0
  111. package/src/components/AssetImportDetails.tsx +218 -0
  112. package/src/components/AssetLibraryPanel.tsx +14 -0
  113. package/src/components/CameraInfo.tsx +11 -20
  114. package/src/components/CenterDocuments.tsx +585 -0
  115. package/src/components/CommandPalette.tsx +75 -30
  116. package/src/components/ConsolePanel.css +55 -0
  117. package/src/components/ConsolePanel.tsx +57 -112
  118. package/src/components/DebugConsolePanel.tsx +461 -0
  119. package/src/components/DebuggerPanel.tsx +165 -0
  120. package/src/components/DefaultEditorLayout.tsx +167 -231
  121. package/src/components/GameHierarchy.tsx +873 -290
  122. package/src/components/GamePerformanceOverlay.tsx +46 -0
  123. package/src/components/HarnessChatPanel.tsx +2352 -0
  124. package/src/components/HistoryPanel.tsx +226 -0
  125. package/src/components/Inspector.tsx +153 -99
  126. package/src/components/InspectorToolSection.tsx +6 -4
  127. package/src/components/MountFailureBanner.tsx +31 -48
  128. package/src/components/NetworkSection.tsx +25 -14
  129. package/src/components/OnlineAssetBrowser.tsx +794 -343
  130. package/src/components/OverlayReportBanner.tsx +38 -64
  131. package/src/components/PerformancePanel.tsx +206 -0
  132. package/src/components/PlayBar.tsx +93 -263
  133. package/src/components/PrefabSaveDialog.tsx +55 -127
  134. package/src/components/ProjectHeader.css +60 -0
  135. package/src/components/ProjectHeader.tsx +170 -105
  136. package/src/components/ProjectScreen.tsx +246 -184
  137. package/src/components/ReactCanvasControls.tsx +185 -0
  138. package/src/components/ResolutionPicker.tsx +85 -0
  139. package/src/components/RootActivateDialog.tsx +39 -68
  140. package/src/components/SaveDialog.tsx +55 -127
  141. package/src/components/SaveStatus.tsx +11 -29
  142. package/src/components/StartupLoadingScreen.tsx +96 -0
  143. package/src/components/StateWatchPanel.tsx +302 -0
  144. package/src/components/StatsOverlay.tsx +12 -5
  145. package/src/components/StatusBar.tsx +64 -0
  146. package/src/components/ThemeManagerDialog.css +89 -0
  147. package/src/components/ThemeManagerDialog.tsx +371 -0
  148. package/src/components/ToolHost.tsx +26 -15
  149. package/src/components/Toolbar.tsx +117 -214
  150. package/src/components/VgaiVoxelLogo.tsx +89 -0
  151. package/src/components/ViewportOverlay.tsx +97 -172
  152. package/src/components/ViewportPanel.tsx +130 -31
  153. package/src/components/WorkspaceDock.tsx +891 -0
  154. package/src/components/WorldSelectionOverlay.tsx +51 -63
  155. package/src/components/WorldTextEditor.tsx +9 -5
  156. package/src/components/asset-documents.tsx +390 -0
  157. package/src/components/asset-editor-persistence.ts +44 -0
  158. package/src/components/asset-selection-section.tsx +338 -0
  159. package/src/components/asset-viewers/AudioViewer.tsx +73 -21
  160. package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
  161. package/src/components/asset-viewers/ImageViewer.tsx +43 -11
  162. package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
  163. package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
  164. package/src/components/asset-viewers/ModelViewer.tsx +805 -121
  165. package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
  166. package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
  167. package/src/components/asset-workflow.css +506 -0
  168. package/src/components/build-documents.tsx +696 -0
  169. package/src/components/core-utilities.tsx +75 -0
  170. package/src/components/data-documents.tsx +364 -0
  171. package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
  172. package/src/components/debugger-panel-state.ts +57 -0
  173. package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
  174. package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
  175. package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
  176. package/src/components/inspector-widgets/FontPicker.tsx +13 -21
  177. package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
  178. package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
  179. package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
  180. package/src/components/inspector-widgets/shared.tsx +90 -49
  181. package/src/components/inspectors/AnimationSection.tsx +39 -40
  182. package/src/components/inspectors/AudioSection.tsx +28 -39
  183. package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
  184. package/src/components/inspectors/CameraSection.tsx +14 -23
  185. package/src/components/inspectors/ComponentsSection.tsx +39 -43
  186. package/src/components/inspectors/JointsSection.tsx +34 -43
  187. package/src/components/inspectors/LightSection.tsx +90 -35
  188. package/src/components/inspectors/MaterialSection.tsx +137 -174
  189. package/src/components/inspectors/MeshSection.tsx +9 -17
  190. package/src/components/inspectors/NavigationSection.tsx +8 -15
  191. package/src/components/inspectors/ParticleSection.tsx +91 -76
  192. package/src/components/inspectors/PhysicsSection.tsx +42 -46
  193. package/src/components/inspectors/PostProcessingSection.tsx +28 -36
  194. package/src/components/inspectors/RenderingSection.tsx +12 -20
  195. package/src/components/inspectors/ShadowSection.tsx +7 -8
  196. package/src/components/inspectors/SplineSection.tsx +31 -42
  197. package/src/components/inspectors/TransformSection.tsx +14 -22
  198. package/src/components/patterns/Dialog.tsx +95 -0
  199. package/src/components/patterns/Fields.tsx +44 -0
  200. package/src/components/patterns/List.tsx +25 -0
  201. package/src/components/patterns/StateSurface.tsx +40 -0
  202. package/src/components/patterns/Surfaces.tsx +115 -0
  203. package/src/components/patterns/Tabs.tsx +80 -0
  204. package/src/components/patterns/Toolbar.tsx +51 -0
  205. package/src/components/patterns/Tree.tsx +72 -0
  206. package/src/components/primitives/Button.tsx +46 -0
  207. package/src/components/primitives/ColorInput.tsx +11 -47
  208. package/src/components/primitives/EditorIcon.tsx +99 -0
  209. package/src/components/primitives/FormControls.tsx +71 -0
  210. package/src/components/primitives/JsonInput.tsx +15 -2
  211. package/src/components/primitives/Layout.tsx +100 -0
  212. package/src/components/primitives/Menu.tsx +105 -0
  213. package/src/components/primitives/NumberInput.tsx +8 -35
  214. package/src/components/primitives/Panel.tsx +49 -44
  215. package/src/components/primitives/SectionHeader.tsx +10 -28
  216. package/src/components/primitives/Text.tsx +46 -0
  217. package/src/components/primitives/Tooltip.tsx +20 -12
  218. package/src/components/primitives/Vec3Input.tsx +3 -0
  219. package/src/components/primitives/banner-tones.ts +33 -0
  220. package/src/components/primitives/editor-icons.ts +102 -0
  221. package/src/components/primitives/panel-header-styles.ts +30 -0
  222. package/src/components/project-operation-documents.tsx +226 -0
  223. package/src/components/status-contributions.tsx +175 -0
  224. package/src/components/story-documents-register.ts +15 -0
  225. package/src/components/story-documents.tsx +1185 -0
  226. package/src/components/tool-documents.tsx +110 -0
  227. package/src/components/workspace-dock.css +293 -0
  228. package/src/components/workspace-history.ts +33 -0
  229. package/src/components/world-documents.tsx +145 -0
  230. package/src/components/xstate-documents.tsx +209 -0
  231. package/src/composite-screenshot.ts +248 -0
  232. package/src/design-system/index.ts +70 -0
  233. package/src/design-system-stories/Foundation.stories.tsx +183 -0
  234. package/src/design-system-stories/Patterns.stories.tsx +251 -0
  235. package/src/design-system-stories/Workspace.stories.tsx +538 -0
  236. package/src/doctor/report.ts +57 -13
  237. package/src/editor-api.ts +295 -31
  238. package/src/editor-compatibility.ts +200 -0
  239. package/src/editor-console.ts +35 -17
  240. package/src/editor-hotkeys.ts +85 -33
  241. package/src/editor-store.ts +538 -233
  242. package/src/editor-viewport.ts +160 -34
  243. package/src/entity-factory.ts +14 -3
  244. package/src/gizmo-registry.ts +21 -5
  245. package/src/harness-chat-editor-context.ts +37 -0
  246. package/src/harness-chat-indicator.ts +68 -0
  247. package/src/harness-chat-layout.ts +53 -0
  248. package/src/harness-chat-lifecycle.ts +170 -0
  249. package/src/harness-chat-reconnect.ts +85 -0
  250. package/src/harness-chat-types.ts +409 -0
  251. package/src/hierarchy-drop.ts +50 -0
  252. package/src/hierarchy-expansion-state.ts +100 -0
  253. package/src/hierarchy-menu-registry.ts +1 -1
  254. package/src/hierarchy-projection.ts +76 -0
  255. package/src/history/editor-session.ts +21 -0
  256. package/src/history/history-commands.ts +147 -0
  257. package/src/history/history-service.ts +904 -0
  258. package/src/history/json-history-resource.ts +165 -0
  259. package/src/history/persistence-coordinator.ts +35 -0
  260. package/src/history/project-file-history.ts +266 -0
  261. package/src/history/project-root-history-backends.ts +53 -0
  262. package/src/history/resource-registry.ts +209 -0
  263. package/src/history/snapshot-store.ts +108 -0
  264. package/src/history/source-history-backend.ts +374 -0
  265. package/src/history/types.ts +100 -0
  266. package/src/hmr-registration-group.ts +67 -0
  267. package/src/hmr-stable-react-context.ts +23 -0
  268. package/src/hosted-example.ts +12 -2
  269. package/src/hotkeys.ts +89 -18
  270. package/src/ingest/discovery-react.ts +9 -10
  271. package/src/ingest/discovery.ts +4 -4
  272. package/src/ingest/discovery2d.ts +5 -5
  273. package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
  274. package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
  275. package/src/ingest/games/games-fps/vgai.game.json +3 -3
  276. package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
  277. package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
  278. package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
  279. package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
  280. package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
  281. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
  282. package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
  283. package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
  284. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
  285. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
  286. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
  287. package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
  288. package/src/ingest/types.ts +1 -1
  289. package/src/ingest-mode.ts +47 -52
  290. package/src/ingest-siblings.ts +23 -33
  291. package/src/initial-scene.ts +1 -2
  292. package/src/main.tsx +16 -8
  293. package/src/manifest-project.ts +10 -12
  294. package/src/model-thumbnail.ts +261 -26
  295. package/src/module-mode.ts +44 -50
  296. package/src/play-log-events.ts +26 -0
  297. package/src/play-mode.ts +495 -401
  298. package/src/play-scene-adoption.ts +23 -0
  299. package/src/playground/main.ts +68 -24
  300. package/src/playwright-shim.ts +473 -0
  301. package/src/project-manager.ts +90 -10
  302. package/src/project-operations.ts +112 -0
  303. package/src/project-story-discovery.ts +25 -0
  304. package/src/project-tool-discovery.ts +43 -0
  305. package/src/scene-serializer.ts +0 -6
  306. package/src/scene-sync.ts +7 -2
  307. package/src/script-loader.ts +3 -3
  308. package/src/skeleton-helper.ts +57 -0
  309. package/src/storage/seed.ts +68 -8
  310. package/src/stories/StoryPreviewMount.tsx +265 -0
  311. package/src/stories/action-stories.fixture.tsx +44 -0
  312. package/src/stories/button-stories.fixture.tsx +50 -0
  313. package/src/stories/compose-project-stories.ts +182 -0
  314. package/src/stories/loader-stories.fixture.tsx +53 -0
  315. package/src/stories/story-discovery.ts +130 -0
  316. package/src/stories/story-registry.ts +146 -0
  317. package/src/theme-css.ts +1049 -0
  318. package/src/theme-library.ts +238 -0
  319. package/src/theme-preference.ts +130 -0
  320. package/src/theme.ts +527 -0
  321. package/src/tool-loader.ts +80 -24
  322. package/src/ui-source/inspect.ts +27 -4
  323. package/src/ui-source/source-write-backend.ts +83 -22
  324. package/src/widgets/index.ts +9 -18
  325. package/src/workspace-aux-commands.ts +11 -0
  326. package/src/workspace-dock-controller.ts +64 -0
  327. package/src/workspace-document-ids.ts +5 -0
  328. package/src/workspace-document-registry.ts +367 -0
  329. package/src/workspace-state-persistence.ts +455 -0
  330. package/src/workspace-status-registry.ts +110 -0
  331. package/src/workspace-utility-commands.ts +49 -0
  332. package/src/workspace-utility-registry.ts +161 -0
  333. package/src/world-document-routing.ts +16 -0
  334. package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
  335. package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
  336. package/src/xstate-inspect/use-live-actor-state.ts +39 -0
  337. package/src/xstate-inspect/xstate-graph.ts +265 -0
  338. package/src/xstate-inspect/xstate-layout.ts +76 -0
  339. package/src/z-index.ts +26 -0
  340. package/template/.claude/skills/editor/SKILL.md +129 -90
  341. package/template/AGENTS.md +1110 -0
  342. package/template/CLAUDE.md +1 -512
  343. package/template/IDIOMS.md +50 -0
  344. package/template/check-idioms.ts +706 -0
  345. package/template/manifest-entry-modules-plugin.ts +53 -0
  346. package/template/package.json +13 -4
  347. package/template/public/scenes/default.scn2d.json +6 -0
  348. package/template/public/scenes/default.vscn.json +7 -1
  349. package/template/public/scenes/untitled.vscn.json +10 -0
  350. package/template/scripts/playtest.ts +40 -0
  351. package/template/server/loopback.ts +75 -0
  352. package/template/server/rooms/arena-room.ts +69 -1
  353. package/template/server/rooms/game-room.ts +68 -1
  354. package/template/src/data/README.md +9 -4
  355. package/template/src/data/data-ref.ts +35 -0
  356. package/template/src/data/tuning.schema.ts +1 -1
  357. package/template/src/main.ts +41 -62
  358. package/template/src/manifest-entry-modules.d.ts +3 -0
  359. package/template/src/operations/README.md +88 -0
  360. package/template/src/runtime/pixi-adapter.ts +24 -0
  361. package/template/src/scripts/components/data-spinner.ts +43 -2
  362. package/template/src/scripts/components/spin-lap-math.ts +27 -0
  363. package/template/src/tools/README.md +5 -3
  364. package/template/src/tools/example.tool.tsx +13 -7
  365. package/template/src/ui/Button.stories.tsx +66 -0
  366. package/template/src/ui/RebindPanel.tsx +2 -6
  367. package/template/src/ui/game.tsx +17 -0
  368. package/template/tests/acceptance/example.spec.ts +64 -0
  369. package/template/tests/logic/example.test.ts +31 -0
  370. package/template/tests/logic/room.test.ts +100 -0
  371. package/template/tests/tsconfig.json +30 -0
  372. package/template/validate-manifest.ts +5 -5
  373. package/template/validate-scenes.ts +50 -18
  374. package/template/vgai.game.json +20 -13
  375. package/template/vite.config.ts +77 -16
  376. package/vite-plugin-ui-oid.ts +261 -17
  377. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
  378. package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
  379. package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
  380. package/dist/assets/browserAll-CEkWYlFk.js +0 -1
  381. package/dist/assets/create-runtime-DYIWr3va.js +0 -1
  382. package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
  383. package/dist/assets/game-8SDZPPgH.js +0 -1
  384. package/dist/assets/game-CevorxyV.js +0 -1
  385. package/dist/assets/game-Di7XlywD.js +0 -1
  386. package/dist/assets/game-DjnCaw8R.js +0 -1
  387. package/dist/assets/game-Q9A0aXSV.js +0 -1
  388. package/dist/assets/index-CPVV6pjw.js +0 -1
  389. package/dist/assets/index-CpOnYyeD.js +0 -213
  390. package/dist/assets/index-Dr8H_6Rl.js +0 -1
  391. package/dist/assets/index-_bCaipUk.js +0 -13650
  392. package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
  393. package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
  394. package/dist/assets/scene-query-BivAHhJu.js +0 -1
  395. package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
  396. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
  397. package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
  398. package/src/authoring/scene-ui-layer.ts +0 -485
  399. package/src/authoring/ui-authoring-adapter.ts +0 -325
  400. package/src/authoring/ui-hierarchy-menu.ts +0 -40
  401. package/src/authoring/ui-inspector-sections.tsx +0 -156
  402. package/src/authoring/undo-timeline.ts +0 -145
  403. package/src/components/BottomPanel.tsx +0 -326
  404. package/src/components/BuildPanel.tsx +0 -455
  405. package/src/components/FolderTree.tsx +0 -179
  406. package/src/components/RightPanel.tsx +0 -166
  407. package/src/components/WorldContextMenu.tsx +0 -257
  408. package/src/components/asset-viewers/AssetViewer.tsx +0 -38
  409. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
  410. package/src/scene-ui-mode/active-scene-ui.ts +0 -58
  411. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
  412. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
  413. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
  414. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
  415. package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
  416. package/src/ui-editor/inspector.tsx +0 -563
  417. package/src/ui-editor/node-ops.ts +0 -266
  418. package/src/ui-editor/overlay.tsx +0 -380
  419. package/src/ui-editor/palette.tsx +0 -114
  420. package/src/ui-editor/react-store.ts +0 -78
  421. package/src/ui-editor/timeline-panel.tsx +0 -443
  422. package/src/ui-editor/ui-edit-store.ts +0 -552
  423. package/src/ui-editor/ui-prefab.ts +0 -95
  424. package/template/project.json +0 -9
  425. package/template/src/scripts/main.ts +0 -94
  426. package/template/src/ui/DefaultHud.tsx +0 -40
  427. package/template/src/ui/game-state.tsx +0 -37
  428. package/template/src/ui/mount-default-hud.tsx +0 -19
  429. package/template/src/ui/mount-ui.ts +0 -15
  430. package/vite-plugin-react-world-provider.ts +0 -81
@@ -1,6 +1,12 @@
1
- import { faCheck, faSpinner } from '@fortawesome/free-solid-svg-icons';
2
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
- import { useCallback, useEffect, useRef, useState } from 'react';
1
+ import { faCheck, faCloud, faSpinner } from '@fortawesome/free-solid-svg-icons';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import {
4
+ type AssetLibrarySort,
5
+ assetLibraryStateKey,
6
+ onlineAssetKey,
7
+ queryAssetLibrary,
8
+ } from '../asset-workflow/asset-library-query';
9
+ import { Button, Checkbox, EditorIcon, Select, TextInput, text } from '../design-system';
4
10
  import { useEditorStore } from '../EditorContext';
5
11
  import {
6
12
  getAssetLibraryCategories,
@@ -10,12 +16,14 @@ import {
10
16
  onAssetDownloaded,
11
17
  searchOnlineAssets,
12
18
  } from '../editor-api';
13
-
14
- // ---------------------------------------------------------------------------
15
- // Constants
16
- // ---------------------------------------------------------------------------
19
+ import { getCurrentProject } from '../project-manager';
20
+ import { openOnlineAssetDocument } from './asset-documents';
21
+ import { OnlineAssetDetail } from './asset-viewers/OnlineAssetDetail';
22
+ import './asset-workflow.css';
17
23
 
18
24
  type Source = 'local' | 'polyhaven' | 'ambientcg';
25
+ type Density = 'compact' | 'standard' | 'large';
26
+ type Sort = AssetLibrarySort;
19
27
 
20
28
  interface SourceOption {
21
29
  value: Source;
@@ -28,10 +36,10 @@ const SOURCES: SourceOption[] = [
28
36
  value: 'local',
29
37
  label: 'Full Catalog',
30
38
  types: [
31
- { value: 'all', label: 'All' },
39
+ { value: 'all', label: 'All assets' },
32
40
  { value: 'models', label: 'Models' },
33
41
  { value: 'animations', label: 'Animations' },
34
- { value: 'sources', label: 'Source Files' },
42
+ { value: 'sources', label: 'Source files' },
35
43
  ],
36
44
  },
37
45
  {
@@ -53,396 +61,839 @@ const SOURCES: SourceOption[] = [
53
61
  },
54
62
  ];
55
63
 
56
- const PAGE_SIZE = 48;
57
-
58
- // ---------------------------------------------------------------------------
59
- // Styles
60
- // ---------------------------------------------------------------------------
64
+ const PAGE_SIZE = 96;
65
+ interface PersistedLibraryState {
66
+ source: Source;
67
+ assetType: string;
68
+ query: string;
69
+ density: Density;
70
+ sort: Sort;
71
+ favorites: string[];
72
+ savedSearches: string[];
73
+ recent: string[];
74
+ sourceWidth: number;
75
+ detailsWidth: number;
76
+ sourceCollapsed: boolean;
77
+ detailsCollapsed: boolean;
78
+ }
61
79
 
62
- const selectStyle: React.CSSProperties = {
63
- background: '#1e1e1e',
64
- border: '1px solid #333',
65
- borderRadius: 3,
66
- color: '#c5c8ce',
67
- fontSize: 11,
68
- padding: '3px 6px',
69
- outline: 'none',
70
- cursor: 'pointer',
80
+ const DEFAULT_STATE: PersistedLibraryState = {
81
+ source: 'local',
82
+ assetType: 'all',
83
+ query: '',
84
+ density: 'standard',
85
+ sort: 'relevance',
86
+ favorites: [],
87
+ savedSearches: [],
88
+ recent: [],
89
+ sourceWidth: 210,
90
+ detailsWidth: 360,
91
+ sourceCollapsed: false,
92
+ detailsCollapsed: false,
71
93
  };
72
94
 
73
- const inputStyle: React.CSSProperties = {
74
- background: '#1e1e1e',
75
- border: '1px solid #333',
76
- borderRadius: 3,
77
- color: '#c5c8ce',
78
- fontSize: 11,
79
- padding: '3px 6px',
80
- outline: 'none',
81
- flex: 1,
82
- minWidth: 80,
83
- };
95
+ function readState(): PersistedLibraryState {
96
+ try {
97
+ const projectId = getCurrentProject()?.rootPath ?? '__unscoped__';
98
+ const parsed = JSON.parse(
99
+ localStorage.getItem(assetLibraryStateKey(projectId)) ?? '{}',
100
+ ) as Partial<PersistedLibraryState>;
101
+ return { ...DEFAULT_STATE, ...parsed };
102
+ } catch {
103
+ return DEFAULT_STATE;
104
+ }
105
+ }
106
+
107
+ function toOnlineInfo(asset: OnlineAsset) {
108
+ return {
109
+ source: asset.source,
110
+ id: asset.id,
111
+ name: asset.name,
112
+ thumbnailUrl: asset.thumbnailUrl,
113
+ type: asset.type,
114
+ tags: asset.tags,
115
+ ...(asset.license ? { license: asset.license } : {}),
116
+ ...(asset.author ? { author: asset.author } : {}),
117
+ ...(asset.sourceUrl ? { sourceUrl: asset.sourceUrl } : {}),
118
+ };
119
+ }
120
+
121
+ function SourcePane(props: {
122
+ source: Source;
123
+ assetType: string;
124
+ category: string;
125
+ categoryQuery: string;
126
+ categories: readonly string[];
127
+ savedSearches: readonly string[];
128
+ favoritesOnly: boolean;
129
+ animatedOnly: boolean;
130
+ runtimeReadyOnly: boolean;
131
+ downloaded: 'all' | 'downloaded' | 'not-downloaded';
132
+ license: string;
133
+ delivery: string;
134
+ health: string;
135
+ licenseCounts: ReadonlyMap<string, number>;
136
+ deliveryCounts: ReadonlyMap<string, number>;
137
+ healthCounts: ReadonlyMap<string, number>;
138
+ onSource(source: Source): void;
139
+ onType(type: string): void;
140
+ onCategory(category: string): void;
141
+ onCategoryQuery(query: string): void;
142
+ onSavedSearch(query: string): void;
143
+ onFavoritesOnly(value: boolean): void;
144
+ onAnimatedOnly(value: boolean): void;
145
+ onRuntimeReadyOnly(value: boolean): void;
146
+ onDownloaded(value: 'all' | 'downloaded' | 'not-downloaded'): void;
147
+ onLicense(value: string): void;
148
+ onDelivery(value: string): void;
149
+ onHealth(value: string): void;
150
+ }) {
151
+ const sourceConfig = SOURCES.find((entry) => entry.value === props.source)!;
152
+ const visibleCategories = props.categories
153
+ .filter((value) => value.toLowerCase().includes(props.categoryQuery.toLowerCase()))
154
+ .slice(0, 80);
155
+ return (
156
+ <aside className="vgai-asset-library__sources" aria-label="Library sources and filters">
157
+ <div className="vgai-asset-facet-title">Sources</div>
158
+ {SOURCES.map((entry) => (
159
+ <Button
160
+ className="vgai-asset-facet-button"
161
+ type="button"
162
+ key={entry.value}
163
+ aria-pressed={props.source === entry.value}
164
+ onClick={() => props.onSource(entry.value)}
165
+ >
166
+ {entry.label}
167
+ </Button>
168
+ ))}
169
+
170
+ <div className="vgai-asset-facet-title">Type</div>
171
+ {sourceConfig.types.map((entry) => (
172
+ <Button
173
+ className="vgai-asset-facet-button"
174
+ type="button"
175
+ key={entry.value}
176
+ aria-pressed={props.assetType === entry.value}
177
+ onClick={() => props.onType(entry.value)}
178
+ >
179
+ {entry.label}
180
+ </Button>
181
+ ))}
182
+
183
+ <div className="vgai-asset-facet-title">Saved views</div>
184
+ <Button
185
+ className="vgai-asset-facet-button"
186
+ type="button"
187
+ aria-pressed={props.favoritesOnly}
188
+ onClick={() => props.onFavoritesOnly(!props.favoritesOnly)}
189
+ >
190
+ Favorites
191
+ </Button>
192
+ {props.savedSearches.map((query) => (
193
+ <Button
194
+ className="vgai-asset-facet-button"
195
+ type="button"
196
+ key={query}
197
+ onClick={() => props.onSavedSearch(query)}
198
+ >
199
+ {query}
200
+ </Button>
201
+ ))}
202
+
203
+ <div className="vgai-asset-facet-title">Readiness</div>
204
+ <label className="vgai-asset-facet-check">
205
+ <Checkbox
206
+ checked={props.runtimeReadyOnly}
207
+ onChange={(event) => props.onRuntimeReadyOnly(event.target.checked)}
208
+ />
209
+ Runtime-ready
210
+ </label>
211
+ <label className="vgai-asset-facet-check">
212
+ <Checkbox
213
+ checked={props.animatedOnly}
214
+ onChange={(event) => props.onAnimatedOnly(event.target.checked)}
215
+ />
216
+ Animated
217
+ </label>
218
+ <label className="vgai-asset-facet-check">
219
+ Project status
220
+ <Select
221
+ className="vgai-select"
222
+ value={props.downloaded}
223
+ onChange={(event) =>
224
+ props.onDownloaded(event.target.value as 'all' | 'downloaded' | 'not-downloaded')
225
+ }
226
+ >
227
+ <option value="all">All</option>
228
+ <option value="downloaded">In project</option>
229
+ <option value="not-downloaded">Not in project</option>
230
+ </Select>
231
+ </label>
232
+ <label className="vgai-asset-facet-check">
233
+ License
234
+ <Select
235
+ className="vgai-select"
236
+ value={props.license}
237
+ onChange={(event) => props.onLicense(event.target.value)}
238
+ >
239
+ <option value="">All licenses</option>
240
+ {[...props.licenseCounts].map(([value, count]) => (
241
+ <option key={value} value={value}>
242
+ {value} ({count})
243
+ </option>
244
+ ))}
245
+ </Select>
246
+ </label>
247
+ <label className="vgai-asset-facet-check">
248
+ Delivery
249
+ <Select
250
+ className="vgai-select"
251
+ value={props.delivery}
252
+ onChange={(event) => props.onDelivery(event.target.value)}
253
+ >
254
+ <option value="">All deliveries</option>
255
+ {[...props.deliveryCounts].map(([value, count]) => (
256
+ <option key={value} value={value}>
257
+ {value} ({count})
258
+ </option>
259
+ ))}
260
+ </Select>
261
+ </label>
262
+ <label className="vgai-asset-facet-check">
263
+ Health
264
+ <Select
265
+ className="vgai-select"
266
+ value={props.health}
267
+ onChange={(event) => props.onHealth(event.target.value)}
268
+ >
269
+ <option value="">All health states</option>
270
+ {[...props.healthCounts].map(([value, count]) => (
271
+ <option key={value} value={value}>
272
+ {value} ({count})
273
+ </option>
274
+ ))}
275
+ </Select>
276
+ </label>
277
+
278
+ {props.categories.length > 0 && (
279
+ <>
280
+ <div className="vgai-asset-facet-title">Pack / category</div>
281
+ <TextInput
282
+ className="vgai-input"
283
+ aria-label="Filter categories"
284
+ placeholder="Filter categories"
285
+ value={props.categoryQuery}
286
+ onChange={(event) => props.onCategoryQuery(event.target.value)}
287
+ style={{ width: '100%', boxSizing: 'border-box', marginBottom: 4 }}
288
+ />
289
+ <Button
290
+ className="vgai-asset-facet-button"
291
+ type="button"
292
+ aria-pressed={!props.category}
293
+ onClick={() => props.onCategory('')}
294
+ >
295
+ All categories
296
+ </Button>
297
+ {visibleCategories.map((value) => (
298
+ <Button
299
+ className="vgai-asset-facet-button"
300
+ type="button"
301
+ key={value}
302
+ aria-pressed={props.category === value}
303
+ onClick={() => props.onCategory(value)}
304
+ >
305
+ {value}
306
+ </Button>
307
+ ))}
308
+ </>
309
+ )}
310
+ </aside>
311
+ );
312
+ }
313
+ function AssetCardThumbnail({ asset }: { asset: OnlineAsset }) {
314
+ const [failed, setFailed] = useState(false);
315
+ return (
316
+ <span className="vgai-asset-card__preview">
317
+ <img
318
+ src={failed ? '/__editor/asset-library/local-placeholder.svg' : asset.thumbnailUrl}
319
+ alt={failed ? `${asset.name} preview unavailable` : ''}
320
+ loading="lazy"
321
+ onError={() => setFailed(true)}
322
+ />
323
+ {failed && <span className="vgai-asset-card__fallback">Preview unavailable</span>}
324
+ </span>
325
+ );
326
+ }
327
+
328
+ function VirtualAssetGrid(props: {
329
+ assets: readonly OnlineAsset[];
330
+ selectedId: string | null;
331
+ density: Density;
332
+ loading: boolean;
333
+ hasMore: boolean;
334
+ isFavorite(asset: OnlineAsset): boolean;
335
+ onSelect(asset: OnlineAsset): void;
336
+ onOpen(asset: OnlineAsset): void;
337
+ onLoadMore(): void;
338
+ }) {
339
+ const viewportRef = useRef<HTMLDivElement>(null);
340
+ const typeahead = useRef({ value: '', timer: 0 });
341
+ const [metrics, setMetrics] = useState({ width: 800, height: 600, scrollTop: 0 });
342
+ const cardWidth = props.density === 'compact' ? 136 : props.density === 'large' ? 208 : 168;
343
+ const rowHeight = props.density === 'compact' ? 178 : props.density === 'large' ? 254 : 214;
344
+ const gap = 10;
345
+ const columns = Math.max(1, Math.floor((metrics.width - gap) / (cardWidth + gap)));
346
+ const rows = Math.ceil(props.assets.length / columns);
347
+ const firstRow = Math.max(0, Math.floor(metrics.scrollTop / rowHeight) - 2);
348
+ const lastRow = Math.min(rows, Math.ceil((metrics.scrollTop + metrics.height) / rowHeight) + 3);
349
+ const firstIndex = firstRow * columns;
350
+ const lastIndex = Math.min(props.assets.length, lastRow * columns);
351
+ const visible = props.assets.slice(firstIndex, lastIndex);
84
352
 
85
- // ---------------------------------------------------------------------------
86
- // Component
87
- // ---------------------------------------------------------------------------
353
+ useEffect(() => {
354
+ const element = viewportRef.current;
355
+ if (!element || typeof ResizeObserver === 'undefined') return;
356
+ const observer = new ResizeObserver(([entry]) => {
357
+ if (!entry) return;
358
+ setMetrics((current) => ({
359
+ ...current,
360
+ width: entry.contentRect.width,
361
+ height: entry.contentRect.height,
362
+ }));
363
+ });
364
+ observer.observe(element);
365
+ return () => observer.disconnect();
366
+ }, []);
367
+
368
+ useEffect(() => () => window.clearTimeout(typeahead.current.timer), []);
369
+
370
+ const { hasMore, loading, onLoadMore } = props;
371
+ useEffect(() => {
372
+ if (hasMore && !loading && rows > 0 && lastRow >= rows - 2) onLoadMore();
373
+ }, [hasMore, lastRow, loading, onLoadMore, rows]);
374
+
375
+ const selectedIndex = props.assets.findIndex((asset) => asset.id === props.selectedId);
376
+ const moveSelection = (nextIndex: number) => {
377
+ const asset = props.assets[Math.max(0, Math.min(props.assets.length - 1, nextIndex))];
378
+ if (!asset) return;
379
+ props.onSelect(asset);
380
+ const row = Math.floor(Math.max(0, nextIndex) / columns);
381
+ if (viewportRef.current) viewportRef.current.scrollTop = row * rowHeight;
382
+ };
383
+ const onGridKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
384
+ const pageSize = Math.max(columns, Math.floor(metrics.height / rowHeight) * columns);
385
+ switch (event.key) {
386
+ case 'Enter':
387
+ if (selectedIndex >= 0) props.onOpen(props.assets[selectedIndex]!);
388
+ else return;
389
+ break;
390
+ case 'ArrowRight':
391
+ moveSelection(selectedIndex + 1);
392
+ break;
393
+ case 'ArrowLeft':
394
+ moveSelection(selectedIndex - 1);
395
+ break;
396
+ case 'ArrowDown':
397
+ moveSelection(selectedIndex + columns);
398
+ break;
399
+ case 'ArrowUp':
400
+ moveSelection(selectedIndex - columns);
401
+ break;
402
+ case 'Home':
403
+ moveSelection(0);
404
+ break;
405
+ case 'End':
406
+ moveSelection(props.assets.length - 1);
407
+ break;
408
+ case 'PageDown':
409
+ moveSelection(selectedIndex + pageSize);
410
+ break;
411
+ case 'PageUp':
412
+ moveSelection(selectedIndex - pageSize);
413
+ break;
414
+ default: {
415
+ if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;
416
+ window.clearTimeout(typeahead.current.timer);
417
+ typeahead.current.value += event.key.toLowerCase();
418
+ typeahead.current.timer = window.setTimeout(() => {
419
+ typeahead.current.value = '';
420
+ }, 700);
421
+ const match = props.assets.findIndex((asset) =>
422
+ asset.name.toLowerCase().startsWith(typeahead.current.value),
423
+ );
424
+ if (match < 0) return;
425
+ moveSelection(match);
426
+ }
427
+ }
428
+ event.preventDefault();
429
+ };
430
+
431
+ return (
432
+ <div
433
+ ref={viewportRef}
434
+ className="vgai-asset-library__grid"
435
+ role="listbox"
436
+ aria-label="Asset results"
437
+ aria-busy={props.loading}
438
+ tabIndex={0}
439
+ onScroll={(event) =>
440
+ setMetrics((current) => ({ ...current, scrollTop: event.currentTarget.scrollTop }))
441
+ }
442
+ onKeyDown={onGridKeyDown}
443
+ >
444
+ <div
445
+ style={{ position: 'relative', height: Math.max(metrics.height, rows * rowHeight + gap) }}
446
+ >
447
+ {visible.map((asset, localIndex) => {
448
+ const index = firstIndex + localIndex;
449
+ const column = index % columns;
450
+ const row = Math.floor(index / columns);
451
+ const downloaded = getDownloadedAssetPath(asset.source, asset.id) != null;
452
+ return (
453
+ <Button
454
+ key={onlineAssetKey(asset)}
455
+ type="button"
456
+ role="option"
457
+ aria-selected={props.selectedId === asset.id}
458
+ aria-label={`${asset.name}, ${asset.type}, ${asset.source}`}
459
+ className="vgai-asset-card"
460
+ style={{ left: gap + column * (cardWidth + gap), top: gap + row * rowHeight }}
461
+ onClick={() => props.onSelect(asset)}
462
+ onDoubleClick={() => props.onOpen(asset)}
463
+ >
464
+ <AssetCardThumbnail asset={asset} />
465
+ <span className="vgai-asset-card__name" data-testid="online-asset-name">
466
+ {asset.name}
467
+ </span>
468
+ <span className="vgai-asset-card__meta">
469
+ <span>{asset.type}</span>
470
+ <span>·</span>
471
+ <span>{asset.source === 'local' ? 'Catalog' : asset.source}</span>
472
+ </span>
473
+ <span className="vgai-asset-card__badges">
474
+ {downloaded && <span className="vgai-asset-badge">In project</span>}
475
+ {!downloaded && asset.cloudHosted && (
476
+ <span className="vgai-asset-badge">
477
+ <EditorIcon icon={faCloud} /> Cloud
478
+ </span>
479
+ )}
480
+ {(asset.variantCount ?? 0) > 1 && (
481
+ <span className="vgai-asset-badge">{asset.variantCount} variants</span>
482
+ )}
483
+ {props.isFavorite(asset) && <span className="vgai-asset-badge">Favorite</span>}
484
+ </span>
485
+ </Button>
486
+ );
487
+ })}
488
+ {props.loading && (
489
+ <div role="status" style={{ position: 'absolute', left: 16, top: rows * rowHeight + 12 }}>
490
+ <EditorIcon icon={faSpinner} spin /> Loading assets…
491
+ </div>
492
+ )}
493
+ </div>
494
+ </div>
495
+ );
496
+ }
88
497
 
89
498
  export function OnlineAssetBrowser() {
90
499
  const store = useEditorStore();
91
- const [source, setSource] = useState<Source>('local');
92
- const [assetType, setAssetType] = useState('all');
500
+ const initial = useMemo(readState, []);
501
+ const [source, setSource] = useState<Source>(initial.source);
502
+ const [assetType, setAssetType] = useState(initial.assetType);
93
503
  const [category, setCategory] = useState('');
94
- const [searchQuery, setSearchQuery] = useState('');
504
+ const [categoryQuery, setCategoryQuery] = useState('');
505
+ const [searchQuery, setSearchQuery] = useState(initial.query);
506
+ const [density, setDensity] = useState<Density>(initial.density);
507
+ const [sort, setSort] = useState<Sort>(initial.sort);
508
+ const [favorites, setFavorites] = useState(new Set(initial.favorites));
509
+ const [savedSearches, setSavedSearches] = useState(initial.savedSearches);
510
+ const [recent, setRecent] = useState(initial.recent);
511
+ const [sourceWidth, setSourceWidth] = useState(initial.sourceWidth);
512
+ const [detailsWidth, setDetailsWidth] = useState(initial.detailsWidth);
513
+ const [sourceCollapsed, setSourceCollapsed] = useState(initial.sourceCollapsed);
514
+ const [detailsCollapsed, setDetailsCollapsed] = useState(initial.detailsCollapsed);
515
+ const [favoritesOnly, setFavoritesOnly] = useState(false);
516
+ const [runtimeReadyOnly, setRuntimeReadyOnly] = useState(false);
517
+ const [animatedOnly, setAnimatedOnly] = useState(false);
518
+ const [downloaded, setDownloaded] = useState<'all' | 'downloaded' | 'not-downloaded'>('all');
519
+ const [license, setLicense] = useState('');
520
+ const [delivery, setDelivery] = useState('');
521
+ const [health, setHealth] = useState('');
95
522
  const [assets, setAssets] = useState<OnlineAsset[]>([]);
96
523
  const [total, setTotal] = useState(0);
97
524
  const [loading, setLoading] = useState(false);
525
+ const [error, setError] = useState<string | null>(null);
98
526
  const [offset, setOffset] = useState(0);
99
-
100
- // Track which asset is selected (highlighted in grid)
101
527
  const [selectedId, setSelectedId] = useState<string | null>(null);
528
+ const [categories, setCategories] = useState<string[]>([]);
529
+ const requestId = useRef(0);
530
+
531
+ useEffect(() => {
532
+ const projectId = getCurrentProject()?.rootPath ?? '__unscoped__';
533
+ localStorage.setItem(
534
+ assetLibraryStateKey(projectId),
535
+ JSON.stringify({
536
+ source,
537
+ assetType,
538
+ query: searchQuery,
539
+ density,
540
+ sort,
541
+ favorites: [...favorites],
542
+ savedSearches,
543
+ recent,
544
+ sourceWidth,
545
+ detailsWidth,
546
+ sourceCollapsed,
547
+ detailsCollapsed,
548
+ } satisfies PersistedLibraryState),
549
+ );
550
+ }, [
551
+ assetType,
552
+ density,
553
+ detailsCollapsed,
554
+ detailsWidth,
555
+ favorites,
556
+ recent,
557
+ savedSearches,
558
+ searchQuery,
559
+ sort,
560
+ source,
561
+ sourceCollapsed,
562
+ sourceWidth,
563
+ ]);
102
564
 
103
- // Seed cache from server on mount, re-render when assets get downloaded
104
- const [, setDownloadTick] = useState(0);
105
565
  useEffect(() => {
106
566
  loadDownloadedAssets();
107
- return onAssetDownloaded(() => setDownloadTick((t) => t + 1));
567
+ return onAssetDownloaded(() => setAssets((current) => [...current]));
108
568
  }, []);
109
569
 
110
- // Categories (Poly Haven)
111
- const [categories, setCategories] = useState<string[]>([]);
112
-
113
- // Debounce timer
114
- const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
115
-
116
- // Current source config
117
- const sourceConfig = SOURCES.find((s) => s.value === source)!;
118
-
119
- // Load categories when source/type changes (Poly Haven only)
120
570
  useEffect(() => {
121
571
  if (source === 'polyhaven' || source === 'local') {
122
572
  getAssetLibraryCategories(assetType, source)
123
573
  .then(setCategories)
124
574
  .catch(() => setCategories([]));
125
- } else {
126
- setCategories([]);
127
- }
575
+ } else setCategories([]);
128
576
  }, [source, assetType]);
129
577
 
130
- // Search function
131
578
  const doSearch = useCallback(
132
- async (q: string, off: number, append: boolean) => {
579
+ async (nextOffset: number, append: boolean) => {
580
+ const currentRequest = ++requestId.current;
133
581
  setLoading(true);
582
+ setError(null);
583
+ const params: Parameters<typeof searchOnlineAssets>[0] = {
584
+ source,
585
+ type: assetType,
586
+ offset: nextOffset,
587
+ limit: PAGE_SIZE,
588
+ ...(searchQuery ? { q: searchQuery } : {}),
589
+ ...(category ? { category } : {}),
590
+ };
134
591
  try {
135
- const params: Parameters<typeof searchOnlineAssets>[0] = {
136
- source,
137
- type: assetType,
138
- offset: off,
139
- limit: PAGE_SIZE,
140
- };
141
- if (q) params.q = q;
142
- if (category) params.category = category;
143
592
  const result = await searchOnlineAssets(params);
144
- setAssets((prev) => (append ? [...prev, ...result.assets] : result.assets));
593
+ if (currentRequest !== requestId.current) return;
594
+ setAssets((current) => (append ? [...current, ...result.assets] : result.assets));
145
595
  setTotal(result.total);
146
596
  } catch {
147
597
  if (!append) setAssets([]);
598
+ setError(
599
+ 'The Library could not load this page. Check the SSD or network delivery and retry.',
600
+ );
148
601
  } finally {
149
- setLoading(false);
602
+ if (currentRequest === requestId.current) setLoading(false);
150
603
  }
151
604
  },
152
- [source, assetType, category],
605
+ [assetType, category, searchQuery, source],
153
606
  );
154
607
 
155
- // Trigger search on filter changes
156
608
  useEffect(() => {
157
- setOffset(0);
158
- setSelectedId(null);
159
- if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
160
- searchTimerRef.current = setTimeout(() => {
161
- doSearch(searchQuery, 0, false);
162
- }, 300);
163
- return () => {
164
- if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
165
- };
166
- }, [source, assetType, category, searchQuery, doSearch]);
609
+ const timer = window.setTimeout(() => {
610
+ setOffset(0);
611
+ setSelectedId(null);
612
+ void doSearch(0, false);
613
+ }, 250);
614
+ return () => window.clearTimeout(timer);
615
+ }, [doSearch]);
167
616
 
168
- // Load more
169
617
  const loadMore = useCallback(() => {
170
- const newOffset = offset + PAGE_SIZE;
171
- setOffset(newOffset);
172
- doSearch(searchQuery, newOffset, true);
173
- }, [offset, searchQuery, doSearch]);
174
-
175
- // Single-click selects, double-click opens detail in right panel
176
- const onClickAsset = useCallback((asset: OnlineAsset) => {
177
- setSelectedId(asset.id);
178
- }, []);
179
-
180
- const onDoubleClickAsset = useCallback(
181
- (asset: OnlineAsset) => {
182
- store.openOnlineAssetTab({
183
- source: asset.source,
184
- id: asset.id,
185
- name: asset.name,
186
- thumbnailUrl: asset.thumbnailUrl,
187
- type: asset.type,
188
- tags: asset.tags,
189
- ...(asset.license ? { license: asset.license } : {}),
190
- ...(asset.author ? { author: asset.author } : {}),
191
- ...(asset.sourceUrl ? { sourceUrl: asset.sourceUrl } : {}),
192
- });
193
- },
194
- [store],
618
+ if (loading || assets.length >= total) return;
619
+ const next = offset + PAGE_SIZE;
620
+ setOffset(next);
621
+ void doSearch(next, true);
622
+ }, [assets.length, doSearch, loading, offset, total]);
623
+
624
+ const downloadedAsset = useCallback(
625
+ (asset: OnlineAsset) => getDownloadedAssetPath(asset.source, asset.id) != null,
626
+ [],
195
627
  );
196
-
197
- // Drag start — for model assets, set online payload (viewport resolves file options on drop)
198
- const onDragStart = useCallback((e: React.DragEvent, asset: OnlineAsset) => {
199
- if (asset.type !== 'model') return;
200
- const payload = {
201
- path: '',
202
- kind: 'model',
203
- name: asset.name,
204
- online: {
205
- source: asset.source,
206
- id: asset.id,
207
- name: asset.name,
208
- },
628
+ const filteredAssets = useMemo(
629
+ () =>
630
+ queryAssetLibrary(
631
+ assets,
632
+ {
633
+ licenses: new Set(license ? [license] : []),
634
+ deliveryKinds: new Set(delivery ? [delivery] : []),
635
+ health: new Set(health ? [health] : []),
636
+ animatedOnly,
637
+ runtimeReadyOnly,
638
+ downloaded,
639
+ favoritesOnly,
640
+ },
641
+ sort,
642
+ { favorites, recent, isDownloaded: downloadedAsset },
643
+ ),
644
+ [
645
+ animatedOnly,
646
+ assets,
647
+ delivery,
648
+ downloaded,
649
+ downloadedAsset,
650
+ favorites,
651
+ favoritesOnly,
652
+ health,
653
+ license,
654
+ recent,
655
+ runtimeReadyOnly,
656
+ sort,
657
+ ],
658
+ );
659
+ const facetCounts = useMemo(() => {
660
+ const count = (values: readonly string[]) => {
661
+ const result = new Map<string, number>();
662
+ for (const value of values) result.set(value, (result.get(value) ?? 0) + 1);
663
+ return result;
209
664
  };
210
- e.dataTransfer.setData('text/plain', asset.name);
211
- e.dataTransfer.setData('application/x-editor-asset', JSON.stringify(payload));
212
- }, []);
213
-
214
- // Source change resets type
215
- const onSourceChange = useCallback((newSource: Source) => {
216
- setSource(newSource);
217
- const newConfig = SOURCES.find((s) => s.value === newSource)!;
218
- setAssetType(newConfig.types[0]!.value);
665
+ return {
666
+ licenses: count(assets.map((asset) => asset.license ?? 'Unknown')),
667
+ deliveries: count(assets.flatMap((asset) => asset.deliveryKinds ?? [])),
668
+ health: count(assets.map((asset) => asset.capabilities?.health ?? 'unsupported')),
669
+ };
670
+ }, [assets]);
671
+ const selected = assets.find((asset) => asset.id === selectedId) ?? null;
672
+
673
+ const onSelect = (asset: OnlineAsset) => setSelectedId(asset.id);
674
+ const onOpen = (asset: OnlineAsset) => {
675
+ openOnlineAssetDocument(store, toOnlineInfo(asset));
676
+ const key = onlineAssetKey(asset);
677
+ setRecent((current) => [key, ...current.filter((item) => item !== key)].slice(0, 20));
678
+ };
679
+ const onSource = (nextSource: Source) => {
680
+ setSource(nextSource);
681
+ setAssetType(SOURCES.find((entry) => entry.value === nextSource)!.types[0]!.value);
219
682
  setCategory('');
220
- setSearchQuery('');
221
- }, []);
222
-
223
- const hasMore = assets.length < total;
683
+ };
684
+ const toggleFavorite = (asset: OnlineAsset) => {
685
+ const key = onlineAssetKey(asset);
686
+ setFavorites((current) => {
687
+ const next = new Set(current);
688
+ if (next.has(key)) next.delete(key);
689
+ else next.add(key);
690
+ return next;
691
+ });
692
+ };
224
693
 
225
694
  return (
226
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
227
- {/* Filter bar */}
228
- <div
229
- style={{
230
- padding: '6px 8px',
231
- borderBottom: '1px solid #333',
232
- display: 'flex',
233
- flexWrap: 'wrap',
234
- gap: 6,
235
- alignItems: 'center',
236
- flexShrink: 0,
695
+ <div
696
+ className="vgai-asset-library"
697
+ data-density={density}
698
+ data-source-collapsed={sourceCollapsed}
699
+ data-details-collapsed={detailsCollapsed}
700
+ style={
701
+ {
702
+ '--asset-source-width': `${sourceCollapsed ? 0 : sourceWidth}px`,
703
+ '--asset-details-width': `${detailsCollapsed ? 0 : detailsWidth}px`,
704
+ } as React.CSSProperties
705
+ }
706
+ >
707
+ <SourcePane
708
+ source={source}
709
+ assetType={assetType}
710
+ category={category}
711
+ categoryQuery={categoryQuery}
712
+ categories={categories}
713
+ savedSearches={savedSearches}
714
+ favoritesOnly={favoritesOnly}
715
+ animatedOnly={animatedOnly}
716
+ runtimeReadyOnly={runtimeReadyOnly}
717
+ downloaded={downloaded}
718
+ license={license}
719
+ delivery={delivery}
720
+ health={health}
721
+ licenseCounts={facetCounts.licenses}
722
+ deliveryCounts={facetCounts.deliveries}
723
+ healthCounts={facetCounts.health}
724
+ onSource={onSource}
725
+ onType={(value) => {
726
+ setAssetType(value);
727
+ setCategory('');
237
728
  }}
238
- >
239
- <select
240
- value={source}
241
- onChange={(e) => onSourceChange(e.target.value as Source)}
242
- style={selectStyle}
243
- >
244
- {SOURCES.map((s) => (
245
- <option key={s.value} value={s.value}>
246
- {s.label}
247
- </option>
248
- ))}
249
- </select>
250
-
251
- <select
252
- value={assetType}
253
- onChange={(e) => {
254
- setAssetType(e.target.value);
255
- setCategory('');
256
- }}
257
- style={selectStyle}
258
- >
259
- {sourceConfig.types.map((t) => (
260
- <option key={t.value} value={t.value}>
261
- {t.label}
262
- </option>
263
- ))}
264
- </select>
265
-
266
- {categories.length > 0 && (
267
- <select
268
- value={category}
269
- onChange={(e) => setCategory(e.target.value)}
270
- style={selectStyle}
729
+ onCategory={setCategory}
730
+ onCategoryQuery={setCategoryQuery}
731
+ onSavedSearch={setSearchQuery}
732
+ onFavoritesOnly={setFavoritesOnly}
733
+ onAnimatedOnly={setAnimatedOnly}
734
+ onRuntimeReadyOnly={setRuntimeReadyOnly}
735
+ onDownloaded={setDownloaded}
736
+ onLicense={setLicense}
737
+ onDelivery={setDelivery}
738
+ onHealth={setHealth}
739
+ />
740
+ <main className="vgai-asset-library__results">
741
+ <div className="vgai-asset-library__toolbar">
742
+ <Button
743
+ className="vgai-asset-library__filters-toggle"
744
+ variant="ghost"
745
+ size="comfortable"
746
+ onClick={() => setSourceCollapsed((value) => !value)}
271
747
  >
272
- <option value="">All Categories</option>
273
- {categories.map((c) => (
274
- <option key={c} value={c}>
275
- {c}
276
- </option>
277
- ))}
278
- </select>
279
- )}
280
-
281
- <input
282
- type="text"
283
- placeholder="Search..."
284
- value={searchQuery}
285
- onChange={(e) => setSearchQuery(e.target.value)}
286
- style={inputStyle}
287
- />
288
- </div>
289
-
290
- {/* Thumbnail grid */}
291
- <div style={{ flex: 1, overflowY: 'auto', scrollbarWidth: 'none', minHeight: 0 }}>
292
- <div
293
- style={{
294
- padding: 8,
295
- display: 'grid',
296
- gridTemplateColumns: 'repeat(auto-fill, 88px)',
297
- gap: 6,
298
- alignContent: 'start',
299
- }}
300
- >
301
- {assets.map((asset) => {
302
- const isSelected = selectedId === asset.id;
303
- const isDownloaded = getDownloadedAssetPath(asset.source, asset.name) != null;
304
- return (
305
- <div
306
- key={`${asset.source}-${asset.id}`}
307
- draggable={asset.type === 'model'}
308
- onDragStart={(e) => onDragStart(e, asset)}
309
- onClick={() => onClickAsset(asset)}
310
- onDoubleClick={() => onDoubleClickAsset(asset)}
311
- title={asset.name}
312
- style={{
313
- width: 88,
314
- padding: 4,
315
- borderRadius: 4,
316
- cursor: asset.type === 'model' ? 'grab' : 'pointer',
317
- background: isSelected ? '#579EFF22' : 'transparent',
318
- border: isSelected ? '1px solid #579EFF' : '1px solid transparent',
319
- textAlign: 'center',
320
- userSelect: 'none',
321
- position: 'relative',
322
- }}
323
- onMouseEnter={(e) => {
324
- if (!isSelected) e.currentTarget.style.background = '#333';
325
- }}
326
- onMouseLeave={(e) => {
327
- if (!isSelected) e.currentTarget.style.background = 'transparent';
328
- }}
329
- >
330
- {/* Thumbnail */}
331
- <div
332
- style={{
333
- width: 72,
334
- height: 72,
335
- margin: '0 auto 4px',
336
- borderRadius: 3,
337
- overflow: 'hidden',
338
- background: '#111',
339
- }}
340
- >
341
- <img
342
- src={asset.thumbnailUrl}
343
- alt={asset.name}
344
- loading="lazy"
345
- style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
346
- />
347
- </div>
348
- {/* Name */}
349
- <div
350
- style={{
351
- fontSize: 10,
352
- lineHeight: '13px',
353
- overflow: 'hidden',
354
- textOverflow: 'ellipsis',
355
- whiteSpace: 'nowrap',
356
- color: '#c5c8ce',
357
- }}
358
- >
359
- {asset.name}
360
- </div>
361
- {/* Downloaded badge */}
362
- {asset.cloudHosted && !isDownloaded && (
363
- <div
364
- title="Available from VGAI Cloud"
365
- style={{
366
- position: 'absolute',
367
- top: 4,
368
- right: 4,
369
- borderRadius: 3,
370
- background: '#579EFF',
371
- color: '#fff',
372
- fontSize: 8,
373
- lineHeight: '14px',
374
- padding: '0 4px',
375
- }}
376
- >
377
- CLOUD
378
- </div>
379
- )}
380
- {isDownloaded && (
381
- <div
382
- style={{
383
- position: 'absolute',
384
- top: 4,
385
- right: 4,
386
- width: 16,
387
- height: 16,
388
- borderRadius: '50%',
389
- background: '#22c55e',
390
- display: 'flex',
391
- alignItems: 'center',
392
- justifyContent: 'center',
393
- }}
394
- >
395
- <FontAwesomeIcon icon={faCheck} style={{ fontSize: 9, color: '#fff' }} />
396
- </div>
397
- )}
398
- </div>
399
- );
400
- })}
401
-
402
- {assets.length === 0 && !loading && (
403
- <div
404
- style={{
405
- gridColumn: '1 / -1',
406
- color: '#666',
407
- fontSize: 12,
408
- padding: 24,
409
- textAlign: 'center',
748
+ {sourceCollapsed ? 'Show filters' : 'Hide filters'}
749
+ </Button>
750
+ <TextInput
751
+ className="vgai-input"
752
+ aria-label="Search asset library"
753
+ placeholder="Search models, packs, authors, clips…"
754
+ value={searchQuery}
755
+ onChange={(event) => setSearchQuery(event.target.value)}
756
+ style={{ flex: '1 1 220px' }}
757
+ />
758
+ <Select
759
+ className="vgai-select vgai-asset-library__sort"
760
+ aria-label="Sort assets"
761
+ value={sort}
762
+ onChange={(event) => setSort(event.target.value as Sort)}
763
+ >
764
+ <option value="relevance">Relevance</option>
765
+ <option value="name">Name</option>
766
+ <option value="source">Source</option>
767
+ <option value="size">Size</option>
768
+ <option value="recently-added">Recently added</option>
769
+ <option value="recently-used">Recently used</option>
770
+ </Select>
771
+ <Select
772
+ className="vgai-select vgai-asset-library__density"
773
+ aria-label="Card density"
774
+ value={density}
775
+ onChange={(event) => setDensity(event.target.value as Density)}
776
+ >
777
+ <option value="compact">Compact</option>
778
+ <option value="standard">Standard</option>
779
+ <option value="large">Large</option>
780
+ </Select>
781
+ <Button
782
+ className="vgai-asset-library__save-search"
783
+ variant="ghost"
784
+ size="comfortable"
785
+ disabled={!searchQuery || savedSearches.includes(searchQuery)}
786
+ onClick={() => setSavedSearches((current) => [...current, searchQuery])}
787
+ >
788
+ Save search
789
+ </Button>
790
+ <Button
791
+ className="vgai-asset-library__details-toggle"
792
+ variant="ghost"
793
+ size="comfortable"
794
+ onClick={() => setDetailsCollapsed((value) => !value)}
795
+ >
796
+ {detailsCollapsed ? 'Show details' : 'Hide details'}
797
+ </Button>
798
+ <span
799
+ className="vgai-asset-library__status"
800
+ role="status"
801
+ aria-live="polite"
802
+ style={{ fontSize: 11, color: text[3] }}
803
+ >
804
+ {filteredAssets.length} loaded · {total.toLocaleString()} total
805
+ </span>
806
+ </div>
807
+ {error ? (
808
+ <div role="alert" style={{ padding: 32, color: text[2], textAlign: 'center' }}>
809
+ <div>{error}</div>
810
+ <Button
811
+ variant="ghost"
812
+ size="comfortable"
813
+ onClick={() => {
814
+ setOffset(0);
815
+ void doSearch(0, false);
410
816
  }}
411
817
  >
412
- {searchQuery ? 'No matching assets found.' : 'No assets found.'}
413
- </div>
414
- )}
415
- </div>
416
-
417
- {/* Loading indicator */}
418
- {loading && (
419
- <div style={{ textAlign: 'center', padding: 16, color: '#888', fontSize: 12 }}>
420
- <FontAwesomeIcon icon={faSpinner} spin style={{ marginRight: 6 }} />
421
- Loading...
818
+ Retry
819
+ </Button>
820
+ </div>
821
+ ) : filteredAssets.length === 0 && !loading ? (
822
+ <div role="status" style={{ padding: 32, color: text[3], textAlign: 'center' }}>
823
+ No assets match the current search and filters.
422
824
  </div>
825
+ ) : (
826
+ <VirtualAssetGrid
827
+ assets={filteredAssets}
828
+ selectedId={selectedId}
829
+ density={density}
830
+ loading={loading}
831
+ hasMore={assets.length < total}
832
+ isFavorite={(asset) => favorites.has(onlineAssetKey(asset))}
833
+ onSelect={onSelect}
834
+ onOpen={onOpen}
835
+ onLoadMore={loadMore}
836
+ />
423
837
  )}
424
-
425
- {/* Load more */}
426
- {!loading && hasMore && (
427
- <div style={{ textAlign: 'center', padding: 12 }}>
428
- <button
429
- type="button"
430
- onClick={loadMore}
838
+ </main>
839
+ <aside className="vgai-asset-library__details" aria-label="Selected asset details">
840
+ <div className="vgai-asset-pane-size">
841
+ <label>
842
+ Detail width
843
+ <TextInput
844
+ aria-label="Detail pane width"
845
+ type="range"
846
+ min="280"
847
+ max="640"
848
+ value={detailsWidth}
849
+ onChange={(event) => setDetailsWidth(Number(event.target.value))}
850
+ />
851
+ </label>
852
+ </div>
853
+ {selected ? (
854
+ <>
855
+ <div
431
856
  style={{
432
- background: '#333',
433
- border: '1px solid #444',
434
- borderRadius: 4,
435
- color: '#c5c8ce',
436
- fontSize: 11,
437
- padding: '4px 16px',
438
- cursor: 'pointer',
857
+ display: 'flex',
858
+ gap: 8,
859
+ padding: '8px 10px',
860
+ borderBottom: '1px solid var(--vgai-border-1)',
439
861
  }}
440
862
  >
441
- Load more...
442
- </button>
863
+ <Button variant="solid" size="comfortable" onClick={() => onOpen(selected)}>
864
+ Open in Asset Editor
865
+ </Button>
866
+ <Button variant="ghost" size="comfortable" onClick={() => toggleFavorite(selected)}>
867
+ {favorites.has(onlineAssetKey(selected)) ? 'Remove favorite' : 'Add favorite'}
868
+ </Button>
869
+ {getDownloadedAssetPath(selected.source, selected.id) && (
870
+ <span className="vgai-asset-badge">
871
+ <EditorIcon icon={faCheck} /> In project
872
+ </span>
873
+ )}
874
+ </div>
875
+ <OnlineAssetDetail
876
+ online={toOnlineInfo(selected)}
877
+ onResolved={() => setAssets((current) => [...current])}
878
+ />
879
+ </>
880
+ ) : (
881
+ <div role="status" style={{ padding: 24, color: text[3] }}>
882
+ Select an asset to inspect its preview, variants, license, and project status.
443
883
  </div>
444
884
  )}
445
- </div>
885
+ </aside>
886
+ {!sourceCollapsed && (
887
+ <TextInput
888
+ className="vgai-asset-source-size"
889
+ aria-label="Source pane width"
890
+ type="range"
891
+ min="176"
892
+ max="360"
893
+ value={sourceWidth}
894
+ onChange={(event) => setSourceWidth(Number(event.target.value))}
895
+ />
896
+ )}
446
897
  </div>
447
898
  );
448
899
  }