@pascal-app/editor 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +62 -0
- package/src/components/editor/custom-camera-controls.tsx +387 -0
- package/src/components/editor/editor-layout-v2.tsx +220 -0
- package/src/components/editor/export-manager.tsx +78 -0
- package/src/components/editor/first-person-controls.tsx +249 -0
- package/src/components/editor/floating-action-menu.tsx +231 -0
- package/src/components/editor/floorplan-panel.tsx +9609 -0
- package/src/components/editor/grid.tsx +161 -0
- package/src/components/editor/index.tsx +928 -0
- package/src/components/editor/node-action-menu.tsx +66 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
- package/src/components/editor/selection-manager.tsx +897 -0
- package/src/components/editor/site-edge-labels.tsx +90 -0
- package/src/components/editor/thumbnail-generator.tsx +166 -0
- package/src/components/editor/wall-measurement-label.tsx +258 -0
- package/src/components/feedback-dialog.tsx +265 -0
- package/src/components/pascal-radio.tsx +280 -0
- package/src/components/preview-button.tsx +16 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
- package/src/components/systems/roof/roof-edit-system.tsx +69 -0
- package/src/components/systems/stair/stair-edit-system.tsx +69 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
- package/src/components/systems/zone/zone-system.tsx +87 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
- package/src/components/tools/door/door-math.ts +110 -0
- package/src/components/tools/door/door-tool.tsx +293 -0
- package/src/components/tools/door/move-door-tool.tsx +373 -0
- package/src/components/tools/item/item-tool.tsx +26 -0
- package/src/components/tools/item/move-tool.tsx +90 -0
- package/src/components/tools/item/placement-math.ts +85 -0
- package/src/components/tools/item/placement-strategies.ts +556 -0
- package/src/components/tools/item/placement-types.ts +117 -0
- package/src/components/tools/item/use-draft-node.ts +227 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
- package/src/components/tools/roof/move-roof-tool.tsx +288 -0
- package/src/components/tools/roof/roof-tool.tsx +318 -0
- package/src/components/tools/select/box-select-tool.tsx +626 -0
- package/src/components/tools/shared/cursor-sphere.tsx +119 -0
- package/src/components/tools/shared/polygon-editor.tsx +361 -0
- package/src/components/tools/site/site-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
- package/src/components/tools/slab/slab-tool.tsx +322 -0
- package/src/components/tools/stair/stair-defaults.ts +7 -0
- package/src/components/tools/stair/stair-tool.tsx +194 -0
- package/src/components/tools/tool-manager.tsx +120 -0
- package/src/components/tools/wall/wall-drafting.ts +140 -0
- package/src/components/tools/wall/wall-tool.tsx +210 -0
- package/src/components/tools/window/move-window-tool.tsx +410 -0
- package/src/components/tools/window/window-math.ts +117 -0
- package/src/components/tools/window/window-tool.tsx +303 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
- package/src/components/tools/zone/zone-tool.tsx +364 -0
- package/src/components/ui/action-menu/action-button.tsx +59 -0
- package/src/components/ui/action-menu/camera-actions.tsx +74 -0
- package/src/components/ui/action-menu/control-modes.tsx +240 -0
- package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
- package/src/components/ui/action-menu/index.tsx +152 -0
- package/src/components/ui/action-menu/structure-tools.tsx +100 -0
- package/src/components/ui/action-menu/view-toggles.tsx +397 -0
- package/src/components/ui/command-palette/editor-commands.tsx +396 -0
- package/src/components/ui/command-palette/index.tsx +730 -0
- package/src/components/ui/controls/action-button.tsx +33 -0
- package/src/components/ui/controls/material-picker.tsx +194 -0
- package/src/components/ui/controls/metric-control.tsx +262 -0
- package/src/components/ui/controls/panel-section.tsx +65 -0
- package/src/components/ui/controls/segmented-control.tsx +45 -0
- package/src/components/ui/controls/slider-control.tsx +245 -0
- package/src/components/ui/controls/toggle-control.tsx +38 -0
- package/src/components/ui/floating-level-selector.tsx +355 -0
- package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
- package/src/components/ui/helpers/helper-manager.tsx +33 -0
- package/src/components/ui/helpers/item-helper.tsx +40 -0
- package/src/components/ui/helpers/roof-helper.tsx +16 -0
- package/src/components/ui/helpers/slab-helper.tsx +20 -0
- package/src/components/ui/helpers/wall-helper.tsx +20 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
- package/src/components/ui/panels/ceiling-panel.tsx +230 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
- package/src/components/ui/panels/door-panel.tsx +600 -0
- package/src/components/ui/panels/item-panel.tsx +306 -0
- package/src/components/ui/panels/panel-manager.tsx +59 -0
- package/src/components/ui/panels/panel-wrapper.tsx +80 -0
- package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
- package/src/components/ui/panels/reference-panel.tsx +177 -0
- package/src/components/ui/panels/roof-panel.tsx +262 -0
- package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
- package/src/components/ui/panels/slab-panel.tsx +228 -0
- package/src/components/ui/panels/stair-panel.tsx +304 -0
- package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
- package/src/components/ui/panels/wall-panel.tsx +123 -0
- package/src/components/ui/panels/window-panel.tsx +441 -0
- package/src/components/ui/primitives/button.tsx +69 -0
- package/src/components/ui/primitives/card.tsx +75 -0
- package/src/components/ui/primitives/color-dot.tsx +61 -0
- package/src/components/ui/primitives/context-menu.tsx +227 -0
- package/src/components/ui/primitives/dialog.tsx +129 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
- package/src/components/ui/primitives/error-boundary.tsx +52 -0
- package/src/components/ui/primitives/input.tsx +21 -0
- package/src/components/ui/primitives/number-input.tsx +187 -0
- package/src/components/ui/primitives/opacity-control.tsx +79 -0
- package/src/components/ui/primitives/popover.tsx +42 -0
- package/src/components/ui/primitives/separator.tsx +28 -0
- package/src/components/ui/primitives/sheet.tsx +130 -0
- package/src/components/ui/primitives/shortcut-token.tsx +64 -0
- package/src/components/ui/primitives/sidebar.tsx +855 -0
- package/src/components/ui/primitives/skeleton.tsx +13 -0
- package/src/components/ui/primitives/slider.tsx +58 -0
- package/src/components/ui/primitives/switch.tsx +29 -0
- package/src/components/ui/primitives/tooltip.tsx +57 -0
- package/src/components/ui/scene-loader.tsx +40 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
- package/src/components/ui/sidebar/icon-rail.tsx +147 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
- package/src/components/ui/sidebar/tab-bar.tsx +39 -0
- package/src/components/ui/slider-demo.tsx +36 -0
- package/src/components/ui/slider.tsx +81 -0
- package/src/components/ui/viewer-toolbar.tsx +342 -0
- package/src/components/viewer-overlay.tsx +499 -0
- package/src/components/viewer-zone-system.tsx +48 -0
- package/src/contexts/presets-context.tsx +121 -0
- package/src/hooks/use-auto-save.ts +194 -0
- package/src/hooks/use-contextual-tools.ts +52 -0
- package/src/hooks/use-grid-events.ts +106 -0
- package/src/hooks/use-keyboard.ts +214 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-reduced-motion.ts +20 -0
- package/src/index.tsx +33 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/level-selection.ts +31 -0
- package/src/lib/scene.ts +394 -0
- package/src/lib/sfx/index.ts +2 -0
- package/src/lib/sfx-bus.ts +49 -0
- package/src/lib/sfx-player.ts +60 -0
- package/src/lib/utils.ts +43 -0
- package/src/store/use-audio.tsx +45 -0
- package/src/store/use-command-registry.ts +36 -0
- package/src/store/use-editor.tsx +522 -0
- package/src/store/use-palette-view-registry.ts +45 -0
- package/src/store/use-upload.ts +90 -0
- package/src/three-types.ts +3 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from '../../../lib/utils'
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
className={cn('animate-pulse rounded-md bg-accent', className)}
|
|
7
|
+
data-slot="skeleton"
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as SliderPrimitive from '@radix-ui/react-slider'
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../../../lib/utils'
|
|
7
|
+
|
|
8
|
+
function Slider({
|
|
9
|
+
className,
|
|
10
|
+
defaultValue,
|
|
11
|
+
value,
|
|
12
|
+
min = 0,
|
|
13
|
+
max = 100,
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
|
16
|
+
const _values = React.useMemo(
|
|
17
|
+
() => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),
|
|
18
|
+
[value, defaultValue, min, max],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<SliderPrimitive.Root
|
|
23
|
+
className={cn(
|
|
24
|
+
'relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[disabled]:opacity-50',
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
data-slot="slider"
|
|
28
|
+
defaultValue={defaultValue}
|
|
29
|
+
max={max}
|
|
30
|
+
min={min}
|
|
31
|
+
value={value}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
<SliderPrimitive.Track
|
|
35
|
+
className={cn(
|
|
36
|
+
'relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5',
|
|
37
|
+
)}
|
|
38
|
+
data-slot="slider-track"
|
|
39
|
+
>
|
|
40
|
+
<SliderPrimitive.Range
|
|
41
|
+
className={cn(
|
|
42
|
+
'absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full',
|
|
43
|
+
)}
|
|
44
|
+
data-slot="slider-range"
|
|
45
|
+
/>
|
|
46
|
+
</SliderPrimitive.Track>
|
|
47
|
+
{Array.from({ length: _values.length }, (_, index) => (
|
|
48
|
+
<SliderPrimitive.Thumb
|
|
49
|
+
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:outline-hidden focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50"
|
|
50
|
+
data-slot="slider-thumb"
|
|
51
|
+
key={index}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
</SliderPrimitive.Root>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Slider }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as SwitchPrimitives from '@radix-ui/react-switch'
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from './../../../lib/utils'
|
|
7
|
+
|
|
8
|
+
const Switch = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<SwitchPrimitives.Root
|
|
13
|
+
className={cn(
|
|
14
|
+
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
ref={ref}
|
|
19
|
+
>
|
|
20
|
+
<SwitchPrimitives.Thumb
|
|
21
|
+
className={cn(
|
|
22
|
+
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
|
|
23
|
+
)}
|
|
24
|
+
/>
|
|
25
|
+
</SwitchPrimitives.Root>
|
|
26
|
+
))
|
|
27
|
+
Switch.displayName = SwitchPrimitives.Root.displayName
|
|
28
|
+
|
|
29
|
+
export { Switch }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
|
4
|
+
import type * as React from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../../../lib/utils'
|
|
7
|
+
|
|
8
|
+
function TooltipProvider({
|
|
9
|
+
delayDuration = 0,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
12
|
+
return (
|
|
13
|
+
<TooltipPrimitive.Provider
|
|
14
|
+
data-slot="tooltip-provider"
|
|
15
|
+
delayDuration={delayDuration}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
22
|
+
return (
|
|
23
|
+
<TooltipProvider>
|
|
24
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
25
|
+
</TooltipProvider>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
30
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function TooltipContent({
|
|
34
|
+
className,
|
|
35
|
+
sideOffset = 0,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipPrimitive.Portal>
|
|
41
|
+
<TooltipPrimitive.Content
|
|
42
|
+
className={cn(
|
|
43
|
+
'fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-foreground px-3 py-1.5 font-barlow text-background text-xs data-[state=closed]:animate-out',
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
data-slot="tooltip-content"
|
|
47
|
+
sideOffset={sideOffset}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
|
52
|
+
</TooltipPrimitive.Content>
|
|
53
|
+
</TooltipPrimitive.Portal>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
const LOADERS = [
|
|
7
|
+
'pascal-loader-1',
|
|
8
|
+
'pascal-loader-2',
|
|
9
|
+
'pascal-loader-3',
|
|
10
|
+
'pascal-loader-4',
|
|
11
|
+
'pascal-loader-5',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
interface SceneLoaderProps {
|
|
15
|
+
className?: string
|
|
16
|
+
fullScreen?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SceneLoader({ className, fullScreen = false }: SceneLoaderProps) {
|
|
20
|
+
const [loaderClass, setLoaderClass] = useState<string | null>(null)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Pick a random loader on mount
|
|
24
|
+
setLoaderClass(LOADERS[Math.floor(Math.random() * LOADERS.length)] ?? LOADERS[0]!)
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
if (!loaderClass) return null
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={cn(
|
|
32
|
+
'z-100 flex items-center justify-center bg-background/80 backdrop-blur-md transition-opacity duration-300',
|
|
33
|
+
fullScreen ? 'fixed inset-0' : 'absolute inset-0',
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
<div className={cn(loaderClass, 'text-foreground opacity-80')} />
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useEffect } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
CommandPalette,
|
|
6
|
+
type CommandPaletteEmptyAction,
|
|
7
|
+
} from './../../../components/ui/command-palette'
|
|
8
|
+
import { EditorCommands } from './../../../components/ui/command-palette/editor-commands'
|
|
9
|
+
import {
|
|
10
|
+
SidebarContent,
|
|
11
|
+
SidebarHeader,
|
|
12
|
+
useSidebarStore,
|
|
13
|
+
} from './../../../components/ui/primitives/sidebar'
|
|
14
|
+
import { cn } from './../../../lib/utils'
|
|
15
|
+
import useEditor from './../../../store/use-editor'
|
|
16
|
+
import { type ExtraPanel, IconRail } from './icon-rail'
|
|
17
|
+
import { SettingsPanel, type SettingsPanelProps } from './panels/settings-panel'
|
|
18
|
+
import { SitePanel, type SitePanelProps } from './panels/site-panel'
|
|
19
|
+
|
|
20
|
+
interface AppSidebarProps {
|
|
21
|
+
appMenuButton?: ReactNode
|
|
22
|
+
sidebarTop?: ReactNode
|
|
23
|
+
settingsPanelProps?: SettingsPanelProps
|
|
24
|
+
sitePanelProps?: SitePanelProps
|
|
25
|
+
extraPanels?: ExtraPanel[]
|
|
26
|
+
commandPaletteEmptyAction?: CommandPaletteEmptyAction
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function AppSidebar({
|
|
30
|
+
appMenuButton,
|
|
31
|
+
sidebarTop,
|
|
32
|
+
settingsPanelProps,
|
|
33
|
+
sitePanelProps,
|
|
34
|
+
extraPanels,
|
|
35
|
+
commandPaletteEmptyAction,
|
|
36
|
+
}: AppSidebarProps) {
|
|
37
|
+
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
38
|
+
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
39
|
+
const hasActivePanel =
|
|
40
|
+
activePanel === 'site' ||
|
|
41
|
+
activePanel === 'settings' ||
|
|
42
|
+
Boolean(extraPanels?.some((panel) => panel.id === activePanel))
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
// Widen default sidebar (288px → 432px) for better project title visibility
|
|
46
|
+
const store = useSidebarStore.getState()
|
|
47
|
+
if (store.width <= 288) {
|
|
48
|
+
store.setWidth(432)
|
|
49
|
+
}
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!hasActivePanel) {
|
|
54
|
+
setActivePanel('site')
|
|
55
|
+
}
|
|
56
|
+
}, [hasActivePanel, setActivePanel])
|
|
57
|
+
|
|
58
|
+
const renderPanelContent = () => {
|
|
59
|
+
switch (activePanel) {
|
|
60
|
+
case 'site':
|
|
61
|
+
return <SitePanel {...sitePanelProps} />
|
|
62
|
+
case 'settings':
|
|
63
|
+
return <SettingsPanel {...settingsPanelProps} />
|
|
64
|
+
default: {
|
|
65
|
+
const extra = extraPanels?.find((p) => p.id === activePanel)
|
|
66
|
+
if (extra) {
|
|
67
|
+
const Component = extra.component
|
|
68
|
+
return <Component />
|
|
69
|
+
}
|
|
70
|
+
return <SitePanel {...sitePanelProps} />
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<div className={cn('dark flex h-full w-full bg-sidebar text-sidebar-foreground')}>
|
|
78
|
+
{/* Icon Rail */}
|
|
79
|
+
<IconRail
|
|
80
|
+
activePanel={activePanel}
|
|
81
|
+
appMenuButton={appMenuButton}
|
|
82
|
+
extraPanels={extraPanels}
|
|
83
|
+
onPanelChange={setActivePanel}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{/* Panel Content */}
|
|
87
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
88
|
+
{sidebarTop && (
|
|
89
|
+
<SidebarHeader className="relative flex-col items-start justify-center gap-1 border-border/50 border-b px-3 py-3">
|
|
90
|
+
{sidebarTop}
|
|
91
|
+
</SidebarHeader>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<SidebarContent className={cn('no-scrollbar flex flex-1 flex-col overflow-hidden')}>
|
|
95
|
+
{renderPanelContent()}
|
|
96
|
+
</SidebarContent>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<EditorCommands />
|
|
100
|
+
<CommandPalette emptyAction={commandPaletteEmptyAction} />
|
|
101
|
+
</>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from './../../../components/ui/primitives/tooltip'
|
|
9
|
+
import { cn } from './../../../lib/utils'
|
|
10
|
+
|
|
11
|
+
export type PanelId = string
|
|
12
|
+
|
|
13
|
+
export type ExtraPanel = { id: string; icon: ReactNode; label: string; component: ComponentType }
|
|
14
|
+
|
|
15
|
+
interface IconRailProps {
|
|
16
|
+
activePanel: PanelId
|
|
17
|
+
onPanelChange: (panel: PanelId) => void
|
|
18
|
+
appMenuButton?: ReactNode
|
|
19
|
+
extraPanels?: ExtraPanel[]
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sitePanel: { id: PanelId; iconSrc: string; label: string } = {
|
|
24
|
+
id: 'site',
|
|
25
|
+
iconSrc: '/icons/level.png',
|
|
26
|
+
label: 'Site',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const settingsPanel: { id: PanelId; iconSrc: string; label: string } = {
|
|
30
|
+
id: 'settings',
|
|
31
|
+
iconSrc: '/icons/settings.png',
|
|
32
|
+
label: 'Settings',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const panels: { id: PanelId; iconSrc: string; label: string }[] = [sitePanel, settingsPanel]
|
|
36
|
+
|
|
37
|
+
export function IconRail({
|
|
38
|
+
activePanel,
|
|
39
|
+
onPanelChange,
|
|
40
|
+
appMenuButton,
|
|
41
|
+
extraPanels,
|
|
42
|
+
className,
|
|
43
|
+
}: IconRailProps) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={cn(
|
|
47
|
+
'flex h-full w-11 flex-col items-center gap-1 border-border/50 border-r py-2',
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{/* App menu slot */}
|
|
52
|
+
{appMenuButton}
|
|
53
|
+
|
|
54
|
+
{/* Divider */}
|
|
55
|
+
<div className="mb-1 h-px w-8 bg-border/50" />
|
|
56
|
+
|
|
57
|
+
{/* Site panel */}
|
|
58
|
+
{[sitePanel].map((panel) => {
|
|
59
|
+
const isActive = activePanel === panel.id
|
|
60
|
+
return (
|
|
61
|
+
<Tooltip key={panel.id}>
|
|
62
|
+
<TooltipTrigger asChild>
|
|
63
|
+
<button
|
|
64
|
+
className={cn(
|
|
65
|
+
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
66
|
+
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
67
|
+
)}
|
|
68
|
+
onClick={() => onPanelChange(panel.id)}
|
|
69
|
+
type="button"
|
|
70
|
+
>
|
|
71
|
+
<img
|
|
72
|
+
alt={panel.label}
|
|
73
|
+
className={cn(
|
|
74
|
+
'h-6 w-6 object-contain transition-all',
|
|
75
|
+
!isActive && 'opacity-50 saturate-0',
|
|
76
|
+
)}
|
|
77
|
+
src={panel.iconSrc}
|
|
78
|
+
/>
|
|
79
|
+
</button>
|
|
80
|
+
</TooltipTrigger>
|
|
81
|
+
<TooltipContent side="right">{panel.label}</TooltipContent>
|
|
82
|
+
</Tooltip>
|
|
83
|
+
)
|
|
84
|
+
})}
|
|
85
|
+
|
|
86
|
+
{/* Extra panels (injected between site and settings) */}
|
|
87
|
+
{extraPanels?.map((panel) => {
|
|
88
|
+
const isActive = activePanel === panel.id
|
|
89
|
+
return (
|
|
90
|
+
<Tooltip key={panel.id}>
|
|
91
|
+
<TooltipTrigger asChild>
|
|
92
|
+
<button
|
|
93
|
+
className={cn(
|
|
94
|
+
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
95
|
+
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
96
|
+
)}
|
|
97
|
+
onClick={() => onPanelChange(panel.id)}
|
|
98
|
+
type="button"
|
|
99
|
+
>
|
|
100
|
+
<span
|
|
101
|
+
className={cn(
|
|
102
|
+
'flex h-6 w-6 items-center justify-center transition-all',
|
|
103
|
+
!isActive && 'opacity-50',
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{panel.icon}
|
|
107
|
+
</span>
|
|
108
|
+
</button>
|
|
109
|
+
</TooltipTrigger>
|
|
110
|
+
<TooltipContent side="right">{panel.label}</TooltipContent>
|
|
111
|
+
</Tooltip>
|
|
112
|
+
)
|
|
113
|
+
})}
|
|
114
|
+
|
|
115
|
+
{/* Settings panel */}
|
|
116
|
+
{[settingsPanel].map((panel) => {
|
|
117
|
+
const isActive = activePanel === panel.id
|
|
118
|
+
return (
|
|
119
|
+
<Tooltip key={panel.id}>
|
|
120
|
+
<TooltipTrigger asChild>
|
|
121
|
+
<button
|
|
122
|
+
className={cn(
|
|
123
|
+
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
124
|
+
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
125
|
+
)}
|
|
126
|
+
onClick={() => onPanelChange(panel.id)}
|
|
127
|
+
type="button"
|
|
128
|
+
>
|
|
129
|
+
<img
|
|
130
|
+
alt={panel.label}
|
|
131
|
+
className={cn(
|
|
132
|
+
'h-6 w-6 object-contain transition-all',
|
|
133
|
+
!isActive && 'opacity-50 saturate-0',
|
|
134
|
+
)}
|
|
135
|
+
src={panel.iconSrc}
|
|
136
|
+
/>
|
|
137
|
+
</button>
|
|
138
|
+
</TooltipTrigger>
|
|
139
|
+
<TooltipContent side="right">{panel.label}</TooltipContent>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
)
|
|
142
|
+
})}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { panels }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Volume2, VolumeX } from 'lucide-react'
|
|
2
|
+
import { Button } from '../../../../../components/ui/primitives/button'
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogTrigger,
|
|
10
|
+
} from '../../../../../components/ui/primitives/dialog'
|
|
11
|
+
import { Slider } from '../../../../../components/ui/slider'
|
|
12
|
+
import useAudio from '../../../../../store/use-audio'
|
|
13
|
+
|
|
14
|
+
export function AudioSettingsDialog() {
|
|
15
|
+
const {
|
|
16
|
+
masterVolume,
|
|
17
|
+
sfxVolume,
|
|
18
|
+
radioVolume,
|
|
19
|
+
muted,
|
|
20
|
+
setMasterVolume,
|
|
21
|
+
setSfxVolume,
|
|
22
|
+
setRadioVolume,
|
|
23
|
+
toggleMute,
|
|
24
|
+
} = useAudio()
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Dialog>
|
|
28
|
+
<DialogTrigger asChild>
|
|
29
|
+
<Button className="w-full justify-start gap-2" variant="outline">
|
|
30
|
+
{muted ? <VolumeX className="size-4" /> : <Volume2 className="size-4" />}
|
|
31
|
+
Audio Settings
|
|
32
|
+
</Button>
|
|
33
|
+
</DialogTrigger>
|
|
34
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
35
|
+
<DialogHeader>
|
|
36
|
+
<DialogTitle>Audio Settings</DialogTitle>
|
|
37
|
+
<DialogDescription>Adjust volume levels and mute settings</DialogDescription>
|
|
38
|
+
</DialogHeader>
|
|
39
|
+
<div className="space-y-6 py-4">
|
|
40
|
+
{/* Master Volume */}
|
|
41
|
+
<div className="space-y-2">
|
|
42
|
+
<div className="flex items-center justify-between">
|
|
43
|
+
<label className="font-medium text-sm">Master Volume</label>
|
|
44
|
+
<span className="text-muted-foreground text-sm">{masterVolume}%</span>
|
|
45
|
+
</div>
|
|
46
|
+
<Slider
|
|
47
|
+
disabled={muted}
|
|
48
|
+
max={100}
|
|
49
|
+
onValueChange={(value) => value[0] !== undefined && setMasterVolume(value[0])}
|
|
50
|
+
step={1}
|
|
51
|
+
value={[masterVolume]}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Radio Volume */}
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<div className="flex items-center justify-between">
|
|
58
|
+
<label className="font-medium text-sm">Radio Volume</label>
|
|
59
|
+
<span className="text-muted-foreground text-sm">{radioVolume}%</span>
|
|
60
|
+
</div>
|
|
61
|
+
<Slider
|
|
62
|
+
disabled={muted}
|
|
63
|
+
max={100}
|
|
64
|
+
onValueChange={(value) => value[0] !== undefined && setRadioVolume(value[0])}
|
|
65
|
+
step={1}
|
|
66
|
+
value={[radioVolume]}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* SFX Volume */}
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<div className="flex items-center justify-between">
|
|
73
|
+
<label className="font-medium text-sm">Sound Effects</label>
|
|
74
|
+
<span className="text-muted-foreground text-sm">{sfxVolume}%</span>
|
|
75
|
+
</div>
|
|
76
|
+
<Slider
|
|
77
|
+
disabled={muted}
|
|
78
|
+
max={100}
|
|
79
|
+
onValueChange={(value) => value[0] !== undefined && setSfxVolume(value[0])}
|
|
80
|
+
step={1}
|
|
81
|
+
value={[sfxVolume]}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Mute Toggle */}
|
|
86
|
+
<div className="border-t pt-4">
|
|
87
|
+
<Button
|
|
88
|
+
className="w-full justify-start gap-2"
|
|
89
|
+
onClick={toggleMute}
|
|
90
|
+
variant={muted ? 'default' : 'outline'}
|
|
91
|
+
>
|
|
92
|
+
{muted ? <VolumeX className="size-4" /> : <Volume2 className="size-4" />}
|
|
93
|
+
{muted ? 'Unmute All Sounds' : 'Mute All Sounds'}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</DialogContent>
|
|
98
|
+
</Dialog>
|
|
99
|
+
)
|
|
100
|
+
}
|