@pascal-app/editor 1.0.0 → 1.0.3
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 -6
- package/src/components/editor/bake-exporter.tsx +4 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floating-action-menu.tsx +14 -1
- package/src/components/editor/floorplan-panel.tsx +233 -166
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +13 -2
- package/src/components/editor/handles/use-handle-drag.ts +80 -8
- package/src/components/editor/index.tsx +36 -13
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +162 -69
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/selection-manager.tsx +49 -0
- package/src/components/editor/thumbnail-generator.tsx +11 -2
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +169 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
- package/src/components/tools/item/placement-strategies.ts +212 -200
- package/src/components/tools/item/placement-types.ts +4 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/use-draft-node.ts +136 -47
- package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +509 -0
- package/src/components/tools/registry/item-surface-move.ts +463 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +27 -13
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +107 -0
- package/src/components/tools/shared/surface-rejection.tsx +43 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/command-palette/editor-commands.tsx +54 -1
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/slider-control.tsx +87 -35
- package/src/components/ui/floating-level-selector.tsx +81 -5
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +9 -17
- package/src/components/ui/panels/reference-panel.tsx +3 -3
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer/viewer-scene-header.tsx +3 -0
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +25 -2
- package/src/hooks/use-ceiling-events.ts +7 -1
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +43 -28
- package/src/index.tsx +68 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floorplan/floorplan-export.test.ts +142 -2
- package/src/lib/floorplan/floorplan-export.tsx +9 -3
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/floorplan/floorplan-readonly.ts +4 -0
- package/src/lib/fresh-planar-placement.ts +173 -33
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +265 -16
- package/src/lib/glb-export.ts +197 -37
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/portable-export.test.ts +89 -0
- package/src/lib/portable-export.ts +84 -11
- package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
- package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
- package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
- package/src/lib/print-shell-compiler-protocol.ts +8 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +43 -16
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-send-to-app.ts +21 -0
|
@@ -28,7 +28,15 @@ import {
|
|
|
28
28
|
useScene,
|
|
29
29
|
} from '@pascal-app/core'
|
|
30
30
|
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
ClipboardPaste,
|
|
33
|
+
Copy,
|
|
34
|
+
GripVertical,
|
|
35
|
+
MoreVertical,
|
|
36
|
+
Plus,
|
|
37
|
+
Trash2,
|
|
38
|
+
X,
|
|
39
|
+
} from 'lucide-react'
|
|
32
40
|
import {
|
|
33
41
|
type ButtonHTMLAttributes,
|
|
34
42
|
type CSSProperties,
|
|
@@ -45,6 +53,7 @@ import {
|
|
|
45
53
|
} from '../../lib/level-duplication'
|
|
46
54
|
import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
|
|
47
55
|
import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
|
|
56
|
+
import { unitMemberLevels, leaveUnitFocus } from '../../lib/units'
|
|
48
57
|
import { useLinearDisplay } from '../../lib/use-linear-display'
|
|
49
58
|
import { cn } from '../../lib/utils'
|
|
50
59
|
import { ActionButton } from './controls/action-button'
|
|
@@ -131,12 +140,15 @@ function LevelRow({
|
|
|
131
140
|
onDuplicate,
|
|
132
141
|
onPaste,
|
|
133
142
|
onRequestDelete,
|
|
143
|
+
unitDotColor,
|
|
134
144
|
}: {
|
|
135
145
|
level: LevelNode
|
|
136
146
|
isSelected: boolean
|
|
137
147
|
isDragging?: boolean
|
|
138
148
|
dragHandleProps?: ButtonHTMLAttributes<HTMLButtonElement>
|
|
139
149
|
dragHandleRef?: (element: HTMLButtonElement | null) => void
|
|
150
|
+
/** Focused-unit color when the unit has zones on this level. */
|
|
151
|
+
unitDotColor?: string
|
|
140
152
|
onSelect: () => void
|
|
141
153
|
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
142
154
|
onPaste?: () => void
|
|
@@ -145,11 +157,14 @@ function LevelRow({
|
|
|
145
157
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
146
158
|
const [isEditing, setIsEditing] = useState(false)
|
|
147
159
|
const updateNode = useScene((s) => s.updateNode)
|
|
148
|
-
const { isImperial, toDisplay, displayUnit } = useLinearDisplay('m', 2)
|
|
160
|
+
const { isImperial, toDisplay, displayUnit, precision: displayPrecision } = useLinearDisplay('m', 2)
|
|
149
161
|
|
|
150
162
|
const storeyHeight = getStoredLevelHeight(level)
|
|
151
|
-
//
|
|
152
|
-
const
|
|
163
|
+
// Decimal units keep the compact readout; integer millimeters must retain trailing zeroes.
|
|
164
|
+
const formattedStoreyHeight = toDisplay(storeyHeight).toFixed(displayPrecision)
|
|
165
|
+
const storeyHeightLabel = `${
|
|
166
|
+
displayPrecision > 0 ? formattedStoreyHeight.replace(/0$/, '') : formattedStoreyHeight
|
|
167
|
+
} ${displayUnit}`
|
|
153
168
|
// Same rule as the site panel and command palette: the ordinal-0 ground
|
|
154
169
|
// floor is the vertical model's zero anchor and must never be deletable.
|
|
155
170
|
const canDeleteLevel = level.level !== 0
|
|
@@ -215,6 +230,13 @@ function LevelRow({
|
|
|
215
230
|
type="button"
|
|
216
231
|
>
|
|
217
232
|
<span className="truncate">{getLevelDisplayName(level)}</span>
|
|
233
|
+
{unitDotColor && (
|
|
234
|
+
<span
|
|
235
|
+
className="ml-1.5 h-1.5 w-1.5 shrink-0 rounded-full"
|
|
236
|
+
style={{ backgroundColor: unitDotColor }}
|
|
237
|
+
title="Focused unit has zones here"
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
218
240
|
</button>
|
|
219
241
|
|
|
220
242
|
{/* Storey height badge — opens the height popover */}
|
|
@@ -244,7 +266,7 @@ function LevelRow({
|
|
|
244
266
|
precision={3}
|
|
245
267
|
step={0.1}
|
|
246
268
|
unit="m"
|
|
247
|
-
value={
|
|
269
|
+
value={storeyHeight}
|
|
248
270
|
/>
|
|
249
271
|
<div className="mt-1.5 grid grid-cols-3 gap-1.5">
|
|
250
272
|
{heightPresets.map((preset) => (
|
|
@@ -343,9 +365,11 @@ function SortableLevelRow({
|
|
|
343
365
|
onDuplicate,
|
|
344
366
|
onPaste,
|
|
345
367
|
onRequestDelete,
|
|
368
|
+
unitDotColor,
|
|
346
369
|
}: {
|
|
347
370
|
level: LevelNode
|
|
348
371
|
isSelected: boolean
|
|
372
|
+
unitDotColor?: string
|
|
349
373
|
onSelect: () => void
|
|
350
374
|
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
351
375
|
onPaste?: () => void
|
|
@@ -381,7 +405,42 @@ function SortableLevelRow({
|
|
|
381
405
|
onPaste={onPaste}
|
|
382
406
|
onRequestDelete={onRequestDelete}
|
|
383
407
|
onSelect={onSelect}
|
|
408
|
+
unitDotColor={unitDotColor}
|
|
409
|
+
/>
|
|
410
|
+
</div>
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── Unit focus chip: name + × to end focus, only while a unit is focused ────
|
|
415
|
+
|
|
416
|
+
function UnitFocusChip() {
|
|
417
|
+
const focusedUnitId = useViewer((s) => s.focusedUnitId)
|
|
418
|
+
const focusedUnit = useScene((s) => {
|
|
419
|
+
const unit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
|
|
420
|
+
return unit?.type === 'unit' ? unit : null
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
if (!focusedUnit) return null
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<div
|
|
427
|
+
className="mt-4 flex h-7 max-w-48 items-center gap-1.5 rounded-full border border-border bg-background/90 pr-1 pl-2.5 font-medium text-xs shadow-2xl backdrop-blur-md"
|
|
428
|
+
data-testid="unit-focus-chip"
|
|
429
|
+
>
|
|
430
|
+
<span
|
|
431
|
+
className="h-2 w-2 shrink-0 rounded-full"
|
|
432
|
+
style={{ backgroundColor: focusedUnit.color }}
|
|
384
433
|
/>
|
|
434
|
+
<span className="truncate">{focusedUnit.name || 'Unit'}</span>
|
|
435
|
+
<button
|
|
436
|
+
aria-label="Exit unit focus"
|
|
437
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
|
|
438
|
+
onClick={() => leaveUnitFocus()}
|
|
439
|
+
title="Exit unit focus"
|
|
440
|
+
type="button"
|
|
441
|
+
>
|
|
442
|
+
<X className="h-3 w-3" />
|
|
443
|
+
</button>
|
|
385
444
|
</div>
|
|
386
445
|
)
|
|
387
446
|
}
|
|
@@ -398,6 +457,17 @@ export function FloatingLevelSelector() {
|
|
|
398
457
|
|
|
399
458
|
const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
|
|
400
459
|
const [draggingLevelId, setDraggingLevelId] = useState<string | null>(null)
|
|
460
|
+
const focusedUnitId = useViewer((s) => s.focusedUnitId)
|
|
461
|
+
const focusedUnitColor = useScene((s) => {
|
|
462
|
+
const unit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
|
|
463
|
+
return unit?.type === 'unit' ? unit.color : null
|
|
464
|
+
})
|
|
465
|
+
const focusedUnitLevelIds = useScene(
|
|
466
|
+
useShallow((s) => {
|
|
467
|
+
const unit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
|
|
468
|
+
return unit?.type === 'unit' ? unitMemberLevels(unit, s.nodes).map((level) => level.id) : []
|
|
469
|
+
}),
|
|
470
|
+
)
|
|
401
471
|
const sensors = useSensors(
|
|
402
472
|
useSensor(PointerSensor, {
|
|
403
473
|
activationConstraint: { distance: 4 },
|
|
@@ -628,6 +698,11 @@ export function FloatingLevelSelector() {
|
|
|
628
698
|
<SortableLevelRow
|
|
629
699
|
isSelected={isSelected}
|
|
630
700
|
level={level}
|
|
701
|
+
unitDotColor={
|
|
702
|
+
focusedUnitColor && focusedUnitLevelIds.includes(level.id)
|
|
703
|
+
? focusedUnitColor
|
|
704
|
+
: undefined
|
|
705
|
+
}
|
|
631
706
|
onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
|
|
632
707
|
onPaste={() => handlePasteToLevel(level)}
|
|
633
708
|
onRequestDelete={() => setDeletingLevel(level)}
|
|
@@ -658,6 +733,7 @@ export function FloatingLevelSelector() {
|
|
|
658
733
|
</SortableContext>
|
|
659
734
|
</DndContext>
|
|
660
735
|
</div>
|
|
736
|
+
<UnitFocusChip />
|
|
661
737
|
</div>
|
|
662
738
|
|
|
663
739
|
{/* Delete confirmation dialog */}
|
|
@@ -29,6 +29,7 @@ import useEditor, { getActiveContinuationContext } from '../../../store/use-edit
|
|
|
29
29
|
import useInteractionScope, {
|
|
30
30
|
useActiveHandleDrag,
|
|
31
31
|
useMovingNode,
|
|
32
|
+
useReshapingNode,
|
|
32
33
|
} from '../../../store/use-interaction-scope'
|
|
33
34
|
import { BuildingHelper } from './building-helper'
|
|
34
35
|
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
@@ -84,6 +85,7 @@ function terrainSculptHints(verb: TerrainVerb, sampling: boolean): ContextualSho
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
type ActiveModifierKeys = {
|
|
88
|
+
alt: boolean
|
|
87
89
|
command: boolean
|
|
88
90
|
shift: boolean
|
|
89
91
|
}
|
|
@@ -93,6 +95,7 @@ const NO_CONTEXTUAL_HELP_SUBSCRIPTION = () => () => {}
|
|
|
93
95
|
|
|
94
96
|
function useActiveModifierKeys(): ActiveModifierKeys {
|
|
95
97
|
const [modifiers, setModifiers] = useState<ActiveModifierKeys>({
|
|
98
|
+
alt: false,
|
|
96
99
|
command: false,
|
|
97
100
|
shift: false,
|
|
98
101
|
})
|
|
@@ -101,6 +104,7 @@ function useActiveModifierKeys(): ActiveModifierKeys {
|
|
|
101
104
|
const updateModifiers = (event: KeyboardEvent) => {
|
|
102
105
|
const isKeyDown = event.type === 'keydown'
|
|
103
106
|
setModifiers({
|
|
107
|
+
alt: event.altKey || (isKeyDown && event.key === 'Alt'),
|
|
104
108
|
command:
|
|
105
109
|
event.metaKey ||
|
|
106
110
|
event.ctrlKey ||
|
|
@@ -109,7 +113,7 @@ function useActiveModifierKeys(): ActiveModifierKeys {
|
|
|
109
113
|
})
|
|
110
114
|
}
|
|
111
115
|
const clearModifiers = () => {
|
|
112
|
-
setModifiers({ command: false, shift: false })
|
|
116
|
+
setModifiers({ alt: false, command: false, shift: false })
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
window.addEventListener('keydown', updateModifiers)
|
|
@@ -135,6 +139,7 @@ export function HelperManager() {
|
|
|
135
139
|
const workspaceMode = useEditor((s) => s.workspaceMode)
|
|
136
140
|
const scope = useInteractionScope((s) => s.scope)
|
|
137
141
|
const movingNode = useMovingNode()
|
|
142
|
+
const reshapingNode = useReshapingNode()
|
|
138
143
|
const activeHandleDrag = useActiveHandleDrag()
|
|
139
144
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
140
145
|
const isMobile = useIsMobile()
|
|
@@ -190,10 +195,12 @@ export function HelperManager() {
|
|
|
190
195
|
: single?.type === 'duct-fitting' || single?.type === 'pipe-fitting'
|
|
191
196
|
? 'fitting'
|
|
192
197
|
: null
|
|
198
|
+
const hasOpeningRadiusSelection = single?.type === 'door' || single?.type === 'window'
|
|
193
199
|
return resolveSelectModeHelpHints({
|
|
194
200
|
selectedCount: selectedNodes.length,
|
|
195
201
|
hasMovableSelection: selectedNodes.some((node) => canDirectMoveNode(node)),
|
|
196
202
|
hasRotatableSelection: selectedNodes.some((node) => canDirectRotateNode(node)),
|
|
203
|
+
hasOpeningRadiusSelection,
|
|
197
204
|
commandPressed: modifiers.command,
|
|
198
205
|
shiftPressed: modifiers.shift,
|
|
199
206
|
mepSelection,
|
|
@@ -211,15 +218,16 @@ export function HelperManager() {
|
|
|
211
218
|
// tools — editor shortcut hints would only mislead there.
|
|
212
219
|
if (workspaceMode === 'studio') return null
|
|
213
220
|
|
|
214
|
-
// Rotating a node (or a multi-selection group) via its in-world gizmo:
|
|
215
|
-
// advertise Shift = free rotation, the same angle-step bypass wall drafting
|
|
216
|
-
// exposes. Takes priority over the idle select-mode hints since a handle
|
|
217
|
-
// drag is the active interaction.
|
|
218
221
|
if (
|
|
219
222
|
activeHandleDrag?.label === ROTATE_HANDLE_DRAG_LABEL ||
|
|
220
223
|
activeHandleDrag?.label === GROUP_ROTATE_DRAG_LABEL
|
|
221
224
|
) {
|
|
222
|
-
return
|
|
225
|
+
return (
|
|
226
|
+
<ContextualHelperPanel
|
|
227
|
+
hints={resolveRotateHandleHelpHints(modifiers.alt)}
|
|
228
|
+
snapContext={snapContext}
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
223
231
|
}
|
|
224
232
|
|
|
225
233
|
// Group-move drag / pick-up: the drag resolves to the 'item' snap context
|
|
@@ -254,6 +262,15 @@ export function HelperManager() {
|
|
|
254
262
|
// before the select branch so the idle "drag selected / add objects" hints
|
|
255
263
|
// never leak over an in-progress reshape — and it gets its own snapping chip.
|
|
256
264
|
if (scope.kind === 'reshaping') {
|
|
265
|
+
// A kind's own reshape brings its hints (`def.affordanceHints[reshape]`).
|
|
266
|
+
const hints = reshapingNode
|
|
267
|
+
? nodeRegistry.get(reshapingNode.type)?.affordanceHints?.[scope.reshape]
|
|
268
|
+
: undefined
|
|
269
|
+
if (hints) {
|
|
270
|
+
return (
|
|
271
|
+
<RegisteredToolHelper hints={hints} shiftPressed={modifiers.shift} snapContext={snapContext} />
|
|
272
|
+
)
|
|
273
|
+
}
|
|
257
274
|
return <ContextualHelperPanel hints={reshapingHints(scope.reshape)} snapContext={snapContext} />
|
|
258
275
|
}
|
|
259
276
|
|
|
@@ -297,6 +314,7 @@ export function HelperManager() {
|
|
|
297
314
|
return <ContextualHelperPanel hints={contextualEditHints} snapContext={snapContext} />
|
|
298
315
|
}
|
|
299
316
|
|
|
317
|
+
|
|
300
318
|
// Idle select only — an active scope (handle-drag, box-select, …) must not show
|
|
301
319
|
// the idle selection hints.
|
|
302
320
|
if (mode === 'select' && scope.kind === 'idle') {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ToolHint } from '@pascal-app/core'
|
|
2
|
-
import { useMemo, useSyncExternalStore } from 'react'
|
|
3
2
|
import type { ContinuationContext } from '../../../lib/continuation'
|
|
4
3
|
import type { SnapContext } from '../../../lib/snapping-mode'
|
|
5
|
-
import
|
|
4
|
+
import { useVisibleToolHints } from '../../../lib/panel-tool-options'
|
|
6
5
|
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -25,36 +24,7 @@ export function RegisteredToolHelper({
|
|
|
25
24
|
snapContext?: SnapContext | null
|
|
26
25
|
continuationContext?: ContinuationContext | null
|
|
27
26
|
}) {
|
|
28
|
-
|
|
29
|
-
// minimum (e.g. "Finish" at ≥ 3) only appear once they're actually possible.
|
|
30
|
-
const draftVertexCount = useEditor((s) => s.draftVertexCount)
|
|
31
|
-
const visibilityStore = useMemo(
|
|
32
|
-
() => ({
|
|
33
|
-
subscribe: (onChange: () => void) => {
|
|
34
|
-
const unsubscribers = hints.flatMap((hint) =>
|
|
35
|
-
hint.visible ? [hint.visible.subscribe(onChange)] : [],
|
|
36
|
-
)
|
|
37
|
-
return () => {
|
|
38
|
-
for (const unsubscribe of unsubscribers) unsubscribe()
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
getSnapshot: () => hints.map((hint) => (hint.visible?.value() === false ? '0' : '1')).join(''),
|
|
42
|
-
}),
|
|
43
|
-
[hints],
|
|
44
|
-
)
|
|
45
|
-
useSyncExternalStore(
|
|
46
|
-
visibilityStore.subscribe,
|
|
47
|
-
visibilityStore.getSnapshot,
|
|
48
|
-
visibilityStore.getSnapshot,
|
|
49
|
-
)
|
|
50
|
-
// Some hints are replaced by live contextual chips, so keep the generic
|
|
51
|
-
// registry renderer from duplicating stale/static versions.
|
|
52
|
-
const visible = hints.filter(
|
|
53
|
-
(hint) =>
|
|
54
|
-
!(hint.key === 'Shift' && hint.label === 'Cycle snapping mode') &&
|
|
55
|
-
hint.visible?.value() !== false &&
|
|
56
|
-
(hint.minDraftVertices == null || draftVertexCount >= hint.minDraftVertices),
|
|
57
|
-
)
|
|
27
|
+
const visible = useVisibleToolHints(hints)
|
|
58
28
|
if (visible.length === 0 && !snapContext && !continuationContext) return null
|
|
59
29
|
// Hints carrying a live-state `chip` render as mode chips next to the
|
|
60
30
|
// snapping / continuation rows; the rest stay static key rows.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { AssetInput } from '@pascal-app/core'
|
|
4
|
-
import { resolveCdnUrl
|
|
4
|
+
import { resolveCdnUrl } from '@pascal-app/viewer'
|
|
5
5
|
import { useEffect } from 'react'
|
|
6
6
|
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
7
7
|
import { cn } from './../../../lib/utils'
|
|
8
8
|
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
9
9
|
import { resolveAssetSnapTarget, SnapTargetBadge } from '../snap-target-badge'
|
|
10
|
-
import {
|
|
10
|
+
import { activateCatalogItem, filterCatalogItems, isCatalogItemSelected } from '../../../lib/catalog-panel-model'
|
|
11
11
|
|
|
12
12
|
export function ItemCatalog({
|
|
13
13
|
category,
|
|
@@ -32,26 +32,7 @@ export function ItemCatalog({
|
|
|
32
32
|
emptyState?: React.ReactNode
|
|
33
33
|
}) {
|
|
34
34
|
const selectedItem = useEditor((state) => state.selectedItem)
|
|
35
|
-
const
|
|
36
|
-
const setMode = useEditor((state) => state.setMode)
|
|
37
|
-
const setTool = useEditor((state) => state.setTool)
|
|
38
|
-
|
|
39
|
-
const sourceItems: CatalogItem[] = itemsOverride ?? CATALOG_ITEMS
|
|
40
|
-
// Server-provided results bypass all local filtering; otherwise filter by category/search/tags
|
|
41
|
-
const filteredItems: CatalogItem[] =
|
|
42
|
-
overrideItems ??
|
|
43
|
-
(() => {
|
|
44
|
-
const categoryItems = search
|
|
45
|
-
? sourceItems
|
|
46
|
-
: sourceItems.filter((item) => item.category === category)
|
|
47
|
-
return categoryItems.filter((item) => {
|
|
48
|
-
const tags = item.tags ?? []
|
|
49
|
-
if (activePlacementTag && !tags.includes(activePlacementTag)) return false
|
|
50
|
-
if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false
|
|
51
|
-
if (search && !item.name.toLowerCase().includes(search.toLowerCase())) return false
|
|
52
|
-
return true
|
|
53
|
-
})
|
|
54
|
-
})()
|
|
35
|
+
const filteredItems = filterCatalogItems({ category, items: itemsOverride, overrideItems, search, activePlacementTag, activeFunctionalTag })
|
|
55
36
|
|
|
56
37
|
if (filteredItems.length === 0 && emptyState) {
|
|
57
38
|
return <>{emptyState}</>
|
|
@@ -64,7 +45,7 @@ export function ItemCatalog({
|
|
|
64
45
|
>
|
|
65
46
|
{leadingTile}
|
|
66
47
|
{filteredItems.map((item, index) => {
|
|
67
|
-
const isSelected = selectedItem
|
|
48
|
+
const isSelected = isCatalogItemSelected(item, selectedItem)
|
|
68
49
|
const snapTarget = resolveAssetSnapTarget(item?.attachTo)
|
|
69
50
|
return (
|
|
70
51
|
<button
|
|
@@ -75,13 +56,7 @@ export function ItemCatalog({
|
|
|
75
56
|
key={index}
|
|
76
57
|
onClick={() => {
|
|
77
58
|
triggerSFX('sfx:menu-click')
|
|
78
|
-
|
|
79
|
-
// it would route shortcuts (rotate & co) to both the ghost and
|
|
80
|
-
// the selected node.
|
|
81
|
-
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
82
|
-
setSelectedItem(item)
|
|
83
|
-
setTool(item.tool ?? 'item')
|
|
84
|
-
setMode('build')
|
|
59
|
+
activateCatalogItem(item)
|
|
85
60
|
}}
|
|
86
61
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
87
62
|
type="button"
|
|
@@ -58,6 +58,32 @@ function ceilingCustomHeight(node: CeilingNode, nodes: Record<string, AnyNode>):
|
|
|
58
58
|
return Math.min(resolved, max)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function applyMultiHeightMode(
|
|
62
|
+
nodeIds: AnyNodeId[],
|
|
63
|
+
next: 'storey' | 'custom',
|
|
64
|
+
parametrics: ParametricDescriptor<AnyNode>,
|
|
65
|
+
) {
|
|
66
|
+
const nodes = useScene.getState().nodes as Record<string, AnyNode>
|
|
67
|
+
commitMultiNodeFields(
|
|
68
|
+
nodeIds,
|
|
69
|
+
(node) => {
|
|
70
|
+
const isCustom = (node as { height?: number }).height != null
|
|
71
|
+
if (next === 'custom') {
|
|
72
|
+
if (isCustom) return {}
|
|
73
|
+
if (node.type === 'ceiling') return { height: ceilingCustomHeight(node, nodes) }
|
|
74
|
+
if (node.type === 'wall') {
|
|
75
|
+
return { height: Math.max(0.1, getWallEffectiveHeightForNodes(node, nodes)) }
|
|
76
|
+
}
|
|
77
|
+
return {}
|
|
78
|
+
}
|
|
79
|
+
if (!isCustom) return {}
|
|
80
|
+
if (node.type === 'wall') return wallFollowsLevelPatch(node, nodes)
|
|
81
|
+
return { height: undefined }
|
|
82
|
+
},
|
|
83
|
+
parametrics,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
61
87
|
export function MultiHeightModeField({
|
|
62
88
|
nodeIds,
|
|
63
89
|
nodeType,
|
|
@@ -97,27 +123,7 @@ export function MultiHeightModeField({
|
|
|
97
123
|
|
|
98
124
|
const applyMode = useCallback(
|
|
99
125
|
(next: 'storey' | 'custom') => {
|
|
100
|
-
|
|
101
|
-
commitMultiNodeFields(
|
|
102
|
-
nodeIds,
|
|
103
|
-
(node) => {
|
|
104
|
-
const isCustom = (node as { height?: number }).height != null
|
|
105
|
-
if (next === 'custom') {
|
|
106
|
-
if (isCustom) return {}
|
|
107
|
-
if (node.type === 'ceiling') {
|
|
108
|
-
return { height: ceilingCustomHeight(node, nodes) }
|
|
109
|
-
}
|
|
110
|
-
if (node.type === 'wall') {
|
|
111
|
-
return { height: Math.max(0.1, getWallEffectiveHeightForNodes(node, nodes)) }
|
|
112
|
-
}
|
|
113
|
-
return {}
|
|
114
|
-
}
|
|
115
|
-
if (!isCustom) return {}
|
|
116
|
-
if (node.type === 'wall') return wallFollowsLevelPatch(node, nodes)
|
|
117
|
-
return { height: undefined }
|
|
118
|
-
},
|
|
119
|
-
parametrics,
|
|
120
|
-
)
|
|
126
|
+
applyMultiHeightMode(nodeIds, next, parametrics)
|
|
121
127
|
},
|
|
122
128
|
[nodeIds, parametrics],
|
|
123
129
|
)
|
|
@@ -72,8 +72,9 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
|
|
|
72
72
|
)}
|
|
73
73
|
{parametrics.groups.map((group, gi) => (
|
|
74
74
|
<MultiGroupFields
|
|
75
|
+
defaultExpanded={group.defaultExpanded}
|
|
75
76
|
fields={group.fields as ParamField<AnyNode>[]}
|
|
76
|
-
key={
|
|
77
|
+
key={`${nodeType}-${group.label}-${gi}`}
|
|
77
78
|
nodeIds={nodeIds}
|
|
78
79
|
nodeType={nodeType}
|
|
79
80
|
parametrics={parametrics}
|
|
@@ -88,12 +89,14 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
function MultiGroupFields({
|
|
92
|
+
defaultExpanded,
|
|
91
93
|
title,
|
|
92
94
|
fields,
|
|
93
95
|
nodeIds,
|
|
94
96
|
nodeType,
|
|
95
97
|
parametrics,
|
|
96
98
|
}: {
|
|
99
|
+
defaultExpanded?: boolean
|
|
97
100
|
title: string
|
|
98
101
|
fields: ParamField<AnyNode>[]
|
|
99
102
|
nodeIds: AnyNodeId[]
|
|
@@ -108,7 +111,7 @@ function MultiGroupFields({
|
|
|
108
111
|
)
|
|
109
112
|
if (genericFields.length === 0 || !anyVisible) return null
|
|
110
113
|
return (
|
|
111
|
-
<PanelSection title={title}>
|
|
114
|
+
<PanelSection defaultExpanded={defaultExpanded} title={title}>
|
|
112
115
|
{genericFields.map((field, fi) => {
|
|
113
116
|
if (
|
|
114
117
|
String(field.key) === 'height' &&
|
|
@@ -254,7 +257,7 @@ function MultiVec3Field({
|
|
|
254
257
|
restoreOnCommit={false}
|
|
255
258
|
step={0.05}
|
|
256
259
|
unit="m"
|
|
257
|
-
value={
|
|
260
|
+
value={axisValue}
|
|
258
261
|
/>
|
|
259
262
|
)
|
|
260
263
|
})}
|
|
@@ -15,6 +15,7 @@ import { Icon } from '@iconify/react'
|
|
|
15
15
|
import { Move, Trash2 } from 'lucide-react'
|
|
16
16
|
import { type ComponentType, lazy, Suspense, useCallback } from 'react'
|
|
17
17
|
import { resolveMoveActionNode } from '../../../lib/direct-manipulation'
|
|
18
|
+
import { commitParametricNodeFields } from '../../../lib/parametric-node-update'
|
|
18
19
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
19
20
|
import { collectZoneContentIds } from '../../../lib/zone-content'
|
|
20
21
|
import useEditor from '../../../store/use-editor'
|
|
@@ -61,22 +62,9 @@ export function ParametricInspector({
|
|
|
61
62
|
const handleUpdate = useCallback(
|
|
62
63
|
(patch: Partial<AnyNode>) => {
|
|
63
64
|
if (!selectedId) return
|
|
64
|
-
|
|
65
|
-
const node = scene.nodes[selectedId]
|
|
66
|
-
if (parametrics?.derive && node) {
|
|
67
|
-
const next = { ...node, ...patch } as AnyNode
|
|
68
|
-
patch = { ...patch, ...parametrics.derive(next, patch, node as AnyNode) }
|
|
69
|
-
}
|
|
70
|
-
// Bundle the edited node + any reconcile follow-ups into ONE
|
|
71
|
-
// updateNodes call so a single inspector edit is a single undo step.
|
|
72
|
-
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = [{ id: selectedId, data: patch }]
|
|
73
|
-
if (parametrics?.reconcile && node) {
|
|
74
|
-
const next = { ...node, ...patch } as AnyNode
|
|
75
|
-
updates.push(...parametrics.reconcile(node as AnyNode, next))
|
|
76
|
-
}
|
|
77
|
-
scene.updateNodes(updates)
|
|
65
|
+
commitParametricNodeFields(selectedId, patch)
|
|
78
66
|
},
|
|
79
|
-
[selectedId
|
|
67
|
+
[selectedId],
|
|
80
68
|
)
|
|
81
69
|
|
|
82
70
|
const clearSelection = useCallback(() => {
|
|
@@ -157,7 +145,11 @@ export function ParametricInspector({
|
|
|
157
145
|
width={320}
|
|
158
146
|
>
|
|
159
147
|
{parametrics.groups.map((group, gi) => (
|
|
160
|
-
<PanelSection
|
|
148
|
+
<PanelSection
|
|
149
|
+
defaultExpanded={group.defaultExpanded}
|
|
150
|
+
key={`${nodeType}-${group.label}-${gi}`}
|
|
151
|
+
title={group.label}
|
|
152
|
+
>
|
|
161
153
|
{group.fields.map((field, fi) => (
|
|
162
154
|
<FieldRenderer
|
|
163
155
|
key={`field-${gi}-${fi}-${String(field.key)}`}
|
|
@@ -175,7 +167,7 @@ export function ParametricInspector({
|
|
|
175
167
|
)}
|
|
176
168
|
{(canMove || canDelete || (parametrics.actions && parametrics.actions.length > 0)) && (
|
|
177
169
|
<PanelSection title="Actions">
|
|
178
|
-
<ActionGroup className={isZone ? 'flex-col' : undefined}>
|
|
170
|
+
<ActionGroup className={isZone ? 'flex-col' : parametrics.actions?.length ? 'grid grid-cols-2 gap-2' : undefined}>
|
|
179
171
|
{canMove && (
|
|
180
172
|
<ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
|
|
181
173
|
)}
|
|
@@ -376,7 +376,7 @@ export function ReferencePanel() {
|
|
|
376
376
|
precision={2}
|
|
377
377
|
step={0.1}
|
|
378
378
|
unit="m"
|
|
379
|
-
value={
|
|
379
|
+
value={node.position[0]}
|
|
380
380
|
/>
|
|
381
381
|
<SliderControl
|
|
382
382
|
label={
|
|
@@ -394,7 +394,7 @@ export function ReferencePanel() {
|
|
|
394
394
|
precision={2}
|
|
395
395
|
step={0.1}
|
|
396
396
|
unit="m"
|
|
397
|
-
value={
|
|
397
|
+
value={node.position[1]}
|
|
398
398
|
/>
|
|
399
399
|
<SliderControl
|
|
400
400
|
label={
|
|
@@ -412,7 +412,7 @@ export function ReferencePanel() {
|
|
|
412
412
|
precision={2}
|
|
413
413
|
step={0.1}
|
|
414
414
|
unit="m"
|
|
415
|
-
value={
|
|
415
|
+
value={node.position[2]}
|
|
416
416
|
/>
|
|
417
417
|
</PanelSection>
|
|
418
418
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Check } from 'lucide-react'
|
|
4
|
+
import type { ButtonHTMLAttributes } from 'react'
|
|
5
|
+
import { cn } from './../../../lib/utils'
|
|
6
|
+
|
|
7
|
+
interface CheckboxProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
|
8
|
+
checked: boolean
|
|
9
|
+
onCheckedChange: (checked: boolean) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Checkbox({ checked, onCheckedChange, className, ...props }: CheckboxProps) {
|
|
13
|
+
return (
|
|
14
|
+
<button
|
|
15
|
+
aria-checked={checked}
|
|
16
|
+
className={cn(
|
|
17
|
+
'flex h-3.5 w-3.5 shrink-0 cursor-pointer items-center justify-center rounded-[4px] border transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring',
|
|
18
|
+
checked
|
|
19
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
20
|
+
: 'border-border bg-background/60 text-transparent hover:border-foreground/40',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
role="checkbox"
|
|
24
|
+
type="button"
|
|
25
|
+
{...props}
|
|
26
|
+
onClick={(event) => {
|
|
27
|
+
event.stopPropagation()
|
|
28
|
+
onCheckedChange(!checked)
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<Check className="h-2.5 w-2.5" strokeWidth={3} />
|
|
32
|
+
</button>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -20,6 +20,10 @@ const MOUSE_SHORTCUTS = {
|
|
|
20
20
|
icon: 'ph:mouse-right-click-fill',
|
|
21
21
|
label: 'Right click',
|
|
22
22
|
},
|
|
23
|
+
Scroll: {
|
|
24
|
+
icon: 'ph:mouse-scroll-fill',
|
|
25
|
+
label: 'Scroll',
|
|
26
|
+
},
|
|
23
27
|
} as const
|
|
24
28
|
|
|
25
29
|
// The platform-agnostic command modifier. Both Cmd and Ctrl bind the action; we
|
|
@@ -28,12 +28,12 @@ export function AudioSettingsDialog() {
|
|
|
28
28
|
<DialogTrigger asChild>
|
|
29
29
|
<Button className="w-full justify-start gap-2" variant="outline">
|
|
30
30
|
{muted ? <VolumeX className="size-4" /> : <Volume2 className="size-4" />}
|
|
31
|
-
Audio
|
|
31
|
+
Audio settings
|
|
32
32
|
</Button>
|
|
33
33
|
</DialogTrigger>
|
|
34
34
|
<DialogContent className="sm:max-w-[425px]">
|
|
35
35
|
<DialogHeader>
|
|
36
|
-
<DialogTitle>Audio
|
|
36
|
+
<DialogTitle>Audio settings</DialogTitle>
|
|
37
37
|
<DialogDescription>Adjust volume levels and mute settings</DialogDescription>
|
|
38
38
|
</DialogHeader>
|
|
39
39
|
<div className="space-y-6 py-4">
|