@pascal-app/editor 0.4.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 (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. package/tsconfig.json +9 -0
@@ -0,0 +1,306 @@
1
+ 'use client'
2
+
3
+ import { type AnyNode, getScaledDimensions, ItemNode, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { Copy, Link, Link2Off, Move, Trash2 } from 'lucide-react'
6
+ import { useCallback, useState } from 'react'
7
+ import { sfxEmitter } from '../../../lib/sfx-bus'
8
+ import { cn } from '../../../lib/utils'
9
+ import useEditor from '../../../store/use-editor'
10
+ import { ActionButton, ActionGroup } from '../controls/action-button'
11
+ import { PanelSection } from '../controls/panel-section'
12
+ import { SliderControl } from '../controls/slider-control'
13
+ import { CollectionsPopover } from './collections/collections-popover'
14
+ import { PanelWrapper } from './panel-wrapper'
15
+
16
+ export function ItemPanel() {
17
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
18
+ const setSelection = useViewer((s) => s.setSelection)
19
+ const nodes = useScene((s) => s.nodes)
20
+ const updateNode = useScene((s) => s.updateNode)
21
+ const deleteNode = useScene((s) => s.deleteNode)
22
+ const setMovingNode = useEditor((s) => s.setMovingNode)
23
+
24
+ const selectedId = selectedIds[0]
25
+ const node = selectedId ? (nodes[selectedId as AnyNode['id']] as ItemNode | undefined) : undefined
26
+
27
+ const [uniformScale, setUniformScale] = useState(true)
28
+
29
+ const handleUpdate = useCallback(
30
+ (updates: Partial<ItemNode>) => {
31
+ if (!(selectedId && node)) return
32
+ updateNode(selectedId as AnyNode['id'], updates)
33
+
34
+ if (node.asset.attachTo === 'wall' && node.parentId) {
35
+ requestAnimationFrame(() => {
36
+ useScene.getState().dirtyNodes.add(node.parentId as AnyNode['id'])
37
+ })
38
+ }
39
+ },
40
+ [selectedId, node, updateNode],
41
+ )
42
+
43
+ const handleClose = useCallback(() => {
44
+ setSelection({ selectedIds: [] })
45
+ }, [setSelection])
46
+
47
+ const handleMove = useCallback(() => {
48
+ if (node) {
49
+ sfxEmitter.emit('sfx:item-pick')
50
+ setMovingNode(node)
51
+ setSelection({ selectedIds: [] })
52
+ }
53
+ }, [node, setMovingNode, setSelection])
54
+
55
+ const handleDuplicate = useCallback(() => {
56
+ if (!node) return
57
+ sfxEmitter.emit('sfx:item-pick')
58
+ const proto = ItemNode.parse({
59
+ position: [...node.position] as [number, number, number],
60
+ rotation: [...node.rotation] as [number, number, number],
61
+ name: node.name,
62
+ asset: node.asset,
63
+ parentId: node.parentId,
64
+ side: node.side,
65
+ metadata: { isNew: true },
66
+ })
67
+ setMovingNode(proto)
68
+ setSelection({ selectedIds: [] })
69
+ }, [node, setMovingNode, setSelection])
70
+
71
+ const handleDelete = useCallback(() => {
72
+ if (!selectedId) return
73
+ sfxEmitter.emit('sfx:item-delete')
74
+ deleteNode(selectedId as AnyNode['id'])
75
+ setSelection({ selectedIds: [] })
76
+ }, [selectedId, deleteNode, setSelection])
77
+
78
+ if (!node || node.type !== 'item' || selectedIds.length !== 1) return null
79
+
80
+ return (
81
+ <PanelWrapper
82
+ icon={node.asset.thumbnail || '/icons/furniture.png'}
83
+ onClose={handleClose}
84
+ title={node.name || node.asset.name}
85
+ width={300}
86
+ >
87
+ <PanelSection title="Position">
88
+ <SliderControl
89
+ label={
90
+ <>
91
+ X<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
92
+ </>
93
+ }
94
+ max={node.position[0] + 2}
95
+ min={node.position[0] - 2}
96
+ onChange={(value) =>
97
+ handleUpdate({ position: [value, node.position[1], node.position[2]] })
98
+ }
99
+ precision={2}
100
+ step={0.01}
101
+ unit="m"
102
+ value={Math.round(node.position[0] * 100) / 100}
103
+ />
104
+ <SliderControl
105
+ label={
106
+ <>
107
+ Y<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
108
+ </>
109
+ }
110
+ max={node.position[1] + 2}
111
+ min={node.position[1] - 2}
112
+ onChange={(value) =>
113
+ handleUpdate({ position: [node.position[0], value, node.position[2]] })
114
+ }
115
+ precision={2}
116
+ step={0.01}
117
+ unit="m"
118
+ value={Math.round(node.position[1] * 100) / 100}
119
+ />
120
+ <SliderControl
121
+ label={
122
+ <>
123
+ Z<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
124
+ </>
125
+ }
126
+ max={node.position[2] + 2}
127
+ min={node.position[2] - 2}
128
+ onChange={(value) =>
129
+ handleUpdate({ position: [node.position[0], node.position[1], value] })
130
+ }
131
+ precision={2}
132
+ step={0.01}
133
+ unit="m"
134
+ value={Math.round(node.position[2] * 100) / 100}
135
+ />
136
+ </PanelSection>
137
+
138
+ <PanelSection title="Rotation">
139
+ <SliderControl
140
+ label={
141
+ <>
142
+ Y<sub className="ml-[1px] text-[11px] opacity-70">rot</sub>
143
+ </>
144
+ }
145
+ max={Math.round((node.rotation[1] * 180) / Math.PI) + 45}
146
+ min={Math.round((node.rotation[1] * 180) / Math.PI) - 45}
147
+ onChange={(degrees) => {
148
+ const radians = (degrees * Math.PI) / 180
149
+ handleUpdate({ rotation: [node.rotation[0], radians, node.rotation[2]] })
150
+ }}
151
+ precision={0}
152
+ step={1}
153
+ unit="°"
154
+ value={Math.round((node.rotation[1] * 180) / Math.PI)}
155
+ />
156
+ <div className="flex gap-1.5 px-1 pt-2 pb-1">
157
+ <ActionButton
158
+ label="-45°"
159
+ onClick={() => {
160
+ sfxEmitter.emit('sfx:item-rotate')
161
+ const currentDegrees = (node.rotation[1] * 180) / Math.PI
162
+ const radians = ((currentDegrees - 45) * Math.PI) / 180
163
+ handleUpdate({ rotation: [node.rotation[0], radians, node.rotation[2]] })
164
+ }}
165
+ />
166
+ <ActionButton
167
+ label="+45°"
168
+ onClick={() => {
169
+ sfxEmitter.emit('sfx:item-rotate')
170
+ const currentDegrees = (node.rotation[1] * 180) / Math.PI
171
+ const radians = ((currentDegrees + 45) * Math.PI) / 180
172
+ handleUpdate({ rotation: [node.rotation[0], radians, node.rotation[2]] })
173
+ }}
174
+ />
175
+ </div>
176
+ </PanelSection>
177
+
178
+ <PanelSection title="Scale">
179
+ <div className="flex items-center justify-between px-2 pb-2">
180
+ <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
181
+ Uniform Scale
182
+ </span>
183
+ <button
184
+ className={cn(
185
+ 'flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground',
186
+ uniformScale ? 'bg-[#3e3e3e]' : 'bg-[#2C2C2E] hover:bg-[#3e3e3e]',
187
+ )}
188
+ onClick={() => setUniformScale((v) => !v)}
189
+ type="button"
190
+ >
191
+ {uniformScale ? <Link className="h-3.5 w-3.5" /> : <Link2Off className="h-3.5 w-3.5" />}
192
+ </button>
193
+ </div>
194
+
195
+ {uniformScale ? (
196
+ <SliderControl
197
+ label={
198
+ <>
199
+ XYZ<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
200
+ </>
201
+ }
202
+ max={10}
203
+ min={0.01}
204
+ onChange={(value) => {
205
+ const v = Math.max(0.01, value)
206
+ handleUpdate({ scale: [v, v, v] })
207
+ }}
208
+ precision={2}
209
+ step={0.1}
210
+ value={Math.round(node.scale[0] * 100) / 100}
211
+ />
212
+ ) : (
213
+ <>
214
+ <SliderControl
215
+ label={
216
+ <>
217
+ X<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
218
+ </>
219
+ }
220
+ max={10}
221
+ min={0.01}
222
+ onChange={(value) =>
223
+ handleUpdate({ scale: [Math.max(0.01, value), node.scale[1], node.scale[2]] })
224
+ }
225
+ precision={2}
226
+ step={0.1}
227
+ value={Math.round(node.scale[0] * 100) / 100}
228
+ />
229
+ <SliderControl
230
+ label={
231
+ <>
232
+ Y<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
233
+ </>
234
+ }
235
+ max={10}
236
+ min={0.01}
237
+ onChange={(value) =>
238
+ handleUpdate({ scale: [node.scale[0], Math.max(0.01, value), node.scale[2]] })
239
+ }
240
+ precision={2}
241
+ step={0.1}
242
+ value={Math.round(node.scale[1] * 100) / 100}
243
+ />
244
+ <SliderControl
245
+ label={
246
+ <>
247
+ Z<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
248
+ </>
249
+ }
250
+ max={10}
251
+ min={0.01}
252
+ onChange={(value) =>
253
+ handleUpdate({ scale: [node.scale[0], node.scale[1], Math.max(0.01, value)] })
254
+ }
255
+ precision={2}
256
+ step={0.1}
257
+ value={Math.round(node.scale[2] * 100) / 100}
258
+ />
259
+ </>
260
+ )}
261
+ </PanelSection>
262
+
263
+ <PanelSection title="Info">
264
+ <div className="flex items-center justify-between px-2 py-1 text-muted-foreground text-sm">
265
+ <span>Dimensions</span>
266
+ {(() => {
267
+ const [w, h, d] = getScaledDimensions(node)
268
+ return (
269
+ <span className="font-mono text-white">
270
+ {Math.round(w * 100) / 100}×{Math.round(h * 100) / 100}×{Math.round(d * 100) / 100}
271
+ </span>
272
+ )
273
+ })()}
274
+ </div>
275
+ </PanelSection>
276
+
277
+ <PanelSection title="Collections">
278
+ <ActionGroup>
279
+ <CollectionsPopover
280
+ collectionIds={node.collectionIds}
281
+ nodeId={selectedId as AnyNode['id']}
282
+ >
283
+ <ActionButton label="Manage collections…" />
284
+ </CollectionsPopover>
285
+ </ActionGroup>
286
+ </PanelSection>
287
+
288
+ <PanelSection title="Actions">
289
+ <ActionGroup>
290
+ <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
291
+ <ActionButton
292
+ icon={<Copy className="h-3.5 w-3.5" />}
293
+ label="Duplicate"
294
+ onClick={handleDuplicate}
295
+ />
296
+ <ActionButton
297
+ className="hover:bg-red-500/20"
298
+ icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
299
+ label="Delete"
300
+ onClick={handleDelete}
301
+ />
302
+ </ActionGroup>
303
+ </PanelSection>
304
+ </PanelWrapper>
305
+ )
306
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import useEditor from '../../../store/use-editor'
6
+ import { CeilingPanel } from './ceiling-panel'
7
+ import { DoorPanel } from './door-panel'
8
+ import { ItemPanel } from './item-panel'
9
+ import { ReferencePanel } from './reference-panel'
10
+ import { RoofPanel } from './roof-panel'
11
+ import { RoofSegmentPanel } from './roof-segment-panel'
12
+ import { SlabPanel } from './slab-panel'
13
+ import { StairPanel } from './stair-panel'
14
+ import { StairSegmentPanel } from './stair-segment-panel'
15
+ import { WallPanel } from './wall-panel'
16
+ import { WindowPanel } from './window-panel'
17
+
18
+ export function PanelManager() {
19
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
20
+ const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
21
+ const nodes = useScene((s) => s.nodes)
22
+
23
+ // Show reference panel if a reference is selected
24
+ if (selectedReferenceId) {
25
+ return <ReferencePanel />
26
+ }
27
+
28
+ // Show appropriate panel based on selected node type
29
+ if (selectedIds.length === 1) {
30
+ const selectedNode = selectedIds[0]
31
+ const node = nodes[selectedNode as AnyNodeId]
32
+ if (node) {
33
+ switch (node.type) {
34
+ case 'item':
35
+ return <ItemPanel />
36
+ case 'roof':
37
+ return <RoofPanel />
38
+ case 'roof-segment':
39
+ return <RoofSegmentPanel />
40
+ case 'stair':
41
+ return <StairPanel />
42
+ case 'stair-segment':
43
+ return <StairSegmentPanel />
44
+ case 'slab':
45
+ return <SlabPanel />
46
+ case 'ceiling':
47
+ return <CeilingPanel />
48
+ case 'wall':
49
+ return <WallPanel />
50
+ case 'door':
51
+ return <DoorPanel />
52
+ case 'window':
53
+ return <WindowPanel />
54
+ }
55
+ }
56
+ }
57
+
58
+ return null
59
+ }
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import { ChevronLeft, RotateCcw, X } from 'lucide-react'
4
+ import Image from 'next/image'
5
+ import { cn } from '../../../lib/utils'
6
+
7
+ interface PanelWrapperProps {
8
+ title: string
9
+ icon?: string
10
+ onClose?: () => void
11
+ onReset?: () => void
12
+ onBack?: () => void
13
+ children: React.ReactNode
14
+ className?: string
15
+ width?: number | string
16
+ }
17
+
18
+ export function PanelWrapper({
19
+ title,
20
+ icon,
21
+ onClose,
22
+ onReset,
23
+ onBack,
24
+ children,
25
+ className,
26
+ width = 320, // default width
27
+ }: PanelWrapperProps) {
28
+ return (
29
+ <div
30
+ className={cn(
31
+ 'pointer-events-auto fixed top-20 right-4 z-50 flex max-h-[calc(100dvh-100px)] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground',
32
+ className,
33
+ )}
34
+ style={{ width }}
35
+ >
36
+ {/* Header */}
37
+ <div className="flex items-center justify-between border-border/50 border-b px-3 py-3">
38
+ <div className="flex items-center gap-2">
39
+ {onBack && (
40
+ <button
41
+ className="mr-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
42
+ onClick={onBack}
43
+ type="button"
44
+ >
45
+ <ChevronLeft className="h-4 w-4" />
46
+ </button>
47
+ )}
48
+ {icon && (
49
+ <Image alt="" className="shrink-0 object-contain" height={16} src={icon} width={16} />
50
+ )}
51
+ <h2 className="truncate font-semibold text-foreground text-sm tracking-tight">{title}</h2>
52
+ </div>
53
+
54
+ <div className="flex items-center gap-1">
55
+ {onReset && (
56
+ <button
57
+ className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
58
+ onClick={onReset}
59
+ type="button"
60
+ >
61
+ <RotateCcw className="h-4 w-4" />
62
+ </button>
63
+ )}
64
+ {onClose && (
65
+ <button
66
+ className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
67
+ onClick={onClose}
68
+ type="button"
69
+ >
70
+ <X className="h-4 w-4" />
71
+ </button>
72
+ )}
73
+ </div>
74
+ </div>
75
+
76
+ {/* Content */}
77
+ <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>
78
+ </div>
79
+ )
80
+ }