@pascal-app/editor 0.7.0 → 0.9.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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Ephemeral store for a placement tool's 2D floor-plan ghost. A registry
|
|
2
|
+
// placement tool (e.g. column / elevator) publishes a fully-positioned,
|
|
3
|
+
// transient preview node on each `grid:move`; the floor-plan
|
|
4
|
+
// placement-preview layer subscribes and renders the node's `def.floorplan`
|
|
5
|
+
// footprint as a faint ghost that follows the cursor. The 3D view already
|
|
6
|
+
// shows a translucent mesh preview, so this only feeds the 2D layer.
|
|
7
|
+
//
|
|
8
|
+
// Editor-only: the read-only viewer route never places nodes. Lives here
|
|
9
|
+
// rather than in `core` for that reason; node-kind tools (e.g. column) reach
|
|
10
|
+
// it through the `@pascal-app/editor` public surface, the same way they
|
|
11
|
+
// already consume `triggerSFX`. Producers clear on commit, cancel, and
|
|
12
|
+
// unmount.
|
|
13
|
+
|
|
14
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
|
|
17
|
+
type PlacementPreviewState = {
|
|
18
|
+
/** Transient preview node, already positioned + rotated at the (snapped,
|
|
19
|
+
* aligned) cursor. `null` when no placement is active. */
|
|
20
|
+
node: AnyNode | null
|
|
21
|
+
set(node: AnyNode | null): void
|
|
22
|
+
clear(): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const usePlacementPreview = create<PlacementPreviewState>((set) => ({
|
|
26
|
+
node: null,
|
|
27
|
+
set: (node) => set({ node }),
|
|
28
|
+
clear: () => set({ node: null }),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
export default usePlacementPreview
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ephemeral preview state for wall move's bridge walls.
|
|
5
|
+
*
|
|
6
|
+
* When the user drags a wall whose corner neighbours are off-axis to
|
|
7
|
+
* the move direction, the junction planner emits a `bridgePlan` for
|
|
8
|
+
* each — a new wall that would be inserted between the original and
|
|
9
|
+
* new corner on commit. The 3D `MoveWallTool` already renders these
|
|
10
|
+
* as translucent ghost meshes mid-drag; the 2D `wallFloorplanMoveTarget`
|
|
11
|
+
* writes here so the floor-plan SVG layer can render the same hint.
|
|
12
|
+
*
|
|
13
|
+
* Writer: `packages/nodes/src/wall/floorplan-move.ts` (on each `apply`,
|
|
14
|
+
* cleared on `commit` and by the move overlay's cleanup).
|
|
15
|
+
* Reader: `packages/editor/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx`.
|
|
16
|
+
*/
|
|
17
|
+
export type WallMoveGhostBridge = {
|
|
18
|
+
id: string
|
|
19
|
+
start: [number, number]
|
|
20
|
+
end: [number, number]
|
|
21
|
+
/** Plan-space thickness already passed through `getFloorplanWallThickness`. */
|
|
22
|
+
thickness: number
|
|
23
|
+
color: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type WallMoveGhostsState = {
|
|
27
|
+
bridges: WallMoveGhostBridge[]
|
|
28
|
+
setBridges: (bridges: WallMoveGhostBridge[]) => void
|
|
29
|
+
clear: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const useWallMoveGhosts = create<WallMoveGhostsState>((set) => ({
|
|
33
|
+
bridges: [],
|
|
34
|
+
setBridges: (bridges) => set({ bridges }),
|
|
35
|
+
clear: () => set({ bridges: [] }),
|
|
36
|
+
}))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Ephemeral store for the "magnetic" wall-snap beacon shown during wall
|
|
2
|
+
// drafting. The wall tool writes the active snap point on pointermove when
|
|
3
|
+
// the draft endpoint locks onto existing wall geometry (a corner or a point
|
|
4
|
+
// along a wall); the 3D beacon layer subscribes and draws a vertical marker
|
|
5
|
+
// there. Cleared on commit, cancel, and unmount — same lifecycle as
|
|
6
|
+
// `use-alignment-guides`.
|
|
7
|
+
|
|
8
|
+
import { create } from 'zustand'
|
|
9
|
+
|
|
10
|
+
/** Which kind of wall geometry the draft point snapped to. */
|
|
11
|
+
export type WallSnapKind = 'endpoint' | 'midpoint' | 'intersection' | 'wall'
|
|
12
|
+
|
|
13
|
+
export type WallSnapPoint = {
|
|
14
|
+
/** Building-local plan coordinates (XZ meters). */
|
|
15
|
+
x: number
|
|
16
|
+
z: number
|
|
17
|
+
kind: WallSnapKind
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type WallSnapIndicatorState = {
|
|
21
|
+
point: WallSnapPoint | null
|
|
22
|
+
set(point: WallSnapPoint | null): void
|
|
23
|
+
clear(): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const useWallSnapIndicator = create<WallSnapIndicatorState>((set) => ({
|
|
27
|
+
point: null,
|
|
28
|
+
set: (point) => set({ point }),
|
|
29
|
+
clear: () => set({ point: null }),
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
export default useWallSnapIndicator
|
package/tsconfig.json
CHANGED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { emitter, sceneRegistry } from '@pascal-app/core'
|
|
4
|
-
import { useThree } from '@react-three/fiber'
|
|
5
|
-
import { useCallback, useEffect } from 'react'
|
|
6
|
-
import * as THREE from 'three'
|
|
7
|
-
import { usePresetsAdapter } from '../../contexts/presets-context'
|
|
8
|
-
|
|
9
|
-
const THUMBNAIL_SIZE = 1080
|
|
10
|
-
const CAMERA_FOV = 45
|
|
11
|
-
|
|
12
|
-
export const PresetThumbnailGenerator = () => {
|
|
13
|
-
const gl = useThree((state) => state.gl)
|
|
14
|
-
const scene = useThree((state) => state.scene)
|
|
15
|
-
const adapter = usePresetsAdapter()
|
|
16
|
-
|
|
17
|
-
const generate = useCallback(
|
|
18
|
-
async ({ presetId, nodeId }: { presetId: string; nodeId: string }) => {
|
|
19
|
-
const target = sceneRegistry.nodes.get(nodeId)
|
|
20
|
-
if (!target) {
|
|
21
|
-
console.error('❌ PresetThumbnail: node not found', nodeId)
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Compute each mesh's transform relative to the target node (cancels world
|
|
26
|
-
// position/rotation), so the item is always rendered at origin with a known
|
|
27
|
-
// neutral orientation regardless of where it's placed in the scene.
|
|
28
|
-
target.updateWorldMatrix(true, true)
|
|
29
|
-
const targetInverse = new THREE.Matrix4().copy(target.matrixWorld).invert()
|
|
30
|
-
const relMatrix = new THREE.Matrix4()
|
|
31
|
-
|
|
32
|
-
const clones: THREE.Object3D[] = []
|
|
33
|
-
target.traverse((obj) => {
|
|
34
|
-
if (
|
|
35
|
-
!(obj instanceof THREE.Mesh || obj instanceof THREE.Line || obj instanceof THREE.Points)
|
|
36
|
-
)
|
|
37
|
-
return
|
|
38
|
-
const c = obj.clone(false) // shallow clone: copies geometry, material, visible — no children
|
|
39
|
-
relMatrix.multiplyMatrices(targetInverse, obj.matrixWorld)
|
|
40
|
-
relMatrix.decompose(c.position, c.quaternion, c.scale)
|
|
41
|
-
scene.add(c)
|
|
42
|
-
clones.push(c)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
if (clones.length === 0) {
|
|
46
|
-
console.error('❌ PresetThumbnail: no renderable objects found', nodeId)
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Combined bounding box across all clones
|
|
51
|
-
const box = new THREE.Box3()
|
|
52
|
-
for (const c of clones) box.expandByObject(c)
|
|
53
|
-
|
|
54
|
-
if (box.isEmpty()) {
|
|
55
|
-
for (const c of clones) scene.remove(c)
|
|
56
|
-
console.error('❌ PresetThumbnail: empty bounding box', nodeId)
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const sphere = new THREE.Sphere()
|
|
61
|
-
box.getBoundingSphere(sphere)
|
|
62
|
-
|
|
63
|
-
// Camera: aspect matches canvas (center-cropped to square after render)
|
|
64
|
-
const { width, height } = gl.domElement
|
|
65
|
-
const camera = new THREE.PerspectiveCamera(CAMERA_FOV, width / height, 0.01, 1000)
|
|
66
|
-
const dir = new THREE.Vector3(-0.5, 0.5, 0.5).normalize()
|
|
67
|
-
const fovRad = (CAMERA_FOV * Math.PI) / 180
|
|
68
|
-
const dist = (sphere.radius / Math.tan(fovRad / 2)) * 1.3
|
|
69
|
-
camera.position.copy(sphere.center).addScaledVector(dir, dist)
|
|
70
|
-
camera.lookAt(sphere.center)
|
|
71
|
-
camera.updateProjectionMatrix()
|
|
72
|
-
|
|
73
|
-
// Hide all scene geometry except the clones — leave lights, cameras, etc. intact
|
|
74
|
-
const cloneSet = new Set<THREE.Object3D>(clones)
|
|
75
|
-
const snapshot = new Map<THREE.Object3D, boolean>()
|
|
76
|
-
scene.traverse((obj) => {
|
|
77
|
-
if (cloneSet.has(obj)) return
|
|
78
|
-
if (
|
|
79
|
-
!(obj instanceof THREE.Mesh || obj instanceof THREE.Line || obj instanceof THREE.Points)
|
|
80
|
-
)
|
|
81
|
-
return
|
|
82
|
-
snapshot.set(obj, obj.visible)
|
|
83
|
-
obj.visible = false
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
gl.render(scene, camera)
|
|
87
|
-
|
|
88
|
-
// Restore visibility and remove clones
|
|
89
|
-
snapshot.forEach((wasVisible, obj) => {
|
|
90
|
-
obj.visible = wasVisible
|
|
91
|
-
})
|
|
92
|
-
for (const c of clones) scene.remove(c)
|
|
93
|
-
|
|
94
|
-
// Center-crop to square and scale to THUMBNAIL_SIZE
|
|
95
|
-
const minDim = Math.min(width, height)
|
|
96
|
-
const sx = Math.round((width - minDim) / 2)
|
|
97
|
-
const sy = Math.round((height - minDim) / 2)
|
|
98
|
-
const offscreen = document.createElement('canvas')
|
|
99
|
-
offscreen.width = THUMBNAIL_SIZE
|
|
100
|
-
offscreen.height = THUMBNAIL_SIZE
|
|
101
|
-
const ctx = offscreen.getContext('2d')!
|
|
102
|
-
ctx.drawImage(gl.domElement, sx, sy, minDim, minDim, 0, 0, THUMBNAIL_SIZE, THUMBNAIL_SIZE)
|
|
103
|
-
|
|
104
|
-
offscreen.toBlob(async (blob) => {
|
|
105
|
-
if (!blob) {
|
|
106
|
-
console.error('❌ PresetThumbnail: failed to create blob')
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
if (!adapter.uploadPresetThumbnail) return
|
|
110
|
-
const thumbnailUrl = await adapter.uploadPresetThumbnail(presetId, blob)
|
|
111
|
-
if (thumbnailUrl) {
|
|
112
|
-
emitter.emit('preset:thumbnail-updated', { presetId, thumbnailUrl })
|
|
113
|
-
}
|
|
114
|
-
}, 'image/png')
|
|
115
|
-
},
|
|
116
|
-
[gl, scene, adapter],
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
emitter.on('preset:generate-thumbnail', generate)
|
|
121
|
-
return () => emitter.off('preset:generate-thumbnail', generate)
|
|
122
|
-
}, [generate])
|
|
123
|
-
|
|
124
|
-
return null
|
|
125
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { Point2D, RoofNode, RoofSegmentNode } from '@pascal-app/core'
|
|
4
|
-
import { memo } from 'react'
|
|
5
|
-
import { toSvgX, toSvgY } from '../svg-paths'
|
|
6
|
-
|
|
7
|
-
type FloorplanLineSegment = {
|
|
8
|
-
start: Point2D
|
|
9
|
-
end: Point2D
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type FloorplanRoofSegmentEntry = {
|
|
13
|
-
segment: RoofSegmentNode
|
|
14
|
-
points: string
|
|
15
|
-
ridgeLine: FloorplanLineSegment | null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type FloorplanRoofEntry = {
|
|
19
|
-
roof: RoofNode
|
|
20
|
-
segments: FloorplanRoofSegmentEntry[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type FloorplanRoofPalette = {
|
|
24
|
-
roofFill: string
|
|
25
|
-
roofActiveFill: string
|
|
26
|
-
roofSelectedFill: string
|
|
27
|
-
roofStroke: string
|
|
28
|
-
roofActiveStroke: string
|
|
29
|
-
roofSelectedStroke: string
|
|
30
|
-
roofRidgeStroke: string
|
|
31
|
-
roofSelectedRidgeStroke: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type FloorplanRoofLayerProps = {
|
|
35
|
-
highlightedIdSet: ReadonlySet<string>
|
|
36
|
-
palette: FloorplanRoofPalette
|
|
37
|
-
roofEntries: FloorplanRoofEntry[]
|
|
38
|
-
selectedIdSet: ReadonlySet<string>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
|
|
42
|
-
highlightedIdSet,
|
|
43
|
-
palette,
|
|
44
|
-
roofEntries,
|
|
45
|
-
selectedIdSet,
|
|
46
|
-
}: FloorplanRoofLayerProps) {
|
|
47
|
-
if (roofEntries.length === 0) {
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<>
|
|
53
|
-
{roofEntries.map(({ roof, segments }) => {
|
|
54
|
-
const roofSelected = selectedIdSet.has(roof.id)
|
|
55
|
-
const roofHighlighted = highlightedIdSet.has(roof.id)
|
|
56
|
-
const hasSelectedSegment = segments.some(({ segment }) => selectedIdSet.has(segment.id))
|
|
57
|
-
const hasHighlightedSegment = segments.some(({ segment }) =>
|
|
58
|
-
highlightedIdSet.has(segment.id),
|
|
59
|
-
)
|
|
60
|
-
const isRoofActive =
|
|
61
|
-
roofSelected || roofHighlighted || hasSelectedSegment || hasHighlightedSegment
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<g key={roof.id} pointerEvents="none">
|
|
65
|
-
{segments.map(({ points, ridgeLine, segment }) => {
|
|
66
|
-
const isSegmentSelected = selectedIdSet.has(segment.id)
|
|
67
|
-
const isSegmentHighlighted = highlightedIdSet.has(segment.id)
|
|
68
|
-
const isSegmentActive = isSegmentSelected || isSegmentHighlighted
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<g key={segment.id}>
|
|
72
|
-
<polygon
|
|
73
|
-
fill={
|
|
74
|
-
isSegmentActive
|
|
75
|
-
? palette.roofSelectedFill
|
|
76
|
-
: isRoofActive
|
|
77
|
-
? palette.roofActiveFill
|
|
78
|
-
: palette.roofFill
|
|
79
|
-
}
|
|
80
|
-
points={points}
|
|
81
|
-
stroke={
|
|
82
|
-
isSegmentActive
|
|
83
|
-
? palette.roofSelectedStroke
|
|
84
|
-
: isRoofActive
|
|
85
|
-
? palette.roofActiveStroke
|
|
86
|
-
: palette.roofStroke
|
|
87
|
-
}
|
|
88
|
-
strokeWidth={isSegmentActive ? '2.25' : isRoofActive ? '1.75' : '1.1'}
|
|
89
|
-
vectorEffect="non-scaling-stroke"
|
|
90
|
-
/>
|
|
91
|
-
{ridgeLine ? (
|
|
92
|
-
<line
|
|
93
|
-
fill="none"
|
|
94
|
-
stroke={
|
|
95
|
-
isSegmentActive ? palette.roofSelectedRidgeStroke : palette.roofRidgeStroke
|
|
96
|
-
}
|
|
97
|
-
strokeWidth={isSegmentActive ? '2' : '1.4'}
|
|
98
|
-
vectorEffect="non-scaling-stroke"
|
|
99
|
-
x1={toSvgX(ridgeLine.start.x)}
|
|
100
|
-
x2={toSvgX(ridgeLine.end.x)}
|
|
101
|
-
y1={toSvgY(ridgeLine.start.y)}
|
|
102
|
-
y2={toSvgY(ridgeLine.end.y)}
|
|
103
|
-
/>
|
|
104
|
-
) : null}
|
|
105
|
-
</g>
|
|
106
|
-
)
|
|
107
|
-
})}
|
|
108
|
-
</g>
|
|
109
|
-
)
|
|
110
|
-
})}
|
|
111
|
-
</>
|
|
112
|
-
)
|
|
113
|
-
})
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useScene } from '@pascal-app/core'
|
|
4
|
-
import { ImageIcon, MessageSquare, X } from 'lucide-react'
|
|
5
|
-
import { useCallback, useRef, useState } from 'react'
|
|
6
|
-
import { Button } from './ui/primitives/button'
|
|
7
|
-
import {
|
|
8
|
-
Dialog,
|
|
9
|
-
DialogContent,
|
|
10
|
-
DialogDescription,
|
|
11
|
-
DialogHeader,
|
|
12
|
-
DialogTitle,
|
|
13
|
-
} from './ui/primitives/dialog'
|
|
14
|
-
|
|
15
|
-
const MAX_IMAGES = 5
|
|
16
|
-
const MAX_IMAGE_SIZE = 5 * 1024 * 1024
|
|
17
|
-
|
|
18
|
-
type ImagePreview = { file: File; url: string }
|
|
19
|
-
|
|
20
|
-
export function FeedbackDialog({
|
|
21
|
-
projectId: projectIdProp,
|
|
22
|
-
onSubmit,
|
|
23
|
-
}: {
|
|
24
|
-
projectId?: string
|
|
25
|
-
onSubmit?: (data: {
|
|
26
|
-
message: string
|
|
27
|
-
projectId?: string
|
|
28
|
-
sceneGraph: unknown
|
|
29
|
-
images: File[]
|
|
30
|
-
}) => Promise<{ success: boolean; error?: string }>
|
|
31
|
-
}) {
|
|
32
|
-
const projectId = projectIdProp
|
|
33
|
-
|
|
34
|
-
const [open, setOpen] = useState(false)
|
|
35
|
-
const [message, setMessage] = useState('')
|
|
36
|
-
const [images, setImages] = useState<ImagePreview[]>([])
|
|
37
|
-
const [isDragging, setIsDragging] = useState(false)
|
|
38
|
-
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
39
|
-
const [error, setError] = useState<string | null>(null)
|
|
40
|
-
const [sent, setSent] = useState(false)
|
|
41
|
-
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
42
|
-
const dragCounter = useRef(0)
|
|
43
|
-
|
|
44
|
-
const handleOpen = () => {
|
|
45
|
-
setOpen(true)
|
|
46
|
-
setSent(false)
|
|
47
|
-
setError(null)
|
|
48
|
-
setMessage('')
|
|
49
|
-
setImages([])
|
|
50
|
-
setIsDragging(false)
|
|
51
|
-
dragCounter.current = 0
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const handleClose = () => {
|
|
55
|
-
if (isSubmitting) return
|
|
56
|
-
setOpen(false)
|
|
57
|
-
images.forEach((img) => {
|
|
58
|
-
URL.revokeObjectURL(img.url)
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const addFiles = useCallback((files: FileList | File[]) => {
|
|
63
|
-
const incoming = Array.from(files).filter(
|
|
64
|
-
(f) => f.type.startsWith('image/') && f.size <= MAX_IMAGE_SIZE,
|
|
65
|
-
)
|
|
66
|
-
setImages((prev) => {
|
|
67
|
-
const remaining = MAX_IMAGES - prev.length
|
|
68
|
-
const added = incoming.slice(0, remaining).map((file) => ({
|
|
69
|
-
file,
|
|
70
|
-
url: URL.createObjectURL(file),
|
|
71
|
-
}))
|
|
72
|
-
return [...prev, ...added]
|
|
73
|
-
})
|
|
74
|
-
}, [])
|
|
75
|
-
|
|
76
|
-
const removeImage = (index: number) => {
|
|
77
|
-
setImages((prev) => {
|
|
78
|
-
const img = prev[index]
|
|
79
|
-
if (img) URL.revokeObjectURL(img.url)
|
|
80
|
-
return prev.filter((_, i) => i !== index)
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ── Drag handlers (on the entire dialog content) ──
|
|
85
|
-
const onDragEnter = (e: React.DragEvent) => {
|
|
86
|
-
e.preventDefault()
|
|
87
|
-
e.stopPropagation()
|
|
88
|
-
dragCounter.current++
|
|
89
|
-
if (e.dataTransfer.types.includes('Files')) {
|
|
90
|
-
setIsDragging(true)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const onDragLeave = (e: React.DragEvent) => {
|
|
95
|
-
e.preventDefault()
|
|
96
|
-
e.stopPropagation()
|
|
97
|
-
dragCounter.current--
|
|
98
|
-
if (dragCounter.current === 0) {
|
|
99
|
-
setIsDragging(false)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const onDragOver = (e: React.DragEvent) => {
|
|
104
|
-
e.preventDefault()
|
|
105
|
-
e.stopPropagation()
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const onDrop = (e: React.DragEvent) => {
|
|
109
|
-
e.preventDefault()
|
|
110
|
-
e.stopPropagation()
|
|
111
|
-
dragCounter.current = 0
|
|
112
|
-
setIsDragging(false)
|
|
113
|
-
if (e.dataTransfer.files.length > 0) {
|
|
114
|
-
addFiles(e.dataTransfer.files)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
119
|
-
e.preventDefault()
|
|
120
|
-
setError(null)
|
|
121
|
-
setIsSubmitting(true)
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
if (!onSubmit) return
|
|
125
|
-
const { nodes, rootNodeIds } = useScene.getState()
|
|
126
|
-
const sceneGraph = { nodes, rootNodeIds }
|
|
127
|
-
const result = await onSubmit({
|
|
128
|
-
message,
|
|
129
|
-
projectId,
|
|
130
|
-
sceneGraph,
|
|
131
|
-
images: images.map((img) => img.file),
|
|
132
|
-
})
|
|
133
|
-
if (result.success) {
|
|
134
|
-
setSent(true)
|
|
135
|
-
setTimeout(() => setOpen(false), 1500)
|
|
136
|
-
} else {
|
|
137
|
-
setError(result.error ?? 'Something went wrong')
|
|
138
|
-
}
|
|
139
|
-
} finally {
|
|
140
|
-
setIsSubmitting(false)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<>
|
|
146
|
-
<button
|
|
147
|
-
className="flex items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 font-medium text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent/90"
|
|
148
|
-
onClick={handleOpen}
|
|
149
|
-
>
|
|
150
|
-
<MessageSquare className="h-4 w-4" />
|
|
151
|
-
Feedback
|
|
152
|
-
</button>
|
|
153
|
-
|
|
154
|
-
<Dialog onOpenChange={handleClose} open={open}>
|
|
155
|
-
<DialogContent
|
|
156
|
-
className="sm:max-w-[460px]"
|
|
157
|
-
onDragEnter={onDragEnter}
|
|
158
|
-
onDragLeave={onDragLeave}
|
|
159
|
-
onDragOver={onDragOver}
|
|
160
|
-
onDrop={onDrop}
|
|
161
|
-
>
|
|
162
|
-
{/* Drag overlay — only visible when dragging files over the dialog */}
|
|
163
|
-
{isDragging && (
|
|
164
|
-
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg border-2 border-primary/50 border-dashed bg-primary/5 backdrop-blur-sm transition-all">
|
|
165
|
-
<div className="flex flex-col items-center gap-2 text-primary/70">
|
|
166
|
-
<ImageIcon className="h-8 w-8" />
|
|
167
|
-
<p className="font-medium text-sm">Drop images here</p>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
<DialogHeader>
|
|
173
|
-
<DialogTitle>Send Feedback</DialogTitle>
|
|
174
|
-
<DialogDescription>We'd love to hear your thoughts</DialogDescription>
|
|
175
|
-
</DialogHeader>
|
|
176
|
-
|
|
177
|
-
{sent ? (
|
|
178
|
-
<p className="py-4 text-center text-muted-foreground text-sm">
|
|
179
|
-
Thanks for your feedback!
|
|
180
|
-
</p>
|
|
181
|
-
) : (
|
|
182
|
-
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
183
|
-
<div>
|
|
184
|
-
<label className="font-medium text-sm" htmlFor="feedback-message">
|
|
185
|
-
Your feedback
|
|
186
|
-
</label>
|
|
187
|
-
<textarea
|
|
188
|
-
autoFocus
|
|
189
|
-
className="mt-1 w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
190
|
-
disabled={isSubmitting}
|
|
191
|
-
id="feedback-message"
|
|
192
|
-
onChange={(e) => setMessage(e.target.value)}
|
|
193
|
-
placeholder="Share your thoughts, suggestions, feature requests, or report issues..."
|
|
194
|
-
rows={5}
|
|
195
|
-
value={message}
|
|
196
|
-
/>
|
|
197
|
-
</div>
|
|
198
|
-
|
|
199
|
-
{/* Image thumbnails */}
|
|
200
|
-
{images.length > 0 && (
|
|
201
|
-
<div className="flex flex-wrap gap-2">
|
|
202
|
-
{images.map((img, i) => (
|
|
203
|
-
<div
|
|
204
|
-
className="group relative h-14 w-14 overflow-hidden rounded-md border border-border"
|
|
205
|
-
key={img.url}
|
|
206
|
-
>
|
|
207
|
-
<img alt="" className="h-full w-full object-cover" src={img.url} />
|
|
208
|
-
<button
|
|
209
|
-
className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
|
|
210
|
-
onClick={() => removeImage(i)}
|
|
211
|
-
type="button"
|
|
212
|
-
>
|
|
213
|
-
<X className="h-4 w-4 text-white" />
|
|
214
|
-
</button>
|
|
215
|
-
</div>
|
|
216
|
-
))}
|
|
217
|
-
</div>
|
|
218
|
-
)}
|
|
219
|
-
|
|
220
|
-
{error && <p className="text-destructive text-sm">{error}</p>}
|
|
221
|
-
|
|
222
|
-
<div className="flex items-center justify-between">
|
|
223
|
-
{/* Subtle attach button */}
|
|
224
|
-
<button
|
|
225
|
-
className="flex items-center gap-1.5 text-muted-foreground text-xs transition-colors hover:text-foreground disabled:opacity-40"
|
|
226
|
-
disabled={isSubmitting || images.length >= MAX_IMAGES}
|
|
227
|
-
onClick={() => fileInputRef.current?.click()}
|
|
228
|
-
type="button"
|
|
229
|
-
>
|
|
230
|
-
<ImageIcon className="h-3.5 w-3.5" />
|
|
231
|
-
{images.length > 0 ? `${images.length}/${MAX_IMAGES}` : 'Attach'}
|
|
232
|
-
</button>
|
|
233
|
-
<input
|
|
234
|
-
accept="image/*"
|
|
235
|
-
className="hidden"
|
|
236
|
-
multiple
|
|
237
|
-
onChange={(e) => {
|
|
238
|
-
if (e.target.files) addFiles(e.target.files)
|
|
239
|
-
e.target.value = ''
|
|
240
|
-
}}
|
|
241
|
-
ref={fileInputRef}
|
|
242
|
-
type="file"
|
|
243
|
-
/>
|
|
244
|
-
|
|
245
|
-
<div className="flex gap-2">
|
|
246
|
-
<Button
|
|
247
|
-
disabled={isSubmitting}
|
|
248
|
-
onClick={handleClose}
|
|
249
|
-
type="button"
|
|
250
|
-
variant="outline"
|
|
251
|
-
>
|
|
252
|
-
Cancel
|
|
253
|
-
</Button>
|
|
254
|
-
<Button disabled={isSubmitting || !message.trim() || !onSubmit} type="submit">
|
|
255
|
-
{isSubmitting ? 'Sending...' : 'Send Feedback'}
|
|
256
|
-
</Button>
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
</form>
|
|
260
|
-
)}
|
|
261
|
-
</DialogContent>
|
|
262
|
-
</Dialog>
|
|
263
|
-
</>
|
|
264
|
-
)
|
|
265
|
-
}
|