@pascal-app/editor 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -22,6 +22,17 @@ const MOUSE_SHORTCUTS = {
|
|
|
22
22
|
},
|
|
23
23
|
} as const
|
|
24
24
|
|
|
25
|
+
// The platform-agnostic command modifier. Both Cmd and Ctrl bind the action; we
|
|
26
|
+
// render the symbol for the *current* device so the hint reads native (⌘ on Mac,
|
|
27
|
+
// Ctrl elsewhere) without implying only one of them works.
|
|
28
|
+
const COMMAND_VALUES = new Set(['Cmd/Ctrl', 'Cmd', 'Command', 'Meta'])
|
|
29
|
+
|
|
30
|
+
// Resolved once on the client at module load — the editor HUD is client-only, so
|
|
31
|
+
// there's no server render to mismatch against. `navigator.platform` is enough
|
|
32
|
+
// here and matches the detection used elsewhere (floorplan rotate hint).
|
|
33
|
+
const IS_MAC =
|
|
34
|
+
typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC')
|
|
35
|
+
|
|
25
36
|
type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
|
|
26
37
|
value: string
|
|
27
38
|
displayValue?: string
|
|
@@ -30,16 +41,23 @@ type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
|
|
|
30
41
|
function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTokenProps) {
|
|
31
42
|
const mouseShortcut =
|
|
32
43
|
value in MOUSE_SHORTCUTS ? MOUSE_SHORTCUTS[value as keyof typeof MOUSE_SHORTCUTS] : null
|
|
44
|
+
const isCommand = COMMAND_VALUES.has(value)
|
|
45
|
+
const isShift = value === 'Shift'
|
|
46
|
+
const commandDisplay = IS_MAC ? '⌘' : 'Ctrl'
|
|
47
|
+
const commandLabel = IS_MAC ? 'Command' : 'Control'
|
|
33
48
|
|
|
34
49
|
return (
|
|
35
50
|
<kbd
|
|
36
|
-
aria-label={
|
|
51
|
+
aria-label={
|
|
52
|
+
mouseShortcut?.label ??
|
|
53
|
+
(isCommand ? commandLabel : isShift ? 'Shift' : (displayValue ?? value))
|
|
54
|
+
}
|
|
37
55
|
className={cn(
|
|
38
56
|
'inline-flex h-6 items-center rounded border border-border bg-muted px-2 font-medium font-mono text-[11px] text-muted-foreground',
|
|
39
|
-
mouseShortcut && 'justify-center px-1.5',
|
|
57
|
+
(mouseShortcut || isShift) && 'justify-center px-1.5',
|
|
40
58
|
className,
|
|
41
59
|
)}
|
|
42
|
-
title={mouseShortcut?.label ?? value}
|
|
60
|
+
title={mouseShortcut?.label ?? (isCommand ? commandLabel : isShift ? 'Shift' : value)}
|
|
43
61
|
{...props}
|
|
44
62
|
>
|
|
45
63
|
{mouseShortcut ? (
|
|
@@ -54,6 +72,24 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
|
|
|
54
72
|
/>
|
|
55
73
|
<span className="sr-only">{mouseShortcut.label}</span>
|
|
56
74
|
</>
|
|
75
|
+
) : isShift ? (
|
|
76
|
+
// Icon rather than the ⇧ text glyph — the font renders the glyph's
|
|
77
|
+
// crossbar too high to read as the shift key symbol.
|
|
78
|
+
<>
|
|
79
|
+
<Icon
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
className="shrink-0"
|
|
82
|
+
color="currentColor"
|
|
83
|
+
height={13}
|
|
84
|
+
icon="ph:arrow-fat-up"
|
|
85
|
+
width={13}
|
|
86
|
+
/>
|
|
87
|
+
<span className="sr-only">Shift</span>
|
|
88
|
+
</>
|
|
89
|
+
) : isCommand ? (
|
|
90
|
+
// The ⌘ glyph reads small next to letters at the same font size, so bump
|
|
91
|
+
// it up a touch on Mac. "Ctrl" stays at the token's normal size.
|
|
92
|
+
<span className={IS_MAC ? 'text-[13px] leading-none' : undefined}>{commandDisplay}</span>
|
|
57
93
|
) : (
|
|
58
94
|
(displayValue ?? value)
|
|
59
95
|
)}
|
|
@@ -17,15 +17,13 @@ interface SceneLoaderProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function SceneLoader({ className, fullScreen = false }: SceneLoaderProps) {
|
|
20
|
-
const [loaderClass, setLoaderClass] = useState
|
|
20
|
+
const [loaderClass, setLoaderClass] = useState(LOADERS[0]!)
|
|
21
21
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
// Pick a random loader on mount
|
|
24
24
|
setLoaderClass(LOADERS[Math.floor(Math.random() * LOADERS.length)] ?? LOADERS[0]!)
|
|
25
25
|
}, [])
|
|
26
26
|
|
|
27
|
-
if (!loaderClass) return null
|
|
28
|
-
|
|
29
27
|
return (
|
|
30
28
|
<div
|
|
31
29
|
className={cn(
|
|
@@ -16,6 +16,7 @@ import useEditor from './../../../store/use-editor'
|
|
|
16
16
|
import { type ExtraPanel, IconRail } from './icon-rail'
|
|
17
17
|
import { SettingsPanel, type SettingsPanelProps } from './panels/settings-panel'
|
|
18
18
|
import { SitePanel, type SitePanelProps } from './panels/site-panel'
|
|
19
|
+
import { useHostPanels } from './use-plugin-panels'
|
|
19
20
|
|
|
20
21
|
interface AppSidebarProps {
|
|
21
22
|
appMenuButton?: ReactNode
|
|
@@ -31,9 +32,12 @@ export function AppSidebar({
|
|
|
31
32
|
sidebarTop,
|
|
32
33
|
settingsPanelProps,
|
|
33
34
|
sitePanelProps,
|
|
34
|
-
extraPanels,
|
|
35
|
+
extraPanels: hostExtraPanels,
|
|
35
36
|
commandPaletteEmptyAction,
|
|
36
37
|
}: AppSidebarProps) {
|
|
38
|
+
// Host-provided panels merged with registered host panels — the icon rail and
|
|
39
|
+
// content area treat both identically.
|
|
40
|
+
const extraPanels = useHostPanels(hostExtraPanels)
|
|
37
41
|
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
38
42
|
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
39
43
|
const hasActivePanel =
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { Plus } from 'lucide-react'
|
|
3
4
|
import type { ComponentType, ReactNode } from 'react'
|
|
4
5
|
import {
|
|
5
6
|
Tooltip,
|
|
@@ -10,7 +11,13 @@ import { cn } from './../../../lib/utils'
|
|
|
10
11
|
|
|
11
12
|
export type PanelId = string
|
|
12
13
|
|
|
13
|
-
export type ExtraPanel = {
|
|
14
|
+
export type ExtraPanel = {
|
|
15
|
+
id: string
|
|
16
|
+
icon: ReactNode
|
|
17
|
+
label: string
|
|
18
|
+
component: ComponentType
|
|
19
|
+
pluginId?: string
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
interface IconRailProps {
|
|
16
23
|
activePanel: PanelId
|
|
@@ -22,13 +29,13 @@ interface IconRailProps {
|
|
|
22
29
|
|
|
23
30
|
const sitePanel: { id: PanelId; iconSrc: string; label: string } = {
|
|
24
31
|
id: 'site',
|
|
25
|
-
iconSrc: '/icons/level.
|
|
32
|
+
iconSrc: '/icons/level.webp',
|
|
26
33
|
label: 'Site',
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
const settingsPanel: { id: PanelId; iconSrc: string; label: string } = {
|
|
30
37
|
id: 'settings',
|
|
31
|
-
iconSrc: '/icons/settings.
|
|
38
|
+
iconSrc: '/icons/settings.webp',
|
|
32
39
|
label: 'Settings',
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -41,6 +48,38 @@ export function IconRail({
|
|
|
41
48
|
extraPanels,
|
|
42
49
|
className,
|
|
43
50
|
}: IconRailProps) {
|
|
51
|
+
const regularExtraPanels = extraPanels?.filter((panel) => !panel.pluginId && panel.id !== 'plugins')
|
|
52
|
+
const pluginPanels = extraPanels?.filter((panel) => panel.pluginId)
|
|
53
|
+
const pluginsPanel = extraPanels?.find((panel) => panel.id === 'plugins')
|
|
54
|
+
|
|
55
|
+
const renderExtraPanel = (panel: ExtraPanel) => {
|
|
56
|
+
const isActive = activePanel === panel.id
|
|
57
|
+
return (
|
|
58
|
+
<Tooltip key={panel.id}>
|
|
59
|
+
<TooltipTrigger asChild>
|
|
60
|
+
<button
|
|
61
|
+
className={cn(
|
|
62
|
+
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
63
|
+
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
64
|
+
)}
|
|
65
|
+
onClick={() => onPanelChange(panel.id)}
|
|
66
|
+
type="button"
|
|
67
|
+
>
|
|
68
|
+
<span
|
|
69
|
+
className={cn(
|
|
70
|
+
'flex h-6 w-6 items-center justify-center transition-all',
|
|
71
|
+
!isActive && 'opacity-50',
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{panel.id === 'plugins' ? <Plus className="h-5 w-5" /> : panel.icon}
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
</TooltipTrigger>
|
|
78
|
+
<TooltipContent side="right">{panel.label}</TooltipContent>
|
|
79
|
+
</Tooltip>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
return (
|
|
45
84
|
<div
|
|
46
85
|
className={cn(
|
|
@@ -83,34 +122,7 @@ export function IconRail({
|
|
|
83
122
|
)
|
|
84
123
|
})}
|
|
85
124
|
|
|
86
|
-
{
|
|
87
|
-
{extraPanels?.map((panel) => {
|
|
88
|
-
const isActive = activePanel === panel.id
|
|
89
|
-
return (
|
|
90
|
-
<Tooltip key={panel.id}>
|
|
91
|
-
<TooltipTrigger asChild>
|
|
92
|
-
<button
|
|
93
|
-
className={cn(
|
|
94
|
-
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
95
|
-
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
96
|
-
)}
|
|
97
|
-
onClick={() => onPanelChange(panel.id)}
|
|
98
|
-
type="button"
|
|
99
|
-
>
|
|
100
|
-
<span
|
|
101
|
-
className={cn(
|
|
102
|
-
'flex h-6 w-6 items-center justify-center transition-all',
|
|
103
|
-
!isActive && 'opacity-50',
|
|
104
|
-
)}
|
|
105
|
-
>
|
|
106
|
-
{panel.icon}
|
|
107
|
-
</span>
|
|
108
|
-
</button>
|
|
109
|
-
</TooltipTrigger>
|
|
110
|
-
<TooltipContent side="right">{panel.label}</TooltipContent>
|
|
111
|
-
</Tooltip>
|
|
112
|
-
)
|
|
113
|
-
})}
|
|
125
|
+
{regularExtraPanels?.map(renderExtraPanel)}
|
|
114
126
|
|
|
115
127
|
{/* Settings panel */}
|
|
116
128
|
{[settingsPanel].map((panel) => {
|
|
@@ -140,6 +152,13 @@ export function IconRail({
|
|
|
140
152
|
</Tooltip>
|
|
141
153
|
)
|
|
142
154
|
})}
|
|
155
|
+
|
|
156
|
+
{(pluginPanels?.length || pluginsPanel) && (
|
|
157
|
+
<div className="mt-1 flex w-9 flex-col items-center gap-1 border-border/70 border-t pt-2">
|
|
158
|
+
{pluginPanels?.map(renderExtraPanel)}
|
|
159
|
+
{pluginsPanel && renderExtraPanel(pluginsPanel)}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
143
162
|
</div>
|
|
144
163
|
)
|
|
145
164
|
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AssetInput } from '@pascal-app/core'
|
|
4
|
+
import { Root as TooltipRoot } from '@radix-ui/react-tooltip'
|
|
5
|
+
import NextImage from 'next/image'
|
|
6
|
+
import { useMemo, useState } from 'react'
|
|
7
|
+
import { triggerSFX } from '../../../../../lib/sfx-bus'
|
|
8
|
+
import { cn } from '../../../../../lib/utils'
|
|
9
|
+
import { ItemCatalog } from '../../../item-catalog/item-catalog'
|
|
10
|
+
import {
|
|
11
|
+
TooltipContent,
|
|
12
|
+
TooltipProvider,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from '../../../../../components/ui/primitives/tooltip'
|
|
15
|
+
|
|
16
|
+
/** A function-axis taxonomy node, assembled into a tree by the embedder. */
|
|
17
|
+
export type FunctionTreeNode = {
|
|
18
|
+
slug: string
|
|
19
|
+
name: string
|
|
20
|
+
iconUrl?: string | null
|
|
21
|
+
children: FunctionTreeNode[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SOURCE_CHIPS: Array<{ id: NonNullable<AssetInput['source']>; label: string }> = [
|
|
25
|
+
{ id: 'library', label: 'Library' },
|
|
26
|
+
{ id: 'community', label: 'Community' },
|
|
27
|
+
{ id: 'mine', label: 'Mine' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/** Every slug at or below `node`, so a non-leaf selection matches descendants. */
|
|
31
|
+
function descendantSlugs(node: FunctionTreeNode): Set<string> {
|
|
32
|
+
const out = new Set<string>()
|
|
33
|
+
const walk = (n: FunctionTreeNode) => {
|
|
34
|
+
out.add(n.slug)
|
|
35
|
+
for (const child of n.children) walk(child)
|
|
36
|
+
}
|
|
37
|
+
walk(node)
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function itemFunctionSlugs(item: AssetInput): string[] {
|
|
42
|
+
if (item.functionTags && item.functionTags.length > 0) return item.functionTags
|
|
43
|
+
return item.category ? [item.category] : []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* DB-driven hierarchical Items browse. Roots render as the category tab bar;
|
|
48
|
+
* a selected root with children exposes those children as a secondary chip
|
|
49
|
+
* row. Selecting any node shows items tagged with that node or any descendant.
|
|
50
|
+
* Library / Community / Mine narrows by source on top of the tree selection.
|
|
51
|
+
*/
|
|
52
|
+
export function FunctionTreePanel({
|
|
53
|
+
functionTree,
|
|
54
|
+
items,
|
|
55
|
+
onSearchChange,
|
|
56
|
+
searchResults,
|
|
57
|
+
leadingTile,
|
|
58
|
+
emptyState,
|
|
59
|
+
}: {
|
|
60
|
+
functionTree: FunctionTreeNode[]
|
|
61
|
+
items?: AssetInput[]
|
|
62
|
+
onSearchChange?: (query: string) => void
|
|
63
|
+
searchResults?: AssetInput[] | null
|
|
64
|
+
leadingTile?: React.ReactNode
|
|
65
|
+
emptyState?: React.ReactNode
|
|
66
|
+
}) {
|
|
67
|
+
const [activeRootSlug, setActiveRootSlug] = useState<string | null>(
|
|
68
|
+
functionTree[0]?.slug ?? null,
|
|
69
|
+
)
|
|
70
|
+
const [activeChildSlug, setActiveChildSlug] = useState<string | null>(null)
|
|
71
|
+
const [activeSource, setActiveSource] = useState<AssetInput['source'] | null>('library')
|
|
72
|
+
const [search, setSearch] = useState('')
|
|
73
|
+
|
|
74
|
+
const isServerSearch = onSearchChange !== undefined
|
|
75
|
+
const isSearchPending = isServerSearch && search.length > 0 && searchResults === null
|
|
76
|
+
|
|
77
|
+
const activeRoot = functionTree.find((n) => n.slug === activeRootSlug) ?? functionTree[0]
|
|
78
|
+
const activeNode =
|
|
79
|
+
(activeChildSlug && activeRoot?.children.find((c) => c.slug === activeChildSlug)) || activeRoot
|
|
80
|
+
|
|
81
|
+
const matchesSource = (item: AssetInput) => {
|
|
82
|
+
if (!activeSource) return true
|
|
83
|
+
const itemSource = item.source ?? 'library'
|
|
84
|
+
if (activeSource === 'mine') return itemSource === 'mine'
|
|
85
|
+
if (activeSource === 'library') return itemSource === 'library'
|
|
86
|
+
if (activeSource === 'community') {
|
|
87
|
+
if (itemSource === 'community') return true
|
|
88
|
+
if (itemSource === 'mine') return !item.isDraft
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const treeItems = useMemo(() => {
|
|
95
|
+
const base = items ?? []
|
|
96
|
+
if (!activeNode) return base.filter(matchesSource)
|
|
97
|
+
const slugs = descendantSlugs(activeNode)
|
|
98
|
+
return base.filter(
|
|
99
|
+
(item) => matchesSource(item) && itemFunctionSlugs(item).some((s) => slugs.has(s)),
|
|
100
|
+
)
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
}, [items, activeNode, activeSource])
|
|
103
|
+
|
|
104
|
+
const searchItems = useMemo(() => {
|
|
105
|
+
if (!(isServerSearch && search && searchResults)) return null
|
|
106
|
+
return activeSource ? searchResults.filter(matchesSource) : searchResults
|
|
107
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
|
+
}, [isServerSearch, search, searchResults, activeSource])
|
|
109
|
+
|
|
110
|
+
function selectRoot(slug: string) {
|
|
111
|
+
setActiveRootSlug(slug)
|
|
112
|
+
setActiveChildSlug(null)
|
|
113
|
+
setSearch('')
|
|
114
|
+
onSearchChange?.('')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex h-full flex-col">
|
|
119
|
+
{/* Root nodes as a category grid — icon when available, otherwise a
|
|
120
|
+
two-letter abbreviation, with the full name in a hover tooltip.
|
|
121
|
+
Mirrors the Build tab's tile grid so the two panels read the same. */}
|
|
122
|
+
<TooltipProvider delayDuration={0} disableHoverableContent>
|
|
123
|
+
<div className="grid shrink-0 grid-cols-5 gap-1.5 border-border/70 border-b p-2">
|
|
124
|
+
{functionTree.map((root) => {
|
|
125
|
+
const isActive = activeRoot?.slug === root.slug
|
|
126
|
+
return (
|
|
127
|
+
<TooltipRoot key={root.slug}>
|
|
128
|
+
<TooltipTrigger asChild>
|
|
129
|
+
<button
|
|
130
|
+
className={cn(
|
|
131
|
+
'relative flex aspect-square items-center justify-center rounded-xl transition-all duration-200',
|
|
132
|
+
isActive
|
|
133
|
+
? 'bg-primary/10 ring-1 ring-primary/50'
|
|
134
|
+
: 'bg-muted/40 opacity-70 grayscale hover:bg-muted hover:opacity-100 hover:grayscale-0',
|
|
135
|
+
)}
|
|
136
|
+
onClick={() => {
|
|
137
|
+
triggerSFX('sfx:menu-click')
|
|
138
|
+
selectRoot(root.slug)
|
|
139
|
+
}}
|
|
140
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
141
|
+
type="button"
|
|
142
|
+
>
|
|
143
|
+
{root.iconUrl ? (
|
|
144
|
+
<NextImage
|
|
145
|
+
alt={root.name}
|
|
146
|
+
className="size-7 object-contain"
|
|
147
|
+
height={28}
|
|
148
|
+
src={root.iconUrl}
|
|
149
|
+
width={28}
|
|
150
|
+
/>
|
|
151
|
+
) : (
|
|
152
|
+
<span className="font-semibold text-muted-foreground text-xs uppercase">
|
|
153
|
+
{root.name.slice(0, 2)}
|
|
154
|
+
</span>
|
|
155
|
+
)}
|
|
156
|
+
</button>
|
|
157
|
+
</TooltipTrigger>
|
|
158
|
+
<TooltipContent className="icon-grid-tooltip pointer-events-none" side="top">
|
|
159
|
+
{root.name}
|
|
160
|
+
</TooltipContent>
|
|
161
|
+
</TooltipRoot>
|
|
162
|
+
)
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
</TooltipProvider>
|
|
166
|
+
|
|
167
|
+
{/* Search + source filter */}
|
|
168
|
+
<div className="flex shrink-0 flex-col gap-2 border-border/70 border-b p-2">
|
|
169
|
+
<div className="flex items-center gap-1.5">
|
|
170
|
+
<input
|
|
171
|
+
className="w-1/2 min-w-0 shrink-0 rounded-lg bg-muted px-2.5 py-1.5 text-xs placeholder:text-muted-foreground focus:outline-none"
|
|
172
|
+
onChange={(e) => {
|
|
173
|
+
setSearch(e.target.value)
|
|
174
|
+
onSearchChange?.(e.target.value)
|
|
175
|
+
}}
|
|
176
|
+
placeholder="Search..."
|
|
177
|
+
type="text"
|
|
178
|
+
value={search}
|
|
179
|
+
/>
|
|
180
|
+
<div className="flex w-1/2 min-w-0 shrink-0 rounded-lg bg-muted p-0.5">
|
|
181
|
+
{SOURCE_CHIPS.map((chip) => {
|
|
182
|
+
const isActive = activeSource === chip.id
|
|
183
|
+
return (
|
|
184
|
+
<button
|
|
185
|
+
className={cn(
|
|
186
|
+
'min-w-0 flex-1 truncate rounded-md px-1 py-1 text-center font-medium text-[10px] transition-colors',
|
|
187
|
+
isActive
|
|
188
|
+
? 'bg-background text-foreground shadow-sm'
|
|
189
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
190
|
+
)}
|
|
191
|
+
key={chip.id}
|
|
192
|
+
onClick={() => setActiveSource(isActive ? null : chip.id)}
|
|
193
|
+
type="button"
|
|
194
|
+
>
|
|
195
|
+
{chip.label}
|
|
196
|
+
</button>
|
|
197
|
+
)
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Child nodes of the active root as a secondary chip row */}
|
|
203
|
+
{!search && activeRoot && activeRoot.children.length > 0 && (
|
|
204
|
+
<div className="flex flex-wrap gap-1">
|
|
205
|
+
<button
|
|
206
|
+
className={cn(
|
|
207
|
+
'cursor-pointer rounded-md px-2 py-0.5 font-medium text-xs transition-colors',
|
|
208
|
+
activeChildSlug === null
|
|
209
|
+
? 'bg-violet-500 text-white'
|
|
210
|
+
: 'bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground',
|
|
211
|
+
)}
|
|
212
|
+
onClick={() => setActiveChildSlug(null)}
|
|
213
|
+
type="button"
|
|
214
|
+
>
|
|
215
|
+
All
|
|
216
|
+
</button>
|
|
217
|
+
{activeRoot.children.map((child) => {
|
|
218
|
+
const isActive = activeChildSlug === child.slug
|
|
219
|
+
return (
|
|
220
|
+
<button
|
|
221
|
+
className={cn(
|
|
222
|
+
'cursor-pointer rounded-md px-2 py-0.5 font-medium text-xs capitalize transition-colors',
|
|
223
|
+
isActive
|
|
224
|
+
? 'bg-violet-500 text-white'
|
|
225
|
+
: 'bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground',
|
|
226
|
+
)}
|
|
227
|
+
key={child.slug}
|
|
228
|
+
onClick={() => setActiveChildSlug(isActive ? null : child.slug)}
|
|
229
|
+
type="button"
|
|
230
|
+
>
|
|
231
|
+
{child.name}
|
|
232
|
+
</button>
|
|
233
|
+
)
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Item grid */}
|
|
240
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
|
241
|
+
{isSearchPending ? (
|
|
242
|
+
<div className="flex h-full items-center justify-center">
|
|
243
|
+
<div className="size-5 animate-spin rounded-full border-2 border-muted-foreground/20 border-t-muted-foreground" />
|
|
244
|
+
</div>
|
|
245
|
+
) : isServerSearch && search && searchResults?.length === 0 ? (
|
|
246
|
+
(emptyState ?? (
|
|
247
|
+
<div className="flex h-full items-center justify-center text-muted-foreground text-xs">
|
|
248
|
+
No results for “{search}”
|
|
249
|
+
</div>
|
|
250
|
+
))
|
|
251
|
+
) : (
|
|
252
|
+
<ItemCatalog
|
|
253
|
+
category={'furnish' as never}
|
|
254
|
+
emptyState={emptyState}
|
|
255
|
+
key={activeNode?.slug ?? 'all'}
|
|
256
|
+
leadingTile={leadingTile}
|
|
257
|
+
overrideItems={isServerSearch && search ? (searchItems ?? undefined) : treeItems}
|
|
258
|
+
/>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
import type { AssetInput } from '@pascal-app/core'
|
|
4
4
|
import NextImage from 'next/image'
|
|
5
5
|
import { useEffect, useState } from 'react'
|
|
6
|
+
import { triggerSFX } from '../../../../../lib/sfx-bus'
|
|
6
7
|
import { cn } from '../../../../../lib/utils'
|
|
7
8
|
import type { CatalogCategory } from '../../../../../store/use-editor'
|
|
8
9
|
import useEditor from '../../../../../store/use-editor'
|
|
9
10
|
import { furnishTools } from '../../../action-menu/furnish-tools'
|
|
10
11
|
import { CATALOG_ITEMS } from '../../../item-catalog/catalog-items'
|
|
11
12
|
import { ItemCatalog } from '../../../item-catalog/item-catalog'
|
|
13
|
+
import { type FunctionTreeNode, FunctionTreePanel } from './function-tree-panel'
|
|
12
14
|
|
|
13
15
|
const PLACEMENT_TAGS = new Set(['floor', 'wall', 'ceiling', 'countertop'])
|
|
14
16
|
|
|
@@ -18,6 +20,9 @@ export function ItemsPanel({
|
|
|
18
20
|
searchResults,
|
|
19
21
|
leadingTile,
|
|
20
22
|
emptyState,
|
|
23
|
+
functionTree,
|
|
24
|
+
showSourceFilter = true,
|
|
25
|
+
showTagFilters = true,
|
|
21
26
|
}: {
|
|
22
27
|
items?: AssetInput[]
|
|
23
28
|
/** Called when the search query changes (community edition uses this for server-side search) */
|
|
@@ -34,6 +39,64 @@ export function ItemsPanel({
|
|
|
34
39
|
* or no search results). Replaces the default "No results" message.
|
|
35
40
|
*/
|
|
36
41
|
emptyState?: React.ReactNode
|
|
42
|
+
/**
|
|
43
|
+
* DB-driven function taxonomy. When provided, the panel renders the
|
|
44
|
+
* hierarchical tree browse instead of the legacy hardcoded category tabs.
|
|
45
|
+
*/
|
|
46
|
+
functionTree?: FunctionTreeNode[]
|
|
47
|
+
/**
|
|
48
|
+
* Library/Community/Mine source chips. The open-source editor has no
|
|
49
|
+
* uploaded items (only the built-in catalog), so it hides these.
|
|
50
|
+
*/
|
|
51
|
+
showSourceFilter?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* Placement/functional tag filter chips under the search row. The
|
|
54
|
+
* open-source editor hides these to keep the panel to plain categories.
|
|
55
|
+
*/
|
|
56
|
+
showTagFilters?: boolean
|
|
57
|
+
}) {
|
|
58
|
+
// When the embedder supplies a function taxonomy, the hierarchical browse
|
|
59
|
+
// replaces the legacy `furnishTools` category tabs entirely.
|
|
60
|
+
if (functionTree && functionTree.length > 0) {
|
|
61
|
+
return (
|
|
62
|
+
<FunctionTreePanel
|
|
63
|
+
emptyState={emptyState}
|
|
64
|
+
functionTree={functionTree}
|
|
65
|
+
items={items}
|
|
66
|
+
leadingTile={leadingTile}
|
|
67
|
+
onSearchChange={onSearchChange}
|
|
68
|
+
searchResults={searchResults}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return <LegacyItemsPanel
|
|
74
|
+
emptyState={emptyState}
|
|
75
|
+
items={items}
|
|
76
|
+
leadingTile={leadingTile}
|
|
77
|
+
onSearchChange={onSearchChange}
|
|
78
|
+
searchResults={searchResults}
|
|
79
|
+
showSourceFilter={showSourceFilter}
|
|
80
|
+
showTagFilters={showTagFilters}
|
|
81
|
+
/>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function LegacyItemsPanel({
|
|
85
|
+
items,
|
|
86
|
+
onSearchChange,
|
|
87
|
+
searchResults,
|
|
88
|
+
leadingTile,
|
|
89
|
+
emptyState,
|
|
90
|
+
showSourceFilter = true,
|
|
91
|
+
showTagFilters = true,
|
|
92
|
+
}: {
|
|
93
|
+
items?: AssetInput[]
|
|
94
|
+
onSearchChange?: (query: string) => void
|
|
95
|
+
searchResults?: AssetInput[] | null
|
|
96
|
+
leadingTile?: React.ReactNode
|
|
97
|
+
emptyState?: React.ReactNode
|
|
98
|
+
showSourceFilter?: boolean
|
|
99
|
+
showTagFilters?: boolean
|
|
37
100
|
}) {
|
|
38
101
|
const mode = useEditor((s) => s.mode)
|
|
39
102
|
const catalogCategory = useEditor((s) => s.catalogCategory)
|
|
@@ -45,8 +108,11 @@ export function ItemsPanel({
|
|
|
45
108
|
const [activeFunctionalTag, setActiveFunctionalTag] = useState<string | null>(null)
|
|
46
109
|
// Library / Community / Mine. Default to Library so first-time users see
|
|
47
110
|
// the curated catalog rather than every uploaded item; clicking the chip
|
|
48
|
-
// again clears the filter (`null` = show everything).
|
|
49
|
-
|
|
111
|
+
// again clears the filter (`null` = show everything). With the chips hidden
|
|
112
|
+
// there is nothing to filter by, so start unfiltered.
|
|
113
|
+
const [activeSource, setActiveSource] = useState<AssetInput['source'] | null>(
|
|
114
|
+
showSourceFilter ? 'library' : null,
|
|
115
|
+
)
|
|
50
116
|
const [search, setSearch] = useState('')
|
|
51
117
|
const isServerSearch = onSearchChange !== undefined
|
|
52
118
|
// True when server search is active but results haven't come back yet
|
|
@@ -108,7 +174,7 @@ export function ItemsPanel({
|
|
|
108
174
|
const allTags = Array.from(new Set(categoryItems.flatMap((item) => item.tags ?? [])))
|
|
109
175
|
const placementTags = allTags.filter((t) => PLACEMENT_TAGS.has(t))
|
|
110
176
|
const functionalTags = allTags.filter((t) => !PLACEMENT_TAGS.has(t))
|
|
111
|
-
const hasFilters = allTags.length > 1
|
|
177
|
+
const hasFilters = showTagFilters && allTags.length > 1
|
|
112
178
|
|
|
113
179
|
const placementCount = (tag: string | null) =>
|
|
114
180
|
categoryItems.filter((item) => {
|
|
@@ -129,7 +195,7 @@ export function ItemsPanel({
|
|
|
129
195
|
return (
|
|
130
196
|
<div className="flex h-full flex-col">
|
|
131
197
|
{/* Category tabs */}
|
|
132
|
-
<div className="flex shrink-0 gap-1
|
|
198
|
+
<div className="flex shrink-0 flex-wrap gap-1 border-border/70 border-b p-2">
|
|
133
199
|
{furnishTools.map((cat) => {
|
|
134
200
|
const isActive = activeCategory.catalogCategory === cat.catalogCategory
|
|
135
201
|
return (
|
|
@@ -141,7 +207,11 @@ export function ItemsPanel({
|
|
|
141
207
|
: 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground',
|
|
142
208
|
)}
|
|
143
209
|
key={cat.catalogCategory}
|
|
144
|
-
onClick={() =>
|
|
210
|
+
onClick={() => {
|
|
211
|
+
triggerSFX('sfx:menu-click')
|
|
212
|
+
selectCategory(cat.catalogCategory)
|
|
213
|
+
}}
|
|
214
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
145
215
|
type="button"
|
|
146
216
|
>
|
|
147
217
|
<NextImage
|
|
@@ -161,9 +231,13 @@ export function ItemsPanel({
|
|
|
161
231
|
<div className="flex shrink-0 flex-col gap-2 border-border/70 border-b p-2">
|
|
162
232
|
<div className="flex items-center gap-1.5">
|
|
163
233
|
{/* Search and source filter take 50/50 of the row. `min-w-0` on
|
|
164
|
-
both sides lets each half shrink to fit when the panel narrows.
|
|
234
|
+
both sides lets each half shrink to fit when the panel narrows.
|
|
235
|
+
With the source chips hidden, search spans the full row. */}
|
|
165
236
|
<input
|
|
166
|
-
className=
|
|
237
|
+
className={cn(
|
|
238
|
+
'min-w-0 shrink-0 rounded-lg bg-muted px-2.5 py-1.5 text-xs placeholder:text-muted-foreground focus:outline-none',
|
|
239
|
+
showSourceFilter ? 'w-1/2' : 'w-full',
|
|
240
|
+
)}
|
|
167
241
|
onChange={(e) => {
|
|
168
242
|
setSearch(e.target.value)
|
|
169
243
|
onSearchChange?.(e.target.value)
|
|
@@ -172,7 +246,7 @@ export function ItemsPanel({
|
|
|
172
246
|
type="text"
|
|
173
247
|
value={search}
|
|
174
248
|
/>
|
|
175
|
-
{sourceChips.length > 0 && (
|
|
249
|
+
{showSourceFilter && sourceChips.length > 0 && (
|
|
176
250
|
<div className="flex w-1/2 min-w-0 shrink-0 rounded-lg bg-muted p-0.5">
|
|
177
251
|
{sourceChips.map((chip) => {
|
|
178
252
|
const isActive = activeSource === chip.id
|