@pascal-app/editor 1.0.4 → 1.0.5
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/README.md +106 -0
- package/package.json +39 -37
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/bake-thumbnail.tsx +6 -15
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +81 -8
- package/src/components/editor/editor-layout-mobile.tsx +1 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/export-manager.tsx +14 -19
- package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
- package/src/components/editor/first-person/build-collider-world.ts +22 -5
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
- package/src/components/editor/first-person/mechanism-targets.ts +52 -0
- package/src/components/editor/first-person-controls.tsx +19 -11
- package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +72 -23
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/group-actions.ts +2 -0
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/index.tsx +166 -79
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-highlight.tsx +13 -5
- package/src/components/editor/room-labels.test.ts +621 -128
- package/src/components/editor/room-labels.tsx +1090 -190
- package/src/components/editor/selection-manager.tsx +38 -117
- package/src/components/editor/slab-hole-highlights.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
- package/src/components/editor/snapshot-capture.test.ts +69 -58
- package/src/components/editor/snapshot-capture.ts +7 -1
- package/src/components/editor/thumbnail-generator.test.ts +350 -4
- package/src/components/editor/thumbnail-generator.tsx +128 -54
- package/src/components/editor/use-floorplan-background-placement.ts +3 -1
- package/src/components/editor/views/plugins-view.tsx +1 -8
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +140 -17
- package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +38 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +3 -6
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +94 -94
- package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
- package/src/components/tools/tool-manager.tsx +14 -5
- package/src/components/tools/wall/wall-drafting.test.ts +170 -57
- package/src/components/ui/action-menu/action-button.tsx +6 -1
- package/src/components/ui/action-menu/camera-actions.tsx +7 -3
- package/src/components/ui/action-menu/control-modes.tsx +13 -7
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +11 -9
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +10 -3
- package/src/components/ui/command-palette/editor-commands.tsx +3 -12
- package/src/components/ui/command-palette/index.tsx +45 -15
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +150 -45
- package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
- package/src/components/ui/helpers/helper-manager.tsx +7 -1
- package/src/components/ui/helpers/item-helper.tsx +9 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
- package/src/components/ui/panels/panel-wrapper.tsx +53 -27
- package/src/components/ui/panels/parametric-field-control.tsx +2 -2
- package/src/components/ui/panels/parametric-inspector.tsx +3 -3
- package/src/components/ui/panels/room-panel.tsx +65 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/sidebar.tsx +1 -1
- package/src/components/ui/right-stack.tsx +21 -2
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +3 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/tab-bar.tsx +4 -4
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-rooms.tsx +16 -0
- package/src/hooks/use-auto-save.integration.test.tsx +96 -38
- package/src/hooks/use-auto-save.ts +26 -1
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +65 -16
- package/src/hooks/use-keyboard.ts +29 -12
- package/src/hooks/use-placement-notice.test.ts +24 -1
- package/src/hooks/use-placement-notice.ts +37 -6
- package/src/index.tsx +71 -24
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.ts +9 -2
- package/src/lib/editor-views.ts +35 -1
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-foundation-support.test.ts +1 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/sheet-export.test.ts +24 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
- package/src/lib/floorplan/site-plan/coverage.ts +2 -4
- package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
- package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
- package/src/lib/floorplan/site-plan/geometry.ts +1 -416
- package/src/lib/floorplan/site-plan/index.ts +0 -16
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
- package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
- package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +112 -0
- package/src/lib/fresh-planar-placement.ts +57 -3
- package/src/lib/geometry-script/author.ts +6 -1
- package/src/lib/geometry-script/client.ts +8 -1
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +1 -0
- package/src/lib/glb-export.test.ts +187 -162
- package/src/lib/glb-export.ts +4 -1
- package/src/lib/history.test.ts +12 -7
- package/src/lib/interaction/scope.ts +4 -0
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/lot/clean-ring.test.ts +2 -1
- package/src/lib/lot/drop-in.ts +9 -24
- package/src/lib/lot/lot-patch.test.ts +1 -1
- package/src/lib/lot/lot-patch.ts +2 -2
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/orbit-floor-clearance.test.ts +36 -0
- package/src/lib/orbit-floor-clearance.ts +48 -0
- package/src/lib/paint-pass-through.test.ts +11 -0
- package/src/lib/paint-pass-through.ts +4 -2
- package/src/lib/paint-scope.ts +1 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plugin-chat.ts +15 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/room-assembly-overlay.ts +2 -3
- package/src/lib/room-selection.test.ts +45 -1
- package/src/lib/scene-clipboard.test.ts +246 -1
- package/src/lib/scene-clipboard.ts +44 -14
- package/src/lib/selection-routing.ts +1 -1
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.ts +2 -1
- package/src/lib/viewer-selection.ts +22 -11
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-stair-build-preview.ts +25 -5
- package/src/store/view-layout.test.ts +27 -1
- package/src/store/view-layout.ts +4 -1
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -15
- package/src/components/tools/stair/stair-tool.tsx +0 -682
|
@@ -26,6 +26,7 @@ import { VIEW_2D } from '../../../store/view-layout'
|
|
|
26
26
|
import useFloorplanMode from '../../../store/use-floorplan-mode'
|
|
27
27
|
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
28
28
|
import { ActionButton } from './action-button'
|
|
29
|
+
import { useActionMenuPopupSide } from './placement'
|
|
29
30
|
|
|
30
31
|
const measurementOptions = [
|
|
31
32
|
{ kind: 'distance', label: 'Distance', icon: Ruler },
|
|
@@ -62,6 +63,7 @@ const constructionDimensionOptions = [
|
|
|
62
63
|
}[]
|
|
63
64
|
|
|
64
65
|
export function MeasurementControl() {
|
|
66
|
+
const popupSide = useActionMenuPopupSide()
|
|
65
67
|
const [isOpen, setIsOpen] = useState(false)
|
|
66
68
|
const mode = useEditor((state) => state.mode)
|
|
67
69
|
const tool = useEditor((state) => state.tool)
|
|
@@ -146,8 +148,8 @@ export function MeasurementControl() {
|
|
|
146
148
|
className={cn(
|
|
147
149
|
'rounded-r-none p-0 text-muted-foreground',
|
|
148
150
|
isControlActive
|
|
149
|
-
? 'bg-cyan-500/20 text-cyan-
|
|
150
|
-
: 'hover:bg-cyan-500/15 hover:text-cyan-400',
|
|
151
|
+
? 'bg-cyan-500/20 text-cyan-700 hover:bg-cyan-500/20 dark:text-cyan-400 dark:hover:text-accent-foreground'
|
|
152
|
+
: 'hover:bg-cyan-500/15 hover:text-cyan-700 dark:hover:text-cyan-400',
|
|
151
153
|
)}
|
|
152
154
|
label={`Measure: ${selectedLabel}`}
|
|
153
155
|
onClick={handlePrimaryClick}
|
|
@@ -166,8 +168,8 @@ export function MeasurementControl() {
|
|
|
166
168
|
className={cn(
|
|
167
169
|
'flex h-11 w-6 items-center justify-center rounded-r-lg text-muted-foreground transition-colors',
|
|
168
170
|
isOpen
|
|
169
|
-
? 'bg-cyan-500/15 text-cyan-400'
|
|
170
|
-
: 'hover:bg-cyan-500/10 hover:text-cyan-400',
|
|
171
|
+
? 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-400'
|
|
172
|
+
: 'hover:bg-cyan-500/10 hover:text-cyan-700 dark:hover:text-cyan-400',
|
|
171
173
|
)}
|
|
172
174
|
type="button"
|
|
173
175
|
>
|
|
@@ -182,7 +184,7 @@ export function MeasurementControl() {
|
|
|
182
184
|
<PopoverContent
|
|
183
185
|
align="center"
|
|
184
186
|
className="max-h-[70vh] w-64 overflow-y-auto rounded-lg border-border/45 bg-background/96 p-2 shadow-elevation-3 backdrop-blur-xl"
|
|
185
|
-
side=
|
|
187
|
+
side={popupSide}
|
|
186
188
|
sideOffset={14}
|
|
187
189
|
>
|
|
188
190
|
<div aria-label="Measurement type" className="space-y-1" role="menu">
|
|
@@ -198,8 +200,8 @@ export function MeasurementControl() {
|
|
|
198
200
|
className={cn(
|
|
199
201
|
'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
|
|
200
202
|
isSelected
|
|
201
|
-
? 'bg-white/10 text-foreground'
|
|
202
|
-
: 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
|
|
203
|
+
? 'bg-black/6 dark:bg-white/10 text-foreground'
|
|
204
|
+
: 'text-muted-foreground hover:bg-black/5 dark:hover:bg-white/8 hover:text-foreground',
|
|
203
205
|
)}
|
|
204
206
|
key={option.kind}
|
|
205
207
|
onClick={() => {
|
|
@@ -234,8 +236,8 @@ export function MeasurementControl() {
|
|
|
234
236
|
className={cn(
|
|
235
237
|
'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
|
|
236
238
|
isSelected
|
|
237
|
-
? 'bg-white/10 text-foreground'
|
|
238
|
-
: 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
|
|
239
|
+
? 'bg-black/6 dark:bg-white/10 text-foreground'
|
|
240
|
+
: 'text-muted-foreground hover:bg-black/5 dark:hover:bg-white/8 hover:text-foreground',
|
|
239
241
|
)}
|
|
240
242
|
key={`${option.mode}-${option.chainMode}`}
|
|
241
243
|
onClick={() => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Viewport edge the action menu docks to. */
|
|
4
|
+
export type ActionMenuPlacement = 'bottom' | 'top'
|
|
5
|
+
|
|
6
|
+
/** Where chrome under a top-docked menu starts: below the host toolbar row and the menu itself. */
|
|
7
|
+
export const TOP_MENU_CLEARANCE = '7.25rem'
|
|
8
|
+
|
|
9
|
+
const ActionMenuPlacementContext = createContext<ActionMenuPlacement>('bottom')
|
|
10
|
+
|
|
11
|
+
export const ActionMenuPlacementProvider = ActionMenuPlacementContext.Provider
|
|
12
|
+
|
|
13
|
+
export function useActionMenuPlacement(): ActionMenuPlacement {
|
|
14
|
+
return useContext(ActionMenuPlacementContext)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Side tooltips and popovers open toward, away from the docked edge. */
|
|
18
|
+
export function useActionMenuPopupSide(): 'top' | 'bottom' {
|
|
19
|
+
return useActionMenuPlacement() === 'top' ? 'bottom' : 'top'
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useHasPointAskHandler } from '@pascal-app/core'
|
|
4
|
+
import { canPoint, enterPointMode, leavePointMode } from '../../../lib/point-ask/session'
|
|
5
|
+
import { cn } from '../../../lib/utils'
|
|
6
|
+
import useEditor from '../../../store/use-editor'
|
|
7
|
+
import { usePointAsk } from '../../../store/use-point-ask'
|
|
8
|
+
import { isViewVisible, VIEW_3D } from '../../../store/view-layout'
|
|
9
|
+
import { ActionButton } from './action-button'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Point and ask (key C): the toolbar's way into the mode where hovering names what Pascal would
|
|
13
|
+
* be told about and a click opens a bubble to ask. Offered only where a host answers asks. In
|
|
14
|
+
* the 2D plan and the walkthrough it is disabled and says so (v1 works in the 3D view).
|
|
15
|
+
*/
|
|
16
|
+
export function PointAskButton() {
|
|
17
|
+
const available = useHasPointAskHandler()
|
|
18
|
+
const active = usePointAsk((s) => s.active)
|
|
19
|
+
const sceneVisible = useEditor((s) => isViewVisible(s, VIEW_3D))
|
|
20
|
+
const firstPerson = useEditor((s) => s.isFirstPersonMode)
|
|
21
|
+
if (!(available || active)) return null
|
|
22
|
+
const unavailable = !active && (!sceneVisible || firstPerson)
|
|
23
|
+
return (
|
|
24
|
+
<ActionButton
|
|
25
|
+
className={cn(
|
|
26
|
+
'group text-muted-foreground',
|
|
27
|
+
active
|
|
28
|
+
? 'bg-[#a99bff]/20 text-[#5d49df] dark:text-[#c9c1ff] dark:hover:text-accent-foreground'
|
|
29
|
+
: 'hover:bg-[#a99bff]/15 hover:text-[#5d49df] dark:hover:text-[#c9c1ff]',
|
|
30
|
+
)}
|
|
31
|
+
data-guide-target="mode-point-ask"
|
|
32
|
+
disabled={unavailable}
|
|
33
|
+
isActive={active}
|
|
34
|
+
label="Point and ask"
|
|
35
|
+
onClick={() => {
|
|
36
|
+
if (active) leavePointMode()
|
|
37
|
+
else if (canPoint()) enterPointMode(true)
|
|
38
|
+
}}
|
|
39
|
+
shortcut="C"
|
|
40
|
+
size="icon"
|
|
41
|
+
tooltipContent={
|
|
42
|
+
unavailable ? <p>Point and ask works in the 3D view</p> : <p>Point and ask (C)</p>
|
|
43
|
+
}
|
|
44
|
+
variant="ghost"
|
|
45
|
+
>
|
|
46
|
+
<svg aria-hidden className="h-5 w-5" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" viewBox="0 0 24 24">
|
|
47
|
+
<path d="M4.5 3.5l8.2 3.7-3.4 1.3-1.3 3.4z" fill="currentColor" fillOpacity="0.25" />
|
|
48
|
+
<path d="M12.5 12.5h6.5a2 2 0 012 2v3a2 2 0 01-2 2h-2.2l-2.3 2v-2h-2a2 2 0 01-2-2v-3a2 2 0 012-2z" />
|
|
49
|
+
</svg>
|
|
50
|
+
</ActionButton>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -20,6 +20,7 @@ import { useUploadStore } from '../../../store/use-upload'
|
|
|
20
20
|
import { SliderControl } from '../controls/slider-control'
|
|
21
21
|
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
22
22
|
import { ActionButton } from './action-button'
|
|
23
|
+
import { useActionMenuPopupSide } from './placement'
|
|
23
24
|
|
|
24
25
|
const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
|
|
25
26
|
const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif'
|
|
@@ -149,7 +150,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
|
|
|
149
150
|
<>
|
|
150
151
|
<button
|
|
151
152
|
aria-label="Upload scan or guide image"
|
|
152
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
|
|
153
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
153
154
|
disabled={isAddingGuide}
|
|
154
155
|
onClick={() => fileInputRef.current?.click()}
|
|
155
156
|
type="button"
|
|
@@ -170,6 +171,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
|
|
|
170
171
|
// ── Guides toggle + dropdown ────────────────────────────────────────────────
|
|
171
172
|
|
|
172
173
|
function GuidesControl() {
|
|
174
|
+
const popupSide = useActionMenuPopupSide()
|
|
173
175
|
const showGuides = useViewer((state) => state.showGuides)
|
|
174
176
|
const setShowGuides = useViewer((state) => state.setShowGuides)
|
|
175
177
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -207,8 +209,8 @@ function GuidesControl() {
|
|
|
207
209
|
className={cn(
|
|
208
210
|
'rounded-r-none p-0',
|
|
209
211
|
showGuides
|
|
210
|
-
? 'bg-white/15'
|
|
211
|
-
: 'opacity-60 grayscale hover:bg-
|
|
212
|
+
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
213
|
+
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
212
214
|
)}
|
|
213
215
|
label={`Guides: ${showGuides ? 'Visible' : 'Hidden'}`}
|
|
214
216
|
onClick={() => setShowGuides(!showGuides)}
|
|
@@ -221,7 +223,7 @@ function GuidesControl() {
|
|
|
221
223
|
className="h-[28px] w-[28px] object-contain"
|
|
222
224
|
src="/icons/floorplan.webp"
|
|
223
225
|
/>
|
|
224
|
-
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
|
|
226
|
+
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
|
|
225
227
|
{guides.length}
|
|
226
228
|
</span>
|
|
227
229
|
</div>
|
|
@@ -236,11 +238,11 @@ function GuidesControl() {
|
|
|
236
238
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
237
239
|
showGuides
|
|
238
240
|
? isOpen
|
|
239
|
-
? 'bg-white/10'
|
|
240
|
-
: 'bg-white/5 hover:bg-white/8'
|
|
241
|
+
? 'bg-black/6 dark:bg-white/10'
|
|
242
|
+
: 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
|
|
241
243
|
: isOpen
|
|
242
|
-
? 'bg-white/8'
|
|
243
|
-
: 'opacity-60 hover:bg-white/5 hover:opacity-100',
|
|
244
|
+
? 'bg-black/5 dark:bg-white/8'
|
|
245
|
+
: 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
|
|
244
246
|
)}
|
|
245
247
|
type="button"
|
|
246
248
|
>
|
|
@@ -252,7 +254,7 @@ function GuidesControl() {
|
|
|
252
254
|
<PopoverContent
|
|
253
255
|
align="center"
|
|
254
256
|
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
255
|
-
side=
|
|
257
|
+
side={popupSide}
|
|
256
258
|
sideOffset={14}
|
|
257
259
|
>
|
|
258
260
|
<div className="space-y-3">
|
|
@@ -284,7 +286,7 @@ function GuidesControl() {
|
|
|
284
286
|
className={cn(
|
|
285
287
|
'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
|
|
286
288
|
selectedReferenceId === guide.id
|
|
287
|
-
? 'border-foreground/35 bg-white/10'
|
|
289
|
+
? 'border-foreground/35 bg-black/6 dark:bg-white/10'
|
|
288
290
|
: 'border-border/45',
|
|
289
291
|
)}
|
|
290
292
|
key={guide.id}
|
|
@@ -349,6 +351,7 @@ function GuidesControl() {
|
|
|
349
351
|
// ── Scans toggle + dropdown ─────────────────────────────────────────────────
|
|
350
352
|
|
|
351
353
|
function ScansControl() {
|
|
354
|
+
const popupSide = useActionMenuPopupSide()
|
|
352
355
|
const showScans = useViewer((state) => state.showScans)
|
|
353
356
|
const setShowScans = useViewer((state) => state.setShowScans)
|
|
354
357
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -386,8 +389,8 @@ function ScansControl() {
|
|
|
386
389
|
className={cn(
|
|
387
390
|
'rounded-r-none p-0',
|
|
388
391
|
showScans
|
|
389
|
-
? 'bg-white/15'
|
|
390
|
-
: 'opacity-60 grayscale hover:bg-
|
|
392
|
+
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
393
|
+
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
391
394
|
)}
|
|
392
395
|
label={`Scans: ${showScans ? 'Visible' : 'Hidden'}`}
|
|
393
396
|
onClick={() => setShowScans(!showScans)}
|
|
@@ -396,7 +399,7 @@ function ScansControl() {
|
|
|
396
399
|
>
|
|
397
400
|
<div className="relative">
|
|
398
401
|
<img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
|
|
399
|
-
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
|
|
402
|
+
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
|
|
400
403
|
{scans.length}
|
|
401
404
|
</span>
|
|
402
405
|
</div>
|
|
@@ -411,11 +414,11 @@ function ScansControl() {
|
|
|
411
414
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
412
415
|
showScans
|
|
413
416
|
? isOpen
|
|
414
|
-
? 'bg-white/10'
|
|
415
|
-
: 'bg-white/5 hover:bg-white/8'
|
|
417
|
+
? 'bg-black/6 dark:bg-white/10'
|
|
418
|
+
: 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
|
|
416
419
|
: isOpen
|
|
417
|
-
? 'bg-white/8'
|
|
418
|
-
: 'opacity-60 hover:bg-white/5 hover:opacity-100',
|
|
420
|
+
? 'bg-black/5 dark:bg-white/8'
|
|
421
|
+
: 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
|
|
419
422
|
)}
|
|
420
423
|
type="button"
|
|
421
424
|
>
|
|
@@ -427,7 +430,7 @@ function ScansControl() {
|
|
|
427
430
|
<PopoverContent
|
|
428
431
|
align="center"
|
|
429
432
|
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
430
|
-
side=
|
|
433
|
+
side={popupSide}
|
|
431
434
|
sideOffset={14}
|
|
432
435
|
>
|
|
433
436
|
<div className="space-y-3">
|
|
@@ -459,7 +462,7 @@ function ScansControl() {
|
|
|
459
462
|
className={cn(
|
|
460
463
|
'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
|
|
461
464
|
selectedReferenceId === scan.id
|
|
462
|
-
? 'border-foreground/35 bg-white/10'
|
|
465
|
+
? 'border-foreground/35 bg-black/6 dark:bg-white/10'
|
|
463
466
|
: 'border-border/45',
|
|
464
467
|
)}
|
|
465
468
|
key={scan.id}
|
|
@@ -578,7 +581,7 @@ function ReferenceListSection({
|
|
|
578
581
|
</div>
|
|
579
582
|
<button
|
|
580
583
|
aria-label={show ? `Hide ${title.toLowerCase()}` : `Show ${title.toLowerCase()}`}
|
|
581
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
|
|
584
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
582
585
|
onClick={() => setShow(!show)}
|
|
583
586
|
type="button"
|
|
584
587
|
>
|
|
@@ -594,7 +597,7 @@ function ReferenceListSection({
|
|
|
594
597
|
className={cn(
|
|
595
598
|
'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
|
|
596
599
|
selectedReferenceId === node.id
|
|
597
|
-
? 'border-foreground/35 bg-white/10'
|
|
600
|
+
? 'border-foreground/35 bg-black/6 dark:bg-white/10'
|
|
598
601
|
: 'border-border/45',
|
|
599
602
|
)}
|
|
600
603
|
key={node.id}
|
|
@@ -657,6 +660,7 @@ function ReferenceListSection({
|
|
|
657
660
|
}
|
|
658
661
|
|
|
659
662
|
function ReferencesControl() {
|
|
663
|
+
const popupSide = useActionMenuPopupSide()
|
|
660
664
|
const showScans = useViewer((state) => state.showScans)
|
|
661
665
|
const setShowScans = useViewer((state) => state.setShowScans)
|
|
662
666
|
const showGuides = useViewer((state) => state.showGuides)
|
|
@@ -682,8 +686,8 @@ function ReferencesControl() {
|
|
|
682
686
|
className={cn(
|
|
683
687
|
'rounded-r-none p-0',
|
|
684
688
|
anyVisible
|
|
685
|
-
? 'bg-white/15'
|
|
686
|
-
: 'opacity-60 grayscale hover:bg-
|
|
689
|
+
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
690
|
+
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
687
691
|
)}
|
|
688
692
|
label={`References: ${anyVisible ? 'Visible' : 'Hidden'}`}
|
|
689
693
|
onClick={toggleAll}
|
|
@@ -696,7 +700,7 @@ function ReferencesControl() {
|
|
|
696
700
|
className="h-[28px] w-[28px] object-contain"
|
|
697
701
|
src="/icons/floorplan.webp"
|
|
698
702
|
/>
|
|
699
|
-
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
|
|
703
|
+
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
|
|
700
704
|
{total}
|
|
701
705
|
</span>
|
|
702
706
|
</div>
|
|
@@ -710,11 +714,11 @@ function ReferencesControl() {
|
|
|
710
714
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
711
715
|
anyVisible
|
|
712
716
|
? isOpen
|
|
713
|
-
? 'bg-white/10'
|
|
714
|
-
: 'bg-white/5 hover:bg-white/8'
|
|
717
|
+
? 'bg-black/6 dark:bg-white/10'
|
|
718
|
+
: 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
|
|
715
719
|
: isOpen
|
|
716
|
-
? 'bg-white/8'
|
|
717
|
-
: 'opacity-60 hover:bg-white/5 hover:opacity-100',
|
|
720
|
+
? 'bg-black/5 dark:bg-white/8'
|
|
721
|
+
: 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
|
|
718
722
|
)}
|
|
719
723
|
type="button"
|
|
720
724
|
>
|
|
@@ -726,7 +730,7 @@ function ReferencesControl() {
|
|
|
726
730
|
<PopoverContent
|
|
727
731
|
align="center"
|
|
728
732
|
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
729
|
-
side=
|
|
733
|
+
side={popupSide}
|
|
730
734
|
sideOffset={14}
|
|
731
735
|
>
|
|
732
736
|
<div className="space-y-3">
|
|
@@ -765,6 +769,7 @@ function ReferencesControl() {
|
|
|
765
769
|
// ── Reference floor control ────────────────────────────────────────────────────────────────────
|
|
766
770
|
|
|
767
771
|
function ReferenceFloorControl() {
|
|
772
|
+
const popupSide = useActionMenuPopupSide()
|
|
768
773
|
const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
|
|
769
774
|
const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
|
|
770
775
|
const referenceFloorOffset = useEditor((state) => state.referenceFloorOffset)
|
|
@@ -784,8 +789,8 @@ function ReferenceFloorControl() {
|
|
|
784
789
|
className={cn(
|
|
785
790
|
'rounded-r-none p-0',
|
|
786
791
|
showReferenceFloor && selectedLevel
|
|
787
|
-
? 'bg-white/15'
|
|
788
|
-
: 'opacity-60 grayscale hover:bg-
|
|
792
|
+
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
793
|
+
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
789
794
|
)}
|
|
790
795
|
disabled={!hasLowerLevels}
|
|
791
796
|
label={
|
|
@@ -801,7 +806,7 @@ function ReferenceFloorControl() {
|
|
|
801
806
|
>
|
|
802
807
|
<div className="relative">
|
|
803
808
|
<Layers2 className="h-4 w-4" />
|
|
804
|
-
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
|
|
809
|
+
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
|
|
805
810
|
{lowerLevels.length}
|
|
806
811
|
</span>
|
|
807
812
|
</div>
|
|
@@ -815,11 +820,11 @@ function ReferenceFloorControl() {
|
|
|
815
820
|
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
816
821
|
showReferenceFloor && selectedLevel
|
|
817
822
|
? isOpen
|
|
818
|
-
? 'bg-white/10'
|
|
819
|
-
: 'bg-white/5 hover:bg-white/8'
|
|
823
|
+
? 'bg-black/6 dark:bg-white/10'
|
|
824
|
+
: 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
|
|
820
825
|
: isOpen
|
|
821
|
-
? 'bg-white/8'
|
|
822
|
-
: 'opacity-60 hover:bg-white/5 hover:opacity-100',
|
|
826
|
+
? 'bg-black/5 dark:bg-white/8'
|
|
827
|
+
: 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
|
|
823
828
|
)}
|
|
824
829
|
disabled={!hasLowerLevels}
|
|
825
830
|
type="button"
|
|
@@ -832,7 +837,7 @@ function ReferenceFloorControl() {
|
|
|
832
837
|
<PopoverContent
|
|
833
838
|
align="center"
|
|
834
839
|
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-[0_14px_28px_-18px_rgba(15,23,42,0.55),0_6px_16px_-10px_rgba(15,23,42,0.2)] backdrop-blur-xl"
|
|
835
|
-
side=
|
|
840
|
+
side={popupSide}
|
|
836
841
|
sideOffset={14}
|
|
837
842
|
>
|
|
838
843
|
<div className="space-y-3">
|
|
@@ -848,7 +853,7 @@ function ReferenceFloorControl() {
|
|
|
848
853
|
</div>
|
|
849
854
|
<button
|
|
850
855
|
aria-label={showReferenceFloor ? 'Hide reference floor' : 'Show reference floor'}
|
|
851
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
|
|
856
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
|
|
852
857
|
disabled={!hasLowerLevels}
|
|
853
858
|
onClick={toggleReferenceFloor}
|
|
854
859
|
type="button"
|
|
@@ -870,8 +875,8 @@ function ReferenceFloorControl() {
|
|
|
870
875
|
return (
|
|
871
876
|
<button
|
|
872
877
|
className={cn(
|
|
873
|
-
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors hover:bg-white/8',
|
|
874
|
-
isSelected && showReferenceFloor && 'bg-white/10 text-foreground',
|
|
878
|
+
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors hover:bg-black/5 dark:hover:bg-white/8',
|
|
879
|
+
isSelected && showReferenceFloor && 'bg-black/6 dark:bg-white/10 text-foreground',
|
|
875
880
|
)}
|
|
876
881
|
key={level.id}
|
|
877
882
|
onClick={() => {
|
|
@@ -928,8 +933,8 @@ function RiserControl() {
|
|
|
928
933
|
<ActionButton
|
|
929
934
|
className={cn(
|
|
930
935
|
isRiserOpen
|
|
931
|
-
? 'bg-white/15'
|
|
932
|
-
: 'opacity-60 grayscale hover:bg-
|
|
936
|
+
? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
|
|
937
|
+
: 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
|
|
933
938
|
)}
|
|
934
939
|
label="Riser diagram"
|
|
935
940
|
onClick={toggleRiserOpen}
|
|
@@ -128,11 +128,12 @@ type BuildToolTileProps = Omit<ComponentPropsWithoutRef<'button'>, 'children' |
|
|
|
128
128
|
iconSrc: string
|
|
129
129
|
label: string
|
|
130
130
|
size?: 'lg' | 'sm'
|
|
131
|
+
tone?: 'primary' | 'neutral'
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
135
|
* One tool tile: the isometric icon over its name. Inactive icons stay grey and
|
|
135
|
-
* take their colour on hover
|
|
136
|
+
* take their colour on hover. Neutral tiles keep that behavior when armed.
|
|
136
137
|
*/
|
|
137
138
|
export function BuildToolTile({
|
|
138
139
|
active,
|
|
@@ -141,6 +142,7 @@ export function BuildToolTile({
|
|
|
141
142
|
iconSrc,
|
|
142
143
|
label,
|
|
143
144
|
size = 'sm',
|
|
145
|
+
tone = 'primary',
|
|
144
146
|
...buttonProps
|
|
145
147
|
}: BuildToolTileProps) {
|
|
146
148
|
return (
|
|
@@ -151,7 +153,11 @@ export function BuildToolTile({
|
|
|
151
153
|
// Small tiles grow to two label lines ("Spawn point"); a grid row's
|
|
152
154
|
// tiles stretch together, so a row keeps one height.
|
|
153
155
|
size === 'lg' ? 'h-[104px] gap-1.5 px-1.5 pt-2 pb-2.5' : 'min-h-[74px] gap-1 px-1 pt-1.5 pb-2',
|
|
154
|
-
active
|
|
156
|
+
active
|
|
157
|
+
? tone === 'neutral'
|
|
158
|
+
? 'bg-black/8 ring-1 ring-foreground/30 dark:bg-muted'
|
|
159
|
+
: 'bg-primary/10 ring-1 ring-primary/50'
|
|
160
|
+
: 'bg-black/4 hover:bg-black/8 dark:bg-muted/40 dark:hover:bg-muted',
|
|
155
161
|
className,
|
|
156
162
|
)}
|
|
157
163
|
type="button"
|
|
@@ -162,7 +168,8 @@ export function BuildToolTile({
|
|
|
162
168
|
className={cn(
|
|
163
169
|
'min-h-0 object-contain transition duration-200 group-hover:scale-110',
|
|
164
170
|
size === 'lg' ? 'size-14' : 'size-9',
|
|
165
|
-
!active
|
|
171
|
+
(!active || tone === 'neutral') &&
|
|
172
|
+
'opacity-70 grayscale group-hover:opacity-100 group-hover:grayscale-0',
|
|
166
173
|
)}
|
|
167
174
|
draggable={false}
|
|
168
175
|
src={iconSrc}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type
|
|
4
|
-
import { DEFAULT_LEVEL_HEIGHT, LevelNode, type UnitNode, useScene } from '@pascal-app/core'
|
|
3
|
+
import { type AnyNodeId, type LevelNode, type UnitNode, useScene } from '@pascal-app/core'
|
|
5
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
6
5
|
import {
|
|
7
6
|
AppWindow,
|
|
@@ -37,6 +36,7 @@ import {
|
|
|
37
36
|
Video,
|
|
38
37
|
} from 'lucide-react'
|
|
39
38
|
import { useEffect } from 'react'
|
|
39
|
+
import { addStoreyOnTop } from '../../../lib/add-storey'
|
|
40
40
|
import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
|
|
41
41
|
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
42
42
|
import { getSavedSceneDocument } from '../../../lib/scene'
|
|
@@ -203,16 +203,7 @@ export function EditorCommands() {
|
|
|
203
203
|
const { nodes } = useScene.getState()
|
|
204
204
|
const building = Object.values(nodes).find((n) => n.type === 'building')
|
|
205
205
|
if (!building) return
|
|
206
|
-
const
|
|
207
|
-
(childId) => nodes[childId as keyof typeof nodes]?.type === 'level',
|
|
208
|
-
).length
|
|
209
|
-
const newLevel = LevelNode.parse({
|
|
210
|
-
level: levelCount,
|
|
211
|
-
height: DEFAULT_LEVEL_HEIGHT,
|
|
212
|
-
children: [],
|
|
213
|
-
parentId: building.id,
|
|
214
|
-
})
|
|
215
|
-
useScene.getState().createNode(newLevel, building.id)
|
|
206
|
+
const newLevel = addStoreyOnTop(building.id)
|
|
216
207
|
useViewer.getState().setSelection({ levelId: newLevel.id })
|
|
217
208
|
}),
|
|
218
209
|
},
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
import type { AnyNodeId, LevelNode } from '@pascal-app/core'
|
|
4
4
|
import { useScene } from '@pascal-app/core'
|
|
5
5
|
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
6
|
-
import { Command, useCommandState } from 'cmdk'
|
|
6
|
+
import { Command, defaultFilter, useCommandState } from 'cmdk'
|
|
7
7
|
import { ChevronRight, Search } from 'lucide-react'
|
|
8
|
-
import type { ReactNode } from 'react'
|
|
9
|
-
import { useEffect, useState } from 'react'
|
|
8
|
+
import type { ReactNode, RefObject } from 'react'
|
|
9
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
10
10
|
import { create } from 'zustand'
|
|
11
11
|
import { useShallow } from 'zustand/shallow'
|
|
12
12
|
import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
|
|
13
13
|
import { getLevelDisplayName } from '@pascal-app/core'
|
|
14
|
-
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
14
|
+
import { type CommandAction, useCommandRegistry } from '../../../store/use-command-registry'
|
|
15
15
|
import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -54,6 +54,28 @@ function resolve(value: string | (() => string)): string {
|
|
|
54
54
|
return typeof value === 'function' ? value() : value
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
type ActionGroup = [group: string, actions: CommandAction[]]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The registered actions by group, in the order they were registered. A search puts the group
|
|
61
|
+
* holding the best match first: cmdk ranks the items inside a group but leaves the groups where
|
|
62
|
+
* they are, so a strong match could sit below a screen of weak ones.
|
|
63
|
+
*/
|
|
64
|
+
export function rankedGroups(actions: CommandAction[], search: string): ActionGroup[] {
|
|
65
|
+
const groups = new Map<string, CommandAction[]>()
|
|
66
|
+
for (const action of actions) {
|
|
67
|
+
groups.set(action.group, [...(groups.get(action.group) ?? []), action])
|
|
68
|
+
}
|
|
69
|
+
const ordered: ActionGroup[] = [...groups]
|
|
70
|
+
if (!search) return ordered
|
|
71
|
+
const bestMatch = ([, list]: ActionGroup) =>
|
|
72
|
+
Math.max(...list.map((action) => defaultFilter(resolve(action.label), search, action.keywords)))
|
|
73
|
+
return ordered
|
|
74
|
+
.map((entry) => ({ entry, score: bestMatch(entry) }))
|
|
75
|
+
.sort((a, b) => b.score - a.score)
|
|
76
|
+
.map(({ entry }) => entry)
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
function Shortcut({ keys }: { keys: string[] }) {
|
|
58
80
|
return (
|
|
59
81
|
<span className="ml-auto flex shrink-0 items-center gap-0.5">
|
|
@@ -162,6 +184,20 @@ export interface CommandPaletteEmptyAction {
|
|
|
162
184
|
onSelect: (query: string) => void
|
|
163
185
|
}
|
|
164
186
|
|
|
187
|
+
/**
|
|
188
|
+
* A search selects the first item; this scrolls the list back to the top to show it. cmdk scrolls
|
|
189
|
+
* before the list re-renders the selection, so once a search has moved the groups it scrolls to
|
|
190
|
+
* wherever the item selected before went. Rendered after the list, whose ref is set by then.
|
|
191
|
+
*/
|
|
192
|
+
function TopOnFirstItem({ list }: { list: RefObject<HTMLDivElement | null> }) {
|
|
193
|
+
const value = useCommandState((s) => s.value)
|
|
194
|
+
useLayoutEffect(() => {
|
|
195
|
+
const first = list.current?.querySelector('[cmdk-item]:not([aria-disabled="true"])')
|
|
196
|
+
if (value && first?.getAttribute('aria-selected') === 'true') list.current?.scrollTo({ top: 0 })
|
|
197
|
+
}, [list, value])
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
165
201
|
function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
|
|
166
202
|
const count = useCommandState((s) => s.filtered.count)
|
|
167
203
|
const search = useCommandState((s) => s.search)
|
|
@@ -193,6 +229,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
193
229
|
|
|
194
230
|
const [meta, setMeta] = useState('⌘')
|
|
195
231
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
232
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
196
233
|
|
|
197
234
|
const page = pages[pages.length - 1]
|
|
198
235
|
|
|
@@ -255,15 +292,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
255
292
|
})
|
|
256
293
|
}
|
|
257
294
|
|
|
258
|
-
|
|
259
|
-
// Group registered actions by group (preserving insertion order)
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
const grouped = actions.reduce<Map<string, typeof actions>>((acc, action) => {
|
|
262
|
-
const list = acc.get(action.group) ?? []
|
|
263
|
-
list.push(action)
|
|
264
|
-
acc.set(action.group, list)
|
|
265
|
-
return acc
|
|
266
|
-
}, new Map())
|
|
295
|
+
const grouped = rankedGroups(actions, inputValue)
|
|
267
296
|
|
|
268
297
|
const onClose = () => setOpen(false)
|
|
269
298
|
const onBack = () => {
|
|
@@ -326,7 +355,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
326
355
|
/>
|
|
327
356
|
</div>
|
|
328
357
|
|
|
329
|
-
<Command.List className="max-h-100 overflow-y-auto p-1.5">
|
|
358
|
+
<Command.List className="max-h-100 overflow-y-auto p-1.5" ref={listRef}>
|
|
330
359
|
{(!emptyAction || page) && (
|
|
331
360
|
<Command.Empty className="py-8 text-center text-muted-foreground text-sm">
|
|
332
361
|
No commands found.
|
|
@@ -344,7 +373,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
344
373
|
|
|
345
374
|
{/* ── Root view: render from registry ───────────────────────── */}
|
|
346
375
|
{!page &&
|
|
347
|
-
|
|
376
|
+
grouped.map(([group, groupActions]) => (
|
|
348
377
|
<Command.Group heading={group} key={group}>
|
|
349
378
|
{groupActions.map((action) => (
|
|
350
379
|
<Item
|
|
@@ -439,6 +468,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
439
468
|
</Command.Group>
|
|
440
469
|
)}
|
|
441
470
|
</Command.List>
|
|
471
|
+
<TopOnFirstItem list={listRef} />
|
|
442
472
|
|
|
443
473
|
{/* Footer hint */}
|
|
444
474
|
<div className="flex items-center justify-between border-border/50 border-t px-3 py-2">
|