@pascal-app/editor 0.5.1 → 0.7.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 (150) hide show
  1. package/package.json +12 -7
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +75 -7
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +29 -0
  6. package/src/components/editor/first-person/build-collider-world.ts +365 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +281 -83
  10. package/src/components/editor/floating-building-action-menu.tsx +4 -3
  11. package/src/components/editor/floorplan-background-selection.ts +113 -0
  12. package/src/components/editor/floorplan-panel.tsx +10442 -3275
  13. package/src/components/editor/index.tsx +270 -20
  14. package/src/components/editor/node-action-menu.tsx +14 -1
  15. package/src/components/editor/selection-manager.tsx +766 -12
  16. package/src/components/editor/site-edge-labels.tsx +9 -3
  17. package/src/components/editor/thumbnail-generator.tsx +350 -157
  18. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  19. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  20. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  21. package/src/components/editor/wall-measurement-label.tsx +377 -58
  22. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  23. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  24. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  25. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  26. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  27. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  28. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  29. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  30. package/src/components/editor-2d/svg-paths.ts +119 -0
  31. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
  32. package/src/components/systems/roof/roof-edit-system.tsx +5 -5
  33. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  34. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
  35. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  36. package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
  37. package/src/components/tools/column/column-tool.tsx +97 -0
  38. package/src/components/tools/column/move-column-tool.tsx +105 -0
  39. package/src/components/tools/door/door-tool.tsx +19 -0
  40. package/src/components/tools/door/move-door-tool.tsx +38 -8
  41. package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
  42. package/src/components/tools/fence/fence-drafting.ts +27 -8
  43. package/src/components/tools/fence/fence-tool.tsx +159 -3
  44. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
  45. package/src/components/tools/fence/move-fence-tool.tsx +102 -27
  46. package/src/components/tools/item/move-tool.tsx +19 -1
  47. package/src/components/tools/item/placement-math.ts +44 -7
  48. package/src/components/tools/item/placement-strategies.ts +111 -33
  49. package/src/components/tools/item/placement-types.ts +7 -0
  50. package/src/components/tools/item/use-draft-node.ts +2 -0
  51. package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
  52. package/src/components/tools/roof/move-roof-tool.tsx +111 -43
  53. package/src/components/tools/shared/polygon-editor.tsx +244 -29
  54. package/src/components/tools/shared/segment-angle.ts +156 -0
  55. package/src/components/tools/slab/move-slab-tool.tsx +182 -0
  56. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  57. package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
  58. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  59. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  60. package/src/components/tools/stair/stair-tool.tsx +11 -3
  61. package/src/components/tools/tool-manager.tsx +30 -3
  62. package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
  63. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
  64. package/src/components/tools/wall/move-wall-tool.tsx +356 -0
  65. package/src/components/tools/wall/wall-drafting.ts +348 -17
  66. package/src/components/tools/wall/wall-tool.tsx +134 -2
  67. package/src/components/tools/window/move-window-tool.tsx +28 -0
  68. package/src/components/tools/window/window-tool.tsx +17 -0
  69. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  70. package/src/components/ui/action-menu/control-modes.tsx +37 -5
  71. package/src/components/ui/action-menu/index.tsx +91 -1
  72. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  73. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  74. package/src/components/ui/command-palette/editor-commands.tsx +27 -5
  75. package/src/components/ui/command-palette/index.tsx +0 -1
  76. package/src/components/ui/controls/material-picker.tsx +189 -169
  77. package/src/components/ui/controls/slider-control.tsx +88 -26
  78. package/src/components/ui/floating-level-selector.tsx +286 -55
  79. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  80. package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
  81. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  82. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  83. package/src/components/ui/panels/ceiling-panel.tsx +47 -27
  84. package/src/components/ui/panels/column-panel.tsx +715 -0
  85. package/src/components/ui/panels/door-panel.tsx +986 -294
  86. package/src/components/ui/panels/fence-panel.tsx +55 -12
  87. package/src/components/ui/panels/item-panel.tsx +5 -5
  88. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  89. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  90. package/src/components/ui/panels/node-display.ts +39 -0
  91. package/src/components/ui/panels/paint-panel.tsx +138 -0
  92. package/src/components/ui/panels/panel-manager.tsx +241 -30
  93. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  94. package/src/components/ui/panels/reference-panel.tsx +243 -9
  95. package/src/components/ui/panels/roof-panel.tsx +30 -62
  96. package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
  97. package/src/components/ui/panels/slab-panel.tsx +46 -24
  98. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  99. package/src/components/ui/panels/stair-panel.tsx +117 -69
  100. package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
  101. package/src/components/ui/panels/wall-panel.tsx +71 -17
  102. package/src/components/ui/panels/window-panel.tsx +665 -146
  103. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
  106. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
  107. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  108. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
  109. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
  110. package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
  111. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
  112. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
  113. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  114. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
  115. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  116. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
  117. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
  118. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
  119. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
  120. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
  121. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
  122. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  123. package/src/components/ui/viewer-toolbar.tsx +96 -2
  124. package/src/components/viewer-overlay.tsx +25 -19
  125. package/src/hooks/use-auto-frame.ts +45 -0
  126. package/src/hooks/use-contextual-tools.ts +14 -13
  127. package/src/hooks/use-keyboard.ts +67 -9
  128. package/src/hooks/use-mobile.ts +12 -12
  129. package/src/index.tsx +2 -1
  130. package/src/lib/door-interaction.ts +88 -0
  131. package/src/lib/floorplan/geometry.ts +263 -0
  132. package/src/lib/floorplan/index.ts +38 -0
  133. package/src/lib/floorplan/items.ts +179 -0
  134. package/src/lib/floorplan/selection-tool.ts +231 -0
  135. package/src/lib/floorplan/stairs.ts +478 -0
  136. package/src/lib/floorplan/types.ts +57 -0
  137. package/src/lib/floorplan/walls.ts +23 -0
  138. package/src/lib/guide-events.ts +10 -0
  139. package/src/lib/history.ts +20 -0
  140. package/src/lib/level-duplication.test.ts +72 -0
  141. package/src/lib/level-duplication.ts +153 -0
  142. package/src/lib/local-guide-image.ts +42 -0
  143. package/src/lib/material-paint.ts +284 -0
  144. package/src/lib/roof-duplication.ts +214 -0
  145. package/src/lib/scene-bounds.test.ts +183 -0
  146. package/src/lib/scene-bounds.ts +169 -0
  147. package/src/lib/sfx-player.ts +96 -13
  148. package/src/lib/stair-duplication.ts +126 -0
  149. package/src/lib/window-interaction.ts +86 -0
  150. package/src/store/use-editor.tsx +279 -15
@@ -1,60 +1,271 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type BuildingNode,
7
+ type CeilingNode,
8
+ type ColumnNode,
9
+ type DoorNode,
10
+ type FenceNode,
11
+ type ItemNode,
12
+ type RoofNode,
13
+ type RoofSegmentNode,
14
+ type SlabNode,
15
+ type StairNode,
16
+ type StairSegmentNode,
17
+ useScene,
18
+ type WallNode,
19
+ type WindowNode,
20
+ } from '@pascal-app/core'
4
21
  import { useViewer } from '@pascal-app/viewer'
22
+ import { useCallback, useEffect, useState } from 'react'
23
+ import { useIsMobile } from '../../../hooks/use-mobile'
24
+ import { sfxEmitter } from '../../../lib/sfx-bus'
5
25
  import useEditor from '../../../store/use-editor'
6
26
  import { CeilingPanel } from './ceiling-panel'
27
+ import { ColumnPanel } from './column-panel'
7
28
  import { DoorPanel } from './door-panel'
8
29
  import { FencePanel } from './fence-panel'
9
30
  import { ItemPanel } from './item-panel'
31
+ import { MobilePanelSheet } from './mobile-panel-sheet'
32
+ import { MobileSelectionBar } from './mobile-selection-bar'
33
+ import { getNodeDisplay } from './node-display'
34
+ import { PaintPanel } from './paint-panel'
10
35
  import { ReferencePanel } from './reference-panel'
11
36
  import { RoofPanel } from './roof-panel'
12
37
  import { RoofSegmentPanel } from './roof-segment-panel'
13
38
  import { SlabPanel } from './slab-panel'
39
+ import { SpawnPanel } from './spawn-panel'
14
40
  import { StairPanel } from './stair-panel'
15
41
  import { StairSegmentPanel } from './stair-segment-panel'
16
42
  import { WallPanel } from './wall-panel'
17
43
  import { WindowPanel } from './window-panel'
18
44
 
45
+ type MovableNode =
46
+ | ItemNode
47
+ | WindowNode
48
+ | DoorNode
49
+ | CeilingNode
50
+ | ColumnNode
51
+ | SlabNode
52
+ | WallNode
53
+ | FenceNode
54
+ | RoofNode
55
+ | RoofSegmentNode
56
+ | StairNode
57
+ | StairSegmentNode
58
+ | BuildingNode
59
+
60
+ const MOVABLE_TYPES = new Set<string>([
61
+ 'item',
62
+ 'window',
63
+ 'door',
64
+ 'ceiling',
65
+ 'column',
66
+ 'slab',
67
+ 'wall',
68
+ 'fence',
69
+ 'roof',
70
+ 'roof-segment',
71
+ 'stair',
72
+ 'stair-segment',
73
+ 'building',
74
+ ])
75
+
76
+ function isMovableNode(node: AnyNode | null): node is MovableNode {
77
+ return !!node && MOVABLE_TYPES.has(node.type)
78
+ }
79
+
80
+ function panelForType(type: string | null) {
81
+ if (!type) return null
82
+ switch (type) {
83
+ case 'item':
84
+ return <ItemPanel />
85
+ case 'roof':
86
+ return <RoofPanel />
87
+ case 'roof-segment':
88
+ return <RoofSegmentPanel />
89
+ case 'stair':
90
+ return <StairPanel />
91
+ case 'stair-segment':
92
+ return <StairSegmentPanel />
93
+ case 'slab':
94
+ return <SlabPanel />
95
+ case 'ceiling':
96
+ return <CeilingPanel />
97
+ case 'column':
98
+ return <ColumnPanel />
99
+ case 'wall':
100
+ return <WallPanel />
101
+ case 'fence':
102
+ return <FencePanel />
103
+ case 'door':
104
+ return <DoorPanel />
105
+ case 'window':
106
+ return <WindowPanel />
107
+ default:
108
+ return null
109
+ }
110
+ }
111
+
112
+ function MobilePanelLayer({
113
+ node,
114
+ panel,
115
+ isReference,
116
+ }: {
117
+ node: AnyNode | null
118
+ panel: React.ReactNode
119
+ isReference: boolean
120
+ }) {
121
+ const setSelection = useViewer((s) => s.setSelection)
122
+ const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
123
+ const setMovingNode = useEditor((s) => s.setMovingNode)
124
+ const deleteNode = useScene((s) => s.deleteNode)
125
+ const [isSheetOpen, setIsSheetOpen] = useState(false)
126
+
127
+ // Reset sheet open state when the selection changes / clears
128
+ const selectionKey = node?.id ?? (isReference ? 'reference' : null)
129
+ useEffect(() => {
130
+ setIsSheetOpen(false)
131
+ }, [selectionKey])
132
+
133
+ const clearSelection = useCallback(() => {
134
+ setSelection({ selectedIds: [] })
135
+ setSelectedReferenceId(null)
136
+ }, [setSelection, setSelectedReferenceId])
137
+
138
+ const handleMove = useCallback(() => {
139
+ if (!isMovableNode(node)) return
140
+ sfxEmitter.emit('sfx:item-pick')
141
+ setMovingNode(node)
142
+ clearSelection()
143
+ }, [node, setMovingNode, clearSelection])
144
+
145
+ const handleDuplicate = useCallback(() => {
146
+ if (!isMovableNode(node)) return
147
+ sfxEmitter.emit('sfx:item-pick')
148
+ const cloned = structuredClone(node) as MovableNode & { id?: AnyNodeId }
149
+ delete (cloned as { id?: AnyNodeId }).id
150
+ const prevMeta =
151
+ cloned.metadata && typeof cloned.metadata === 'object' && !Array.isArray(cloned.metadata)
152
+ ? (cloned.metadata as Record<string, unknown>)
153
+ : {}
154
+ cloned.metadata = { ...prevMeta, isNew: true }
155
+ setMovingNode(cloned as MovableNode)
156
+ clearSelection()
157
+ }, [node, setMovingNode, clearSelection])
158
+
159
+ const handleDelete = useCallback(() => {
160
+ if (!node) return
161
+ sfxEmitter.emit('sfx:item-delete')
162
+ deleteNode(node.id)
163
+ clearSelection()
164
+ }, [node, deleteNode, clearSelection])
165
+
166
+ if (!(node || isReference)) return null
167
+
168
+ const display = getNodeDisplay(node)
169
+
170
+ return (
171
+ <>
172
+ {node && (
173
+ <MobileSelectionBar
174
+ node={node}
175
+ onDelete={handleDelete}
176
+ onDuplicate={handleDuplicate}
177
+ onEdit={() => setIsSheetOpen((v) => !v)}
178
+ onMove={handleMove}
179
+ />
180
+ )}
181
+ <MobilePanelSheet
182
+ icon={display.icon}
183
+ onClose={() => setIsSheetOpen(false)}
184
+ open={isSheetOpen}
185
+ title={display.label}
186
+ >
187
+ {panel}
188
+ </MobilePanelSheet>
189
+ </>
190
+ )
191
+ }
192
+
19
193
  export function PanelManager() {
194
+ const isMobile = useIsMobile()
20
195
  const selectedIds = useViewer((s) => s.selection.selectedIds)
21
196
  const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
22
- const nodes = useScene((s) => s.nodes)
197
+ const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
198
+ const mode = useEditor((s) => s.mode)
199
+ const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
200
+ // Only subscribe to the *type* of the single-selected node — string primitive
201
+ // so we don't re-render on unrelated scene mutations.
202
+ const selectedNodeType = useScene((s) => {
203
+ if (selectedIds.length !== 1) return null
204
+ const id = selectedIds[0]
205
+ return id ? (s.nodes[id as AnyNodeId]?.type ?? null) : null
206
+ })
207
+ const selectedNode = useScene((s) => {
208
+ if (selectedIds.length !== 1) return null
209
+ const id = selectedIds[0]
210
+ return id ? (s.nodes[id as AnyNodeId] ?? null) : null
211
+ })
212
+
213
+ if (isMobile) {
214
+ if (selectedReferenceId) {
215
+ return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
216
+ }
217
+ return (
218
+ <MobilePanelLayer
219
+ isReference={false}
220
+ node={selectedNode}
221
+ panel={panelForType(selectedNodeType)}
222
+ />
223
+ )
224
+ }
23
225
 
24
226
  // Show reference panel if a reference is selected
25
227
  if (selectedReferenceId) {
26
228
  return <ReferencePanel />
27
229
  }
28
230
 
231
+ if (
232
+ isPaintPanelOpen &&
233
+ mode === 'material-paint' &&
234
+ activePaintMaterial?.material?.properties &&
235
+ !activePaintMaterial.materialPreset
236
+ ) {
237
+ return <PaintPanel />
238
+ }
239
+
29
240
  // Show appropriate panel based on selected node type
30
- if (selectedIds.length === 1) {
31
- const selectedNode = selectedIds[0]
32
- const node = nodes[selectedNode as AnyNodeId]
33
- if (node) {
34
- switch (node.type) {
35
- case 'item':
36
- return <ItemPanel />
37
- case 'roof':
38
- return <RoofPanel />
39
- case 'roof-segment':
40
- return <RoofSegmentPanel />
41
- case 'stair':
42
- return <StairPanel />
43
- case 'stair-segment':
44
- return <StairSegmentPanel />
45
- case 'slab':
46
- return <SlabPanel />
47
- case 'ceiling':
48
- return <CeilingPanel />
49
- case 'wall':
50
- return <WallPanel />
51
- case 'fence':
52
- return <FencePanel />
53
- case 'door':
54
- return <DoorPanel />
55
- case 'window':
56
- return <WindowPanel />
57
- }
241
+ if (selectedNodeType) {
242
+ switch (selectedNodeType) {
243
+ case 'item':
244
+ return <ItemPanel />
245
+ case 'roof':
246
+ return <RoofPanel />
247
+ case 'roof-segment':
248
+ return <RoofSegmentPanel />
249
+ case 'stair':
250
+ return <StairPanel />
251
+ case 'stair-segment':
252
+ return <StairSegmentPanel />
253
+ case 'slab':
254
+ return <SlabPanel />
255
+ case 'spawn':
256
+ return <SpawnPanel />
257
+ case 'ceiling':
258
+ return <CeilingPanel />
259
+ case 'column':
260
+ return <ColumnPanel />
261
+ case 'wall':
262
+ return <WallPanel />
263
+ case 'fence':
264
+ return <FencePanel />
265
+ case 'door':
266
+ return <DoorPanel />
267
+ case 'window':
268
+ return <WindowPanel />
58
269
  }
59
270
  }
60
271
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { ChevronLeft, RotateCcw, X } from 'lucide-react'
4
4
  import Image from 'next/image'
5
+ import { useIsMobile } from '../../../hooks/use-mobile'
5
6
  import { cn } from '../../../lib/utils'
6
7
 
7
8
  interface PanelWrapperProps {
@@ -25,53 +26,61 @@ export function PanelWrapper({
25
26
  className,
26
27
  width = 320, // default width
27
28
  }: PanelWrapperProps) {
29
+ const isMobile = useIsMobile()
30
+
28
31
  return (
29
32
  <div
30
33
  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',
34
+ isMobile
35
+ ? 'flex h-full w-full flex-col overflow-hidden bg-transparent dark:text-foreground'
36
+ : '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
37
  className,
33
38
  )}
34
- style={{ width }}
39
+ style={isMobile ? undefined : { width }}
35
40
  >
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>
41
+ {/* Header — desktop only; mobile sheet provides its own header */}
42
+ {!isMobile && (
43
+ <div className="flex items-center justify-between border-border/50 border-b px-3 py-3">
44
+ <div className="flex items-center gap-2">
45
+ {onBack && (
46
+ <button
47
+ 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"
48
+ onClick={onBack}
49
+ type="button"
50
+ >
51
+ <ChevronLeft className="h-4 w-4" />
52
+ </button>
53
+ )}
54
+ {icon && (
55
+ <Image alt="" className="shrink-0 object-contain" height={16} src={icon} width={16} />
56
+ )}
57
+ <h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
58
+ {title}
59
+ </h2>
60
+ </div>
53
61
 
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
- )}
62
+ <div className="flex items-center gap-1">
63
+ {onReset && (
64
+ <button
65
+ 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"
66
+ onClick={onReset}
67
+ type="button"
68
+ >
69
+ <RotateCcw className="h-4 w-4" />
70
+ </button>
71
+ )}
72
+ {onClose && (
73
+ <button
74
+ 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"
75
+ onClick={onClose}
76
+ type="button"
77
+ >
78
+ <X className="h-4 w-4" />
79
+ </button>
80
+ )}
81
+ </div>
73
82
  </div>
74
- </div>
83
+ )}
75
84
 
76
85
  {/* Content */}
77
86
  <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>