@pascal-app/editor 1.0.3 → 1.0.4

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 (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,670 @@
1
+ 'use client'
2
+
3
+ import { useScene } from '@pascal-app/core'
4
+ import {
5
+ Check,
6
+ ChevronRight,
7
+ Coins,
8
+ ExternalLink,
9
+ Image,
10
+ Link2,
11
+ Lock,
12
+ type LucideIcon,
13
+ Map as MapIcon,
14
+ Search,
15
+ Sparkles,
16
+ Users,
17
+ X,
18
+ } from 'lucide-react'
19
+ import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
20
+ import {
21
+ type EditorHostPanel,
22
+ editorHostPanelRegistry,
23
+ type PluginAccess,
24
+ type PluginDirectoryContext,
25
+ type PluginInstallLock,
26
+ pluginDirectoryContext,
27
+ pluginInstallLocks,
28
+ } from '../../../lib/plugin-panels'
29
+ import { openSidebarPanel } from '../../../lib/sidebar-panel'
30
+ import { cn } from '../../../lib/utils'
31
+ import useEditor from '../../../store/use-editor'
32
+ import { IconRefGlyph } from '../../ui/icon-ref'
33
+ import { Button } from '../../ui/primitives/button'
34
+ import { PLUGINS_VIEW_ID } from './use-editor-views'
35
+
36
+ const PLUGIN_AUTHORING_URL = 'https://editor.pascal.app/docs/developers/plugins'
37
+ const OTHER_CATEGORY = 'Other'
38
+ /** Below this width the categories fold into a select and the detail floats over the list. */
39
+ const WIDE = 900
40
+
41
+ const ACCESS_ICON: Record<PluginAccess['kind'], LucideIcon> = {
42
+ ai: Sparkles,
43
+ media: Image,
44
+ data: MapIcon,
45
+ account: Link2,
46
+ sharing: Users,
47
+ }
48
+
49
+ type PluginEntry = {
50
+ id: string
51
+ panel: EditorHostPanel
52
+ category: string
53
+ installed: boolean
54
+ }
55
+
56
+ type Status = 'all' | 'installed' | 'available'
57
+
58
+ function useRegistry<T>(store: {
59
+ subscribe: (cb: () => void) => () => void
60
+ getSnapshot: () => T
61
+ }) {
62
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
63
+ }
64
+
65
+ function useWidth<T extends HTMLElement>() {
66
+ const ref = useRef<T>(null)
67
+ const [width, setWidth] = useState(WIDE)
68
+ useLayoutEffect(() => {
69
+ const el = ref.current
70
+ if (!el) return
71
+ const observer = new ResizeObserver(([entry]) => {
72
+ if (entry) setWidth(entry.contentRect.width)
73
+ })
74
+ observer.observe(el)
75
+ return () => observer.disconnect()
76
+ }, [])
77
+ return [ref, width] as const
78
+ }
79
+
80
+ function usesCredits(panel: EditorHostPanel) {
81
+ return panel.access?.some((access) => access.usesCredits) ?? false
82
+ }
83
+
84
+ function creditsLabel(credits: PluginDirectoryContext['credits']) {
85
+ if (!credits) return null
86
+ return 'unlimited' in credits
87
+ ? 'Unlimited'
88
+ : `${Math.ceil(credits.remaining).toLocaleString('en-US')} left`
89
+ }
90
+
91
+ function PluginIcon({ panel, size }: { panel: EditorHostPanel; size: 'md' | 'lg' }) {
92
+ return (
93
+ <span
94
+ className={cn(
95
+ 'flex shrink-0 items-center justify-center overflow-hidden border border-border/60 bg-background/60',
96
+ size === 'md' ? 'h-11 w-11 rounded-xl' : 'h-14 w-14 rounded-2xl',
97
+ )}
98
+ >
99
+ <IconRefGlyph icon={panel.icon} size={size === 'md' ? 28 : 36} />
100
+ </span>
101
+ )
102
+ }
103
+
104
+ function Badge({ label }: { label: string }) {
105
+ return (
106
+ <span className="rounded-[5px] border border-border px-1.5 py-px font-mono font-medium text-[10px] text-muted-foreground uppercase tracking-wide">
107
+ {label}
108
+ </span>
109
+ )
110
+ }
111
+
112
+ function StatusLine({ entry, projectName }: { entry: PluginEntry; projectName?: string }) {
113
+ return (
114
+ <span className="flex min-w-0 items-center gap-1.5 text-muted-foreground text-xs">
115
+ {entry.installed && <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-foreground/80" />}
116
+ <span className="truncate">
117
+ {entry.installed
118
+ ? projectName
119
+ ? `Installed in ${projectName}`
120
+ : 'Installed'
121
+ : 'Not installed'}{' '}
122
+ · {entry.category}
123
+ </span>
124
+ </span>
125
+ )
126
+ }
127
+
128
+ function AccessChips({ panel }: { panel: EditorHostPanel }) {
129
+ const chips = new Map<string, LucideIcon>()
130
+ for (const access of panel.access ?? []) {
131
+ if (access.usesCredits) chips.set('Uses credits', Coins)
132
+ else chips.set(access.chip ?? access.label, ACCESS_ICON[access.kind])
133
+ }
134
+ if (chips.size === 0) chips.set('No services or credits', Check)
135
+ return (
136
+ <>
137
+ {[...chips].map(([label, Glyph]) => (
138
+ <span
139
+ className="inline-flex h-6 items-center gap-1.5 rounded-[7px] bg-accent/60 px-2 text-muted-foreground text-xs"
140
+ key={label}
141
+ >
142
+ <Glyph className="h-3.5 w-3.5" />
143
+ {label}
144
+ </span>
145
+ ))}
146
+ </>
147
+ )
148
+ }
149
+
150
+ function PluginCard({
151
+ entry,
152
+ selected,
153
+ lock,
154
+ readOnly,
155
+ onOpen,
156
+ onInstall,
157
+ }: {
158
+ entry: PluginEntry
159
+ selected: boolean
160
+ lock?: PluginInstallLock
161
+ readOnly: boolean
162
+ onOpen: () => void
163
+ onInstall: () => void
164
+ }) {
165
+ const { panel } = entry
166
+ return (
167
+ <div
168
+ aria-label={panel.label}
169
+ className={cn(
170
+ 'grid min-h-44 cursor-pointer grid-rows-[auto_1fr_auto] gap-3 rounded-2xl border bg-background/40 p-4 text-left transition-colors',
171
+ selected
172
+ ? 'border-primary ring-[3px] ring-primary/20'
173
+ : 'border-border/60 hover:border-border hover:bg-background/60',
174
+ )}
175
+ onClick={onOpen}
176
+ onKeyDown={(event) => {
177
+ if (event.target !== event.currentTarget) return
178
+ if (event.key === 'Enter' || event.key === ' ') {
179
+ event.preventDefault()
180
+ onOpen()
181
+ }
182
+ }}
183
+ role="button"
184
+ tabIndex={0}
185
+ >
186
+ <div className="flex min-w-0 items-center gap-3">
187
+ <PluginIcon panel={panel} size="md" />
188
+ <div className="min-w-0">
189
+ <div className="flex items-center gap-2 font-semibold text-[14.5px] text-foreground">
190
+ <span className="truncate">{panel.label}</span>
191
+ {panel.badge && <Badge label={panel.badge} />}
192
+ </div>
193
+ <StatusLine entry={entry} />
194
+ </div>
195
+ </div>
196
+ <p className="line-clamp-2 text-foreground/80 text-sm">
197
+ {panel.summary ?? panel.description ?? 'Adds a new tool panel to the editor.'}
198
+ </p>
199
+ <div className="flex flex-wrap items-center gap-1.5">
200
+ <AccessChips panel={panel} />
201
+ {entry.installed ? (
202
+ <span className="ml-auto inline-flex items-center gap-1 text-muted-foreground text-xs">
203
+ Manage
204
+ <ChevronRight className="h-3.5 w-3.5" />
205
+ </span>
206
+ ) : (
207
+ <Button
208
+ className="ml-auto h-7 rounded-full px-3"
209
+ disabled={readOnly && !lock}
210
+ onClick={(event) => {
211
+ event.stopPropagation()
212
+ if (lock) lock.onAction()
213
+ else onInstall()
214
+ }}
215
+ size="sm"
216
+ variant="outline"
217
+ >
218
+ {lock ? lock.actionLabel : 'Install'}
219
+ </Button>
220
+ )}
221
+ </div>
222
+ </div>
223
+ )
224
+ }
225
+
226
+ function PluginDetail({
227
+ entry,
228
+ context,
229
+ lock,
230
+ readOnly,
231
+ floating,
232
+ onClose,
233
+ onToggle,
234
+ onOpen,
235
+ }: {
236
+ entry: PluginEntry
237
+ context: PluginDirectoryContext
238
+ lock?: PluginInstallLock
239
+ readOnly: boolean
240
+ floating: boolean
241
+ onClose: () => void
242
+ onToggle: () => void
243
+ /** Opens the plugin's panel; absent when it has none in this workspace. */
244
+ onOpen?: () => void
245
+ }) {
246
+ const { panel } = entry
247
+ const credits = creditsLabel(context.credits)
248
+ return (
249
+ <aside
250
+ aria-label={`${panel.label} details`}
251
+ className={cn(
252
+ 'flex w-[360px] shrink-0 flex-col gap-5 overflow-y-auto border-border/60 border-l bg-sidebar p-5',
253
+ floating && 'absolute inset-y-0 right-0 z-10 max-w-full shadow-2xl',
254
+ )}
255
+ >
256
+ <div className="flex items-start gap-3.5">
257
+ <PluginIcon panel={panel} size="lg" />
258
+ <div className="min-w-0 pt-0.5">
259
+ <h3 className="flex items-center gap-2 font-semibold text-foreground text-lg">
260
+ {panel.label}
261
+ {panel.badge && <Badge label={panel.badge} />}
262
+ </h3>
263
+ <StatusLine entry={entry} projectName={context.projectName} />
264
+ </div>
265
+ <button
266
+ aria-label="Close details"
267
+ className="ml-auto flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
268
+ onClick={onClose}
269
+ type="button"
270
+ >
271
+ <X className="h-4 w-4" />
272
+ </button>
273
+ </div>
274
+
275
+ {lock && !entry.installed ? (
276
+ <div className="flex flex-col gap-3">
277
+ <p className="flex items-start gap-2 text-muted-foreground text-sm">
278
+ <Lock className="mt-0.5 h-3.5 w-3.5 shrink-0" />
279
+ {lock.reason}
280
+ </p>
281
+ <Button className="h-10 rounded-full" onClick={lock.onAction}>
282
+ {lock.actionLabel}
283
+ </Button>
284
+ </div>
285
+ ) : entry.installed ? (
286
+ <div className="flex gap-2">
287
+ {onOpen && (
288
+ <Button className="h-10 flex-1 rounded-full" onClick={onOpen}>
289
+ Open {panel.label}
290
+ </Button>
291
+ )}
292
+ <Button
293
+ className="h-10 flex-1 rounded-full"
294
+ disabled={readOnly}
295
+ onClick={onToggle}
296
+ variant="outline"
297
+ >
298
+ Uninstall
299
+ </Button>
300
+ </div>
301
+ ) : (
302
+ <Button className="h-10 rounded-full" disabled={readOnly} onClick={onToggle}>
303
+ <Check />
304
+ {context.projectName ? `Install in ${context.projectName}` : 'Install'}
305
+ </Button>
306
+ )}
307
+
308
+ <section>
309
+ <h4 className="mb-2 font-mono text-[10.5px] text-muted-foreground uppercase tracking-[0.08em]">
310
+ About
311
+ </h4>
312
+ <p className="text-foreground/90 text-sm leading-relaxed">
313
+ {panel.description ?? panel.summary ?? 'Adds a new tool panel to the editor.'}
314
+ </p>
315
+ </section>
316
+
317
+ <section>
318
+ <h4 className="mb-2 font-mono text-[10.5px] text-muted-foreground uppercase tracking-[0.08em]">
319
+ Can access
320
+ </h4>
321
+ {panel.access?.length ? (
322
+ <ul className="overflow-hidden rounded-xl border border-border/60">
323
+ {panel.access.map((access) => {
324
+ const Glyph = ACCESS_ICON[access.kind]
325
+ return (
326
+ <li
327
+ className="flex gap-3 border-border/60 border-t px-3.5 py-3 first:border-t-0"
328
+ key={access.label}
329
+ >
330
+ <Glyph className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
331
+ <div className="min-w-0">
332
+ <p className="font-semibold text-foreground text-sm">{access.label}</p>
333
+ <p className="text-muted-foreground text-xs leading-relaxed">{access.detail}</p>
334
+ </div>
335
+ </li>
336
+ )
337
+ })}
338
+ </ul>
339
+ ) : (
340
+ <p className="rounded-xl border border-border/60 border-dashed px-3.5 py-3 text-muted-foreground text-sm">
341
+ Runs in the editor. No Pascal services, no outside accounts.
342
+ </p>
343
+ )}
344
+ </section>
345
+
346
+ <section>
347
+ <h4 className="mb-2 font-mono text-[10.5px] text-muted-foreground uppercase tracking-[0.08em]">
348
+ Credits
349
+ </h4>
350
+ <div className="flex items-center gap-2.5 rounded-xl bg-accent/60 px-3.5 py-3 text-sm">
351
+ <Coins className="h-4 w-4 shrink-0 text-muted-foreground" />
352
+ <span>{usesCredits(panel) ? "Uses this project's credits" : "Doesn't use credits"}</span>
353
+ {credits && usesCredits(panel) && (
354
+ <span className="ml-auto font-mono text-muted-foreground text-xs tabular-nums">
355
+ {credits}
356
+ </span>
357
+ )}
358
+ </div>
359
+ </section>
360
+
361
+ {(panel.creator || panel.pluginUrl) && (
362
+ <section className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground text-sm">
363
+ {panel.creator &&
364
+ (panel.creator.url ? (
365
+ <a
366
+ className="inline-flex items-center gap-1 underline-offset-4 hover:text-foreground hover:underline"
367
+ href={panel.creator.url}
368
+ rel="noreferrer"
369
+ target="_blank"
370
+ >
371
+ By {panel.creator.name}
372
+ <ExternalLink className="h-3 w-3" />
373
+ </a>
374
+ ) : (
375
+ <span>By {panel.creator.name}</span>
376
+ ))}
377
+ {panel.pluginUrl && (
378
+ <a
379
+ className="inline-flex items-center gap-1 underline-offset-4 hover:text-foreground hover:underline"
380
+ href={panel.pluginUrl}
381
+ rel="noreferrer"
382
+ target="_blank"
383
+ >
384
+ View plugin
385
+ <ExternalLink className="h-3 w-3" />
386
+ </a>
387
+ )}
388
+ </section>
389
+ )}
390
+ </aside>
391
+ )
392
+ }
393
+
394
+ /**
395
+ * The plugin directory: categories, search and an installed/available filter
396
+ * over the plugins the host registered, a card per plugin and a detail with
397
+ * what it can reach before you install it. A view of its own, so it has the
398
+ * room to read like one.
399
+ */
400
+ export function PluginsView() {
401
+ const panels = useRegistry(editorHostPanelRegistry)
402
+ const installLocks = useRegistry(pluginInstallLocks)
403
+ const context = useRegistry(pluginDirectoryContext)
404
+ const installedPlugins = useScene((state) => state.installedPlugins)
405
+ const setInstalledPlugins = useScene((state) => state.setInstalledPlugins)
406
+ const readOnly = useScene((state) => state.readOnly)
407
+ const [rootRef, width] = useWidth<HTMLDivElement>()
408
+ const wide = width >= WIDE
409
+
410
+ const [category, setCategory] = useState<string | null>(null)
411
+ const [status, setStatus] = useState<Status>('all')
412
+ const [query, setQuery] = useState('')
413
+ const [selectedId, setSelectedId] = useState<string | null>(null)
414
+ const [undo, setUndo] = useState<{ id: string; installed: boolean } | null>(null)
415
+
416
+ useEffect(() => {
417
+ if (!undo) return
418
+ const timer = setTimeout(() => setUndo(null), 5000)
419
+ return () => clearTimeout(timer)
420
+ }, [undo])
421
+
422
+ // One entry per plugin: a plugin can register several panels, the first carries its card.
423
+ const entries: PluginEntry[] = Array.from(
424
+ new Map(
425
+ panels.filter((panel) => panel.pluginId).map((panel) => [panel.pluginId as string, panel]),
426
+ ),
427
+ )
428
+ .map(([id, panel]) => ({
429
+ id,
430
+ panel,
431
+ category: panel.category ?? OTHER_CATEGORY,
432
+ installed: installedPlugins.includes(id),
433
+ }))
434
+ .sort((a, b) => a.panel.label.localeCompare(b.panel.label))
435
+
436
+ const categories = Array.from(new Set(entries.map((entry) => entry.category))).sort((a, b) =>
437
+ a === OTHER_CATEGORY ? 1 : b === OTHER_CATEGORY ? -1 : a.localeCompare(b),
438
+ )
439
+ const inCategory = entries.filter((entry) => !category || entry.category === category)
440
+ const needle = query.trim().toLowerCase()
441
+ const shown = inCategory.filter(
442
+ (entry) =>
443
+ !needle ||
444
+ `${entry.panel.label} ${entry.panel.summary ?? ''} ${entry.panel.description ?? ''} ${entry.category}`
445
+ .toLowerCase()
446
+ .includes(needle),
447
+ )
448
+ const installed = shown.filter((entry) => entry.installed)
449
+ const available = shown.filter((entry) => !entry.installed)
450
+ const selected = entries.find((entry) => entry.id === selectedId)
451
+ // A category is a new list: the detail of a plugin from the last one closes.
452
+ const pickCategory = (name: string | null) => {
453
+ setCategory(name)
454
+ setSelectedId(null)
455
+ }
456
+ const workspaceMode = useEditor((state) => state.workspaceMode)
457
+ const panelIdsOf = (pluginId: string) =>
458
+ panels
459
+ .filter(
460
+ (panel) =>
461
+ panel.pluginId === pluginId && (panel.workspaces ?? ['edit']).includes(workspaceMode),
462
+ )
463
+ .map((panel) => panel.id)
464
+ /** Hands the stage back to the scene and shows the plugin's panel in the sidebar. */
465
+ const openPlugin = (pluginId: string) => {
466
+ if (!openSidebarPanel(panelIdsOf(pluginId))) return
467
+ useEditor.getState().closeView(PLUGINS_VIEW_ID)
468
+ }
469
+
470
+ const setInstalled = (id: string, next: boolean, remember = true) => {
471
+ const current = useScene.getState().installedPlugins
472
+ setInstalledPlugins(
473
+ next
474
+ ? [...current.filter((pluginId) => pluginId !== id), id]
475
+ : current.filter((pluginId) => pluginId !== id),
476
+ { explicit: true },
477
+ )
478
+ setSelectedId(id)
479
+ setUndo(remember ? { id, installed: next } : null)
480
+ }
481
+
482
+ const statusOptions: [Status, string, number][] = [
483
+ ['all', 'All', inCategory.length],
484
+ ['installed', 'Installed', inCategory.filter((entry) => entry.installed).length],
485
+ ['available', 'Available', inCategory.filter((entry) => !entry.installed).length],
486
+ ]
487
+
488
+ const renderSection = (title: string, list: PluginEntry[], aside?: string) =>
489
+ list.length > 0 && (
490
+ <section className="mt-7 first:mt-0">
491
+ <div className="mb-3 flex items-baseline gap-2.5">
492
+ <h2 className="font-semibold text-[15px] text-foreground">{title}</h2>
493
+ <span className="font-mono text-muted-foreground text-xs">{list.length}</span>
494
+ {aside && <span className="ml-auto text-muted-foreground text-xs">{aside}</span>}
495
+ </div>
496
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-3">
497
+ {list.map((entry) => (
498
+ <PluginCard
499
+ entry={entry}
500
+ key={entry.id}
501
+ lock={entry.installed ? undefined : installLocks[entry.id]}
502
+ onInstall={() => setInstalled(entry.id, true)}
503
+ onOpen={() => setSelectedId(entry.id)}
504
+ readOnly={readOnly}
505
+ selected={entry.id === selectedId}
506
+ />
507
+ ))}
508
+ </div>
509
+ </section>
510
+ )
511
+
512
+ const undoEntry = undo && entries.find((entry) => entry.id === undo.id)
513
+
514
+ return (
515
+ <div
516
+ className="relative flex h-full min-h-0 bg-sidebar text-sidebar-foreground"
517
+ onKeyDown={(event) => {
518
+ if (event.key === 'Escape' && selectedId) setSelectedId(null)
519
+ }}
520
+ ref={rootRef}
521
+ >
522
+ {wide && (
523
+ <nav
524
+ aria-label="Plugin categories"
525
+ className="flex w-56 shrink-0 flex-col gap-0.5 overflow-y-auto border-border/60 border-r px-3.5 py-6"
526
+ >
527
+ <h4 className="mx-2.5 mb-1.5 font-mono text-[10.5px] text-muted-foreground uppercase tracking-[0.08em]">
528
+ Browse
529
+ </h4>
530
+ {[null, ...categories].map((name) => {
531
+ const count = name
532
+ ? entries.filter((entry) => entry.category === name).length
533
+ : entries.length
534
+ return (
535
+ <button
536
+ aria-pressed={category === name}
537
+ className={cn(
538
+ 'flex h-9 w-full items-center gap-2.5 rounded-lg px-2.5 text-left text-sm transition-colors',
539
+ category === name
540
+ ? 'bg-accent text-foreground'
541
+ : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
542
+ )}
543
+ key={name ?? 'all'}
544
+ onClick={() => pickCategory(name)}
545
+ type="button"
546
+ >
547
+ <span className="flex-1 truncate">{name ?? 'All plugins'}</span>
548
+ <span className="font-mono text-muted-foreground text-xs">{count}</span>
549
+ </button>
550
+ )
551
+ })}
552
+ <div className="mt-auto flex flex-col gap-2 border-border/60 border-t px-2.5 pt-3.5 text-muted-foreground text-xs leading-relaxed">
553
+ {context.notes?.map((note) => (
554
+ <p key={note}>{note}</p>
555
+ ))}
556
+ <a
557
+ className="inline-flex items-center gap-1.5 underline-offset-4 hover:text-foreground hover:underline"
558
+ href={PLUGIN_AUTHORING_URL}
559
+ rel="noreferrer"
560
+ target="_blank"
561
+ >
562
+ Create a Pascal plugin
563
+ <ExternalLink className="h-3 w-3" />
564
+ </a>
565
+ </div>
566
+ </nav>
567
+ )}
568
+
569
+ <main className={cn('min-w-0 flex-1 overflow-y-auto pt-7 pb-12', wide ? 'px-8' : 'px-5')}>
570
+ <div className="mb-6 flex flex-wrap items-end gap-4">
571
+ <div className={cn('min-w-0', wide ? 'flex-1' : 'basis-full')}>
572
+ <h1 className="font-semibold text-2xl text-foreground tracking-tight">Plugins</h1>
573
+ <p className="mt-1 text-muted-foreground text-sm">
574
+ Add focused tools and content to {context.projectName ?? 'this project'}.
575
+ </p>
576
+ </div>
577
+ {!wide && categories.length > 1 && (
578
+ <select
579
+ aria-label="Category"
580
+ className="h-9 rounded-lg border border-border bg-accent/40 px-2.5 text-sm"
581
+ onChange={(event) => pickCategory(event.target.value || null)}
582
+ value={category ?? ''}
583
+ >
584
+ <option value="">All plugins</option>
585
+ {categories.map((name) => (
586
+ <option key={name} value={name}>
587
+ {name}
588
+ </option>
589
+ ))}
590
+ </select>
591
+ )}
592
+ <label className="flex h-9 w-64 max-w-full items-center gap-2 rounded-lg border border-border bg-accent/40 px-3 text-muted-foreground focus-within:border-foreground/30">
593
+ <Search className="h-4 w-4 shrink-0" />
594
+ <input
595
+ aria-label="Search plugins"
596
+ className="min-w-0 flex-1 bg-transparent text-foreground text-sm outline-none placeholder:text-muted-foreground"
597
+ onChange={(event) => setQuery(event.target.value)}
598
+ placeholder="Search plugins"
599
+ type="search"
600
+ value={query}
601
+ />
602
+ </label>
603
+ <div className="inline-flex gap-0.5 rounded-[10px] bg-accent/40 p-[3px]" role="group">
604
+ {statusOptions.map(([id, label, count]) => (
605
+ <button
606
+ aria-pressed={status === id}
607
+ className={cn(
608
+ 'inline-flex h-[30px] items-center gap-1.5 rounded-lg px-3 font-medium text-sm transition-colors',
609
+ status === id
610
+ ? 'bg-background text-foreground shadow-sm'
611
+ : 'text-muted-foreground hover:text-foreground',
612
+ )}
613
+ key={id}
614
+ onClick={() => setStatus(id)}
615
+ type="button"
616
+ >
617
+ {label}
618
+ <span className="font-mono text-muted-foreground text-xs">{count}</span>
619
+ </button>
620
+ ))}
621
+ </div>
622
+ </div>
623
+
624
+ {installed.length + available.length === 0 ? (
625
+ <p className="py-12 text-center text-muted-foreground text-sm">
626
+ {needle
627
+ ? `No plugins match “${query.trim()}”${category ? ` in ${category}` : ''}.`
628
+ : 'No plugins here yet.'}
629
+ </p>
630
+ ) : (
631
+ <>
632
+ {status !== 'available' && renderSection('In this project', installed)}
633
+ {status !== 'installed' && renderSection('Available', available, 'Installing is free')}
634
+ </>
635
+ )}
636
+ </main>
637
+
638
+ {selected && (
639
+ <PluginDetail
640
+ context={context}
641
+ entry={selected}
642
+ floating={!wide}
643
+ lock={installLocks[selected.id]}
644
+ onClose={() => setSelectedId(null)}
645
+ onOpen={panelIdsOf(selected.id).length > 0 ? () => openPlugin(selected.id) : undefined}
646
+ onToggle={() => setInstalled(selected.id, !selected.installed)}
647
+ readOnly={readOnly}
648
+ />
649
+ )}
650
+
651
+ {undoEntry && (
652
+ <div
653
+ className="absolute bottom-6 left-1/2 z-20 flex -translate-x-1/2 items-center gap-2.5 rounded-xl border border-border bg-popover py-1.5 pr-1.5 pl-4 text-sm shadow-2xl"
654
+ role="status"
655
+ >
656
+ <span>
657
+ {undoEntry.panel.label} {undo.installed ? 'installed' : 'uninstalled'}
658
+ </span>
659
+ <button
660
+ className="h-8 rounded-lg px-2.5 font-semibold transition-colors hover:bg-accent"
661
+ onClick={() => setInstalled(undo.id, !undo.installed, false)}
662
+ type="button"
663
+ >
664
+ Undo
665
+ </button>
666
+ </div>
667
+ )}
668
+ </div>
669
+ )
670
+ }