@pascal-app/editor 1.1.0 → 1.1.1
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 +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +11 -11
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1981
- package/src/components/editor/floating-action-menu.tsx +1 -1
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +6 -2
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +26 -22
- package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +24 -14
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/room-controls.tsx +7 -4
- package/src/components/editor/room-handles-2d.tsx +2 -3
- package/src/components/editor/room-handles.tsx +1 -2
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +40 -1671
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +1 -1
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
- package/src/components/systems/roof/roof-edit-system.tsx +4 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +1 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
- package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +1 -1
- package/src/components/ui/panels/panel-wrapper.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +6 -6
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-panel.tsx +6 -8
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +180 -182
- package/src/i18n/locales/es/messages.po +180 -182
- package/src/i18n/locales/fr/messages.po +180 -182
- package/src/i18n/locales/ja/messages.po +180 -182
- package/src/i18n/locales/zh-CN/messages.po +180 -182
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +109 -131
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/floor-footprints.test.ts +2 -2
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurements.test.ts +2 -1
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-handle-drag.ts +2 -12
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/store/use-editor.tsx +17 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurements.ts +0 -8
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -327
- package/src/lib/scene-bounds.ts +0 -233
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { emitter, type PascalI18n, usePascalI18n } from '@pascal-app/core'
|
|
4
|
-
import { CLAY_PALETTE, getSceneTheme, SCENE_THEMES, useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import {
|
|
6
|
-
Camera,
|
|
7
|
-
Check,
|
|
8
|
-
Contrast,
|
|
9
|
-
Diamond,
|
|
10
|
-
Drone,
|
|
11
|
-
Eye,
|
|
12
|
-
EyeOff,
|
|
13
|
-
Footprints,
|
|
14
|
-
Layers,
|
|
15
|
-
Layers2,
|
|
16
|
-
Palette,
|
|
17
|
-
SlidersHorizontal,
|
|
18
|
-
Square,
|
|
19
|
-
SwatchBook,
|
|
20
|
-
} from 'lucide-react'
|
|
21
|
-
import { useIsMobile } from '../../hooks/use-mobile'
|
|
22
|
-
import { sceneThemeName } from '../../i18n/scene-theme-name'
|
|
23
|
-
import { cn } from '../../lib/utils'
|
|
24
|
-
import { ActionButton } from '../ui/action-menu/action-button'
|
|
25
|
-
import { FollowPascalToggle } from '../ui/action-menu/follow-pascal-toggle'
|
|
26
|
-
import {
|
|
27
|
-
DropdownMenu,
|
|
28
|
-
DropdownMenuContent,
|
|
29
|
-
DropdownMenuItem,
|
|
30
|
-
DropdownMenuSeparator,
|
|
31
|
-
DropdownMenuSub,
|
|
32
|
-
DropdownMenuSubContent,
|
|
33
|
-
DropdownMenuSubTrigger,
|
|
34
|
-
DropdownMenuTrigger,
|
|
35
|
-
} from '../ui/primitives/dropdown-menu'
|
|
36
|
-
import { TooltipProvider } from '../ui/primitives/tooltip'
|
|
37
|
-
import { DaylightMenu, EdgesToggleItem, QualityMenu } from './render-settings-menu'
|
|
38
|
-
import { SKY_DOCK_HEIGHT, SkyControl } from './sky-control'
|
|
39
|
-
|
|
40
|
-
const wallModeConfig = {
|
|
41
|
-
up: {
|
|
42
|
-
icon: '/icons/room.webp',
|
|
43
|
-
},
|
|
44
|
-
cutaway: {
|
|
45
|
-
icon: '/icons/wallcut.webp',
|
|
46
|
-
},
|
|
47
|
-
down: {
|
|
48
|
-
icon: '/icons/walllow.webp',
|
|
49
|
-
},
|
|
50
|
-
} as const
|
|
51
|
-
|
|
52
|
-
function levelModeLabel(
|
|
53
|
-
mode: 'stacked' | 'exploded' | 'solo' | 'manual',
|
|
54
|
-
i18n: PascalI18n,
|
|
55
|
-
): string {
|
|
56
|
-
if (mode === 'stacked') return i18n._({ id: 'viewer.levels.stacked', message: 'Stacked' })
|
|
57
|
-
if (mode === 'exploded') return i18n._({ id: 'viewer.levels.exploded', message: 'Exploded' })
|
|
58
|
-
if (mode === 'solo') return i18n._({ id: 'viewer.levels.solo', message: 'Solo' })
|
|
59
|
-
return i18n._({ id: 'viewer.levels.manual', message: 'Manual' })
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function wallModeLabel(mode: keyof typeof wallModeConfig, i18n: PascalI18n): string {
|
|
63
|
-
if (mode === 'up') return i18n._({ id: 'viewer.walls.fullHeight', message: 'Full height' })
|
|
64
|
-
if (mode === 'down') return i18n._({ id: 'viewer.walls.low', message: 'Low' })
|
|
65
|
-
return i18n._({ id: 'viewer.walls.cutaway', message: 'Cutaway' })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Keep the dropdown open when flipping an in-place toggle row.
|
|
69
|
-
const keepOpen = (event: Event, fn: () => void) => {
|
|
70
|
-
event.preventDefault()
|
|
71
|
-
fn()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Scans + guides folded into one control. A baked GLB carries none of its own,
|
|
75
|
-
// but the GLB viewer re-adds them from scene data when the privacy flags allow,
|
|
76
|
-
// so the toggle shows for whichever exist.
|
|
77
|
-
function VisibilityMenu({
|
|
78
|
-
canShowScans,
|
|
79
|
-
canShowGuides,
|
|
80
|
-
}: {
|
|
81
|
-
canShowScans: boolean
|
|
82
|
-
canShowGuides: boolean
|
|
83
|
-
}) {
|
|
84
|
-
const i18n = usePascalI18n()
|
|
85
|
-
const showScans = useViewer((s) => s.showScans)
|
|
86
|
-
const showGuides = useViewer((s) => s.showGuides)
|
|
87
|
-
return (
|
|
88
|
-
<DropdownMenu>
|
|
89
|
-
<DropdownMenuTrigger asChild>
|
|
90
|
-
<ActionButton
|
|
91
|
-
className="hover:bg-white/5 hover:text-foreground"
|
|
92
|
-
label={i18n._({ id: 'viewer.visibility.title', message: 'Visibility' })}
|
|
93
|
-
size="icon"
|
|
94
|
-
tooltipClassName="dark"
|
|
95
|
-
tooltipSide="top"
|
|
96
|
-
variant="ghost"
|
|
97
|
-
>
|
|
98
|
-
<Eye className="h-5 w-5" />
|
|
99
|
-
</ActionButton>
|
|
100
|
-
</DropdownMenuTrigger>
|
|
101
|
-
<DropdownMenuContent align="center" className="dark min-w-44" side="top">
|
|
102
|
-
{canShowScans && (
|
|
103
|
-
<DropdownMenuItem
|
|
104
|
-
onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowScans(!showScans))}
|
|
105
|
-
>
|
|
106
|
-
<img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
|
|
107
|
-
<span>{i18n._({ id: 'viewer.visibility.scans', message: 'Scans' })}</span>
|
|
108
|
-
{showScans ? (
|
|
109
|
-
<Eye className="ml-auto h-4 w-4 text-foreground" />
|
|
110
|
-
) : (
|
|
111
|
-
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
|
|
112
|
-
)}
|
|
113
|
-
</DropdownMenuItem>
|
|
114
|
-
)}
|
|
115
|
-
{canShowGuides && (
|
|
116
|
-
<DropdownMenuItem
|
|
117
|
-
onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowGuides(!showGuides))}
|
|
118
|
-
>
|
|
119
|
-
<img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
|
|
120
|
-
<span>{i18n._({ id: 'viewer.visibility.guides', message: 'Guides' })}</span>
|
|
121
|
-
{showGuides ? (
|
|
122
|
-
<Eye className="ml-auto h-4 w-4 text-foreground" />
|
|
123
|
-
) : (
|
|
124
|
-
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
|
|
125
|
-
)}
|
|
126
|
-
</DropdownMenuItem>
|
|
127
|
-
)}
|
|
128
|
-
</DropdownMenuContent>
|
|
129
|
-
</DropdownMenu>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// One "Display" button gathering shadows, camera projection, colors, render
|
|
134
|
-
// mode, scene theme and edges.
|
|
135
|
-
function DisplayMenu() {
|
|
136
|
-
const i18n = usePascalI18n()
|
|
137
|
-
const cameraMode = useViewer((s) => s.cameraMode)
|
|
138
|
-
const textures = useViewer((s) => s.textures)
|
|
139
|
-
const shadows = useViewer((s) => s.shadows)
|
|
140
|
-
const sceneTheme = useViewer((s) => s.sceneTheme)
|
|
141
|
-
const activeTheme = getSceneTheme(sceneTheme)
|
|
142
|
-
return (
|
|
143
|
-
<DropdownMenu>
|
|
144
|
-
<DropdownMenuTrigger asChild>
|
|
145
|
-
<ActionButton
|
|
146
|
-
className="hover:bg-white/5 hover:text-foreground"
|
|
147
|
-
label={i18n._({ id: 'viewer.display.title', message: 'Display settings' })}
|
|
148
|
-
size="icon"
|
|
149
|
-
tooltipClassName="dark"
|
|
150
|
-
tooltipSide="top"
|
|
151
|
-
variant="ghost"
|
|
152
|
-
>
|
|
153
|
-
<SlidersHorizontal className="h-5 w-5" />
|
|
154
|
-
</ActionButton>
|
|
155
|
-
</DropdownMenuTrigger>
|
|
156
|
-
<DropdownMenuContent align="center" className="dark min-w-56" side="top">
|
|
157
|
-
<DropdownMenuItem
|
|
158
|
-
onSelect={(e) => keepOpen(e, () => useViewer.getState().setShadows(!shadows))}
|
|
159
|
-
>
|
|
160
|
-
<Contrast className="h-4 w-4" />
|
|
161
|
-
<span>{i18n._({ id: 'viewer.display.shadows', message: 'Shadows' })}</span>
|
|
162
|
-
<span className="ml-auto text-muted-foreground text-xs">
|
|
163
|
-
{shadows
|
|
164
|
-
? i18n._({ id: 'viewer.common.on', message: 'On' })
|
|
165
|
-
: i18n._({ id: 'viewer.common.off', message: 'Off' })}
|
|
166
|
-
</span>
|
|
167
|
-
</DropdownMenuItem>
|
|
168
|
-
<DropdownMenuItem
|
|
169
|
-
onSelect={(e) =>
|
|
170
|
-
keepOpen(e, () =>
|
|
171
|
-
useViewer
|
|
172
|
-
.getState()
|
|
173
|
-
.setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective'),
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
>
|
|
177
|
-
<Camera className="h-4 w-4" />
|
|
178
|
-
<span>{i18n._({ id: 'viewer.display.camera', message: 'Camera' })}</span>
|
|
179
|
-
<span className="ml-auto text-muted-foreground text-xs">
|
|
180
|
-
{cameraMode === 'perspective'
|
|
181
|
-
? i18n._({ id: 'viewer.display.perspective', message: 'Perspective' })
|
|
182
|
-
: i18n._({ id: 'viewer.display.orthographic', message: 'Orthographic' })}
|
|
183
|
-
</span>
|
|
184
|
-
</DropdownMenuItem>
|
|
185
|
-
<DropdownMenuItem
|
|
186
|
-
onSelect={(e) => keepOpen(e, () => useViewer.getState().setTextures(!textures))}
|
|
187
|
-
>
|
|
188
|
-
{textures ? <Palette className="h-4 w-4" /> : <Square className="h-4 w-4" />}
|
|
189
|
-
<span>{i18n._({ id: 'viewer.display.colors', message: 'Colors' })}</span>
|
|
190
|
-
<span className="ml-auto text-muted-foreground text-xs">
|
|
191
|
-
{textures
|
|
192
|
-
? i18n._({ id: 'viewer.display.colored', message: 'Colored' })
|
|
193
|
-
: i18n._({ id: 'viewer.display.monochrome', message: 'Monochrome' })}
|
|
194
|
-
</span>
|
|
195
|
-
</DropdownMenuItem>
|
|
196
|
-
|
|
197
|
-
<DropdownMenuSeparator />
|
|
198
|
-
|
|
199
|
-
<QualityMenu subContentClassName="dark min-w-56" />
|
|
200
|
-
|
|
201
|
-
{textures ? (
|
|
202
|
-
<DaylightMenu subContentClassName="dark min-w-48" />
|
|
203
|
-
) : (
|
|
204
|
-
<>
|
|
205
|
-
<DropdownMenuSub>
|
|
206
|
-
<DropdownMenuSubTrigger>
|
|
207
|
-
<SwatchBook className="h-4 w-4" />
|
|
208
|
-
<span>{i18n._({ id: 'viewer.theme.title', message: 'Scene theme' })}</span>
|
|
209
|
-
<span className="ml-auto truncate text-muted-foreground text-xs">
|
|
210
|
-
{sceneThemeName(activeTheme.id, activeTheme.name, i18n)}
|
|
211
|
-
</span>
|
|
212
|
-
</DropdownMenuSubTrigger>
|
|
213
|
-
<DropdownMenuSubContent className="dark min-w-48">
|
|
214
|
-
{SCENE_THEMES.map((theme) => {
|
|
215
|
-
const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
|
|
216
|
-
(role) => theme.clayTints?.[role] ?? CLAY_PALETTE[role],
|
|
217
|
-
)
|
|
218
|
-
return (
|
|
219
|
-
<DropdownMenuItem
|
|
220
|
-
className="gap-2"
|
|
221
|
-
key={theme.id}
|
|
222
|
-
onSelect={() => useViewer.getState().setSceneTheme(theme.id)}
|
|
223
|
-
>
|
|
224
|
-
<span
|
|
225
|
-
className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
|
|
226
|
-
style={{ backgroundColor: theme.background }}
|
|
227
|
-
>
|
|
228
|
-
{swatches.map((color, index) => (
|
|
229
|
-
<span key={`${theme.id}-${index}`} style={{ backgroundColor: color }} />
|
|
230
|
-
))}
|
|
231
|
-
</span>
|
|
232
|
-
<span>{sceneThemeName(theme.id, theme.name, i18n)}</span>
|
|
233
|
-
{sceneTheme === theme.id ? <Check className="ml-auto h-4 w-4" /> : null}
|
|
234
|
-
</DropdownMenuItem>
|
|
235
|
-
)
|
|
236
|
-
})}
|
|
237
|
-
</DropdownMenuSubContent>
|
|
238
|
-
</DropdownMenuSub>
|
|
239
|
-
<EdgesToggleItem />
|
|
240
|
-
</>
|
|
241
|
-
)}
|
|
242
|
-
</DropdownMenuContent>
|
|
243
|
-
</DropdownMenu>
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export type ViewerControlsBarProps = {
|
|
248
|
-
canShowScans?: boolean
|
|
249
|
-
canShowGuides?: boolean
|
|
250
|
-
/** A baked GLB is the active artifact: hide controls it can't honor (wall
|
|
251
|
-
* modes aren't baked into the GLB). */
|
|
252
|
-
glbActive?: boolean
|
|
253
|
-
/** In GLB mode, whether scans/guides were re-added from scene data — so the
|
|
254
|
-
* visibility control surfaces the matching toggle even though the artifact
|
|
255
|
-
* itself carries none. */
|
|
256
|
-
glbHasScans?: boolean
|
|
257
|
-
glbHasGuides?: boolean
|
|
258
|
-
walkthroughActive?: boolean
|
|
259
|
-
onWalkthroughToggle: () => void
|
|
260
|
-
/** The free-flying drone camera; its button shows only with a handler, and never on touch. */
|
|
261
|
-
droneActive?: boolean
|
|
262
|
-
onDroneToggle?: () => void
|
|
263
|
-
className?: string
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export const ViewerControlsBar = ({
|
|
267
|
-
canShowScans = true,
|
|
268
|
-
canShowGuides = true,
|
|
269
|
-
glbActive = false,
|
|
270
|
-
glbHasScans = false,
|
|
271
|
-
glbHasGuides = false,
|
|
272
|
-
walkthroughActive = false,
|
|
273
|
-
onWalkthroughToggle,
|
|
274
|
-
droneActive = false,
|
|
275
|
-
onDroneToggle,
|
|
276
|
-
className,
|
|
277
|
-
}: ViewerControlsBarProps) => {
|
|
278
|
-
const i18n = usePascalI18n()
|
|
279
|
-
const levelMode = useViewer((s) => s.levelMode)
|
|
280
|
-
const wallMode = useViewer((s) => s.wallMode)
|
|
281
|
-
const textures = useViewer((s) => s.textures)
|
|
282
|
-
const isMobile = useIsMobile()
|
|
283
|
-
// Sessions may carry a stale mode outside the cycle (e.g. the retired
|
|
284
|
-
// 'translucent'); render and cycle it as cutaway instead of crashing.
|
|
285
|
-
const safeWallMode = (
|
|
286
|
-
wallMode in wallModeConfig ? wallMode : 'cutaway'
|
|
287
|
-
) as keyof typeof wallModeConfig
|
|
288
|
-
|
|
289
|
-
return (
|
|
290
|
-
<>
|
|
291
|
-
{textures && !isMobile ? (
|
|
292
|
-
// The sky (time and weather) beside the compass in the scene's bottom-left corner.
|
|
293
|
-
<SkyControl dark height={SKY_DOCK_HEIGHT.viewer} style={{ left: 52 }} />
|
|
294
|
-
) : null}
|
|
295
|
-
<div
|
|
296
|
-
className={cn(
|
|
297
|
-
'dark absolute bottom-4 left-1/2 z-20 -translate-x-1/2 text-foreground sm:bottom-6',
|
|
298
|
-
className,
|
|
299
|
-
)}
|
|
300
|
-
>
|
|
301
|
-
<TooltipProvider delayDuration={0}>
|
|
302
|
-
<div
|
|
303
|
-
data-surface="viewer-dock"
|
|
304
|
-
className="corner-smooth pointer-events-auto flex h-12 max-w-[calc(100vw-1rem)] flex-row items-center justify-center gap-0.5 overflow-hidden rounded-2xl border border-border/40 bg-background/95 p-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out sm:h-14 sm:gap-1.5 sm:p-1.5"
|
|
305
|
-
>
|
|
306
|
-
{((canShowScans && (!glbActive || glbHasScans)) ||
|
|
307
|
-
(canShowGuides && (!glbActive || glbHasGuides))) && (
|
|
308
|
-
<>
|
|
309
|
-
<VisibilityMenu
|
|
310
|
-
canShowGuides={canShowGuides && (!glbActive || glbHasGuides)}
|
|
311
|
-
canShowScans={canShowScans && (!glbActive || glbHasScans)}
|
|
312
|
-
/>
|
|
313
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
314
|
-
</>
|
|
315
|
-
)}
|
|
316
|
-
|
|
317
|
-
{/* Level mode */}
|
|
318
|
-
<ActionButton
|
|
319
|
-
className={
|
|
320
|
-
levelMode === 'stacked'
|
|
321
|
-
? 'hover:bg-white/5 hover:text-amber-400'
|
|
322
|
-
: 'bg-amber-500/20 text-amber-400'
|
|
323
|
-
}
|
|
324
|
-
label={i18n._({
|
|
325
|
-
id: 'viewer.levels.label',
|
|
326
|
-
message: 'Levels: {mode}',
|
|
327
|
-
values: { mode: levelModeLabel(levelMode, i18n) },
|
|
328
|
-
})}
|
|
329
|
-
onClick={() => {
|
|
330
|
-
if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked')
|
|
331
|
-
const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
|
|
332
|
-
const nextIndex = (modes.indexOf(levelMode as any) + 1) % modes.length
|
|
333
|
-
useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
|
|
334
|
-
}}
|
|
335
|
-
size="icon"
|
|
336
|
-
tooltipClassName="dark"
|
|
337
|
-
tooltipSide="top"
|
|
338
|
-
variant="ghost"
|
|
339
|
-
>
|
|
340
|
-
{levelMode === 'solo' && <Diamond className="h-6 w-6" />}
|
|
341
|
-
{levelMode === 'exploded' && <Layers2 className="h-6 w-6" />}
|
|
342
|
-
{(levelMode === 'stacked' || levelMode === 'manual') && (
|
|
343
|
-
<Layers className="h-6 w-6" />
|
|
344
|
-
)}
|
|
345
|
-
</ActionButton>
|
|
346
|
-
|
|
347
|
-
{/* Wall mode — parametric only; baked GLB walls are fixed-height. */}
|
|
348
|
-
{!glbActive && (
|
|
349
|
-
<ActionButton
|
|
350
|
-
className={
|
|
351
|
-
safeWallMode === 'cutaway'
|
|
352
|
-
? 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
|
|
353
|
-
: 'bg-white/10'
|
|
354
|
-
}
|
|
355
|
-
label={i18n._({
|
|
356
|
-
id: 'viewer.walls.label',
|
|
357
|
-
message: 'Walls: {mode}',
|
|
358
|
-
values: { mode: wallModeLabel(safeWallMode, i18n) },
|
|
359
|
-
})}
|
|
360
|
-
onClick={() => {
|
|
361
|
-
const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down']
|
|
362
|
-
const nextIndex = (modes.indexOf(safeWallMode) + 1) % modes.length
|
|
363
|
-
useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
|
|
364
|
-
}}
|
|
365
|
-
size="icon"
|
|
366
|
-
tooltipClassName="dark"
|
|
367
|
-
tooltipSide="top"
|
|
368
|
-
variant="ghost"
|
|
369
|
-
>
|
|
370
|
-
<img
|
|
371
|
-
alt={wallModeLabel(safeWallMode, i18n)}
|
|
372
|
-
className="h-[28px] w-[28px]"
|
|
373
|
-
height={28}
|
|
374
|
-
src={wallModeConfig[safeWallMode].icon}
|
|
375
|
-
width={28}
|
|
376
|
-
/>
|
|
377
|
-
</ActionButton>
|
|
378
|
-
)}
|
|
379
|
-
|
|
380
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
381
|
-
|
|
382
|
-
<DisplayMenu />
|
|
383
|
-
|
|
384
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
385
|
-
|
|
386
|
-
{/* Walkthrough */}
|
|
387
|
-
<ActionButton
|
|
388
|
-
className={
|
|
389
|
-
walkthroughActive
|
|
390
|
-
? 'bg-emerald-500/20 text-emerald-400'
|
|
391
|
-
: 'hover:bg-white/5 hover:text-emerald-400'
|
|
392
|
-
}
|
|
393
|
-
label={i18n._({
|
|
394
|
-
id: 'viewer.walkthrough.label',
|
|
395
|
-
message: 'Walkthrough: {state}',
|
|
396
|
-
values: {
|
|
397
|
-
state: walkthroughActive
|
|
398
|
-
? i18n._({ id: 'viewer.common.on', message: 'On' })
|
|
399
|
-
: i18n._({ id: 'viewer.common.off', message: 'Off' }),
|
|
400
|
-
},
|
|
401
|
-
})}
|
|
402
|
-
onClick={onWalkthroughToggle}
|
|
403
|
-
size="icon"
|
|
404
|
-
tooltipClassName="dark"
|
|
405
|
-
tooltipSide="top"
|
|
406
|
-
variant="ghost"
|
|
407
|
-
>
|
|
408
|
-
<Footprints className="h-6 w-6" />
|
|
409
|
-
</ActionButton>
|
|
410
|
-
|
|
411
|
-
{/* Drone — flying needs a keyboard. */}
|
|
412
|
-
{onDroneToggle && !isMobile && (
|
|
413
|
-
<ActionButton
|
|
414
|
-
className={
|
|
415
|
-
droneActive
|
|
416
|
-
? 'bg-emerald-500/20 text-emerald-400'
|
|
417
|
-
: 'hover:bg-white/5 hover:text-emerald-400'
|
|
418
|
-
}
|
|
419
|
-
label={i18n._({
|
|
420
|
-
id: 'viewer.drone.label',
|
|
421
|
-
message: 'Drone: {state}',
|
|
422
|
-
values: {
|
|
423
|
-
state: droneActive
|
|
424
|
-
? i18n._({ id: 'viewer.common.on', message: 'On' })
|
|
425
|
-
: i18n._({ id: 'viewer.common.off', message: 'Off' }),
|
|
426
|
-
},
|
|
427
|
-
})}
|
|
428
|
-
onClick={onDroneToggle}
|
|
429
|
-
size="icon"
|
|
430
|
-
tooltipClassName="dark"
|
|
431
|
-
tooltipSide="top"
|
|
432
|
-
variant="ghost"
|
|
433
|
-
>
|
|
434
|
-
<Drone className="h-6 w-6" />
|
|
435
|
-
</ActionButton>
|
|
436
|
-
)}
|
|
437
|
-
|
|
438
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
439
|
-
|
|
440
|
-
{/* Follow Pascal */}
|
|
441
|
-
<FollowPascalToggle className="hidden sm:inline-flex" tooltipClassName="dark" />
|
|
442
|
-
|
|
443
|
-
{/* Camera actions */}
|
|
444
|
-
<ActionButton
|
|
445
|
-
className="group hidden hover:bg-white/5 sm:inline-flex"
|
|
446
|
-
label={i18n._({ id: 'viewer.camera.orbitLeft', message: 'Orbit left' })}
|
|
447
|
-
onClick={() => emitter.emit('camera-controls:orbit-ccw')}
|
|
448
|
-
size="icon"
|
|
449
|
-
tooltipClassName="dark"
|
|
450
|
-
tooltipSide="top"
|
|
451
|
-
variant="ghost"
|
|
452
|
-
>
|
|
453
|
-
<img
|
|
454
|
-
alt={i18n._({ id: 'viewer.camera.orbitLeft', message: 'Orbit left' })}
|
|
455
|
-
className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
456
|
-
src="/icons/rotate.webp"
|
|
457
|
-
/>
|
|
458
|
-
</ActionButton>
|
|
459
|
-
|
|
460
|
-
<ActionButton
|
|
461
|
-
className="group hidden hover:bg-white/5 sm:inline-flex"
|
|
462
|
-
label={i18n._({ id: 'viewer.camera.orbitRight', message: 'Orbit right' })}
|
|
463
|
-
onClick={() => emitter.emit('camera-controls:orbit-cw')}
|
|
464
|
-
size="icon"
|
|
465
|
-
tooltipClassName="dark"
|
|
466
|
-
tooltipSide="top"
|
|
467
|
-
variant="ghost"
|
|
468
|
-
>
|
|
469
|
-
<img
|
|
470
|
-
alt={i18n._({ id: 'viewer.camera.orbitRight', message: 'Orbit right' })}
|
|
471
|
-
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
472
|
-
src="/icons/rotate.webp"
|
|
473
|
-
/>
|
|
474
|
-
</ActionButton>
|
|
475
|
-
|
|
476
|
-
<ActionButton
|
|
477
|
-
className="group hover:bg-white/5"
|
|
478
|
-
label={i18n._({ id: 'viewer.camera.topView', message: 'Top view' })}
|
|
479
|
-
onClick={() => emitter.emit('camera-controls:top-view')}
|
|
480
|
-
size="icon"
|
|
481
|
-
tooltipClassName="dark"
|
|
482
|
-
tooltipSide="top"
|
|
483
|
-
variant="ghost"
|
|
484
|
-
>
|
|
485
|
-
<img
|
|
486
|
-
alt={i18n._({ id: 'viewer.camera.topView', message: 'Top view' })}
|
|
487
|
-
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
488
|
-
src="/icons/topview.webp"
|
|
489
|
-
/>
|
|
490
|
-
</ActionButton>
|
|
491
|
-
</div>
|
|
492
|
-
</TooltipProvider>
|
|
493
|
-
</div>
|
|
494
|
-
</>
|
|
495
|
-
)
|
|
496
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { usePascalI18n, useScene } from '@pascal-app/core'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import { nodeDisplayLabel } from '../../lib/node-display-label'
|
|
6
|
-
import type { DescriptorTranslator } from '../../lib/room-name-messages'
|
|
7
|
-
import { type ViewerNodes, zoneLabel } from '../../lib/viewer-selection'
|
|
8
|
-
|
|
9
|
-
/** The hovered node's name, for the bottom pill. A room reads as the room. */
|
|
10
|
-
export function viewerHoverLabel(
|
|
11
|
-
hoveredId: string | null,
|
|
12
|
-
nodes: ViewerNodes,
|
|
13
|
-
i18n: DescriptorTranslator,
|
|
14
|
-
): string | null {
|
|
15
|
-
const node = hoveredId ? nodes[hoveredId] : undefined
|
|
16
|
-
if (!node) return null
|
|
17
|
-
return node.type === 'zone' ? zoneLabel(node, i18n) : nodeDisplayLabel(node)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* What a click would select, named in a pill above the controls bar. Every
|
|
22
|
-
* viewer surface's hit source sets the hover (`applyViewerHover`); hidden in
|
|
23
|
-
* the walkthrough and on devices without a hovering pointer. Names come from
|
|
24
|
-
* the parametric scene store unless the surface passes its own graph.
|
|
25
|
-
*/
|
|
26
|
-
export function ViewerHoverLabel({ nodes: nodesProp }: { nodes?: ViewerNodes }) {
|
|
27
|
-
const i18n = usePascalI18n()
|
|
28
|
-
const sceneNodes = useScene((s) => s.nodes)
|
|
29
|
-
const nodes = nodesProp ?? sceneNodes
|
|
30
|
-
const label = useViewer((s) =>
|
|
31
|
-
s.walkthroughMode ? null : viewerHoverLabel(s.hoveredId, nodes, i18n),
|
|
32
|
-
)
|
|
33
|
-
if (!label) return null
|
|
34
|
-
return (
|
|
35
|
-
<div
|
|
36
|
-
className="pointer-events-none absolute bottom-24 left-1/2 z-20 -translate-x-1/2 rounded-full border border-border/60 bg-background/90 px-3 py-1 text-foreground text-sm shadow-elevation-3 backdrop-blur [@media(hover:none)]:hidden"
|
|
37
|
-
data-testid="viewer-hover-label"
|
|
38
|
-
>
|
|
39
|
-
{label}
|
|
40
|
-
</div>
|
|
41
|
-
)
|
|
42
|
-
}
|