@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.
Files changed (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. 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
+ }