@pascal-app/editor 1.0.0-beta.4 → 1.0.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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AnyNode, ParamField } from '@pascal-app/core'
|
|
4
|
+
import { SegmentedControl } from '../controls/segmented-control'
|
|
5
|
+
import { SliderControl } from '../controls/slider-control'
|
|
6
|
+
import { ToggleControl } from '../controls/toggle-control'
|
|
7
|
+
import { precisionForStep, prettifyEnumValue, prettifyKey } from './parametric-field-utils'
|
|
8
|
+
|
|
9
|
+
interface ParametricFieldControlProps {
|
|
10
|
+
field: ParamField<AnyNode>
|
|
11
|
+
value: unknown
|
|
12
|
+
mixed?: boolean
|
|
13
|
+
onChange: (patch: Partial<AnyNode>) => void
|
|
14
|
+
onCommit?: (patch: Partial<AnyNode>) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ParametricFieldControl({
|
|
18
|
+
field,
|
|
19
|
+
value,
|
|
20
|
+
mixed = false,
|
|
21
|
+
onChange,
|
|
22
|
+
onCommit,
|
|
23
|
+
}: ParametricFieldControlProps) {
|
|
24
|
+
const key = String(field.key)
|
|
25
|
+
|
|
26
|
+
switch (field.kind) {
|
|
27
|
+
case 'number': {
|
|
28
|
+
const num = typeof value === 'number' ? value : 0
|
|
29
|
+
const step = field.step ?? 0.01
|
|
30
|
+
const precision = precisionForStep(step)
|
|
31
|
+
return (
|
|
32
|
+
<SliderControl
|
|
33
|
+
label={prettifyKey(key)}
|
|
34
|
+
max={field.max}
|
|
35
|
+
min={field.min}
|
|
36
|
+
mixed={mixed}
|
|
37
|
+
onChange={(next) => onChange({ [key]: next } as Partial<AnyNode>)}
|
|
38
|
+
onCommit={onCommit ? (next) => onCommit({ [key]: next } as Partial<AnyNode>) : undefined}
|
|
39
|
+
precision={precision}
|
|
40
|
+
restoreOnCommit={!onCommit}
|
|
41
|
+
step={step}
|
|
42
|
+
unit={field.unit ?? ''}
|
|
43
|
+
value={num}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'boolean': {
|
|
49
|
+
const checked = !mixed && value === true
|
|
50
|
+
return (
|
|
51
|
+
<ToggleControl
|
|
52
|
+
checked={checked}
|
|
53
|
+
label={prettifyKey(key)}
|
|
54
|
+
mixed={mixed}
|
|
55
|
+
onChange={(next) => {
|
|
56
|
+
const patch = { [key]: next } as Partial<AnyNode>
|
|
57
|
+
onChange(patch)
|
|
58
|
+
onCommit?.(patch)
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case 'enum': {
|
|
65
|
+
const str = typeof value === 'string' ? value : (field.options[0] ?? '')
|
|
66
|
+
const apply = (next: string) => {
|
|
67
|
+
const patch = { [key]: next } as Partial<AnyNode>
|
|
68
|
+
onChange(patch)
|
|
69
|
+
onCommit?.(patch)
|
|
70
|
+
}
|
|
71
|
+
if (field.display === 'segmented') {
|
|
72
|
+
return (
|
|
73
|
+
<SegmentedControl
|
|
74
|
+
mixed={mixed}
|
|
75
|
+
onChange={apply}
|
|
76
|
+
options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
|
|
77
|
+
value={str}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex items-center justify-between px-3 py-2">
|
|
83
|
+
<span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
|
|
84
|
+
<select
|
|
85
|
+
className="rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
86
|
+
onChange={(e) => apply(e.target.value)}
|
|
87
|
+
value={mixed ? '' : str}
|
|
88
|
+
>
|
|
89
|
+
{mixed && (
|
|
90
|
+
<option disabled value="">
|
|
91
|
+
Mixed
|
|
92
|
+
</option>
|
|
93
|
+
)}
|
|
94
|
+
{field.options.map((opt) => (
|
|
95
|
+
<option key={opt} value={opt}>
|
|
96
|
+
{prettifyEnumValue(opt)}
|
|
97
|
+
</option>
|
|
98
|
+
))}
|
|
99
|
+
</select>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'color': {
|
|
105
|
+
const str = typeof value === 'string' ? value : '#888888'
|
|
106
|
+
const apply = (next: string) => {
|
|
107
|
+
const patch = { [key]: next } as Partial<AnyNode>
|
|
108
|
+
onChange(patch)
|
|
109
|
+
onCommit?.(patch)
|
|
110
|
+
}
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex items-center justify-between px-3 py-2">
|
|
113
|
+
<span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<input
|
|
116
|
+
className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
|
|
117
|
+
onChange={(e) => apply(e.target.value)}
|
|
118
|
+
type="color"
|
|
119
|
+
value={str}
|
|
120
|
+
/>
|
|
121
|
+
<input
|
|
122
|
+
className="w-20 rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
123
|
+
onChange={(e) => apply(e.target.value)}
|
|
124
|
+
type="text"
|
|
125
|
+
value={mixed ? 'Mixed' : str}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'vec3': {
|
|
133
|
+
const v =
|
|
134
|
+
Array.isArray(value) && value.length >= 3
|
|
135
|
+
? (value as [number, number, number])
|
|
136
|
+
: ([0, 0, 0] as [number, number, number])
|
|
137
|
+
const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
|
|
138
|
+
{ label: 'X', index: 0 },
|
|
139
|
+
{ label: 'Y', index: 1 },
|
|
140
|
+
{ label: 'Z', index: 2 },
|
|
141
|
+
]
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
{axes.map(({ label, index }) => {
|
|
145
|
+
const axisValue = v[index] ?? 0
|
|
146
|
+
const apply = (next: number): Partial<AnyNode> => {
|
|
147
|
+
const updated = [...v] as [number, number, number]
|
|
148
|
+
updated[index] = next
|
|
149
|
+
return { [key]: updated } as Partial<AnyNode>
|
|
150
|
+
}
|
|
151
|
+
return (
|
|
152
|
+
<SliderControl
|
|
153
|
+
key={`${key}-${label}`}
|
|
154
|
+
label={label}
|
|
155
|
+
mixed={mixed}
|
|
156
|
+
onChange={(next) => onChange(apply(next))}
|
|
157
|
+
onCommit={onCommit ? (next) => onCommit(apply(next)) : undefined}
|
|
158
|
+
precision={2}
|
|
159
|
+
restoreOnCommit={!onCommit}
|
|
160
|
+
step={0.05}
|
|
161
|
+
unit="m"
|
|
162
|
+
value={Math.round(axisValue * 100) / 100}
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
})}
|
|
166
|
+
</>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function precisionForStep(step: number): number {
|
|
2
|
+
if (step <= 0) return 0
|
|
3
|
+
return Math.max(0, Math.ceil(-Math.log10(step)))
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function prettifyKey(key: string): string {
|
|
7
|
+
const spaced = key.replace(/([A-Z])/g, ' $1').toLowerCase()
|
|
8
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function prettifyEnumValue(value: string): string {
|
|
12
|
+
return value
|
|
13
|
+
.split(/[-_\s]/)
|
|
14
|
+
.map((word, i) =>
|
|
15
|
+
i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase(),
|
|
16
|
+
)
|
|
17
|
+
.join(' ')
|
|
18
|
+
}
|
|
@@ -20,9 +20,7 @@ import { collectZoneContentIds } from '../../../lib/zone-content'
|
|
|
20
20
|
import useEditor from '../../../store/use-editor'
|
|
21
21
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
22
22
|
import { PanelSection } from '../controls/panel-section'
|
|
23
|
-
import {
|
|
24
|
-
import { SliderControl } from '../controls/slider-control'
|
|
25
|
-
import { ToggleControl } from '../controls/toggle-control'
|
|
23
|
+
import { ParametricFieldControl } from './parametric-field-control'
|
|
26
24
|
import { InspectorFooterContext, PanelWrapper } from './panel-wrapper'
|
|
27
25
|
|
|
28
26
|
/**
|
|
@@ -67,7 +65,7 @@ export function ParametricInspector({
|
|
|
67
65
|
const node = scene.nodes[selectedId]
|
|
68
66
|
if (parametrics?.derive && node) {
|
|
69
67
|
const next = { ...node, ...patch } as AnyNode
|
|
70
|
-
patch = { ...patch, ...parametrics.derive(next, patch) }
|
|
68
|
+
patch = { ...patch, ...parametrics.derive(next, patch, node as AnyNode) }
|
|
71
69
|
}
|
|
72
70
|
// Bundle the edited node + any reconcile follow-ups into ONE
|
|
73
71
|
// updateNodes call so a single inspector edit is a single undo step.
|
|
@@ -133,7 +131,7 @@ export function ParametricInspector({
|
|
|
133
131
|
return (
|
|
134
132
|
<InspectorFooterContext.Provider value={footer}>
|
|
135
133
|
<Suspense fallback={null}>
|
|
136
|
-
<CustomPanel />
|
|
134
|
+
<CustomPanelSlot Component={CustomPanel} nodeId={selectedId} />
|
|
137
135
|
</Suspense>
|
|
138
136
|
</InspectorFooterContext.Provider>
|
|
139
137
|
)
|
|
@@ -172,7 +170,7 @@ export function ParametricInspector({
|
|
|
172
170
|
))}
|
|
173
171
|
{TrailingSection && (
|
|
174
172
|
<Suspense fallback={null}>
|
|
175
|
-
<TrailingSection />
|
|
173
|
+
<CustomPanelSlot Component={TrailingSection} nodeId={selectedId} />
|
|
176
174
|
</Suspense>
|
|
177
175
|
)}
|
|
178
176
|
{(canMove || canDelete || (parametrics.actions && parametrics.actions.length > 0)) && (
|
|
@@ -272,14 +270,31 @@ function renderIcon(ref: IconRef | undefined): React.ReactNode | undefined {
|
|
|
272
270
|
|
|
273
271
|
// Cache lazy custom panel components by their loader so React.lazy isn't
|
|
274
272
|
// re-invoked across renders.
|
|
275
|
-
const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType
|
|
273
|
+
const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType<{ node: AnyNode }>>()
|
|
276
274
|
|
|
277
|
-
function resolveCustomPanel(
|
|
275
|
+
function resolveCustomPanel(
|
|
276
|
+
loader: () => Promise<{ default: ComponentType<any> }>,
|
|
277
|
+
): ComponentType<{ node: AnyNode }> {
|
|
278
278
|
const cached = customPanelCache.get(loader)
|
|
279
279
|
if (cached) return cached
|
|
280
280
|
const Comp = lazy(loader)
|
|
281
|
-
customPanelCache.set(loader, Comp as ComponentType)
|
|
282
|
-
return Comp as ComponentType
|
|
281
|
+
customPanelCache.set(loader, Comp as ComponentType<{ node: AnyNode }>)
|
|
282
|
+
return Comp as ComponentType<{ node: AnyNode }>
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Subscribe to the full node only where the custom panel contract needs it.
|
|
286
|
+
// Keeping this below ParametricInspector preserves the inspector's narrow
|
|
287
|
+
// per-field subscriptions while ensuring lazy panels receive their live node.
|
|
288
|
+
function CustomPanelSlot({
|
|
289
|
+
Component,
|
|
290
|
+
nodeId,
|
|
291
|
+
}: {
|
|
292
|
+
Component: ComponentType<{ node: AnyNode }>
|
|
293
|
+
nodeId: AnyNodeId
|
|
294
|
+
}) {
|
|
295
|
+
const node = useScene((s) => s.nodes[nodeId])
|
|
296
|
+
if (!node) return null
|
|
297
|
+
return <Component node={node as AnyNode} />
|
|
283
298
|
}
|
|
284
299
|
|
|
285
300
|
// ─── Per-field renderers ─────────────────────────────────────────────
|
|
@@ -311,136 +326,11 @@ function FieldRenderer({ field, nodeId, onUpdate }: FieldRendererProps) {
|
|
|
311
326
|
})
|
|
312
327
|
if (!visible) return null
|
|
313
328
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const num = typeof value === 'number' ? value : 0
|
|
317
|
-
const step = field.step ?? 0.01
|
|
318
|
-
const precision = precisionForStep(step)
|
|
319
|
-
return (
|
|
320
|
-
<SliderControl
|
|
321
|
-
label={prettifyKey(key)}
|
|
322
|
-
max={field.max}
|
|
323
|
-
min={field.min}
|
|
324
|
-
onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
|
|
325
|
-
precision={precision}
|
|
326
|
-
step={step}
|
|
327
|
-
unit={field.unit ?? ''}
|
|
328
|
-
value={num}
|
|
329
|
-
/>
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
case 'boolean': {
|
|
334
|
-
const checked = value === true
|
|
335
|
-
return (
|
|
336
|
-
<ToggleControl
|
|
337
|
-
checked={checked}
|
|
338
|
-
label={prettifyKey(key)}
|
|
339
|
-
onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
|
|
340
|
-
/>
|
|
341
|
-
)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
case 'enum': {
|
|
345
|
-
const str = typeof value === 'string' ? value : (field.options[0] ?? '')
|
|
346
|
-
if (field.display === 'segmented') {
|
|
347
|
-
return (
|
|
348
|
-
<SegmentedControl
|
|
349
|
-
onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
|
|
350
|
-
options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
|
|
351
|
-
value={str}
|
|
352
|
-
/>
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
return (
|
|
356
|
-
<div className="flex items-center justify-between px-3 py-2">
|
|
357
|
-
<span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
|
|
358
|
-
<select
|
|
359
|
-
className="rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
360
|
-
onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
|
|
361
|
-
value={str}
|
|
362
|
-
>
|
|
363
|
-
{field.options.map((opt) => (
|
|
364
|
-
<option key={opt} value={opt}>
|
|
365
|
-
{prettifyEnumValue(opt)}
|
|
366
|
-
</option>
|
|
367
|
-
))}
|
|
368
|
-
</select>
|
|
369
|
-
</div>
|
|
370
|
-
)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
case 'color': {
|
|
374
|
-
const str = typeof value === 'string' ? value : '#888888'
|
|
375
|
-
return (
|
|
376
|
-
<div className="flex items-center justify-between px-3 py-2">
|
|
377
|
-
<span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
|
|
378
|
-
<div className="flex items-center gap-2">
|
|
379
|
-
<input
|
|
380
|
-
className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
|
|
381
|
-
onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
|
|
382
|
-
type="color"
|
|
383
|
-
value={str}
|
|
384
|
-
/>
|
|
385
|
-
<input
|
|
386
|
-
className="w-20 rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
387
|
-
onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
|
|
388
|
-
type="text"
|
|
389
|
-
value={str}
|
|
390
|
-
/>
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
case 'vec3': {
|
|
397
|
-
const v = Array.isArray(value) && value.length >= 3
|
|
398
|
-
? (value as [number, number, number])
|
|
399
|
-
: [0, 0, 0]
|
|
400
|
-
const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
|
|
401
|
-
{ label: 'X', index: 0 },
|
|
402
|
-
{ label: 'Y', index: 1 },
|
|
403
|
-
{ label: 'Z', index: 2 },
|
|
404
|
-
]
|
|
405
|
-
return (
|
|
406
|
-
<>
|
|
407
|
-
{axes.map(({ label, index }) => {
|
|
408
|
-
// v is a [number, number, number] tuple; the explicit local
|
|
409
|
-
// resolves TS's noUncheckedIndexedAccess concern that v[index]
|
|
410
|
-
// could be undefined.
|
|
411
|
-
const axisValue = v[index] ?? 0
|
|
412
|
-
return (
|
|
413
|
-
<SliderControl
|
|
414
|
-
key={`${key}-${label}`}
|
|
415
|
-
label={label}
|
|
416
|
-
max={axisValue + 5}
|
|
417
|
-
min={axisValue - 5}
|
|
418
|
-
onChange={(next) => {
|
|
419
|
-
const updated = [...v] as [number, number, number]
|
|
420
|
-
updated[index] = next
|
|
421
|
-
onUpdate({ [key]: updated } as Partial<AnyNode>)
|
|
422
|
-
}}
|
|
423
|
-
precision={2}
|
|
424
|
-
step={0.05}
|
|
425
|
-
unit="m"
|
|
426
|
-
value={Math.round(axisValue * 100) / 100}
|
|
427
|
-
/>
|
|
428
|
-
)
|
|
429
|
-
})}
|
|
430
|
-
</>
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
case 'custom':
|
|
435
|
-
// The field owns its rendering and update logic — used for
|
|
436
|
-
// derived values (length from start/end), dynamic-bounded
|
|
437
|
-
// sliders (curve sagitta), composed editors.
|
|
438
|
-
return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
|
|
439
|
-
|
|
440
|
-
default:
|
|
441
|
-
// material / ref / unrecognized kinds — not implemented in v1.
|
|
442
|
-
return null
|
|
329
|
+
if (field.kind === 'custom') {
|
|
330
|
+
return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
|
|
443
331
|
}
|
|
332
|
+
|
|
333
|
+
return <ParametricFieldControl field={field} onChange={onUpdate} value={value} />
|
|
444
334
|
}
|
|
445
335
|
|
|
446
336
|
function CustomFieldRenderer({
|
|
@@ -459,26 +349,3 @@ function CustomFieldRenderer({
|
|
|
459
349
|
if (!node) return null
|
|
460
350
|
return <Comp node={node} onUpdate={onUpdate} />
|
|
461
351
|
}
|
|
462
|
-
|
|
463
|
-
// ─── helpers ─────────────────────────────────────────────────────────
|
|
464
|
-
|
|
465
|
-
function precisionForStep(step: number): number {
|
|
466
|
-
if (step <= 0) return 0
|
|
467
|
-
return Math.max(0, Math.ceil(-Math.log10(step)))
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function prettifyKey(key: string): string {
|
|
471
|
-
// 'bracketStyle' → 'Bracket style'
|
|
472
|
-
const spaced = key.replace(/([A-Z])/g, ' $1').toLowerCase()
|
|
473
|
-
return spaced.charAt(0).toUpperCase() + spaced.slice(1)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function prettifyEnumValue(value: string): string {
|
|
477
|
-
// 'minimal' → 'Minimal'; 'roof-segment' → 'Roof segment'
|
|
478
|
-
return value
|
|
479
|
-
.split(/[-_\s]/)
|
|
480
|
-
.map((word, i) =>
|
|
481
|
-
i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase(),
|
|
482
|
-
)
|
|
483
|
-
.join(' ')
|
|
484
|
-
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
EyeOff,
|
|
14
14
|
LocateFixed,
|
|
15
15
|
Lock,
|
|
16
|
+
Move,
|
|
16
17
|
RotateCcw,
|
|
17
18
|
Ruler,
|
|
18
19
|
Trash2,
|
|
@@ -144,6 +145,12 @@ export function ReferencePanel() {
|
|
|
144
145
|
guideEmitter.emit('guide:cancel-reference-scale')
|
|
145
146
|
}, [])
|
|
146
147
|
|
|
148
|
+
const handleMoveScan = useCallback(() => {
|
|
149
|
+
if (node?.type !== 'scan') return
|
|
150
|
+
useEditor.getState().setMovingNode(node as never)
|
|
151
|
+
setSelectedReferenceId(null)
|
|
152
|
+
}, [node, setSelectedReferenceId])
|
|
153
|
+
|
|
147
154
|
useEffect(() => {
|
|
148
155
|
if (node?.type !== 'guide' || !node.url.startsWith('asset://')) {
|
|
149
156
|
setIsAssetMissing(false)
|
|
@@ -172,7 +179,7 @@ export function ReferencePanel() {
|
|
|
172
179
|
return (
|
|
173
180
|
<PanelWrapper
|
|
174
181
|
onClose={handleClose}
|
|
175
|
-
title={node.name || (isScan ? '
|
|
182
|
+
title={node.name || (isScan ? 'Capture' : 'Guide Image')}
|
|
176
183
|
width={300}
|
|
177
184
|
>
|
|
178
185
|
{!isScan && (
|
|
@@ -329,6 +336,29 @@ export function ReferencePanel() {
|
|
|
329
336
|
</>
|
|
330
337
|
)}
|
|
331
338
|
|
|
339
|
+
{isScan && (
|
|
340
|
+
<PanelSection title="Capture">
|
|
341
|
+
<ActionGroup>
|
|
342
|
+
<ActionButton
|
|
343
|
+
icon={<Move className="h-3.5 w-3.5" />}
|
|
344
|
+
label="Move"
|
|
345
|
+
onClick={handleMoveScan}
|
|
346
|
+
/>
|
|
347
|
+
<ActionButton
|
|
348
|
+
icon={
|
|
349
|
+
node.visible === false ? (
|
|
350
|
+
<EyeOff className="h-3.5 w-3.5" />
|
|
351
|
+
) : (
|
|
352
|
+
<Eye className="h-3.5 w-3.5" />
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
label={node.visible === false ? 'Show' : 'Hide'}
|
|
356
|
+
onClick={() => handleUpdate({ visible: node.visible === false })}
|
|
357
|
+
/>
|
|
358
|
+
</ActionGroup>
|
|
359
|
+
</PanelSection>
|
|
360
|
+
)}
|
|
361
|
+
|
|
332
362
|
<PanelSection title="Position">
|
|
333
363
|
<SliderControl
|
|
334
364
|
label={
|
|
@@ -33,6 +33,28 @@ const COMMAND_VALUES = new Set(['Cmd/Ctrl', 'Cmd', 'Command', 'Meta'])
|
|
|
33
33
|
const IS_MAC =
|
|
34
34
|
typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC')
|
|
35
35
|
|
|
36
|
+
// Keys whose printed name and their symbol differ. Kept beside the token rather
|
|
37
|
+
// than in any one consumer so every surface that prints a shortcut — the
|
|
38
|
+
// Keyboard Shortcuts dialog, the community getting-started guide — resolves the
|
|
39
|
+
// same glyph for the same key.
|
|
40
|
+
//
|
|
41
|
+
// Only symbols people actually read are listed. The arrows are unambiguous, and
|
|
42
|
+
// ⌘ is printed on the key it means. The rest are not: ⎋, ␣ and ⌫ are typographic
|
|
43
|
+
// conventions that no keyboard prints, so they read as "some symbol" rather than
|
|
44
|
+
// as Escape, Space and Delete. Those spell their names out instead — and for
|
|
45
|
+
// Delete, the name the current platform actually puts on the keycap.
|
|
46
|
+
const KEY_DISPLAY_MAP: Record<string, string> = {
|
|
47
|
+
'Arrow Down': '↓',
|
|
48
|
+
'Arrow Up': '↑',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The glyph to print for a shortcut key on the current platform. */
|
|
52
|
+
function shortcutDisplayValue(value: string): string {
|
|
53
|
+
if (COMMAND_VALUES.has(value)) return IS_MAC ? '⌘' : 'Ctrl'
|
|
54
|
+
if (value === 'Delete / Backspace') return IS_MAC ? 'Delete' : 'Backspace'
|
|
55
|
+
return KEY_DISPLAY_MAP[value] ?? value
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
|
|
37
59
|
value: string
|
|
38
60
|
displayValue?: string
|
|
@@ -97,4 +119,4 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
|
|
|
97
119
|
)
|
|
98
120
|
}
|
|
99
121
|
|
|
100
|
-
export { ShortcutToken }
|
|
122
|
+
export { ShortcutToken, shortcutDisplayValue }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
import { cn } from '../../lib/utils'
|
|
5
|
+
import { Button } from './primitives/button'
|
|
5
6
|
|
|
6
7
|
const LOADERS = [
|
|
7
8
|
'pascal-loader-1',
|
|
@@ -36,3 +37,37 @@ export function SceneLoader({ className, fullScreen = false }: SceneLoaderProps)
|
|
|
36
37
|
</div>
|
|
37
38
|
)
|
|
38
39
|
}
|
|
40
|
+
|
|
41
|
+
interface SceneLoadFailedProps {
|
|
42
|
+
className?: string
|
|
43
|
+
onRetry: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Replaces the loader when the host could not deliver the scene. Rendered
|
|
48
|
+
* INSTEAD of falling back to an empty default scene: a session that shows
|
|
49
|
+
* scaffold nodes after a failed load autosaves that scaffold over the real
|
|
50
|
+
* project (prod scene-wipe class, 2026-09-02).
|
|
51
|
+
*/
|
|
52
|
+
export function SceneLoadFailed({ className, onRetry }: SceneLoadFailedProps) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
'z-100 flex flex-col items-center justify-center gap-4 bg-background/90 px-6 text-center backdrop-blur-md',
|
|
57
|
+
'absolute inset-0',
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
role="alert"
|
|
61
|
+
>
|
|
62
|
+
<div className="flex flex-col gap-1">
|
|
63
|
+
<p className="font-medium text-foreground text-sm">This project couldn't be loaded</p>
|
|
64
|
+
<p className="text-muted-foreground text-sm">
|
|
65
|
+
Nothing was changed. Check your connection and try again.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
<Button className="rounded-full" onClick={onRetry} size="sm" type="button">
|
|
69
|
+
Try again
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -118,9 +118,14 @@ export function FunctionTreePanel({
|
|
|
118
118
|
<div className="flex h-full flex-col">
|
|
119
119
|
{/* Root nodes as a category grid — icon when available, otherwise a
|
|
120
120
|
two-letter abbreviation, with the full name in a hover tooltip.
|
|
121
|
-
Mirrors the Build tab's tile grid so the two panels read the same.
|
|
121
|
+
Mirrors the Build tab's tile grid so the two panels read the same.
|
|
122
|
+
|
|
123
|
+
`functionTree` is embedder-supplied and unbounded, and these tiles are
|
|
124
|
+
`aspect-square`, so row count grows with it while the item list below is
|
|
125
|
+
the only scroller. Without the cap a large taxonomy pushes the list out
|
|
126
|
+
of the panel entirely and nothing can scroll it back. */}
|
|
122
127
|
<TooltipProvider delayDuration={0} disableHoverableContent>
|
|
123
|
-
<div className="grid shrink-0 grid-cols-5 gap-1.5 border-border/70 border-b p-2">
|
|
128
|
+
<div className="grid max-h-[40%] shrink-0 grid-cols-5 gap-1.5 overflow-y-auto border-border/70 border-b p-2">
|
|
124
129
|
{functionTree.map((root) => {
|
|
125
130
|
const isActive = activeRoot?.slug === root.slug
|
|
126
131
|
return (
|