@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.
- package/package.json +62 -0
- package/src/components/editor/custom-camera-controls.tsx +387 -0
- package/src/components/editor/editor-layout-v2.tsx +220 -0
- package/src/components/editor/export-manager.tsx +78 -0
- package/src/components/editor/first-person-controls.tsx +249 -0
- package/src/components/editor/floating-action-menu.tsx +231 -0
- package/src/components/editor/floorplan-panel.tsx +9609 -0
- package/src/components/editor/grid.tsx +161 -0
- package/src/components/editor/index.tsx +928 -0
- package/src/components/editor/node-action-menu.tsx +66 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
- package/src/components/editor/selection-manager.tsx +897 -0
- package/src/components/editor/site-edge-labels.tsx +90 -0
- package/src/components/editor/thumbnail-generator.tsx +166 -0
- package/src/components/editor/wall-measurement-label.tsx +258 -0
- package/src/components/feedback-dialog.tsx +265 -0
- package/src/components/pascal-radio.tsx +280 -0
- package/src/components/preview-button.tsx +16 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
- package/src/components/systems/roof/roof-edit-system.tsx +69 -0
- package/src/components/systems/stair/stair-edit-system.tsx +69 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
- package/src/components/systems/zone/zone-system.tsx +87 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
- package/src/components/tools/door/door-math.ts +110 -0
- package/src/components/tools/door/door-tool.tsx +293 -0
- package/src/components/tools/door/move-door-tool.tsx +373 -0
- package/src/components/tools/item/item-tool.tsx +26 -0
- package/src/components/tools/item/move-tool.tsx +90 -0
- package/src/components/tools/item/placement-math.ts +85 -0
- package/src/components/tools/item/placement-strategies.ts +556 -0
- package/src/components/tools/item/placement-types.ts +117 -0
- package/src/components/tools/item/use-draft-node.ts +227 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
- package/src/components/tools/roof/move-roof-tool.tsx +288 -0
- package/src/components/tools/roof/roof-tool.tsx +318 -0
- package/src/components/tools/select/box-select-tool.tsx +626 -0
- package/src/components/tools/shared/cursor-sphere.tsx +119 -0
- package/src/components/tools/shared/polygon-editor.tsx +361 -0
- package/src/components/tools/site/site-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
- package/src/components/tools/slab/slab-tool.tsx +322 -0
- package/src/components/tools/stair/stair-defaults.ts +7 -0
- package/src/components/tools/stair/stair-tool.tsx +194 -0
- package/src/components/tools/tool-manager.tsx +120 -0
- package/src/components/tools/wall/wall-drafting.ts +140 -0
- package/src/components/tools/wall/wall-tool.tsx +210 -0
- package/src/components/tools/window/move-window-tool.tsx +410 -0
- package/src/components/tools/window/window-math.ts +117 -0
- package/src/components/tools/window/window-tool.tsx +303 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
- package/src/components/tools/zone/zone-tool.tsx +364 -0
- package/src/components/ui/action-menu/action-button.tsx +59 -0
- package/src/components/ui/action-menu/camera-actions.tsx +74 -0
- package/src/components/ui/action-menu/control-modes.tsx +240 -0
- package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
- package/src/components/ui/action-menu/index.tsx +152 -0
- package/src/components/ui/action-menu/structure-tools.tsx +100 -0
- package/src/components/ui/action-menu/view-toggles.tsx +397 -0
- package/src/components/ui/command-palette/editor-commands.tsx +396 -0
- package/src/components/ui/command-palette/index.tsx +730 -0
- package/src/components/ui/controls/action-button.tsx +33 -0
- package/src/components/ui/controls/material-picker.tsx +194 -0
- package/src/components/ui/controls/metric-control.tsx +262 -0
- package/src/components/ui/controls/panel-section.tsx +65 -0
- package/src/components/ui/controls/segmented-control.tsx +45 -0
- package/src/components/ui/controls/slider-control.tsx +245 -0
- package/src/components/ui/controls/toggle-control.tsx +38 -0
- package/src/components/ui/floating-level-selector.tsx +355 -0
- package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
- package/src/components/ui/helpers/helper-manager.tsx +33 -0
- package/src/components/ui/helpers/item-helper.tsx +40 -0
- package/src/components/ui/helpers/roof-helper.tsx +16 -0
- package/src/components/ui/helpers/slab-helper.tsx +20 -0
- package/src/components/ui/helpers/wall-helper.tsx +20 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
- package/src/components/ui/panels/ceiling-panel.tsx +230 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
- package/src/components/ui/panels/door-panel.tsx +600 -0
- package/src/components/ui/panels/item-panel.tsx +306 -0
- package/src/components/ui/panels/panel-manager.tsx +59 -0
- package/src/components/ui/panels/panel-wrapper.tsx +80 -0
- package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
- package/src/components/ui/panels/reference-panel.tsx +177 -0
- package/src/components/ui/panels/roof-panel.tsx +262 -0
- package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
- package/src/components/ui/panels/slab-panel.tsx +228 -0
- package/src/components/ui/panels/stair-panel.tsx +304 -0
- package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
- package/src/components/ui/panels/wall-panel.tsx +123 -0
- package/src/components/ui/panels/window-panel.tsx +441 -0
- package/src/components/ui/primitives/button.tsx +69 -0
- package/src/components/ui/primitives/card.tsx +75 -0
- package/src/components/ui/primitives/color-dot.tsx +61 -0
- package/src/components/ui/primitives/context-menu.tsx +227 -0
- package/src/components/ui/primitives/dialog.tsx +129 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
- package/src/components/ui/primitives/error-boundary.tsx +52 -0
- package/src/components/ui/primitives/input.tsx +21 -0
- package/src/components/ui/primitives/number-input.tsx +187 -0
- package/src/components/ui/primitives/opacity-control.tsx +79 -0
- package/src/components/ui/primitives/popover.tsx +42 -0
- package/src/components/ui/primitives/separator.tsx +28 -0
- package/src/components/ui/primitives/sheet.tsx +130 -0
- package/src/components/ui/primitives/shortcut-token.tsx +64 -0
- package/src/components/ui/primitives/sidebar.tsx +855 -0
- package/src/components/ui/primitives/skeleton.tsx +13 -0
- package/src/components/ui/primitives/slider.tsx +58 -0
- package/src/components/ui/primitives/switch.tsx +29 -0
- package/src/components/ui/primitives/tooltip.tsx +57 -0
- package/src/components/ui/scene-loader.tsx +40 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
- package/src/components/ui/sidebar/icon-rail.tsx +147 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
- package/src/components/ui/sidebar/tab-bar.tsx +39 -0
- package/src/components/ui/slider-demo.tsx +36 -0
- package/src/components/ui/slider.tsx +81 -0
- package/src/components/ui/viewer-toolbar.tsx +342 -0
- package/src/components/viewer-overlay.tsx +499 -0
- package/src/components/viewer-zone-system.tsx +48 -0
- package/src/contexts/presets-context.tsx +121 -0
- package/src/hooks/use-auto-save.ts +194 -0
- package/src/hooks/use-contextual-tools.ts +52 -0
- package/src/hooks/use-grid-events.ts +106 -0
- package/src/hooks/use-keyboard.ts +214 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-reduced-motion.ts +20 -0
- package/src/index.tsx +33 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/level-selection.ts +31 -0
- package/src/lib/scene.ts +394 -0
- package/src/lib/sfx/index.ts +2 -0
- package/src/lib/sfx-bus.ts +49 -0
- package/src/lib/sfx-player.ts +60 -0
- package/src/lib/utils.ts +43 -0
- package/src/store/use-audio.tsx +45 -0
- package/src/store/use-command-registry.ts +36 -0
- package/src/store/use-editor.tsx +522 -0
- package/src/store/use-palette-view-registry.ts +45 -0
- package/src/store/use-upload.ts +90 -0
- package/src/three-types.ts +3 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AnyNodeId, LevelNode } from '@pascal-app/core'
|
|
4
|
+
import { useScene } from '@pascal-app/core'
|
|
5
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
6
|
+
import { Command, useCommandState } from 'cmdk'
|
|
7
|
+
import { ChevronRight, Search } from 'lucide-react'
|
|
8
|
+
import type { ReactNode } from 'react'
|
|
9
|
+
import { useEffect, useState } from 'react'
|
|
10
|
+
import { create } from 'zustand'
|
|
11
|
+
import { useShallow } from 'zustand/shallow'
|
|
12
|
+
import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
|
|
13
|
+
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
14
|
+
import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Open + navigation state store
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
interface CommandPaletteStore {
|
|
20
|
+
open: boolean
|
|
21
|
+
setOpen: (open: boolean) => void
|
|
22
|
+
/** Current rendering mode. 'command' = normal palette; anything else = registered mode view. */
|
|
23
|
+
mode: string
|
|
24
|
+
setMode: (mode: string) => void
|
|
25
|
+
pages: string[]
|
|
26
|
+
inputValue: string
|
|
27
|
+
setInputValue: (value: string) => void
|
|
28
|
+
navigateTo: (page: string) => void
|
|
29
|
+
goBack: () => void
|
|
30
|
+
cameraScope: { nodeId: string; label: string } | null
|
|
31
|
+
setCameraScope: (scope: { nodeId: string; label: string } | null) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
|
35
|
+
open: false,
|
|
36
|
+
setOpen: (open) => {
|
|
37
|
+
set({ open })
|
|
38
|
+
if (!open) set({ pages: [], inputValue: '', cameraScope: null, mode: 'command' })
|
|
39
|
+
},
|
|
40
|
+
mode: 'command',
|
|
41
|
+
setMode: (mode) => set({ mode }),
|
|
42
|
+
pages: [],
|
|
43
|
+
inputValue: '',
|
|
44
|
+
setInputValue: (value) => set({ inputValue: value }),
|
|
45
|
+
navigateTo: (page) => set((s) => ({ pages: [...s.pages, page], inputValue: '' })),
|
|
46
|
+
goBack: () => {
|
|
47
|
+
const { pages } = get()
|
|
48
|
+
if (pages[pages.length - 1] === 'camera-scope') set({ cameraScope: null })
|
|
49
|
+
set((s) => ({ pages: s.pages.slice(0, -1), inputValue: '' }))
|
|
50
|
+
},
|
|
51
|
+
cameraScope: null,
|
|
52
|
+
setCameraScope: (scope) => set({ cameraScope: scope }),
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Helpers
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function resolve(value: string | (() => string)): string {
|
|
59
|
+
return typeof value === 'function' ? value() : value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Shortcut({ keys }: { keys: string[] }) {
|
|
63
|
+
return (
|
|
64
|
+
<span className="ml-auto flex shrink-0 items-center gap-0.5">
|
|
65
|
+
{keys.map((k) => (
|
|
66
|
+
<kbd
|
|
67
|
+
className="flex min-w-4.5 items-center justify-center rounded border border-border/60 bg-muted/60 px-1 py-0.5 text-[10px] text-muted-foreground leading-none"
|
|
68
|
+
key={k}
|
|
69
|
+
>
|
|
70
|
+
{k}
|
|
71
|
+
</kbd>
|
|
72
|
+
))}
|
|
73
|
+
</span>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Item({
|
|
78
|
+
icon,
|
|
79
|
+
label,
|
|
80
|
+
onSelect,
|
|
81
|
+
shortcut,
|
|
82
|
+
disabled = false,
|
|
83
|
+
keywords = [],
|
|
84
|
+
badge,
|
|
85
|
+
navigate = false,
|
|
86
|
+
}: {
|
|
87
|
+
icon: React.ReactNode
|
|
88
|
+
label: string | (() => string)
|
|
89
|
+
onSelect: () => void
|
|
90
|
+
shortcut?: string[]
|
|
91
|
+
disabled?: boolean
|
|
92
|
+
keywords?: string[]
|
|
93
|
+
badge?: string | (() => string)
|
|
94
|
+
navigate?: boolean
|
|
95
|
+
}) {
|
|
96
|
+
const resolvedLabel = resolve(label)
|
|
97
|
+
const resolvedBadge = badge ? resolve(badge) : undefined
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Command.Item
|
|
101
|
+
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-2 text-foreground text-sm transition-colors data-[disabled=true]:cursor-not-allowed data-[selected=true]:bg-accent data-[disabled=true]:opacity-40"
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
keywords={keywords}
|
|
104
|
+
onSelect={onSelect}
|
|
105
|
+
value={resolvedLabel}
|
|
106
|
+
>
|
|
107
|
+
<span className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
|
|
108
|
+
{icon}
|
|
109
|
+
</span>
|
|
110
|
+
<span className="flex-1 truncate">{resolvedLabel}</span>
|
|
111
|
+
{resolvedBadge && (
|
|
112
|
+
<span className="rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">
|
|
113
|
+
{resolvedBadge}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
{shortcut && <Shortcut keys={shortcut} />}
|
|
117
|
+
{(resolvedBadge || navigate) && (
|
|
118
|
+
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
119
|
+
)}
|
|
120
|
+
</Command.Item>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function OptionItem({
|
|
125
|
+
label,
|
|
126
|
+
isActive = false,
|
|
127
|
+
onSelect,
|
|
128
|
+
icon,
|
|
129
|
+
disabled = false,
|
|
130
|
+
}: {
|
|
131
|
+
label: string
|
|
132
|
+
isActive?: boolean
|
|
133
|
+
onSelect: () => void
|
|
134
|
+
icon?: React.ReactNode
|
|
135
|
+
disabled?: boolean
|
|
136
|
+
}) {
|
|
137
|
+
return (
|
|
138
|
+
<Command.Item
|
|
139
|
+
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-2 text-foreground text-sm transition-colors data-[disabled=true]:cursor-not-allowed data-[selected=true]:bg-accent data-[disabled=true]:opacity-40"
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
onSelect={onSelect}
|
|
142
|
+
value={label}
|
|
143
|
+
>
|
|
144
|
+
<span className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
|
|
145
|
+
{isActive ? <div className="h-1.5 w-1.5 rounded-full bg-primary" /> : icon}
|
|
146
|
+
</span>
|
|
147
|
+
<span className="flex-1 truncate">{label}</span>
|
|
148
|
+
</Command.Item>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Sub-page label map
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
const PAGE_LABEL: Record<string, string> = {
|
|
156
|
+
'wall-mode': 'Wall Mode',
|
|
157
|
+
'level-mode': 'Level Mode',
|
|
158
|
+
'rename-level': 'Rename Level',
|
|
159
|
+
'goto-level': 'Go to Level',
|
|
160
|
+
'camera-view': 'Camera Snapshot',
|
|
161
|
+
'camera-scope': '',
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Empty state fallback (force-mounted, visible only when no results)
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
export interface CommandPaletteEmptyAction {
|
|
168
|
+
icon: ReactNode
|
|
169
|
+
label: (query: string) => string
|
|
170
|
+
onSelect: (query: string) => void
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
|
|
174
|
+
const count = useCommandState((s) => s.filtered.count)
|
|
175
|
+
const search = useCommandState((s) => s.search)
|
|
176
|
+
if (count > 0) return null
|
|
177
|
+
// No Command.Group wrapper — groups hide themselves when not in filtered.groups (which is
|
|
178
|
+
// empty when nothing matches), swallowing the force-mounted item even with forceMount on
|
|
179
|
+
// the item itself.
|
|
180
|
+
return (
|
|
181
|
+
<Command.Item
|
|
182
|
+
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-2 text-foreground text-sm transition-colors data-[selected=true]:bg-accent"
|
|
183
|
+
forceMount
|
|
184
|
+
onSelect={() => action.onSelect(search)}
|
|
185
|
+
value="__empty_action__"
|
|
186
|
+
>
|
|
187
|
+
<span className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
|
|
188
|
+
{action.icon}
|
|
189
|
+
</span>
|
|
190
|
+
<span className="flex-1 truncate">{action.label(search)}</span>
|
|
191
|
+
</Command.Item>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Main component
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
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()
|
|
212
|
+
|
|
213
|
+
const [meta, setMeta] = useState('⌘')
|
|
214
|
+
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
215
|
+
|
|
216
|
+
const page = pages[pages.length - 1]
|
|
217
|
+
|
|
218
|
+
const actions = useCommandRegistry((s) => s.actions)
|
|
219
|
+
const views = usePaletteViewRegistry((s) => s.views)
|
|
220
|
+
|
|
221
|
+
const activeLevelId = useViewer((s) => s.selection.levelId)
|
|
222
|
+
const activeLevelNode = useScene((s) => (activeLevelId ? s.nodes[activeLevelId] : null))
|
|
223
|
+
|
|
224
|
+
const wallMode = useViewer((s) => s.wallMode)
|
|
225
|
+
const setWallMode = useViewer((s) => s.setWallMode)
|
|
226
|
+
const levelMode = useViewer((s) => s.levelMode)
|
|
227
|
+
const setLevelMode = useViewer((s) => s.setLevelMode)
|
|
228
|
+
|
|
229
|
+
const allLevels = useScene(
|
|
230
|
+
useShallow((s) =>
|
|
231
|
+
(Object.values(s.nodes).filter((n) => n.type === 'level') as LevelNode[]).sort(
|
|
232
|
+
(a, b) => a.level - b.level,
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const cameraScopeNode = useScene((s) =>
|
|
238
|
+
cameraScope ? s.nodes[cameraScope.nodeId as AnyNodeId] : null,
|
|
239
|
+
)
|
|
240
|
+
const hasScopeSnapshot = !!(cameraScopeNode as any)?.camera
|
|
241
|
+
|
|
242
|
+
// Platform detection
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? '⌘' : 'Ctrl')
|
|
245
|
+
}, [])
|
|
246
|
+
|
|
247
|
+
// Fullscreen tracking
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
const handler = () => setIsFullscreen(!!document.fullscreenElement)
|
|
250
|
+
document.addEventListener('fullscreenchange', handler)
|
|
251
|
+
return () => document.removeEventListener('fullscreenchange', handler)
|
|
252
|
+
}, [])
|
|
253
|
+
|
|
254
|
+
// Cmd/Ctrl+K global shortcut
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const handler = (e: KeyboardEvent) => {
|
|
257
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
258
|
+
e.preventDefault()
|
|
259
|
+
setOpen(true)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
window.addEventListener('keydown', handler)
|
|
263
|
+
return () => window.removeEventListener('keydown', handler)
|
|
264
|
+
}, [setOpen])
|
|
265
|
+
|
|
266
|
+
const run = (fn: () => void) => {
|
|
267
|
+
fn()
|
|
268
|
+
setOpen(false)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const wallModeLabel: Record<'cutaway' | 'up' | 'down', string> = {
|
|
272
|
+
cutaway: 'Cutaway',
|
|
273
|
+
up: 'Up',
|
|
274
|
+
down: 'Down',
|
|
275
|
+
}
|
|
276
|
+
const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
|
|
277
|
+
manual: 'Manual',
|
|
278
|
+
stacked: 'Stacked',
|
|
279
|
+
exploded: 'Exploded',
|
|
280
|
+
solo: 'Solo',
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Camera snapshot helpers (used by sub-pages registered via EditorCommands)
|
|
284
|
+
const confirmRename = () => {
|
|
285
|
+
if (!(activeLevelId && inputValue.trim())) return
|
|
286
|
+
run(() => {
|
|
287
|
+
useScene.getState().updateNode(activeLevelId as AnyNodeId, { name: inputValue.trim() } as any)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const takeSnapshot = () => {
|
|
292
|
+
if (!cameraScope) return
|
|
293
|
+
import('@pascal-app/core').then(({ emitter }) => {
|
|
294
|
+
run(() =>
|
|
295
|
+
emitter.emit('camera-controls:capture', { nodeId: cameraScope.nodeId as AnyNodeId }),
|
|
296
|
+
)
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const viewSnapshot = () => {
|
|
301
|
+
if (!(cameraScope && hasScopeSnapshot)) return
|
|
302
|
+
import('@pascal-app/core').then(({ emitter }) => {
|
|
303
|
+
run(() => emitter.emit('camera-controls:view', { nodeId: cameraScope.nodeId as AnyNodeId }))
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const clearSnapshot = () => {
|
|
308
|
+
if (!(cameraScope && hasScopeSnapshot)) return
|
|
309
|
+
run(() => {
|
|
310
|
+
useScene.getState().updateNode(cameraScope.nodeId as AnyNodeId, { camera: undefined } as any)
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Group registered actions by group (preserving insertion order)
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
const grouped = actions.reduce<Map<string, typeof actions>>((acc, action) => {
|
|
318
|
+
const list = acc.get(action.group) ?? []
|
|
319
|
+
list.push(action)
|
|
320
|
+
acc.set(action.group, list)
|
|
321
|
+
return acc
|
|
322
|
+
}, new Map())
|
|
323
|
+
|
|
324
|
+
const onClose = () => setOpen(false)
|
|
325
|
+
const onBack = () => {
|
|
326
|
+
if (mode !== 'command') {
|
|
327
|
+
setMode('command')
|
|
328
|
+
} else {
|
|
329
|
+
goBack()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Render
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
// Mode view: replaces the entire cmdk shell
|
|
338
|
+
const modeView = mode !== 'command' ? views.get(mode) : undefined
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<Dialog onOpenChange={setOpen} open={open}>
|
|
342
|
+
<DialogContent className="max-w-lg gap-0 overflow-hidden p-0" showCloseButton={false}>
|
|
343
|
+
<DialogTitle className="sr-only">Command Palette</DialogTitle>
|
|
344
|
+
|
|
345
|
+
{modeView && <modeView.Component onBack={onBack} onClose={onClose} />}
|
|
346
|
+
|
|
347
|
+
{!modeView && (
|
|
348
|
+
<Command
|
|
349
|
+
className="**:[[cmdk-group-heading]]:px-2.5 **:[[cmdk-group-heading]]:pt-3 **:[[cmdk-group-heading]]:pb-1 **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:text-[10px] **:[[cmdk-group-heading]]:text-muted-foreground **:[[cmdk-group-heading]]:uppercase **:[[cmdk-group-heading]]:tracking-wider"
|
|
350
|
+
onKeyDown={(e) => {
|
|
351
|
+
if (e.key === 'Backspace' && !inputValue && pages.length > 0) {
|
|
352
|
+
e.preventDefault()
|
|
353
|
+
goBack()
|
|
354
|
+
}
|
|
355
|
+
}}
|
|
356
|
+
shouldFilter={page !== 'rename-level'}
|
|
357
|
+
>
|
|
358
|
+
{/* Search bar */}
|
|
359
|
+
<div className="flex items-center border-border/50 border-b px-3">
|
|
360
|
+
<Search className="mr-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
361
|
+
{page && (
|
|
362
|
+
<button
|
|
363
|
+
className="mr-2 shrink-0 rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-muted/70"
|
|
364
|
+
onClick={goBack}
|
|
365
|
+
type="button"
|
|
366
|
+
>
|
|
367
|
+
{page === 'camera-scope'
|
|
368
|
+
? (cameraScope?.label ?? 'Snapshot')
|
|
369
|
+
: (PAGE_LABEL[page] ?? views.get(page)?.label ?? page)}
|
|
370
|
+
</button>
|
|
371
|
+
)}
|
|
372
|
+
<Command.Input
|
|
373
|
+
autoFocus
|
|
374
|
+
className="flex h-12 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
375
|
+
onValueChange={setInputValue}
|
|
376
|
+
placeholder={
|
|
377
|
+
page === 'rename-level'
|
|
378
|
+
? 'Type a new name…'
|
|
379
|
+
: page
|
|
380
|
+
? 'Filter options…'
|
|
381
|
+
: 'Search actions…'
|
|
382
|
+
}
|
|
383
|
+
value={inputValue}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<Command.List className="max-h-100 overflow-y-auto p-1.5">
|
|
388
|
+
{(!emptyAction || page) && (
|
|
389
|
+
<Command.Empty className="py-8 text-center text-muted-foreground text-sm">
|
|
390
|
+
No commands found.
|
|
391
|
+
</Command.Empty>
|
|
392
|
+
)}
|
|
393
|
+
{emptyAction && !page && <EmptyActionItem action={emptyAction} />}
|
|
394
|
+
|
|
395
|
+
{/* ── Registered page view (e.g. 'ai') ─────────────────────── */}
|
|
396
|
+
{page &&
|
|
397
|
+
views.get(page)?.type === 'page' &&
|
|
398
|
+
(() => {
|
|
399
|
+
const pageView = views.get(page)
|
|
400
|
+
return pageView ? <pageView.Component onBack={onBack} onClose={onClose} /> : null
|
|
401
|
+
})()}
|
|
402
|
+
|
|
403
|
+
{/* ── Root view: render from registry ───────────────────────── */}
|
|
404
|
+
{!page &&
|
|
405
|
+
Array.from(grouped.entries()).map(([group, groupActions]) => (
|
|
406
|
+
<Command.Group heading={group} key={group}>
|
|
407
|
+
{groupActions.map((action) => (
|
|
408
|
+
<Item
|
|
409
|
+
badge={action.badge}
|
|
410
|
+
disabled={action.when ? !action.when() : false}
|
|
411
|
+
icon={action.icon}
|
|
412
|
+
key={action.id}
|
|
413
|
+
keywords={action.keywords}
|
|
414
|
+
label={action.label}
|
|
415
|
+
navigate={action.navigate}
|
|
416
|
+
onSelect={() => action.execute()}
|
|
417
|
+
shortcut={action.shortcut}
|
|
418
|
+
/>
|
|
419
|
+
))}
|
|
420
|
+
</Command.Group>
|
|
421
|
+
))}
|
|
422
|
+
|
|
423
|
+
{/* ── Wall Mode sub-page ────────────────────────────────────── */}
|
|
424
|
+
{page === 'wall-mode' && (
|
|
425
|
+
<Command.Group heading="Wall Mode">
|
|
426
|
+
{(['cutaway', 'up', 'down'] as const).map((mode) => (
|
|
427
|
+
<OptionItem
|
|
428
|
+
isActive={wallMode === mode}
|
|
429
|
+
key={mode}
|
|
430
|
+
label={wallModeLabel[mode]}
|
|
431
|
+
onSelect={() => run(() => setWallMode(mode))}
|
|
432
|
+
/>
|
|
433
|
+
))}
|
|
434
|
+
</Command.Group>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
{/* ── Level Mode sub-page ───────────────────────────────────── */}
|
|
438
|
+
{page === 'level-mode' && (
|
|
439
|
+
<Command.Group heading="Level Mode">
|
|
440
|
+
{(['stacked', 'exploded', 'solo'] as const).map((mode) => (
|
|
441
|
+
<OptionItem
|
|
442
|
+
isActive={levelMode === mode}
|
|
443
|
+
key={mode}
|
|
444
|
+
label={levelModeLabel[mode]}
|
|
445
|
+
onSelect={() => run(() => setLevelMode(mode))}
|
|
446
|
+
/>
|
|
447
|
+
))}
|
|
448
|
+
</Command.Group>
|
|
449
|
+
)}
|
|
450
|
+
|
|
451
|
+
{/* ── Go to Level sub-page ──────────────────────────────────── */}
|
|
452
|
+
{page === 'goto-level' && (
|
|
453
|
+
<Command.Group heading="Go to Level">
|
|
454
|
+
{allLevels.map((level) => (
|
|
455
|
+
<OptionItem
|
|
456
|
+
isActive={level.id === activeLevelId}
|
|
457
|
+
key={level.id}
|
|
458
|
+
label={level.name ?? `Level ${level.level}`}
|
|
459
|
+
onSelect={() =>
|
|
460
|
+
run(() => useViewer.getState().setSelection({ levelId: level.id }))
|
|
461
|
+
}
|
|
462
|
+
/>
|
|
463
|
+
))}
|
|
464
|
+
</Command.Group>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{/* ── Rename Level sub-page ─────────────────────────────────── */}
|
|
468
|
+
{page === 'rename-level' && (
|
|
469
|
+
<Command.Group heading="Rename Level">
|
|
470
|
+
<Command.Item
|
|
471
|
+
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-2 text-foreground text-sm transition-colors data-[disabled=true]:cursor-not-allowed data-[selected=true]:bg-accent data-[disabled=true]:opacity-40"
|
|
472
|
+
disabled={!inputValue.trim()}
|
|
473
|
+
onSelect={confirmRename}
|
|
474
|
+
value="confirm-rename"
|
|
475
|
+
>
|
|
476
|
+
<span className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
|
|
477
|
+
<svg
|
|
478
|
+
className="h-4 w-4"
|
|
479
|
+
fill="none"
|
|
480
|
+
stroke="currentColor"
|
|
481
|
+
strokeWidth={2}
|
|
482
|
+
viewBox="0 0 24 24"
|
|
483
|
+
>
|
|
484
|
+
<path d="M12 20h9" strokeLinecap="round" strokeLinejoin="round" />
|
|
485
|
+
<path
|
|
486
|
+
d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"
|
|
487
|
+
strokeLinecap="round"
|
|
488
|
+
strokeLinejoin="round"
|
|
489
|
+
/>
|
|
490
|
+
</svg>
|
|
491
|
+
</span>
|
|
492
|
+
<span className="flex-1 truncate">
|
|
493
|
+
{inputValue.trim() ? (
|
|
494
|
+
<>
|
|
495
|
+
Rename to <span className="font-medium">"{inputValue.trim()}"</span>
|
|
496
|
+
</>
|
|
497
|
+
) : (
|
|
498
|
+
<span className="text-muted-foreground">Type a new name above…</span>
|
|
499
|
+
)}
|
|
500
|
+
</span>
|
|
501
|
+
</Command.Item>
|
|
502
|
+
</Command.Group>
|
|
503
|
+
)}
|
|
504
|
+
|
|
505
|
+
{/* ── Camera Snapshot: scope picker ─────────────────────────── */}
|
|
506
|
+
{page === 'camera-view' && (
|
|
507
|
+
<Command.Group heading="Camera Snapshot — Select Scope">
|
|
508
|
+
<OptionItem
|
|
509
|
+
icon={
|
|
510
|
+
<svg
|
|
511
|
+
className="h-4 w-4"
|
|
512
|
+
fill="none"
|
|
513
|
+
stroke="currentColor"
|
|
514
|
+
strokeWidth={2}
|
|
515
|
+
viewBox="0 0 24 24"
|
|
516
|
+
>
|
|
517
|
+
<path d="M3 3h18v18H3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
518
|
+
<path d="M3 9h18M9 21V9" strokeLinecap="round" strokeLinejoin="round" />
|
|
519
|
+
</svg>
|
|
520
|
+
}
|
|
521
|
+
label="Site"
|
|
522
|
+
onSelect={() => {
|
|
523
|
+
const { rootNodeIds } = useScene.getState()
|
|
524
|
+
const siteId = rootNodeIds[0]
|
|
525
|
+
if (siteId) {
|
|
526
|
+
setCameraScope({ nodeId: siteId, label: 'Site' })
|
|
527
|
+
navigateTo('camera-scope')
|
|
528
|
+
}
|
|
529
|
+
}}
|
|
530
|
+
/>
|
|
531
|
+
<OptionItem
|
|
532
|
+
icon={
|
|
533
|
+
<svg
|
|
534
|
+
className="h-4 w-4"
|
|
535
|
+
fill="none"
|
|
536
|
+
stroke="currentColor"
|
|
537
|
+
strokeWidth={2}
|
|
538
|
+
viewBox="0 0 24 24"
|
|
539
|
+
>
|
|
540
|
+
<path
|
|
541
|
+
d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
|
|
542
|
+
strokeLinecap="round"
|
|
543
|
+
strokeLinejoin="round"
|
|
544
|
+
/>
|
|
545
|
+
<polyline
|
|
546
|
+
points="9 22 9 12 15 12 15 22"
|
|
547
|
+
strokeLinecap="round"
|
|
548
|
+
strokeLinejoin="round"
|
|
549
|
+
/>
|
|
550
|
+
</svg>
|
|
551
|
+
}
|
|
552
|
+
label="Building"
|
|
553
|
+
onSelect={() => {
|
|
554
|
+
const building = Object.values(useScene.getState().nodes).find(
|
|
555
|
+
(n) => n.type === 'building',
|
|
556
|
+
)
|
|
557
|
+
if (building) {
|
|
558
|
+
setCameraScope({ nodeId: building.id, label: 'Building' })
|
|
559
|
+
navigateTo('camera-scope')
|
|
560
|
+
}
|
|
561
|
+
}}
|
|
562
|
+
/>
|
|
563
|
+
<OptionItem
|
|
564
|
+
disabled={!activeLevelId}
|
|
565
|
+
icon={
|
|
566
|
+
<svg
|
|
567
|
+
className="h-4 w-4"
|
|
568
|
+
fill="none"
|
|
569
|
+
stroke="currentColor"
|
|
570
|
+
strokeWidth={2}
|
|
571
|
+
viewBox="0 0 24 24"
|
|
572
|
+
>
|
|
573
|
+
<path
|
|
574
|
+
d="M12 2L2 7l10 5 10-5-10-5z"
|
|
575
|
+
strokeLinecap="round"
|
|
576
|
+
strokeLinejoin="round"
|
|
577
|
+
/>
|
|
578
|
+
<path
|
|
579
|
+
d="M2 17l10 5 10-5M2 12l10 5 10-5"
|
|
580
|
+
strokeLinecap="round"
|
|
581
|
+
strokeLinejoin="round"
|
|
582
|
+
/>
|
|
583
|
+
</svg>
|
|
584
|
+
}
|
|
585
|
+
label="Level"
|
|
586
|
+
onSelect={() => {
|
|
587
|
+
if (activeLevelId) {
|
|
588
|
+
setCameraScope({ nodeId: activeLevelId, label: 'Level' })
|
|
589
|
+
navigateTo('camera-scope')
|
|
590
|
+
}
|
|
591
|
+
}}
|
|
592
|
+
/>
|
|
593
|
+
<OptionItem
|
|
594
|
+
disabled={!useViewer.getState().selection.selectedIds.length}
|
|
595
|
+
icon={
|
|
596
|
+
<svg
|
|
597
|
+
className="h-4 w-4"
|
|
598
|
+
fill="none"
|
|
599
|
+
stroke="currentColor"
|
|
600
|
+
strokeWidth={2}
|
|
601
|
+
viewBox="0 0 24 24"
|
|
602
|
+
>
|
|
603
|
+
<path d="M5 3l14 9-14 9V3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
604
|
+
</svg>
|
|
605
|
+
}
|
|
606
|
+
label="Selection"
|
|
607
|
+
onSelect={() => {
|
|
608
|
+
const firstId = useViewer.getState().selection.selectedIds[0]
|
|
609
|
+
if (firstId) {
|
|
610
|
+
setCameraScope({ nodeId: firstId, label: 'Selection' })
|
|
611
|
+
navigateTo('camera-scope')
|
|
612
|
+
}
|
|
613
|
+
}}
|
|
614
|
+
/>
|
|
615
|
+
</Command.Group>
|
|
616
|
+
)}
|
|
617
|
+
|
|
618
|
+
{/* ── Camera Snapshot: actions for selected scope ───────────── */}
|
|
619
|
+
{page === 'camera-scope' && cameraScope && (
|
|
620
|
+
<Command.Group heading={`${cameraScope.label} Snapshot`}>
|
|
621
|
+
<OptionItem
|
|
622
|
+
icon={
|
|
623
|
+
<svg
|
|
624
|
+
className="h-4 w-4"
|
|
625
|
+
fill="none"
|
|
626
|
+
stroke="currentColor"
|
|
627
|
+
strokeWidth={2}
|
|
628
|
+
viewBox="0 0 24 24"
|
|
629
|
+
>
|
|
630
|
+
<path
|
|
631
|
+
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"
|
|
632
|
+
strokeLinecap="round"
|
|
633
|
+
strokeLinejoin="round"
|
|
634
|
+
/>
|
|
635
|
+
<circle
|
|
636
|
+
cx="12"
|
|
637
|
+
cy="13"
|
|
638
|
+
r="4"
|
|
639
|
+
strokeLinecap="round"
|
|
640
|
+
strokeLinejoin="round"
|
|
641
|
+
/>
|
|
642
|
+
</svg>
|
|
643
|
+
}
|
|
644
|
+
label={hasScopeSnapshot ? 'Update Snapshot' : 'Take Snapshot'}
|
|
645
|
+
onSelect={takeSnapshot}
|
|
646
|
+
/>
|
|
647
|
+
{hasScopeSnapshot && (
|
|
648
|
+
<OptionItem
|
|
649
|
+
icon={
|
|
650
|
+
<svg
|
|
651
|
+
className="h-4 w-4"
|
|
652
|
+
fill="none"
|
|
653
|
+
stroke="currentColor"
|
|
654
|
+
strokeWidth={2}
|
|
655
|
+
viewBox="0 0 24 24"
|
|
656
|
+
>
|
|
657
|
+
<path
|
|
658
|
+
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
659
|
+
strokeLinecap="round"
|
|
660
|
+
strokeLinejoin="round"
|
|
661
|
+
/>
|
|
662
|
+
<circle
|
|
663
|
+
cx="12"
|
|
664
|
+
cy="12"
|
|
665
|
+
r="3"
|
|
666
|
+
strokeLinecap="round"
|
|
667
|
+
strokeLinejoin="round"
|
|
668
|
+
/>
|
|
669
|
+
</svg>
|
|
670
|
+
}
|
|
671
|
+
label="View Snapshot"
|
|
672
|
+
onSelect={viewSnapshot}
|
|
673
|
+
/>
|
|
674
|
+
)}
|
|
675
|
+
{hasScopeSnapshot && (
|
|
676
|
+
<OptionItem
|
|
677
|
+
icon={
|
|
678
|
+
<svg
|
|
679
|
+
className="h-4 w-4"
|
|
680
|
+
fill="none"
|
|
681
|
+
stroke="currentColor"
|
|
682
|
+
strokeWidth={2}
|
|
683
|
+
viewBox="0 0 24 24"
|
|
684
|
+
>
|
|
685
|
+
<polyline
|
|
686
|
+
points="3 6 5 6 21 6"
|
|
687
|
+
strokeLinecap="round"
|
|
688
|
+
strokeLinejoin="round"
|
|
689
|
+
/>
|
|
690
|
+
<path
|
|
691
|
+
d="M19 6l-1 14H6L5 6"
|
|
692
|
+
strokeLinecap="round"
|
|
693
|
+
strokeLinejoin="round"
|
|
694
|
+
/>
|
|
695
|
+
<path d="M10 11v6M14 11v6" strokeLinecap="round" strokeLinejoin="round" />
|
|
696
|
+
<path d="M9 6V4h6v2" strokeLinecap="round" strokeLinejoin="round" />
|
|
697
|
+
</svg>
|
|
698
|
+
}
|
|
699
|
+
label="Clear Snapshot"
|
|
700
|
+
onSelect={clearSnapshot}
|
|
701
|
+
/>
|
|
702
|
+
)}
|
|
703
|
+
</Command.Group>
|
|
704
|
+
)}
|
|
705
|
+
</Command.List>
|
|
706
|
+
|
|
707
|
+
{/* Footer hint */}
|
|
708
|
+
<div className="flex items-center justify-between border-border/50 border-t px-3 py-2">
|
|
709
|
+
<span className="text-[11px] text-muted-foreground">
|
|
710
|
+
<Shortcut keys={['↑', '↓']} /> navigate
|
|
711
|
+
</span>
|
|
712
|
+
<span className="text-[11px] text-muted-foreground">
|
|
713
|
+
<Shortcut keys={['↵']} /> select
|
|
714
|
+
</span>
|
|
715
|
+
{page ? (
|
|
716
|
+
<span className="text-[11px] text-muted-foreground">
|
|
717
|
+
<Shortcut keys={['⌫']} /> back
|
|
718
|
+
</span>
|
|
719
|
+
) : (
|
|
720
|
+
<span className="text-[11px] text-muted-foreground">
|
|
721
|
+
<Shortcut keys={['Esc']} /> close
|
|
722
|
+
</span>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
</Command>
|
|
726
|
+
)}
|
|
727
|
+
</DialogContent>
|
|
728
|
+
</Dialog>
|
|
729
|
+
)
|
|
730
|
+
}
|