@pascal-app/editor 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/package.json +6 -6
  2. package/src/components/editor/custom-camera-controls.tsx +2 -1
  3. package/src/components/editor/editor-layout-v2.tsx +4 -3
  4. package/src/components/editor/first-person/build-collider-world.ts +5 -7
  5. package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
  6. package/src/components/editor/first-person-controls.tsx +11 -11
  7. package/src/components/editor/floating-action-menu.tsx +0 -0
  8. package/src/components/editor/floorplan-panel.tsx +44 -37
  9. package/src/components/editor/index.tsx +68 -53
  10. package/src/components/editor/selection-manager.tsx +2 -2
  11. package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
  12. package/src/components/editor/thumbnail-generator.tsx +18 -61
  13. package/src/components/editor/use-floorplan-background-placement.ts +3 -3
  14. package/src/components/editor/wall-measurement-label.tsx +0 -0
  15. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  16. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
  17. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
  18. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
  19. package/src/components/systems/roof/roof-edit-system.tsx +1 -1
  20. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  21. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  22. package/src/components/systems/zone/zone-system.tsx +0 -0
  23. package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
  24. package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
  25. package/src/components/tools/fence/fence-tool.tsx +2 -2
  26. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
  27. package/src/components/tools/fence/move-fence-tool.tsx +13 -9
  28. package/src/components/tools/item/move-tool.tsx +3 -6
  29. package/src/components/tools/item/placement-math.ts +2 -4
  30. package/src/components/tools/item/placement-strategies.ts +11 -10
  31. package/src/components/tools/item/use-draft-node.ts +0 -1
  32. package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
  33. package/src/components/tools/roof/move-roof-tool.tsx +7 -2
  34. package/src/components/tools/select/box-select-tool.tsx +12 -17
  35. package/src/components/tools/shared/segment-angle.ts +1 -1
  36. package/src/components/tools/tool-manager.tsx +12 -12
  37. package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
  38. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
  39. package/src/components/tools/wall/move-wall-tool.tsx +6 -4
  40. package/src/components/tools/wall/wall-drafting.ts +0 -0
  41. package/src/components/tools/wall/wall-tool.tsx +3 -3
  42. package/src/components/tools/zone/zone-tool.tsx +20 -5
  43. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  44. package/src/components/ui/action-menu/control-modes.tsx +7 -1
  45. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  46. package/src/components/ui/action-menu/index.tsx +35 -86
  47. package/src/components/ui/action-menu/view-toggles.tsx +19 -31
  48. package/src/components/ui/command-palette/editor-commands.tsx +6 -4
  49. package/src/components/ui/command-palette/index.tsx +4 -255
  50. package/src/components/ui/controls/material-picker.tsx +8 -5
  51. package/src/components/ui/floating-level-selector.tsx +1 -1
  52. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  53. package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
  54. package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
  55. package/src/components/ui/level-duplicate-dialog.tsx +3 -5
  56. package/src/components/ui/panels/ceiling-panel.tsx +2 -3
  57. package/src/components/ui/panels/column-panel.tsx +62 -18
  58. package/src/components/ui/panels/door-panel.tsx +272 -265
  59. package/src/components/ui/panels/fence-panel.tsx +0 -5
  60. package/src/components/ui/panels/paint-panel.tsx +66 -41
  61. package/src/components/ui/panels/panel-manager.tsx +3 -32
  62. package/src/components/ui/panels/reference-panel.tsx +28 -13
  63. package/src/components/ui/panels/roof-panel.tsx +52 -2
  64. package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
  65. package/src/components/ui/panels/slab-panel.tsx +0 -0
  66. package/src/components/ui/panels/spawn-panel.tsx +10 -4
  67. package/src/components/ui/panels/stair-panel.tsx +66 -14
  68. package/src/components/ui/panels/wall-panel.tsx +97 -1
  69. package/src/components/ui/panels/window-panel.tsx +13 -5
  70. package/src/components/ui/primitives/number-input.tsx +1 -1
  71. package/src/components/ui/primitives/sidebar.tsx +0 -0
  72. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  73. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  74. package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
  75. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
  76. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
  77. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  78. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
  79. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
  80. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
  81. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  82. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
  83. package/src/components/ui/slider.tsx +1 -1
  84. package/src/components/viewer-overlay.tsx +0 -0
  85. package/src/components/viewer-zone-system.tsx +0 -0
  86. package/src/hooks/use-auto-save.ts +14 -0
  87. package/src/hooks/use-keyboard.ts +10 -0
  88. package/src/index.tsx +8 -1
  89. package/src/lib/level-duplication.test.ts +0 -2
  90. package/src/lib/level-duplication.ts +1 -1
  91. package/src/lib/material-paint.ts +1 -1
  92. package/src/lib/roof-duplication.ts +1 -1
  93. package/src/lib/scene-bounds.ts +1 -1
  94. package/src/lib/scene.ts +0 -0
  95. package/src/lib/sfx-bus.ts +2 -0
  96. package/src/lib/sfx-player.ts +5 -5
  97. package/src/lib/stair-duplication.ts +2 -2
  98. package/src/store/use-editor.tsx +27 -59
  99. package/tsconfig.json +2 -1
  100. package/src/components/feedback-dialog.tsx +0 -265
  101. package/src/components/pascal-radio.tsx +0 -280
  102. package/src/components/preview-button.tsx +0 -16
  103. package/src/components/ui/viewer-toolbar.tsx +0 -436
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { Icon } from '@iconify/react'
3
4
  import {
4
5
  type AnyNodeId,
5
6
  type BuildingNode,
@@ -258,7 +259,7 @@ function GuidesControl() {
258
259
 
259
260
  <PopoverContent
260
261
  align="center"
261
- 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"
262
+ className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
262
263
  side="top"
263
264
  sideOffset={14}
264
265
  >
@@ -353,9 +354,9 @@ function GuidesControl() {
353
354
  )
354
355
  }
355
356
 
356
- // ── Grid snap ──────────────────────────────────────────────────────────────
357
+ // ── Grid snap toggle ────────────────────────────────────────────────────────
357
358
 
358
- export function GridSnapControl() {
359
+ function GridSnapControl() {
359
360
  const [isOpen, setIsOpen] = useState(false)
360
361
  const gridSnapStep = useEditor((state) => state.gridSnapStep)
361
362
  const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
@@ -374,20 +375,7 @@ export function GridSnapControl() {
374
375
  )}
375
376
  type="button"
376
377
  >
377
- <svg
378
- className="h-4 w-4"
379
- fill="none"
380
- stroke="currentColor"
381
- strokeWidth={1.5}
382
- viewBox="0 0 24 24"
383
- xmlns="http://www.w3.org/2000/svg"
384
- >
385
- <path
386
- d="M3 3h7v7H3V3zm11 0h7v7h-7V3zm0 11h7v7h-7v-7zm-11 0h7v7H3v-7z"
387
- strokeLinecap="round"
388
- strokeLinejoin="round"
389
- />
390
- </svg>
378
+ <Icon height={16} icon="lucide:grid-2x2" width={16} />
391
379
  <span className="mt-1 font-medium text-[9px] leading-none">
392
380
  {formatGridSnapStep(gridSnapStep)}
393
381
  </span>
@@ -510,7 +498,7 @@ function ScansControl() {
510
498
 
511
499
  <PopoverContent
512
500
  align="center"
513
- 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"
501
+ className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
514
502
  side="top"
515
503
  sideOffset={14}
516
504
  >
@@ -605,6 +593,8 @@ function ScansControl() {
605
593
  )
606
594
  }
607
595
 
596
+ // ── Reference floor control ────────────────────────────────────────────────────────────────────
597
+
608
598
  function ReferenceFloorControl() {
609
599
  const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
610
600
  const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
@@ -694,7 +684,11 @@ function ReferenceFloorControl() {
694
684
  onClick={toggleReferenceFloor}
695
685
  type="button"
696
686
  >
697
- {showReferenceFloor ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
687
+ {showReferenceFloor ? (
688
+ <Eye className="h-3.5 w-3.5" />
689
+ ) : (
690
+ <EyeOff className="h-3.5 w-3.5" />
691
+ )}
698
692
  </button>
699
693
  </div>
700
694
 
@@ -728,9 +722,7 @@ function ReferenceFloorControl() {
728
722
  )}
729
723
  />
730
724
  <span className="min-w-0 flex-1 truncate">{levelName}</span>
731
- <span className="text-[10px] text-muted-foreground">
732
- {index + 1} below
733
- </span>
725
+ <span className="text-[10px] text-muted-foreground">{index + 1} below</span>
734
726
  </button>
735
727
  )
736
728
  })}
@@ -757,24 +749,20 @@ function ReferenceFloorControl() {
757
749
  )
758
750
  }
759
751
 
760
- // ── Main ViewToggles ────────────────────────────────────────────────────────
752
+ // ── Exports ─────────────────────────────────────────────────────────────────
761
753
 
762
- export function ViewToggles() {
754
+ export { GridSnapControl }
755
+
756
+ export function SecondaryToggles() {
763
757
  return (
764
758
  <div className="flex items-center gap-1">
765
- {/* Scans (toggle + dropdown) */}
766
759
  <ScansControl />
767
-
768
- {/* Guides (toggle + dropdown) */}
769
760
  <GuidesControl />
770
-
771
- <ReferenceFloorControl />
772
761
  </div>
773
762
  )
774
763
  }
775
764
 
776
- // Secondary toggles for mobile (grid snap + scans + guides)
777
- export function SecondaryToggles() {
765
+ export function ViewToggles() {
778
766
  return (
779
767
  <div className="flex items-center gap-1">
780
768
  <GridSnapControl />
@@ -35,8 +35,8 @@ import {
35
35
  Video,
36
36
  } from 'lucide-react'
37
37
  import { useEffect } from 'react'
38
- import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
39
38
  import { runRedo, runUndo } from '../../../lib/history'
39
+ import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
40
40
  import { useCommandRegistry } from '../../../store/use-command-registry'
41
41
  import type { StructureTool } from '../../../store/use-editor'
42
42
  import useEditor from '../../../store/use-editor'
@@ -294,12 +294,14 @@ export function EditorCommands() {
294
294
  },
295
295
  {
296
296
  id: 'editor.viewer.camera-snapshot',
297
- label: 'Camera Snapshot',
297
+ label: 'Take Snapshot',
298
298
  group: 'Viewer Controls',
299
299
  icon: <Camera className="h-4 w-4" />,
300
300
  keywords: ['camera', 'snapshot', 'capture', 'save', 'view', 'bookmark'],
301
- navigate: true,
302
- execute: () => navigateTo('camera-view'),
301
+ execute: () => {
302
+ setOpen(false)
303
+ useEditor.getState().setCaptureMode(true)
304
+ },
303
305
  },
304
306
 
305
307
  // ── View ─────────────────────────────────────────────────────────────
@@ -27,15 +27,13 @@ interface CommandPaletteStore {
27
27
  setInputValue: (value: string) => void
28
28
  navigateTo: (page: string) => void
29
29
  goBack: () => void
30
- cameraScope: { nodeId: string; label: string } | null
31
- setCameraScope: (scope: { nodeId: string; label: string } | null) => void
32
30
  }
33
31
 
34
32
  export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
35
33
  open: false,
36
34
  setOpen: (open) => {
37
35
  set({ open })
38
- if (!open) set({ pages: [], inputValue: '', cameraScope: null, mode: 'command' })
36
+ if (!open) set({ pages: [], inputValue: '', mode: 'command' })
39
37
  },
40
38
  mode: 'command',
41
39
  setMode: (mode) => set({ mode }),
@@ -44,12 +42,8 @@ export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
44
42
  setInputValue: (value) => set({ inputValue: value }),
45
43
  navigateTo: (page) => set((s) => ({ pages: [...s.pages, page], inputValue: '' })),
46
44
  goBack: () => {
47
- const { pages } = get()
48
- if (pages[pages.length - 1] === 'camera-scope') set({ cameraScope: null })
49
45
  set((s) => ({ pages: s.pages.slice(0, -1), inputValue: '' }))
50
46
  },
51
- cameraScope: null,
52
- setCameraScope: (scope) => set({ cameraScope: scope }),
53
47
  }))
54
48
 
55
49
  // ---------------------------------------------------------------------------
@@ -157,8 +151,6 @@ const PAGE_LABEL: Record<string, string> = {
157
151
  'level-mode': 'Level Mode',
158
152
  'rename-level': 'Rename Level',
159
153
  'goto-level': 'Go to Level',
160
- 'camera-view': 'Camera Snapshot',
161
- 'camera-scope': '',
162
154
  }
163
155
 
164
156
  // ---------------------------------------------------------------------------
@@ -196,19 +188,8 @@ function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
196
188
  // Main component
197
189
  // ---------------------------------------------------------------------------
198
190
  export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEmptyAction }) {
199
- const {
200
- open,
201
- setOpen,
202
- mode,
203
- setMode,
204
- pages,
205
- inputValue,
206
- setInputValue,
207
- navigateTo,
208
- goBack,
209
- cameraScope,
210
- setCameraScope,
211
- } = useCommandPalette()
191
+ const { open, setOpen, mode, setMode, pages, inputValue, setInputValue, navigateTo, goBack } =
192
+ useCommandPalette()
212
193
 
213
194
  const [meta, setMeta] = useState('⌘')
214
195
  const [isFullscreen, setIsFullscreen] = useState(false)
@@ -233,11 +214,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
233
214
  ),
234
215
  )
235
216
 
236
- const cameraScopeNode = useScene((s) =>
237
- cameraScope ? s.nodes[cameraScope.nodeId as AnyNodeId] : null,
238
- )
239
- const hasScopeSnapshot = !!(cameraScopeNode as any)?.camera
240
-
241
217
  // Platform detection
242
218
  useEffect(() => {
243
219
  setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? '⌘' : 'Ctrl')
@@ -279,7 +255,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
279
255
  solo: 'Solo',
280
256
  }
281
257
 
282
- // Camera snapshot helpers (used by sub-pages registered via EditorCommands)
283
258
  const confirmRename = () => {
284
259
  if (!(activeLevelId && inputValue.trim())) return
285
260
  run(() => {
@@ -287,29 +262,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
287
262
  })
288
263
  }
289
264
 
290
- const takeSnapshot = () => {
291
- if (!cameraScope) return
292
- import('@pascal-app/core').then(({ emitter }) => {
293
- run(() =>
294
- emitter.emit('camera-controls:capture', { nodeId: cameraScope.nodeId as AnyNodeId }),
295
- )
296
- })
297
- }
298
-
299
- const viewSnapshot = () => {
300
- if (!(cameraScope && hasScopeSnapshot)) return
301
- import('@pascal-app/core').then(({ emitter }) => {
302
- run(() => emitter.emit('camera-controls:view', { nodeId: cameraScope.nodeId as AnyNodeId }))
303
- })
304
- }
305
-
306
- const clearSnapshot = () => {
307
- if (!(cameraScope && hasScopeSnapshot)) return
308
- run(() => {
309
- useScene.getState().updateNode(cameraScope.nodeId as AnyNodeId, { camera: undefined } as any)
310
- })
311
- }
312
-
313
265
  // ---------------------------------------------------------------------------
314
266
  // Group registered actions by group (preserving insertion order)
315
267
  // ---------------------------------------------------------------------------
@@ -363,9 +315,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
363
315
  onClick={goBack}
364
316
  type="button"
365
317
  >
366
- {page === 'camera-scope'
367
- ? (cameraScope?.label ?? 'Snapshot')
368
- : (PAGE_LABEL[page] ?? views.get(page)?.label ?? page)}
318
+ {PAGE_LABEL[page] ?? views.get(page)?.label ?? page}
369
319
  </button>
370
320
  )}
371
321
  <Command.Input
@@ -500,207 +450,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
500
450
  </Command.Item>
501
451
  </Command.Group>
502
452
  )}
503
-
504
- {/* ── Camera Snapshot: scope picker ─────────────────────────── */}
505
- {page === 'camera-view' && (
506
- <Command.Group heading="Camera Snapshot — Select Scope">
507
- <OptionItem
508
- icon={
509
- <svg
510
- className="h-4 w-4"
511
- fill="none"
512
- stroke="currentColor"
513
- strokeWidth={2}
514
- viewBox="0 0 24 24"
515
- >
516
- <path d="M3 3h18v18H3z" strokeLinecap="round" strokeLinejoin="round" />
517
- <path d="M3 9h18M9 21V9" strokeLinecap="round" strokeLinejoin="round" />
518
- </svg>
519
- }
520
- label="Site"
521
- onSelect={() => {
522
- const { rootNodeIds } = useScene.getState()
523
- const siteId = rootNodeIds[0]
524
- if (siteId) {
525
- setCameraScope({ nodeId: siteId, label: 'Site' })
526
- navigateTo('camera-scope')
527
- }
528
- }}
529
- />
530
- <OptionItem
531
- icon={
532
- <svg
533
- className="h-4 w-4"
534
- fill="none"
535
- stroke="currentColor"
536
- strokeWidth={2}
537
- viewBox="0 0 24 24"
538
- >
539
- <path
540
- d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
541
- strokeLinecap="round"
542
- strokeLinejoin="round"
543
- />
544
- <polyline
545
- points="9 22 9 12 15 12 15 22"
546
- strokeLinecap="round"
547
- strokeLinejoin="round"
548
- />
549
- </svg>
550
- }
551
- label="Building"
552
- onSelect={() => {
553
- const building = Object.values(useScene.getState().nodes).find(
554
- (n) => n.type === 'building',
555
- )
556
- if (building) {
557
- setCameraScope({ nodeId: building.id, label: 'Building' })
558
- navigateTo('camera-scope')
559
- }
560
- }}
561
- />
562
- <OptionItem
563
- disabled={!activeLevelId}
564
- icon={
565
- <svg
566
- className="h-4 w-4"
567
- fill="none"
568
- stroke="currentColor"
569
- strokeWidth={2}
570
- viewBox="0 0 24 24"
571
- >
572
- <path
573
- d="M12 2L2 7l10 5 10-5-10-5z"
574
- strokeLinecap="round"
575
- strokeLinejoin="round"
576
- />
577
- <path
578
- d="M2 17l10 5 10-5M2 12l10 5 10-5"
579
- strokeLinecap="round"
580
- strokeLinejoin="round"
581
- />
582
- </svg>
583
- }
584
- label="Level"
585
- onSelect={() => {
586
- if (activeLevelId) {
587
- setCameraScope({ nodeId: activeLevelId, label: 'Level' })
588
- navigateTo('camera-scope')
589
- }
590
- }}
591
- />
592
- <OptionItem
593
- disabled={!useViewer.getState().selection.selectedIds.length}
594
- icon={
595
- <svg
596
- className="h-4 w-4"
597
- fill="none"
598
- stroke="currentColor"
599
- strokeWidth={2}
600
- viewBox="0 0 24 24"
601
- >
602
- <path d="M5 3l14 9-14 9V3z" strokeLinecap="round" strokeLinejoin="round" />
603
- </svg>
604
- }
605
- label="Selection"
606
- onSelect={() => {
607
- const firstId = useViewer.getState().selection.selectedIds[0]
608
- if (firstId) {
609
- setCameraScope({ nodeId: firstId, label: 'Selection' })
610
- navigateTo('camera-scope')
611
- }
612
- }}
613
- />
614
- </Command.Group>
615
- )}
616
-
617
- {/* ── Camera Snapshot: actions for selected scope ───────────── */}
618
- {page === 'camera-scope' && cameraScope && (
619
- <Command.Group heading={`${cameraScope.label} Snapshot`}>
620
- <OptionItem
621
- icon={
622
- <svg
623
- className="h-4 w-4"
624
- fill="none"
625
- stroke="currentColor"
626
- strokeWidth={2}
627
- viewBox="0 0 24 24"
628
- >
629
- <path
630
- d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"
631
- strokeLinecap="round"
632
- strokeLinejoin="round"
633
- />
634
- <circle
635
- cx="12"
636
- cy="13"
637
- r="4"
638
- strokeLinecap="round"
639
- strokeLinejoin="round"
640
- />
641
- </svg>
642
- }
643
- label={hasScopeSnapshot ? 'Update Snapshot' : 'Take Snapshot'}
644
- onSelect={takeSnapshot}
645
- />
646
- {hasScopeSnapshot && (
647
- <OptionItem
648
- icon={
649
- <svg
650
- className="h-4 w-4"
651
- fill="none"
652
- stroke="currentColor"
653
- strokeWidth={2}
654
- viewBox="0 0 24 24"
655
- >
656
- <path
657
- d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
658
- strokeLinecap="round"
659
- strokeLinejoin="round"
660
- />
661
- <circle
662
- cx="12"
663
- cy="12"
664
- r="3"
665
- strokeLinecap="round"
666
- strokeLinejoin="round"
667
- />
668
- </svg>
669
- }
670
- label="View Snapshot"
671
- onSelect={viewSnapshot}
672
- />
673
- )}
674
- {hasScopeSnapshot && (
675
- <OptionItem
676
- icon={
677
- <svg
678
- className="h-4 w-4"
679
- fill="none"
680
- stroke="currentColor"
681
- strokeWidth={2}
682
- viewBox="0 0 24 24"
683
- >
684
- <polyline
685
- points="3 6 5 6 21 6"
686
- strokeLinecap="round"
687
- strokeLinejoin="round"
688
- />
689
- <path
690
- d="M19 6l-1 14H6L5 6"
691
- strokeLinecap="round"
692
- strokeLinejoin="round"
693
- />
694
- <path d="M10 11v6M14 11v6" strokeLinecap="round" strokeLinejoin="round" />
695
- <path d="M9 6V4h6v2" strokeLinecap="round" strokeLinejoin="round" />
696
- </svg>
697
- }
698
- label="Clear Snapshot"
699
- onSelect={clearSnapshot}
700
- />
701
- )}
702
- </Command.Group>
703
- )}
704
453
  </Command.List>
705
454
 
706
455
  {/* Footer hint */}
@@ -4,9 +4,11 @@ import {
4
4
  getCatalogMaterialById,
5
5
  getLibraryMaterialIdFromRef,
6
6
  getMaterialsForCategory,
7
+ getMaterialsForTarget,
7
8
  MATERIAL_CATEGORIES,
8
- toLibraryMaterialRef,
9
9
  type MaterialSchema,
10
+ type MaterialTarget,
11
+ toLibraryMaterialRef,
10
12
  } from '@pascal-app/core'
11
13
  import { useEffect, useRef, useState } from 'react'
12
14
  import useEditor from '../../../store/use-editor'
@@ -17,6 +19,8 @@ type MaterialPickerProps = {
17
19
  onChange?: (material: MaterialSchema) => void
18
20
  onSelectMaterialPreset?: (materialPreset: string) => void
19
21
  disabled?: boolean
22
+ nodeType?: MaterialTarget
23
+ hideSideControl?: boolean
20
24
  }
21
25
 
22
26
  export function MaterialPicker({
@@ -48,8 +52,7 @@ export function MaterialPicker({
48
52
  return
49
53
  }
50
54
 
51
- const catalogId =
52
- getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
55
+ const catalogId = getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
53
56
  const selectedCatalogEntry = getCatalogMaterialById(catalogId)
54
57
  if (selectedCatalogEntry?.category) {
55
58
  setSelectedCategory(selectedCatalogEntry.category)
@@ -177,7 +180,7 @@ export function MaterialPicker({
177
180
  title={item.label}
178
181
  type="button"
179
182
  >
180
- <div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-inset ring-white/12" />
183
+ <div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-white/12 ring-inset" />
181
184
  {item.previewThumbnailUrl ? (
182
185
  <img
183
186
  alt={item.label}
@@ -193,7 +196,7 @@ export function MaterialPicker({
193
196
  ))}
194
197
  {selectedCategory === 'other' && onChange ? (
195
198
  <button
196
- className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border text-[10px] font-medium transition-all ${
199
+ className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border font-medium text-[10px] transition-all ${
197
200
  showCustom
198
201
  ? 'border-blue-500 ring-2 ring-blue-500/30'
199
202
  : 'border-gray-300 hover:border-gray-400'
@@ -162,7 +162,7 @@ function LevelRow({
162
162
  {...dragHandleProps}
163
163
  aria-label={`Reorder ${getLevelDisplayLabel(level)}`}
164
164
  className={cn(
165
- 'ml-0.5 flex h-6 w-4 shrink-0 touch-none cursor-grab items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
165
+ 'ml-0.5 flex h-6 w-4 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
166
166
  isDragging && 'cursor-grabbing opacity-100',
167
167
  )}
168
168
  onClick={(e) => {
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { useIsMobile } from '../../../hooks/use-mobile'
3
4
  import useEditor from '../../../store/use-editor'
4
5
  import { BuildingHelper } from './building-helper'
5
6
  import { CeilingHelper } from './ceiling-helper'
@@ -12,6 +13,10 @@ export function HelperManager() {
12
13
  const mode = useEditor((s) => s.mode)
13
14
  const tool = useEditor((s) => s.tool)
14
15
  const movingNode = useEditor((state) => state.movingNode)
16
+ const isMobile = useIsMobile()
17
+
18
+ // Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
19
+ if (isMobile) return null
15
20
 
16
21
  if (movingNode) {
17
22
  if (movingNode.type === 'building') return <BuildingHelper showRotate />