Feat/ux round 3 (#124)

* Tree: add isLast prop & draw connectors

Introduce an optional isLast prop across all site panel tree node components and TreeNode so children know when they're the last sibling. TreeNodeWrapper now renders vertical and horizontal connector lines (adjusted by depth and isLast) and keeps the toggle button clickable (z-index). Children are passed isLast when mapped, so the UI can stop the vertical line for final siblings. Also bump several deps in apps/editor package.json and update the lockfile accordingly.

* chore: clean up monorepo dependencies and update packages

Move app/library dependencies out of root package.json into their
respective workspaces. Root now only contains monorepo tooling
(turbo, biome, supabase CLI, typescript, portless, ultracite).

Updates:
- next 16.1.0 → 16.1.6
- drizzle-orm ^0.39.0 → ^0.45.1
- drizzle-kit ^0.30.0 → ^0.31.9
- drizzle-zod ^0.5.1 → ^0.8.3
- three-bvh-csg ^0.0.17 → ^0.0.18
- supabase 2.75.3 → 2.76.15
- typescript 5.9.2 → 5.9.3 (all workspaces)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use semantic theme tokens and enable dark mode

Replace hardcoded zinc color classes with semantic design tokens (text-muted-foreground, text-foreground, border, bg-background, bg-border) across the action menu, camera controls, control modes, and view toggles for consistent theming. Update separators and borders to use bg-border/border-border. Add useEffect in the editor root to add/remove the 'dark' class on document.body and apply 'dark text-foreground' to the root container so the UI renders in dark mode; also import useEffect.

* Persistable sidebar width & resizer

Introduce a zustand store to persist sidebar width (key: `sidebar-preferences`) and track dragging state. Default width is 288px (18rem) and is clamped between 288 and 800px. Wire the store into SidebarProvider to drive the CSS variable for width and expose a data-dragging attribute. Add a SidebarResizer component that handles pointer events to resize the sidebar, toggles dragging state, updates cursor/user-select, and disables transitions while dragging. Small class and structure adjustments ensure the resizer is rendered and resizing behaves smoothly.

* Editor: floating action menu, selection/phase tweaks

Add a FloatingActionMenu (move/duplicate/delete) that attaches to selected 3D nodes and integrates with editor/viewer state; includes new cursor SVG and layout wrapper to use the custom cursor. Improve selection and phase behavior: auto-select first building+level on scene load (moved/centralized in project scene hook and editor init), add resolveBuildingId helper, and enhance SelectionManager to auto-switch between structure/furnish based on clicked nodes and to preserve building/level context when changing selection. Update sidebar tree node click handlers to switch phases for structural vs item nodes and stop hiding nodes by phase (keep tree items visible across phases). Add keyboard escape behavior to clear selections while keeping building/level context and reset selected reference. Misc: register unified event listeners for selection, position floating menu over objects using three.js Box3, and various small integration fixes.

* Add SceneLoader, loading states & editor fixes

Add a SceneLoader component and associated CSS loader styles and wire it into Editor, Viewer and page-level flows so a loading UI displays while project/scene data loads. Replace useLayoutEffect in editor page with a client-mounted useEffect guard to avoid hydration issues. Introduce isSceneLoading state and setter in the project store and set/clear it in useProjectScene; include sceneGraph when creating a new project and clear the scene beforehand.

Other changes: type-guard moving/duplicating logic in FloatingActionMenu and remove duplicated id on clones; refactor auto-selection logic for site/building/level and auto-select build/wall for empty levels in editor index and useProjectScene; optimize ZoneSystem to run per-frame (useFrame) and reduce unnecessary updates to visibility/label positions; add animated active background for layer toggle (motion) in site panel; add keyboard shortcuts to jump between levels (Ctrl/Cmd+ArrowUp/Down); simplify community-hub project-created handler to navigate straight to editor. These changes improve UX during load, enforce safer node operations, and optimize runtime rendering.

* Improve selection logic and refine UI components

Multiple editor and viewer updates:

- Selection: enhance selection-manager to support modifier (Meta/Ctrl) multi-select, compute next selection logic, track modifier keys, prevent grid deselect races, and add debug logs. Adapted selection handlers throughout scene/editor components to pass native events and modifier state.

- Sidebar / Site panel: refactor site panel to inline a LevelReferences UI (replacing the removed references dialog). Add upload flow for scan (.glb/.gltf) and guide images, handling file size/type checks, project asset upload/delete calls, and per-level reference lists. Add MultiSelectionBadge for showing/clearing multi-selection and animated expand/collapse for levels. Several tree-node components now use a shared handleTreeSelection helper and propagate click events properly.

- UI polish & icons: change dark theme color tokens for better contrast; increase button sizes and icon sizes across action menus; swap some lucide icons for image assets (rotate, topview, select, build, mesh, floorplan, level, etc.) and add new public icons.

- PascalRadio: replace popover with a framer-motion expandable panel, add outside-click handling, animated transitions, and minor UI tweaks.

- Other: minor viewer hook/selection-manager updates and tweaks to control modes and view toggles to support image icons and consistent sizing.

These changes improve multi-select reliability, user interactions in the sidebar, and overall visual polish with new icons and animations.

* Fix TypeScript build errors in selection-manager and scene-loader

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aymeric Rabot
2026-02-26 22:09:40 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 441517424d
commit 5ecefbdc06
48 changed files with 1866 additions and 953 deletions
@@ -14,5 +14,9 @@ export default function EditorProjectLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode children: React.ReactNode
}>) { }>) {
return children return (
<div style={{ cursor: "url('/cursor.svg') 4 2, default" }}>
{children}
</div>
)
} }
+13 -3
View File
@@ -2,9 +2,10 @@
import Editor from '@/components/editor' import Editor from '@/components/editor'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { useLayoutEffect } from 'react' import { useEffect, useState } from 'react'
import { useProjectStore } from '@/features/community/lib/projects/store' import { useProjectStore } from '@/features/community/lib/projects/store'
import { useAuth } from '@/features/community/lib/auth/hooks' import { useAuth } from '@/features/community/lib/auth/hooks'
import { SceneLoader } from '@/components/ui/scene-loader'
export default function EditorPage() { export default function EditorPage() {
const params = useParams() const params = useParams()
@@ -12,9 +13,14 @@ export default function EditorPage() {
const { isAuthenticated, isLoading } = useAuth() const { isAuthenticated, isLoading } = useAuth()
const setActiveProject = useProjectStore((state) => state.setActiveProject) const setActiveProject = useProjectStore((state) => state.setActiveProject)
const router = useRouter() const router = useRouter()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Use layoutEffect to set active project BEFORE the editor renders and hooks run // Use layoutEffect to set active project BEFORE the editor renders and hooks run
useLayoutEffect(() => { useEffect(() => {
if (isLoading) return if (isLoading) return
if (!isAuthenticated) { if (!isAuthenticated) {
router.replace('/') router.replace('/')
@@ -25,7 +31,11 @@ export default function EditorPage() {
} }
}, [projectId, isAuthenticated, isLoading, setActiveProject, router]) }, [projectId, isAuthenticated, isLoading, setActiveProject, router])
if (isLoading || !isAuthenticated) { if (!mounted || isLoading) {
return <SceneLoader fullScreen />
}
if (!isAuthenticated) {
return null return null
} }
+95 -3
View File
@@ -85,7 +85,7 @@
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: oklch(0.205 0 0); /* ~171717 */
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0); --card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
@@ -97,7 +97,7 @@
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0); --muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0); --muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0); --accent: oklch(0.235 0 0); /* slightly lighter than background (0.205) but darker than previous (0.269) */
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
@@ -112,7 +112,7 @@
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: oklch(0.235 0 0); /* matching accent */
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
@@ -161,3 +161,95 @@
scroll-behavior: auto !important; scroll-behavior: auto !important;
} }
} }
/* Loaders */
.pascal-loader-1 {
width: 45px;
aspect-ratio: 1;
--c:no-repeat linear-gradient(currentColor 0 0);
background: var(--c), var(--c), var(--c);
animation:
pascal-l1-1 1s infinite,
pascal-l1-2 1s infinite;
}
@keyframes pascal-l1-1 {
0%,100% {background-size:20% 100%}
33%,66% {background-size:20% 20%}
}
@keyframes pascal-l1-2 {
0%,33% {background-position: 0 0,50% 50%,100% 100%}
66%,100% {background-position: 100% 0,50% 50%,0 100%}
}
.pascal-loader-2 {
width: 45px;
aspect-ratio: .75;
--c: no-repeat linear-gradient(currentColor 0 0);
background:
var(--c) 0% 50%,
var(--c) 50% 50%,
var(--c) 100% 50%;
background-size: 20% 50%;
animation: pascal-l2 1s infinite linear;
}
@keyframes pascal-l2 {
20% {background-position: 0% 0% ,50% 50% ,100% 50% }
40% {background-position: 0% 100%,50% 0% ,100% 50% }
60% {background-position: 0% 50% ,50% 100%,100% 0% }
80% {background-position: 0% 50% ,50% 50% ,100% 100%}
}
.pascal-loader-3 {
width: 45px;
aspect-ratio: .75;
--c:no-repeat linear-gradient(currentColor 0 0);
background:
var(--c) 0% 100%,
var(--c) 50% 100%,
var(--c) 100% 100%;
background-size: 20% 65%;
animation: pascal-l3 1s infinite linear;
}
@keyframes pascal-l3 {
16.67% {background-position: 0% 0% ,50% 100%,100% 100%}
33.33% {background-position: 0% 0% ,50% 0% ,100% 100%}
50% {background-position: 0% 0% ,50% 0% ,100% 0% }
66.67% {background-position: 0% 100%,50% 0% ,100% 0% }
83.33% {background-position: 0% 100%,50% 100%,100% 0% }
}
.pascal-loader-4 {
width: 45px;
aspect-ratio: 1;
--c:no-repeat linear-gradient(currentColor 0 0);
background: var(--c), var(--c), var(--c);
animation:
pascal-l4-1 1s infinite,
pascal-l4-2 1s infinite;
}
@keyframes pascal-l4-1 {
0%,100% {background-size:20% 100%}
33%,66% {background-size:20% 40%}
}
@keyframes pascal-l4-2 {
0%,33% {background-position: 0 0,50% 100%,100% 100%}
66%,100% {background-position: 100% 0,0 100%,50% 100%}
}
.pascal-loader-5 {
width: 45px;
aspect-ratio: 1;
--c:no-repeat linear-gradient(currentColor 0 0);
background: var(--c), var(--c), var(--c);
animation:
pascal-l5-1 1s infinite,
pascal-l5-2 1s infinite;
}
@keyframes pascal-l5-1 {
0%,100% {background-size:20% 100%}
33%,66% {background-size:20% 40%}
}
@keyframes pascal-l5-2 {
0%,33% {background-position: 0 0 ,50% 100%,100% 0}
66%,100% {background-position: 0 100%,50% 0 ,100% 100%}
}
+16 -15
View File
@@ -14,6 +14,8 @@ import { ViewerGuestCTA } from './viewer-guest-cta'
import { ViewerOverlay } from './viewer-overlay' import { ViewerOverlay } from './viewer-overlay'
import { ViewerZoneSystem } from './viewer-zone-system' import { ViewerZoneSystem } from './viewer-zone-system'
import { SceneLoader } from '@/components/ui/scene-loader'
export default function ViewerPage() { export default function ViewerPage() {
const params = useParams() const params = useParams()
const id = params.id as string const id = params.id as string
@@ -90,14 +92,6 @@ export default function ViewerPage() {
loadContent() loadContent()
}, [id, setScene]) }, [id, setScene])
if (loading) {
return (
<div className="flex h-screen w-full items-center justify-center bg-neutral-100">
<p className="text-muted-foreground">Loading...</p>
</div>
)
}
if (error) { if (error) {
return ( return (
<div className="flex h-screen w-full items-center justify-center bg-neutral-100"> <div className="flex h-screen w-full items-center justify-center bg-neutral-100">
@@ -108,13 +102,20 @@ export default function ViewerPage() {
return ( return (
<div className="relative h-screen w-full"> <div className="relative h-screen w-full">
<ViewerOverlay {loading && <SceneLoader fullScreen />}
projectName={projectName}
owner={owner} {!loading && (
canShowScans={canShowScans} <>
canShowGuides={canShowGuides} <ViewerOverlay
/> projectName={projectName}
<ViewerGuestCTA /> owner={owner}
canShowScans={canShowScans}
canShowGuides={canShowGuides}
/>
<ViewerGuestCTA />
</>
)}
<Viewer> <Viewer>
<ViewerCameraControls /> <ViewerCameraControls />
<ViewerZoneSystem /> <ViewerZoneSystem />
@@ -0,0 +1,141 @@
'use client'
import { type AnyNode, type AnyNodeId, ItemNode, WindowNode, DoorNode, sceneRegistry, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { Html } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { Copy, Move, Trash2 } from 'lucide-react'
import { useCallback, useRef } from 'react'
import * as THREE from 'three'
import { sfxEmitter } from '@/lib/sfx-bus'
import useEditor from '@/store/use-editor'
const ALLOWED_TYPES = ['item', 'door', 'window']
export function FloatingActionMenu() {
const selectedIds = useViewer((s) => s.selection.selectedIds)
const nodes = useScene((s) => s.nodes)
const deleteNode = useScene((s) => s.deleteNode)
const setMovingNode = useEditor((s) => s.setMovingNode)
const setSelection = useViewer((s) => s.setSelection)
const isEditor = useViewer((state) => state.isEditor)
const groupRef = useRef<THREE.Group>(null)
// Only show for single selection of specific types
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
const node = selectedId ? nodes[selectedId as AnyNodeId] : null
const isValidType = node ? ALLOWED_TYPES.includes(node.type) : false
useFrame(() => {
if (!selectedId || !isValidType || !groupRef.current) return
const obj = sceneRegistry.nodes.get(selectedId)
if (obj) {
// Calculate bounding box in world space
const box = new THREE.Box3().setFromObject(obj)
if (!box.isEmpty()) {
const center = box.getCenter(new THREE.Vector3())
// Position slightly above the object
groupRef.current.position.set(center.x, box.max.y + 0.3, center.z)
}
}
})
const handleMove = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
if (!node) return
sfxEmitter.emit('sfx:item-pick')
if (node.type === 'item' || node.type === 'window' || node.type === 'door') {
setMovingNode(node as any)
}
setSelection({ selectedIds: [] })
}, [node, setMovingNode, setSelection])
const handleDuplicate = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
if (!node || !node.parentId) return
sfxEmitter.emit('sfx:item-pick')
useScene.temporal.getState().pause()
let duplicateInfo = structuredClone(node) as any
delete duplicateInfo.id
duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
let duplicate: AnyNode | null = null
try {
if (node.type === 'door') {
duplicate = DoorNode.parse(duplicateInfo)
} else if (node.type === 'window') {
duplicate = WindowNode.parse(duplicateInfo)
} else if (node.type === 'item') {
duplicate = ItemNode.parse(duplicateInfo)
}
} catch (error) {
console.error('Failed to parse duplicate', error)
return
}
if (duplicate) {
if (duplicate.type === 'door' || duplicate.type === 'window') {
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
}
if (duplicate.type === 'item' || duplicate.type === 'window' || duplicate.type === 'door') {
setMovingNode(duplicate as any)
}
setSelection({ selectedIds: [] })
}
}, [node, setMovingNode, setSelection])
const handleDelete = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
if (!selectedId || !node) return
sfxEmitter.emit('sfx:item-delete')
deleteNode(selectedId as AnyNodeId)
if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
setSelection({ selectedIds: [] })
}, [selectedId, node, deleteNode, setSelection])
if (!isEditor || !selectedId || !node || !isValidType) return null
return (
<group ref={groupRef}>
<Html
center
zIndexRange={[100, 0]}
style={{
pointerEvents: 'auto',
touchAction: 'none'
}}
>
<div
className="flex items-center gap-1 p-1 rounded-lg border border-border bg-background/95 shadow-xl backdrop-blur-md"
onPointerDown={(e) => e.stopPropagation()}
onPointerUp={(e) => e.stopPropagation()}
>
<button
onClick={handleMove}
className="p-1.5 rounded-md hover:bg-accent text-muted-foreground hover:text-foreground transition-colors tooltip-trigger"
title="Move"
>
<Move className="w-4 h-4" />
</button>
<button
onClick={handleDuplicate}
className="p-1.5 rounded-md hover:bg-accent text-muted-foreground hover:text-foreground transition-colors tooltip-trigger"
title="Duplicate"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={handleDelete}
className="p-1.5 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors tooltip-trigger"
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</Html>
</group>
)
}
+45 -3
View File
@@ -1,7 +1,8 @@
'use client' 'use client'
import { useEffect } from 'react'
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core' import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
import { Viewer } from '@pascal-app/viewer' import { Viewer, useViewer } from '@pascal-app/viewer'
import { useProjectScene } from '@/features/community/lib/models/hooks' import { useProjectScene } from '@/features/community/lib/models/hooks'
import { useKeyboard } from '@/hooks/use-keyboard' import { useKeyboard } from '@/hooks/use-keyboard'
import { initSFXBus } from '@/lib/sfx-bus' import { initSFXBus } from '@/lib/sfx-bus'
@@ -18,15 +19,43 @@ import { SidebarProvider } from '../ui/primitives/sidebar'
import { AppSidebar } from '../ui/sidebar/app-sidebar' import { AppSidebar } from '../ui/sidebar/app-sidebar'
import { CustomCameraControls } from './custom-camera-controls' import { CustomCameraControls } from './custom-camera-controls'
import { ExportManager } from './export-manager' import { ExportManager } from './export-manager'
import { FloatingActionMenu } from './floating-action-menu'
import { Grid } from './grid' import { Grid } from './grid'
import { SelectionManager } from './selection-manager' import { SelectionManager } from './selection-manager'
import { ThumbnailGenerator } from './thumbnail-generator' import { ThumbnailGenerator } from './thumbnail-generator'
import { useProjectStore } from '@/features/community/lib/projects/store'
import { SceneLoader } from '../ui/scene-loader'
// Load default scene initially (will be replaced when project loads) // Load default scene initially (will be replaced when project loads)
useScene.getState().loadScene() useScene.getState().loadScene()
initSpatialGridSync() initSpatialGridSync()
initSpaceDetectionSync(useScene, useEditor) initSpaceDetectionSync(useScene, useEditor)
// Auto-select the first building and level for the default scene
const sceneNodes = useScene.getState().nodes as Record<string, any>
const sceneRootIds = useScene.getState().rootNodeIds
const siteNode = sceneRootIds[0] ? sceneNodes[sceneRootIds[0]] : null
const resolve = (child: any) => typeof child === 'string' ? sceneNodes[child] : child
const firstBuilding = siteNode?.children?.map(resolve).find((n: any) => n?.type === 'building')
const firstLevel = firstBuilding?.children?.map(resolve).find((n: any) => n?.type === 'level')
if (firstBuilding && firstLevel) {
useViewer.getState().setSelection({
buildingId: firstBuilding.id,
levelId: firstLevel.id,
selectedIds: [],
zoneId: null,
})
useEditor.getState().setPhase('structure')
useEditor.getState().setStructureLayer('elements')
// Auto-select the wall tool if the level is empty
if (!firstLevel.children || firstLevel.children.length === 0) {
useEditor.getState().setMode('build')
useEditor.getState().setTool('wall')
}
}
// Initialize SFX bus to connect events to sound effects // Initialize SFX bus to connect events to sound effects
initSFXBus() initSFXBus()
@@ -38,14 +67,26 @@ export default function Editor({ projectId }: EditorProps) {
useKeyboard() useKeyboard()
useProjectScene() useProjectScene()
const isProjectLoading = useProjectStore((state) => state.isLoading)
const isSceneLoading = useProjectStore((state) => state.isSceneLoading)
const isLoading = isProjectLoading || isSceneLoading
useEffect(() => {
document.body.classList.add('dark')
return () => {
document.body.classList.remove('dark')
}
}, [])
return ( return (
<div className="w-full h-full"> <div className="w-full h-full dark text-foreground">
{isLoading && <SceneLoader />}
<ActionMenu /> <ActionMenu />
<PanelManager /> <PanelManager />
<HelperManager /> <HelperManager />
{/* Top-right controls */} {/* Top-right controls */}
<div className="pointer-events-none fixed top-4 right-4 z-50 flex items-center gap-2"> <div className="pointer-events-none fixed top-4 right-4 z-50 flex items-start gap-2">
<div className="pointer-events-auto"> <div className="pointer-events-auto">
<PascalRadio /> <PascalRadio />
</div> </div>
@@ -59,6 +100,7 @@ export default function Editor({ projectId }: EditorProps) {
</SidebarProvider> </SidebarProvider>
<Viewer selectionManager="custom" isEditor={true}> <Viewer selectionManager="custom" isEditor={true}>
<SelectionManager /> <SelectionManager />
<FloatingActionMenu />
<ExportManager /> <ExportManager />
{/* Editor only system to toggle zone visibility */} {/* Editor only system to toggle zone visibility */}
<ZoneSystem /> <ZoneSystem />
@@ -10,7 +10,7 @@ import {
} from "@pascal-app/core"; } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import { useEffect } from "react"; import { useEffect, useRef } from "react";
import useEditor from "@/store/use-editor"; import useEditor from "@/store/use-editor";
const isNodeInCurrentLevel = (node: AnyNode): boolean => { const isNodeInCurrentLevel = (node: AnyNode): boolean => {
@@ -22,13 +22,58 @@ const isNodeInCurrentLevel = (node: AnyNode): boolean => {
type SelectableNodeType = "wall" | "item" | "building" | "zone" | 'slab' | 'ceiling' | 'roof' | 'window' | 'door'; type SelectableNodeType = "wall" | "item" | "building" | "zone" | 'slab' | 'ceiling' | 'roof' | 'window' | 'door';
type ModifierKeys = {
meta: boolean;
ctrl: boolean;
};
interface SelectionStrategy { interface SelectionStrategy {
types: SelectableNodeType[]; types: SelectableNodeType[];
handleSelect: (node: AnyNode, isShift: boolean) => void; handleSelect: (node: AnyNode, nativeEvent?: any, modifierKeys?: ModifierKeys) => void;
handleDeselect: () => void; handleDeselect: () => void;
isValid: (node: AnyNode) => boolean; isValid: (node: AnyNode) => boolean;
} }
export const resolveBuildingId = (levelId: string, nodes: Record<string, AnyNode>): string | null => {
const level = nodes[levelId];
if (!level) return null;
if (level.parentId && nodes[level.parentId]?.type === "building") {
return level.parentId;
}
return null;
};
const computeNextIds = (
node: AnyNode,
selectedIds: string[],
event?: any,
modifierKeys?: ModifierKeys
): string[] => {
const isMeta = event?.metaKey || event?.nativeEvent?.metaKey || modifierKeys?.meta || false;
const isCtrl = event?.ctrlKey || event?.nativeEvent?.ctrlKey || modifierKeys?.ctrl || false;
console.log("computeNextIds:", {
nodeId: node.id,
selectedIds,
isMeta,
isCtrl,
eventMeta: event?.metaKey,
nativeMeta: event?.nativeEvent?.metaKey,
modMeta: modifierKeys?.meta
});
if (isMeta || isCtrl) {
if (selectedIds.includes(node.id)) {
return selectedIds.filter((id) => id !== node.id);
} else {
return [...selectedIds, node.id];
}
}
// Not holding modifiers: select only this node
return [node.id];
};
const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = { const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
site: { site: {
types: ["building"], types: ["building"],
@@ -45,17 +90,28 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
structure: { structure: {
types: ["wall", "item", "zone", "slab", "ceiling", "roof", "window", "door"], types: ["wall", "item", "zone", "slab", "ceiling", "roof", "window", "door"],
handleSelect: (node, isShift) => { handleSelect: (node, nativeEvent, modifierKeys) => {
const { selection, setSelection } = useViewer.getState(); const { selection, setSelection } = useViewer.getState();
const nodes = useScene.getState().nodes;
const nodeLevelId = resolveLevelId(node, nodes);
const buildingId = resolveBuildingId(nodeLevelId, nodes);
const updates: any = {};
if (nodeLevelId !== "default" && nodeLevelId !== selection.levelId) {
updates.levelId = nodeLevelId;
}
if (buildingId && buildingId !== selection.buildingId) {
updates.buildingId = buildingId;
}
if (node.type === 'zone') { if (node.type === 'zone') {
setSelection({ zoneId: node.id }); updates.zoneId = node.id;
// Don't reset selectedIds in structure phase for zone, but if we changed level, it might reset them via hierarchy guard.
// Wait, the hierarchy guard resets zoneId if levelId changes. That's fine since we provide zoneId.
setSelection(updates);
} else { } else {
const nextIds = isShift updates.selectedIds = computeNextIds(node, selection.selectedIds, nativeEvent, modifierKeys);
? selection.selectedIds.includes(node.id) setSelection(updates);
? selection.selectedIds.filter((id) => id !== node.id)
: [...selection.selectedIds, node.id]
: [node.id];
setSelection({ selectedIds: nextIds });
} }
}, },
handleDeselect: () => { handleDeselect: () => {
@@ -89,14 +145,22 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
furnish: { furnish: {
types: ["item"], types: ["item"],
handleSelect: (node, isShift) => { handleSelect: (node, nativeEvent, modifierKeys) => {
const { selection, setSelection } = useViewer.getState(); const { selection, setSelection } = useViewer.getState();
const nextIds = isShift const nodes = useScene.getState().nodes;
? selection.selectedIds.includes(node.id) const nodeLevelId = resolveLevelId(node, nodes);
? selection.selectedIds.filter((id) => id !== node.id) const buildingId = resolveBuildingId(nodeLevelId, nodes);
: [...selection.selectedIds, node.id]
: [node.id]; const updates: any = {};
setSelection({ selectedIds: nextIds }); if (nodeLevelId !== "default" && nodeLevelId !== selection.levelId) {
updates.levelId = nodeLevelId;
}
if (buildingId && buildingId !== selection.buildingId) {
updates.buildingId = buildingId;
}
updates.selectedIds = computeNextIds(node, selection.selectedIds, nativeEvent, modifierKeys);
setSelection(updates);
}, },
handleDeselect: () => { handleDeselect: () => {
useViewer.getState().setSelection({ selectedIds: [] }); useViewer.getState().setSelection({ selectedIds: [] });
@@ -113,39 +177,116 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
export const SelectionManager = () => { export const SelectionManager = () => {
const phase = useEditor((s) => s.phase); const phase = useEditor((s) => s.phase);
const mode = useEditor((s) => s.mode); const mode = useEditor((s) => s.mode);
const modifierKeysRef = useRef<ModifierKeys>({
meta: false,
ctrl: false,
});
const clickHandledRef = useRef(false);
const movingNode = useEditor((s) => s.movingNode); const movingNode = useEditor((s) => s.movingNode);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Meta") modifierKeysRef.current.meta = true;
if (event.key === "Control") modifierKeysRef.current.ctrl = true;
};
const onKeyUp = (event: KeyboardEvent) => {
if (event.key === "Meta") modifierKeysRef.current.meta = false;
if (event.key === "Control") modifierKeysRef.current.ctrl = false;
};
const clearModifiers = () => {
modifierKeysRef.current.meta = false;
modifierKeysRef.current.ctrl = false;
};
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
window.addEventListener("blur", clearModifiers);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
window.removeEventListener("blur", clearModifiers);
};
}, []);
useEffect(() => { useEffect(() => {
if (mode !== "select") return; if (mode !== "select") return;
if (movingNode) return; if (movingNode) return;
const strategy = SELECTION_STRATEGIES[phase];
if (!strategy) return;
const onClick = (event: NodeEvent) => { const onClick = (event: NodeEvent) => {
if (!strategy.isValid(event.node)) return; const node = event.node;
let currentPhase = useEditor.getState().phase;
let targetPhase = currentPhase;
event.stopPropagation(); // Auto-switch between structure and furnish phases when clicking elements on the same level
const isShift = event.nativeEvent?.shiftKey; if (currentPhase === "structure" || currentPhase === "furnish") {
strategy.handleSelect(event.node, isShift ?? false); if (isNodeInCurrentLevel(node)) {
if (
node.type === "wall" ||
node.type === "slab" ||
node.type === "ceiling" ||
node.type === "roof" ||
node.type === "window" ||
node.type === "door"
) {
targetPhase = "structure";
} else if (node.type === "item") {
const item = node as ItemNode;
if (item.asset.category === "door" || item.asset.category === "window") {
targetPhase = "structure";
} else {
targetPhase = "furnish";
}
}
if (targetPhase !== currentPhase) {
useEditor.getState().setPhase(targetPhase);
if (targetPhase === "structure" && useEditor.getState().structureLayer === "zones") {
useEditor.getState().setStructureLayer("elements");
}
currentPhase = targetPhase;
}
}
}
const activeStrategy = SELECTION_STRATEGIES[currentPhase];
if (activeStrategy?.isValid(node)) {
event.stopPropagation();
clickHandledRef.current = true;
console.log("[SelectionManager] Valid click on:", node.type, node.id, "Shift:", event.nativeEvent.shiftKey);
activeStrategy.handleSelect(node, event.nativeEvent, modifierKeysRef.current);
// Reset the handled flag after a short delay to allow grid:click to be ignored
setTimeout(() => {
clickHandledRef.current = false;
}, 50);
}
}; };
// Bind listeners for all potential types this strategy might care about const allTypes = ["wall", "item", "building", "zone", "slab", "ceiling", "roof", "window", "door"];
strategy.types.forEach((type) => { allTypes.forEach((type) => {
emitter.on(`${type}:click`, onClick); emitter.on(`${type}:click` as any, onClick as any);
}); });
const onGridClick = () => strategy.handleDeselect(); const onGridClick = () => {
if (clickHandledRef.current) return;
console.log("onGridClick triggered! Deselecting.");
const activeStrategy = SELECTION_STRATEGIES[useEditor.getState().phase];
if (activeStrategy) activeStrategy.handleDeselect();
};
emitter.on("grid:click", onGridClick); emitter.on("grid:click", onGridClick);
return () => { return () => {
strategy.types.forEach((type) => { allTypes.forEach((type) => {
emitter.off(`${type}:click`, onClick); emitter.off(`${type}:click` as any, onClick as any);
}); });
emitter.off("grid:click", onGridClick); emitter.off("grid:click", onGridClick);
}; };
}, [phase, mode, movingNode]); }, [mode, movingNode]);
// Global double-click handler for auto-switching phases and cross-phase hover // Global double-click handler for auto-switching phases and cross-phase hover
useEffect(() => { useEffect(() => {
@@ -231,8 +372,7 @@ export const SelectionManager = () => {
const strategy = SELECTION_STRATEGIES[targetPhase]; const strategy = SELECTION_STRATEGIES[targetPhase];
if (strategy) { if (strategy) {
const isShift = event.nativeEvent?.shiftKey; strategy.handleSelect(node, event.nativeEvent, modifierKeysRef.current);
strategy.handleSelect(node, isShift ?? false);
} }
} }
}; };
+116 -62
View File
@@ -3,10 +3,10 @@
import { Howl } from 'howler' import { Howl } from 'howler'
import { Disc3, Settings2, SkipBack, SkipForward, Volume2, VolumeX } from 'lucide-react' import { Disc3, Settings2, SkipBack, SkipForward, Volume2, VolumeX } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/primitives/popover'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import useAudio from '@/store/use-audio' import useAudio from '@/store/use-audio'
import { motion, AnimatePresence } from 'framer-motion'
const PLAYLIST = [ const PLAYLIST = [
{ {
@@ -66,6 +66,8 @@ export function PascalRadio() {
const [currentTrackIndex, setCurrentTrackIndex] = useState(0) const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
const { masterVolume, radioVolume, muted, isRadioPlaying, setRadioPlaying } = useAudio() const { masterVolume, radioVolume, muted, isRadioPlaying, setRadioPlaying } = useAudio()
const soundRef = useRef<Howl | null>(null) const soundRef = useRef<Howl | null>(null)
const [isOpen, setIsOpen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const currentTrack = shuffledPlaylist[currentTrackIndex]! const currentTrack = shuffledPlaylist[currentTrackIndex]!
@@ -144,74 +146,126 @@ export function PascalRadio() {
useAudio.setState({ radioVolume: value[0] }) useAudio.setState({ radioVolume: value[0] })
} }
// Handle click outside to close
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])
return ( return (
<div className="flex items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 text-sm font-medium shadow-lg backdrop-blur-md"> <motion.div
<Disc3 className={cn('h-4 w-4', isRadioPlaying && 'animate-spin')} /> ref={containerRef}
<span className="hidden sm:inline">Radio Pascal</span> layout
<div onClick={() => {
onClick={handlePlayPause} if (!isOpen) setIsOpen(true)
className="rounded-sm p-1 transition-all cursor-pointer bg-accent/30 hover:bg-accent hover:text-accent-foreground hover:shadow-sm" }}
role="button" transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
tabIndex={0} className={cn(
aria-label={isRadioPlaying ? 'Pause' : 'Play'} "flex flex-col rounded-lg border border-border bg-background/95 shadow-lg backdrop-blur-md overflow-hidden",
onKeyDown={(e) => { !isOpen && "cursor-pointer hover:bg-accent/30 transition-colors"
if (e.key === 'Enter' || e.key === ' ') { )}
e.preventDefault() >
handlePlayPause() <div className="flex items-center justify-between gap-2 px-3 py-2 text-sm font-medium">
} <div className="flex items-center gap-2">
}} <Disc3 className={cn('h-4 w-4 shrink-0', isRadioPlaying && 'animate-spin')} />
> <span className="hidden sm:inline whitespace-nowrap">Radio Pascal</span>
{isRadioPlaying ? <Volume2 className="h-3.5 w-3.5" /> : <VolumeX className="h-3.5 w-3.5" />} </div>
</div> <div className="flex items-center gap-2">
<Popover> <div
<PopoverTrigger asChild> onClick={(e) => {
e.stopPropagation()
handlePlayPause()
}}
className="rounded-sm p-1 transition-all cursor-pointer bg-accent/30 hover:bg-accent hover:text-accent-foreground hover:shadow-sm"
role="button"
tabIndex={0}
aria-label={isRadioPlaying ? 'Pause' : 'Play'}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
e.stopPropagation()
handlePlayPause()
}
}}
>
{isRadioPlaying ? <Volume2 className="h-3.5 w-3.5" /> : <VolumeX className="h-3.5 w-3.5" />}
</div>
<button <button
className="rounded-sm p-1 transition-all cursor-pointer hover:bg-accent hover:text-accent-foreground" onClick={(e) => {
e.stopPropagation()
setIsOpen(!isOpen)
}}
className={cn(
"rounded-sm p-1 transition-all cursor-pointer hover:bg-accent hover:text-accent-foreground",
isOpen && "bg-accent text-accent-foreground"
)}
aria-label="Radio Settings" aria-label="Radio Settings"
> >
<Settings2 className="h-3.5 w-3.5" /> <Settings2 className="h-3.5 w-3.5" />
</button> </button>
</PopoverTrigger> </div>
<PopoverContent className="w-64" align="end"> </div>
<div className="space-y-3">
{/* Current song info with prev/next */} <AnimatePresence>
<div> {isOpen && (
<p className="text-xs text-muted-foreground mb-2">Now Playing</p> <motion.div
<div className="flex items-center justify-between gap-2"> initial={{ opacity: 0, height: 0 }}
<button animate={{ opacity: 1, height: 'auto' }}
onClick={handlePrevious} exit={{ opacity: 0, height: 0 }}
className="rounded-full p-1.5 transition-colors hover:bg-accent shrink-0" transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
aria-label="Previous" >
> <div className="px-3 pb-3 space-y-3 w-[16rem]">
<SkipBack className="h-4 w-4" /> <div className="h-px w-full bg-border/50 mb-3" />
</button> {/* Current song info with prev/next */}
<p className="text-sm font-medium text-center flex-1 truncate">{currentTrack.title}</p> <div>
<button <p className="text-xs text-muted-foreground mb-2">Now Playing</p>
onClick={handleNext} <div className="flex items-center justify-between gap-2">
className="rounded-full p-1.5 transition-colors hover:bg-accent shrink-0" <button
aria-label="Next" onClick={handlePrevious}
> className="rounded-full p-1.5 transition-colors hover:bg-accent shrink-0"
<SkipForward className="h-4 w-4" /> aria-label="Previous"
</button> >
<SkipBack className="h-4 w-4" />
</button>
<p className="text-sm font-medium text-center flex-1 truncate" title={currentTrack.title}>
{currentTrack.title}
</p>
<button
onClick={handleNext}
className="rounded-full p-1.5 transition-colors hover:bg-accent shrink-0"
aria-label="Next"
>
<SkipForward className="h-4 w-4" />
</button>
</div>
</div>
{/* Volume control */}
<div className="flex items-center gap-2">
<Volume2 className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<Slider
value={[radioVolume]}
onValueChange={handleVolumeChange}
max={100}
step={1}
className="flex-1"
aria-label="Radio Volume"
/>
<span className="w-8 text-right text-xs text-muted-foreground shrink-0">{radioVolume}%</span>
</div> </div>
</div> </div>
</motion.div>
{/* Volume control */} )}
<div className="flex items-center gap-2"> </AnimatePresence>
<Volume2 className="h-3.5 w-3.5 text-muted-foreground" /> </motion.div>
<Slider
value={[radioVolume]}
onValueChange={handleVolumeChange}
max={100}
step={1}
className="flex-1"
aria-label="Radio Volume"
/>
<span className="w-8 text-right text-xs text-muted-foreground">{radioVolume}%</span>
</div>
</div>
</PopoverContent>
</Popover>
</div>
) )
} }
@@ -1,14 +1,14 @@
import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core' import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
import { useEffect } from 'react' import { useFrame } from '@react-three/fiber'
import useEditor from '@/store/use-editor' import useEditor from '@/store/use-editor'
export const ZoneSystem = () => { export const ZoneSystem = () => {
const structureLayer = useEditor((state) => state.structureLayer) useFrame(() => {
const levelMode = useViewer((state) => state.levelMode) const structureLayer = useEditor.getState().structureLayer
const selectedLevelId = useViewer((state) => state.selection.levelId) const levelMode = useViewer.getState().levelMode
const selectedLevelId = useViewer.getState().selection.levelId
useEffect(() => {
const visible = structureLayer === 'zones' const visible = structureLayer === 'zones'
const zones = sceneRegistry.byType.zone || new Set() const zones = sceneRegistry.byType.zone || new Set()
const nodes = useScene.getState().nodes const nodes = useScene.getState().nodes
@@ -23,21 +23,27 @@ export const ZoneSystem = () => {
const isOnSelectedLevel = zone?.parentId === selectedLevelId const isOnSelectedLevel = zone?.parentId === selectedLevelId
const hideInSoloMode = levelMode === 'solo' && selectedLevelId && !isOnSelectedLevel const hideInSoloMode = levelMode === 'solo' && selectedLevelId && !isOnSelectedLevel
obj.visible = visible if (obj.visible !== visible) {
obj.visible = visible
}
const label = obj.getObjectByName('label') const label = obj.getObjectByName('label')
if (label) { if (label) {
// Hide label if zone layer is off OR if in solo mode on a different level // Hide label if zone layer is off OR if in solo mode on a different level
const showLabel = visible && !hideInSoloMode; const showLabel = visible && !hideInSoloMode;
const labelPosition = obj.userData.labelPosition as [number, number, number] | undefined const labelPosition = obj.userData.labelPosition as [number, number, number] | undefined
if (showLabel && labelPosition) {
label.position.set(...labelPosition) const targetX = showLabel && labelPosition ? labelPosition[0] : -9999
} else { if (label.position.x !== targetX) {
label.position.set(-9999, -9999, -9999) if (showLabel && labelPosition) {
label.position.set(...labelPosition)
} else {
label.position.set(-9999, -9999, -9999)
}
} }
} }
}) })
}, [structureLayer, levelMode, selectedLevelId]) })
return null return null
} }
@@ -1,7 +1,7 @@
'use client' 'use client'
import { emitter } from '@pascal-app/core' import { emitter } from '@pascal-app/core'
import { RotateCcw, RotateCw, Rotate3D } from 'lucide-react' import Image from 'next/image'
import { Button } from '@/components/ui/primitives/button' import { Button } from '@/components/ui/primitives/button'
import { import {
Tooltip, Tooltip,
@@ -28,12 +28,18 @@ export function CameraActions() {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className="h-8 w-8 text-zinc-400 transition-all hover:text-sky-400" className="group h-9 w-9 text-muted-foreground transition-all hover:bg-white/5"
onClick={orbitCCW} onClick={orbitCCW}
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
<RotateCcw className="h-4 w-4" /> <Image
alt="Orbit Left"
className="h-[30px] w-[30px] object-contain opacity-70 transition-opacity group-hover:opacity-100 -scale-x-100"
height={30}
src="/icons/rotate.png"
width={30}
/>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -45,12 +51,18 @@ export function CameraActions() {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className="h-8 w-8 text-zinc-400 transition-all hover:text-sky-400" className="group h-9 w-9 text-muted-foreground transition-all hover:bg-white/5"
onClick={orbitCW} onClick={orbitCW}
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
<RotateCw className="h-4 w-4" /> <Image
alt="Orbit Right"
className="h-[30px] w-[30px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
height={30}
src="/icons/rotate.png"
width={30}
/>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -62,12 +74,18 @@ export function CameraActions() {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className="h-8 w-8 text-zinc-400 transition-all hover:text-sky-400" className="group h-9 w-9 text-muted-foreground transition-all hover:bg-white/5"
onClick={goToTopView} onClick={goToTopView}
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
<Rotate3D className="h-4 w-4 -rotate-90" /> <Image
alt="Top View"
className="h-[30px] w-[30px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
height={30}
src="/icons/topview.png"
width={30}
/>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -1,19 +1,21 @@
"use client"; "use client";
import Image from "next/image";
import { Button } from "@/components/ui/primitives/button"; import { Button } from "@/components/ui/primitives/button";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/primitives/tooltip"; } from "@/components/ui/primitives/tooltip";
import { Hammer, MousePointer2, Pencil, Trash2 } from "lucide-react"; import { Pencil, Trash2, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import useEditor, { Mode, Phase } from "@/store/use-editor"; import useEditor, { Mode, Phase } from "@/store/use-editor";
type ModeConfig = { type ModeConfig = {
id: Mode; id: Mode;
icon: typeof MousePointer2; icon?: LucideIcon;
imageSrc?: string;
label: string; label: string;
shortcut: string; shortcut: string;
color: string; color: string;
@@ -24,7 +26,7 @@ type ModeConfig = {
const allModes: ModeConfig[] = [ const allModes: ModeConfig[] = [
{ {
id: "select", id: "select",
icon: MousePointer2, imageSrc: "/icons/select.png",
label: "Select", label: "Select",
shortcut: "V", shortcut: "V",
color: "hover:bg-blue-500/20 hover:text-blue-400", color: "hover:bg-blue-500/20 hover:text-blue-400",
@@ -40,7 +42,7 @@ const allModes: ModeConfig[] = [
}, },
{ {
id: "build", id: "build",
icon: Hammer, imageSrc: "/icons/build.png",
label: "Build", label: "Build",
shortcut: "B", shortcut: "B",
color: "hover:bg-green-500/20 hover:text-green-400", color: "hover:bg-green-500/20 hover:text-green-400",
@@ -98,22 +100,39 @@ export function ControlModes() {
{availableModes.map((m) => { {availableModes.map((m) => {
const Icon = m.icon; const Icon = m.icon;
const isActive = mode === m.id; const isActive = mode === m.id;
const isImageMode = Boolean(m.imageSrc);
return ( return (
<Tooltip key={m.id}> <Tooltip key={m.id}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className={cn( className={cn(
"h-8 w-8 transition-all", "h-9 w-9 transition-all",
"text-zinc-400", "text-muted-foreground",
!isActive && m.color, !isImageMode && !isActive && m.color,
isActive && m.activeColor !isImageMode && isActive && m.activeColor,
isImageMode && isActive && "bg-white/10 hover:bg-white/10",
isImageMode && !isActive && "hover:bg-white/5"
)} )}
onClick={() => handleModeClick(m.id)} onClick={() => handleModeClick(m.id)}
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
<Icon className="h-4 w-4" /> {m.imageSrc ? (
<Image
alt={m.label}
className={cn(
"h-[26px] w-[26px] object-contain transition-[opacity,filter] duration-200",
!isActive && "opacity-60 grayscale",
isActive && "opacity-100 grayscale-0"
)}
height={26}
src={m.imageSrc}
width={26}
/>
) : (
Icon && <Icon className="h-5 w-5" />
)}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -30,7 +30,7 @@ export function ActionMenu({ className }: { className?: string }) {
transition={transition} transition={transition}
className={cn( className={cn(
"-translate-x-1/2 fixed bottom-6 left-1/2 z-50", "-translate-x-1/2 fixed bottom-6 left-1/2 z-50",
"rounded-2xl border border-zinc-800 bg-zinc-950/90 shadow-2xl backdrop-blur-md", "rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md",
"transition-colors duration-200 ease-out", "transition-colors duration-200 ease-out",
className, className,
)} )}
@@ -40,7 +40,7 @@ export function ActionMenu({ className }: { className?: string }) {
{mode === "build" && tool === "item" && catalogCategory && ( {mode === "build" && tool === "item" && catalogCategory && (
<motion.div <motion.div
className={cn( className={cn(
"overflow-hidden border-zinc-800 border-b px-2 py-2", "overflow-hidden border-border border-b px-2 py-2",
)} )}
initial={{ initial={{
opacity: 0, opacity: 0,
@@ -74,7 +74,7 @@ export function ActionMenu({ className }: { className?: string }) {
{phase === "furnish" && mode === "build" && ( {phase === "furnish" && mode === "build" && (
<motion.div <motion.div
className={cn( className={cn(
"overflow-hidden border-zinc-800", "overflow-hidden border-border",
"max-h-20 border-b px-2 py-2 opacity-100", "max-h-20 border-b px-2 py-2 opacity-100",
)} )}
initial={{ initial={{
@@ -112,7 +112,7 @@ export function ActionMenu({ className }: { className?: string }) {
{phase === "structure" && mode === "build" && ( {phase === "structure" && mode === "build" && (
<motion.div <motion.div
className={cn( className={cn(
"overflow-hidden border-zinc-800 max-h-20 border-b px-2 py-2", "overflow-hidden border-border max-h-20 border-b px-2 py-2",
)} )}
initial={{ initial={{
opacity: 0, opacity: 0,
@@ -146,9 +146,9 @@ export function ActionMenu({ className }: { className?: string }) {
{/* Control Mode Row - Always visible, centered */} {/* Control Mode Row - Always visible, centered */}
<div className="flex items-center justify-center gap-1 px-2 py-1.5"> <div className="flex items-center justify-center gap-1 px-2 py-1.5">
<ControlModes /> <ControlModes />
<div className="mx-1 h-5 w-px bg-zinc-700" /> <div className="mx-1 h-5 w-px bg-border" />
<ViewToggles /> <ViewToggles />
<div className="mx-1 h-5 w-px bg-zinc-700" /> <div className="mx-1 h-5 w-px bg-border" />
<CameraActions /> <CameraActions />
</div> </div>
</motion.div> </motion.div>
@@ -85,7 +85,7 @@ export function ViewToggles() {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className={cn( className={cn(
'h-8 w-8 text-zinc-400 transition-all', 'h-9 w-9 text-muted-foreground transition-all',
cameraMode === 'orthographic' cameraMode === 'orthographic'
? 'bg-violet-500/20 text-violet-400' ? 'bg-violet-500/20 text-violet-400'
: 'hover:text-violet-400', : 'hover:text-violet-400',
@@ -94,7 +94,7 @@ export function ViewToggles() {
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
<Camera className="h-4 w-4" /> <Camera className="h-5 w-5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -107,7 +107,7 @@ export function ViewToggles() {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className={cn( className={cn(
'h-8 w-8 text-zinc-400 transition-all', 'h-9 w-9 text-muted-foreground transition-all',
levelMode !== 'stacked' levelMode !== 'stacked'
? 'bg-amber-500/20 text-amber-400' ? 'bg-amber-500/20 text-amber-400'
: 'hover:text-amber-400', : 'hover:text-amber-400',
@@ -116,9 +116,9 @@ export function ViewToggles() {
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
{levelMode === 'solo' && <Diamond className="h-4 w-4" />} {levelMode === 'solo' && <Diamond className="h-5 w-5" />}
{levelMode === 'exploded' && <Layers2 className="h-4 w-4" />} {levelMode === 'exploded' && <Layers2 className="h-5 w-5" />}
{(levelMode === 'stacked' || levelMode === 'manual') && <Layers className="h-4 w-4" />} {(levelMode === 'stacked' || levelMode === 'manual') && <Layers className="h-5 w-5" />}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -131,7 +131,7 @@ export function ViewToggles() {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className={cn( className={cn(
'h-8 w-8 text-zinc-400 transition-all p-0', 'h-9 w-9 text-muted-foreground transition-all p-0',
wallMode !== 'cutaway' wallMode !== 'cutaway'
? 'bg-white/10' ? 'bg-white/10'
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0 hover:bg-white/5', : 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0 hover:bg-white/5',
@@ -142,7 +142,7 @@ export function ViewToggles() {
> >
{(() => { {(() => {
const Icon = wallModeConfig[wallMode].icon const Icon = wallModeConfig[wallMode].icon
return <Icon className="h-5 w-5" /> return <Icon className="h-[26px] w-[26px]" />
})()} })()}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -156,7 +156,7 @@ export function ViewToggles() {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className={cn( className={cn(
'h-8 w-8 text-zinc-400 transition-all p-0', 'h-9 w-9 text-muted-foreground transition-all p-0',
showScans showScans
? 'bg-white/10' ? 'bg-white/10'
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0 hover:bg-white/5', : 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0 hover:bg-white/5',
@@ -165,7 +165,7 @@ export function ViewToggles() {
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
<img alt="Scans" className="h-5 w-5 object-contain" src="/icons/mesh.png" /> <img alt="Scans" className="h-[26px] w-[26px] object-contain" src="/icons/mesh.png" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -178,7 +178,7 @@ export function ViewToggles() {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className={cn( className={cn(
'h-8 w-8 text-zinc-400 transition-all p-0', 'h-9 w-9 text-muted-foreground transition-all p-0',
showGuides showGuides
? 'bg-white/10' ? 'bg-white/10'
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0 hover:bg-white/5', : 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0 hover:bg-white/5',
@@ -187,7 +187,7 @@ export function ViewToggles() {
size="icon" size="icon"
variant="ghost" variant="ghost"
> >
<img alt="Guides" className="h-5 w-5 object-contain" src="/icons/floorplan.png" /> <img alt="Guides" className="h-[26px] w-[26px] object-contain" src="/icons/floorplan.png" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -4,6 +4,8 @@ import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react"; import { PanelLeftIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { Button } from "@/components/ui/primitives/button"; import { Button } from "@/components/ui/primitives/button";
import { Input } from "@/components/ui/primitives/input"; import { Input } from "@/components/ui/primitives/input";
import { Separator } from "@/components/ui/primitives/separator"; import { Separator } from "@/components/ui/primitives/separator";
@@ -31,6 +33,28 @@ const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b"; const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarStore = {
width: number;
setWidth: (width: number) => void;
isDragging: boolean;
setIsDragging: (isDragging: boolean) => void;
};
export const useSidebarStore = create<SidebarStore>()(
persist(
(set) => ({
width: 288, // 18rem = 288px
setWidth: (width) => set({ width: Math.max(288, Math.min(width, 800)) }),
isDragging: false,
setIsDragging: (isDragging) => set({ isDragging }),
}),
{
name: "sidebar-preferences",
partialize: (state) => ({ width: state.width }), // Only persist width
}
)
);
type SidebarContextProps = { type SidebarContextProps = {
state: "expanded" | "collapsed"; state: "expanded" | "collapsed";
open: boolean; open: boolean;
@@ -67,6 +91,8 @@ function SidebarProvider({
}) { }) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false); const [openMobile, setOpenMobile] = React.useState(false);
const sidebarWidth = useSidebarStore((state) => state.width);
const isDragging = useSidebarStore((state) => state.isDragging);
// This is the internal state of the sidebar. // This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component. // We use openProp and setOpenProp for control from outside the component.
@@ -136,9 +162,10 @@ function SidebarProvider({
className, className,
)} )}
data-slot="sidebar-wrapper" data-slot="sidebar-wrapper"
data-dragging={isDragging}
style={ style={
{ {
"--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width": `${sidebarWidth}px`,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON, "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style, ...style,
} as React.CSSProperties } as React.CSSProperties
@@ -152,6 +179,52 @@ function SidebarProvider({
); );
} }
function SidebarResizer({ side }: { side: "left" | "right" }) {
const setWidth = useSidebarStore((state) => state.setWidth);
const setIsDragging = useSidebarStore((state) => state.setIsDragging);
const isResizing = React.useRef(false);
const handlePointerDown = (e: React.PointerEvent) => {
isResizing.current = true;
setIsDragging(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
};
React.useEffect(() => {
const handlePointerMove = (e: PointerEvent) => {
if (!isResizing.current) return;
const newWidth = side === "left" ? e.clientX : window.innerWidth - e.clientX;
setWidth(Math.max(288, Math.min(newWidth, 800)));
};
const handlePointerUp = () => {
isResizing.current = false;
setIsDragging(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
return () => {
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
};
}, [setWidth, side]);
return (
<div
onPointerDown={handlePointerDown}
className={cn(
"absolute top-0 bottom-0 w-2 cursor-col-resize z-50 hover:bg-primary/50 transition-colors",
side === "left" ? "-right-1" : "-left-1"
)}
/>
);
}
function Sidebar({ function Sidebar({
side = "left", side = "left",
variant = "sidebar", variant = "sidebar",
@@ -219,6 +292,7 @@ function Sidebar({
<div <div
className={cn( className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[dragging=true]/sidebar-wrapper:transition-none",
"group-data-[collapsible=offcanvas]:w-0", "group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180", "group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
@@ -230,6 +304,7 @@ function Sidebar({
<div <div
className={cn( className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex pointer-events-auto", "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex pointer-events-auto",
"group-data-[dragging=true]/sidebar-wrapper:transition-none",
side === "left" side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
@@ -243,11 +318,12 @@ function Sidebar({
{...props} {...props}
> >
<div <div
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm pointer-events-auto" className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm pointer-events-auto relative"
data-sidebar="sidebar" data-sidebar="sidebar"
data-slot="sidebar-inner" data-slot="sidebar-inner"
> >
{children} {children}
<SidebarResizer side={side} />
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,40 @@
'use client'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
const LOADERS = [
'pascal-loader-1',
'pascal-loader-2',
'pascal-loader-3',
'pascal-loader-4',
'pascal-loader-5',
]
interface SceneLoaderProps {
className?: string
fullScreen?: boolean
}
export function SceneLoader({ className, fullScreen = false }: SceneLoaderProps) {
const [loaderClass, setLoaderClass] = useState<string | null>(null)
useEffect(() => {
// Pick a random loader on mount
setLoaderClass(LOADERS[Math.floor(Math.random() * LOADERS.length)] ?? LOADERS[0]!)
}, [])
if (!loaderClass) return null
return (
<div
className={cn(
"z-100 flex items-center justify-center bg-background/80 backdrop-blur-md transition-opacity duration-300",
fullScreen ? "fixed inset-0" : "absolute inset-0",
className
)}
>
<div className={cn(loaderClass, "text-foreground opacity-80")} />
</div>
)
}
@@ -13,9 +13,10 @@ import {
interface BuildingTreeNodeProps { interface BuildingTreeNodeProps {
node: BuildingNode; node: BuildingNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function BuildingTreeNode({ node, depth }: BuildingTreeNodeProps) { export function BuildingTreeNode({ node, depth, isLast }: BuildingTreeNodeProps) {
const [expanded, setExpanded] = useState(true); const [expanded, setExpanded] = useState(true);
const createNode = useScene((state) => state.createNode); const createNode = useScene((state) => state.createNode);
const isSelected = useViewer((state) => state.selection.buildingId === node.id); const isSelected = useViewer((state) => state.selection.buildingId === node.id);
@@ -47,6 +48,7 @@ export function BuildingTreeNode({ node, depth }: BuildingTreeNodeProps) {
onClick={handleClick} onClick={handleClick}
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isLast={isLast}
actions={ actions={
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<TreeNodeActions node={node} /> <TreeNodeActions node={node} />
@@ -64,8 +66,8 @@ export function BuildingTreeNode({ node, depth }: BuildingTreeNodeProps) {
</div> </div>
} }
> >
{node.children.map((childId) => ( {node.children.map((childId, index) => (
<TreeNode key={childId} nodeId={childId} depth={depth + 1} /> <TreeNode key={childId} nodeId={childId} depth={depth + 1} isLast={index === node.children.length - 1} />
))} ))}
</TreeNodeWrapper> </TreeNodeWrapper>
); );
@@ -2,16 +2,18 @@ import { type AnyNodeId, CeilingNode, useScene } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import Image from "next/image"; import Image from "next/image";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import useEditor from "@/store/use-editor";
import { InlineRenameInput } from "./inline-rename-input"; import { InlineRenameInput } from "./inline-rename-input";
import { TreeNode, TreeNodeWrapper } from "./tree-node"; import { TreeNode, TreeNodeWrapper, handleTreeSelection } from "./tree-node";
import { TreeNodeActions } from "./tree-node-actions"; import { TreeNodeActions } from "./tree-node-actions";
interface CeilingTreeNodeProps { interface CeilingTreeNodeProps {
node: CeilingNode; node: CeilingNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) { export function CeilingTreeNode({ node, depth, isLast }: CeilingTreeNodeProps) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const selectedIds = useViewer((state) => state.selection.selectedIds); const selectedIds = useViewer((state) => state.selection.selectedIds);
@@ -40,8 +42,12 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
} }
}, [selectedIds, node.id]); }, [selectedIds, node.id]);
const handleClick = () => { const handleClick = (e: React.MouseEvent) => {
setSelection({ selectedIds: [node.id] }); e.stopPropagation();
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection);
if (!handled && useEditor.getState().phase === "furnish") {
useEditor.getState().setPhase("structure");
}
}; };
const handleDoubleClick = () => { const handleDoubleClick = () => {
@@ -62,6 +68,7 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
return ( return (
<TreeNodeWrapper <TreeNodeWrapper
nodeId={node.id}
icon={<Image src="/icons/ceiling.png" alt="" width={14} height={14} className="object-contain" />} icon={<Image src="/icons/ceiling.png" alt="" width={14} height={14} className="object-contain" />}
label={ label={
<InlineRenameInput <InlineRenameInput
@@ -83,10 +90,11 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isVisible={node.visible !== false} isVisible={node.visible !== false}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
> >
{node.children.map((childId) => ( {node.children.map((childId, index) => (
<TreeNode key={childId} nodeId={childId} depth={depth + 1} /> <TreeNode key={childId} nodeId={childId} depth={depth + 1} isLast={index === node.children.length - 1} />
))} ))}
</TreeNodeWrapper> </TreeNodeWrapper>
); );
@@ -4,18 +4,21 @@ import { DoorNode } from "@pascal-app/core"
import { useViewer } from "@pascal-app/viewer" import { useViewer } from "@pascal-app/viewer"
import Image from "next/image" import Image from "next/image"
import { useState } from "react" import { useState } from "react"
import useEditor from "@/store/use-editor"
import { InlineRenameInput } from "./inline-rename-input" import { InlineRenameInput } from "./inline-rename-input"
import { TreeNodeWrapper } from "./tree-node" import { TreeNodeWrapper, handleTreeSelection } from "./tree-node"
import { TreeNodeActions } from "./tree-node-actions" import { TreeNodeActions } from "./tree-node-actions"
interface DoorTreeNodeProps { interface DoorTreeNodeProps {
node: DoorNode node: DoorNode
depth: number depth: number
isLast?: boolean
} }
export function DoorTreeNode({ node, depth }: DoorTreeNodeProps) { export function DoorTreeNode({ node, depth, isLast }: DoorTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id)) const selectedIds = useViewer((state) => state.selection.selectedIds)
const isSelected = selectedIds.includes(node.id)
const isHovered = useViewer((state) => state.hoveredId === node.id) const isHovered = useViewer((state) => state.hoveredId === node.id)
const setSelection = useViewer((state) => state.setSelection) const setSelection = useViewer((state) => state.setSelection)
const setHoveredId = useViewer((state) => state.setHoveredId) const setHoveredId = useViewer((state) => state.setHoveredId)
@@ -24,6 +27,7 @@ export function DoorTreeNode({ node, depth }: DoorTreeNodeProps) {
return ( return (
<TreeNodeWrapper <TreeNodeWrapper
nodeId={node.id}
icon={<Image src="/icons/door.png" alt="" width={14} height={14} className="object-contain" />} icon={<Image src="/icons/door.png" alt="" width={14} height={14} className="object-contain" />}
label={ label={
<InlineRenameInput <InlineRenameInput
@@ -38,13 +42,20 @@ export function DoorTreeNode({ node, depth }: DoorTreeNodeProps) {
hasChildren={false} hasChildren={false}
expanded={false} expanded={false}
onToggle={() => {}} onToggle={() => {}}
onClick={() => setSelection({ selectedIds: [node.id] })} onClick={(e: React.MouseEvent) => {
e.stopPropagation()
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection)
if (!handled && useEditor.getState().phase === "furnish") {
useEditor.getState().setPhase("structure")
}
}}
onDoubleClick={() => setIsEditing(true)} onDoubleClick={() => setIsEditing(true)}
onMouseEnter={() => setHoveredId(node.id)} onMouseEnter={() => setHoveredId(node.id)}
onMouseLeave={() => setHoveredId(null)} onMouseLeave={() => setHoveredId(null)}
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isVisible={node.visible !== false} isVisible={node.visible !== false}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
/> />
) )
@@ -7,30 +7,40 @@ import {
type SiteNode, type SiteNode,
useScene, useScene,
type ZoneNode, type ZoneNode,
type ScanNode,
type GuideNode,
ScanNode as ScanNodeSchema,
GuideNode as GuideNodeSchema,
} from "@pascal-app/core"; } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import { import {
Box,
Building2, Building2,
Camera, Camera,
ChevronDown, ChevronDown,
Image as ImageIcon,
Layers, Layers,
Loader2,
Pentagon, Pentagon,
MoreHorizontal, MoreHorizontal,
Pencil, Pencil,
Plus, Plus,
Trash2, Trash2,
X,
} from "lucide-react"; } from "lucide-react";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import useEditor from "@/store/use-editor"; import useEditor from "@/store/use-editor";
import { TreeNode } from "./tree-node"; import { TreeNode } from "./tree-node";
import { ReferencesDialog } from "./references-dialog";
import { InlineRenameInput } from "./inline-rename-input"; import { InlineRenameInput } from "./inline-rename-input";
import { useProjectStore } from '@/features/community/lib/projects/store';
import { deleteProjectAssetByUrl, uploadProjectAsset } from '@/features/community/lib/assets/actions';
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/primitives/popover"; } from "@/components/ui/primitives/popover";
import { motion, AnimatePresence, LayoutGroup } from "motion/react";
// Preset colors for zones // Preset colors for zones
const PRESET_COLORS = [ const PRESET_COLORS = [
@@ -315,11 +325,188 @@ function CameraPopover({
} }
function ReferenceItem({ refNode, isLastRow, setSelectedReferenceId, handleDelete }: {
refNode: ScanNode | GuideNode;
isLastRow: boolean;
setSelectedReferenceId: (id: string) => void;
handleDelete: (id: string, e: React.MouseEvent) => void;
}) {
const [isEditing, setIsEditing] = useState(false);
return (
<div className="relative group/ref flex items-center border-b border-border/50 text-xs pr-2 transition-colors hover:bg-accent/30 h-8 select-none">
<div className={cn("absolute w-px bg-border/50 pointer-events-none z-10", isLastRow ? "top-0 bottom-1/2" : "top-0 bottom-0")} style={{ left: 45 }} />
<div className="absolute top-1/2 h-px bg-border/50 pointer-events-none z-10" style={{ left: 45, width: 8 }} />
<div
className="flex-1 flex items-center gap-2 pl-[60px] py-0 h-8 text-muted-foreground group-hover/ref:text-foreground cursor-pointer min-w-0"
onClick={() => setSelectedReferenceId(refNode.id)}
onDoubleClick={() => setIsEditing(true)}
>
{refNode.type === 'scan' ? <img src="/icons/mesh.png" alt="Scan" className="w-3.5 h-3.5 shrink-0 object-contain opacity-70 group-hover/ref:opacity-100 transition-opacity" /> : <img src="/icons/floorplan.png" alt="Guide" className="w-3.5 h-3.5 shrink-0 object-contain opacity-70 group-hover/ref:opacity-100 transition-opacity" />}
<InlineRenameInput
node={refNode}
isEditing={isEditing}
onStopEditing={() => setIsEditing(false)}
onStartEditing={() => setIsEditing(true)}
defaultName={refNode.type === 'scan' ? '3D Scan' : 'Guide Image'}
/>
</div>
<button
className="opacity-0 group-hover/ref:opacity-100 w-5 h-5 flex items-center justify-center rounded-md hover:bg-black/5 dark:hover:bg-white/10 text-muted-foreground hover:text-foreground transition-colors cursor-pointer shrink-0 z-20"
onClick={(e) => handleDelete(refNode.id, e)}
title="Delete"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
);
}
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
function LevelReferences({ levelId, isLastLevel }: { levelId: string, isLastLevel?: boolean }) {
const nodes = useScene((s) => s.nodes);
const createNode = useScene((s) => s.createNode);
const deleteNode = useScene((s) => s.deleteNode);
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId);
const activeProject = useProjectStore((s) => s.activeProject);
const [uploadError, setUploadError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadingType, setUploadingType] = useState<'scan'|'guide'|null>(null);
const scanInputRef = useRef<HTMLInputElement>(null);
const references = Object.values(nodes).filter(
(node): node is ScanNode | GuideNode =>
(node.type === 'scan' || node.type === 'guide') && node.parentId === levelId,
);
const handleAddAsset = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
e.target.value = '';
const projectId = activeProject?.id;
if (!projectId) {
setUploadError('No active project. Please open a project first.');
return;
}
if (file.size > MAX_FILE_SIZE) {
setUploadError(`File is too large (${(file.size / 1024 / 1024).toFixed(0)} MB). Maximum size is 100 MB.`);
return;
}
// Auto-detect type based on file extension/mime type
const isScan = file.name.toLowerCase().endsWith('.glb') || file.name.toLowerCase().endsWith('.gltf');
const isImage = file.type.startsWith('image/');
if (!isScan && !isImage) {
setUploadError('Invalid file type. Please upload a .glb/.gltf scan or an image.');
return;
}
const type = isScan ? 'scan' : 'guide';
setUploadError(null);
setUploading(true);
setUploadingType(type);
const result = await uploadProjectAsset(projectId, file, type);
setUploading(false);
setUploadingType(null);
if (!result.success) {
setUploadError(result.error);
return;
}
const Schema = type === 'scan' ? ScanNodeSchema : GuideNodeSchema;
const node = Schema.parse({
url: result.url,
name: file.name,
parentId: levelId,
});
createNode(node, levelId as AnyNodeId);
setSelectedReferenceId(node.id);
};
const handleDelete = async (nodeId: string, e: React.MouseEvent) => {
e.stopPropagation();
const refNode = nodes[nodeId as AnyNodeId] as ScanNode | GuideNode | undefined;
const projectId = activeProject?.id;
if (
projectId &&
refNode?.url &&
(refNode.url.startsWith('http://') || refNode.url.startsWith('https://'))
) {
deleteProjectAssetByUrl(projectId, refNode.url);
}
deleteNode(nodeId as AnyNodeId);
};
const rows = [
{ type: 'upload' as const },
...references.map(ref => ({ type: 'ref' as const, data: ref }))
];
return (
<div className="flex flex-col relative">
{!isLastLevel && (
<div className="absolute top-0 bottom-0 w-px bg-border/50 pointer-events-none z-10" style={{ left: 21 }} />
)}
{rows.map((row, i) => {
const isLastRow = i === rows.length - 1;
if (row.type === 'upload') {
return (
<div key="upload" className="relative group/ref border-b border-border/50">
<div className={cn("absolute w-px bg-border/50 pointer-events-none z-10", isLastRow ? "top-0 bottom-1/2" : "top-0 bottom-0")} style={{ left: 45 }} />
<div className="absolute top-1/2 h-px bg-border/50 pointer-events-none z-10" style={{ left: 45, width: 8 }} />
<button
className="flex items-center gap-2 w-full pl-[60px] pr-2 py-0 h-8 text-xs text-muted-foreground hover:bg-accent/30 hover:text-foreground cursor-pointer transition-colors text-left disabled:opacity-50 disabled:cursor-not-allowed select-none"
disabled={uploading}
onClick={() => scanInputRef.current?.click()}
>
{uploading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />}
{uploading ? `Uploading ${uploadingType}...` : "Upload scan/floorplan"}
</button>
<input ref={scanInputRef} type="file" accept=".glb,.gltf,image/jpeg,image/png,image/webp,image/gif" className="hidden" onChange={handleAddAsset} />
</div>
);
}
const ref = row.data as ScanNode | GuideNode;
return (
<ReferenceItem
key={ref.id}
refNode={ref}
isLastRow={isLastRow}
setSelectedReferenceId={setSelectedReferenceId}
handleDelete={handleDelete}
/>
);
})}
{uploadError && (
<div className="relative pl-[60px] pr-2 py-1 text-[10px] text-destructive border-b border-border/50 bg-destructive/5 select-none min-h-8 flex items-center">
<div className="absolute top-0 bottom-0 w-px bg-border/50 pointer-events-none z-10" style={{ left: 45 }} />
{uploadError}
</div>
)}
</div>
);
}
function LevelItem({ function LevelItem({
level, level,
selectedLevelId, selectedLevelId,
setSelection, setSelection,
setReferencesLevelId,
deleteNode, deleteNode,
updateNode, updateNode,
isLast, isLast,
@@ -327,7 +514,6 @@ function LevelItem({
level: LevelNode; level: LevelNode;
selectedLevelId: string | null; selectedLevelId: string | null;
setSelection: (selection: any) => void; setSelection: (selection: any) => void;
setReferencesLevelId: (id: string | null) => void;
deleteNode: (id: AnyNodeId) => void; deleteNode: (id: AnyNodeId) => void;
updateNode: (id: AnyNodeId, updates: Partial<AnyNode>) => void; updateNode: (id: AnyNodeId, updates: Partial<AnyNode>) => void;
isLast?: boolean; isLast?: boolean;
@@ -336,6 +522,11 @@ function LevelItem({
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const itemRef = useRef<HTMLDivElement>(null); const itemRef = useRef<HTMLDivElement>(null);
const isSelected = selectedLevelId === level.id; const isSelected = selectedLevelId === level.id;
const [isExpanded, setIsExpanded] = useState(isSelected);
useEffect(() => {
setIsExpanded(isSelected);
}, [isSelected]);
useEffect(() => { useEffect(() => {
if (isSelected && itemRef.current) { if (isSelected && itemRef.current) {
@@ -344,37 +535,66 @@ function LevelItem({
}, [isSelected]); }, [isSelected]);
return ( return (
<div <div className="flex flex-col relative">
ref={itemRef}
className={cn(
"flex items-center group/level border-b border-border/50 pr-2 transition-all duration-200 relative",
isSelected
? "bg-accent/50 text-foreground"
: "text-muted-foreground hover:bg-accent/30 hover:text-foreground"
)}
>
{/* Vertical tree line */}
<div className={cn("absolute left-[21px] top-0 w-px bg-border/50 pointer-events-none", isLast ? "bottom-1/2" : "bottom-0")} />
{/* Horizontal branch line */}
<div className="absolute left-[21px] top-1/2 w-4 h-px bg-border/50 pointer-events-none" />
<div <div
className="flex-1 flex items-center gap-2 pl-10 py-2 text-sm cursor-pointer min-w-0" ref={itemRef}
onClick={() => setSelection({ levelId: level.id })} className={cn(
onDoubleClick={() => setIsEditing(true)} "flex items-center group/level border-b border-border/50 pr-2 transition-all duration-200 relative h-8 select-none",
isSelected
? "bg-accent/50 text-foreground"
: "text-muted-foreground hover:bg-accent/30 hover:text-foreground"
)}
> >
<Layers className={cn( {/* Vertical tree line */}
"w-3.5 h-3.5 shrink-0 transition-all duration-200", <div className={cn("absolute left-[21px] w-px bg-border/50 pointer-events-none z-10", isLast && !isExpanded ? "top-0 bottom-1/2" : "top-0 bottom-0")} />
!isSelected && "opacity-60 grayscale" {/* Horizontal branch line */}
<div className="absolute left-[21px] top-1/2 w-[11px] h-px bg-border/50 pointer-events-none z-10" />
<div className={cn(
"absolute left-[32px] top-[10px] w-4 h-[12px] pointer-events-none z-10 transition-colors duration-200",
isSelected ? "bg-accent/50" : "bg-background group-hover/level:bg-accent/30"
)} /> )} />
<InlineRenameInput {/* Line down to children */}
node={level} {isExpanded && (
isEditing={isEditing} <div className="absolute left-[45px] top-[16px] bottom-0 w-px bg-border/50 pointer-events-none z-10" />
onStopEditing={() => setIsEditing(false)} )}
onStartEditing={() => setIsEditing(true)}
defaultName={`Level ${level.level}`} <div className="flex items-center pl-[28px] pr-1 z-20 relative h-8">
/> <button
</div> className="w-4 h-4 flex items-center justify-center shrink-0 z-20 bg-inherit cursor-pointer"
onClick={(e) => {
e.stopPropagation();
if (!isSelected) {
setSelection({ levelId: level.id });
} else {
setIsExpanded(!isExpanded);
}
}}
>
{isExpanded ? (
<ChevronDown className="w-3 h-3 text-muted-foreground" />
) : (
<ChevronDown className="w-3 h-3 -rotate-90 text-muted-foreground" />
)}
</button>
</div>
<div className="flex-1 flex items-center gap-2 py-0 text-sm cursor-pointer min-w-0 h-8 pl-0.5"
onClick={() => setSelection({ levelId: level.id })}
onDoubleClick={() => setIsEditing(true)}
>
<img
src="/icons/level.png"
className={cn("w-4 h-4 object-contain shrink-0 transition-all duration-200", !isSelected && "opacity-60 grayscale")}
alt="Level"
/>
<InlineRenameInput
node={level}
isEditing={isEditing}
onStopEditing={() => setIsEditing(false)}
onStartEditing={() => setIsEditing(true)}
defaultName={`Level ${level.level}`}
/>
</div>
{/* Camera snapshot button */} {/* Camera snapshot button */}
<Popover open={cameraPopoverOpen} onOpenChange={setCameraPopoverOpen}> <Popover open={cameraPopoverOpen} onOpenChange={setCameraPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -456,12 +676,6 @@ function LevelItem({
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="start" side="right" className="w-40 p-1"> <PopoverContent align="start" side="right" className="w-40 p-1">
<button
className="flex items-center gap-2 w-full px-3 py-1.5 rounded text-sm hover:bg-accent cursor-pointer"
onClick={() => setReferencesLevelId(level.id)}
>
References
</button>
{level.level !== 0 && ( {level.level !== 0 && (
<button <button
className="flex items-center gap-2 w-full px-3 py-1.5 rounded text-sm hover:bg-accent hover:text-red-600 cursor-pointer" className="flex items-center gap-2 w-full px-3 py-1.5 rounded text-sm hover:bg-accent hover:text-red-600 cursor-pointer"
@@ -474,6 +688,20 @@ function LevelItem({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ type: "spring", bounce: 0, duration: 0.3 }}
className="overflow-hidden"
>
<LevelReferences levelId={level.id} isLastLevel={isLast} />
</motion.div>
)}
</AnimatePresence>
</div>
); );
} }
@@ -486,8 +714,6 @@ function LevelsSection() {
const selectedLevelId = useViewer((state) => state.selection.levelId); const selectedLevelId = useViewer((state) => state.selection.levelId);
const setSelection = useViewer((state) => state.setSelection); const setSelection = useViewer((state) => state.setSelection);
const [referencesLevelId, setReferencesLevelId] = useState<string | null>(null);
const building = selectedBuildingId const building = selectedBuildingId
? (nodes[selectedBuildingId] as BuildingNode) ? (nodes[selectedBuildingId] as BuildingNode)
: null; : null;
@@ -511,24 +737,27 @@ function LevelsSection() {
return ( return (
<div className="flex flex-col relative"> <div className="flex flex-col relative">
{/* Level buttons */} {/* Level buttons */}
<div className="flex flex-col"> <div className="flex flex-col flex-1 min-h-0">
<button <button
className="flex items-center gap-2 pl-10 py-2 text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground cursor-pointer transition-all duration-200 border-b border-border/50 relative" className="flex items-center gap-2 pl-0 py-0 text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground cursor-pointer transition-all duration-200 border-b border-border/50 relative h-8 select-none"
onClick={handleAddLevel} onClick={handleAddLevel}
> >
{/* Vertical tree line */} {/* Vertical tree line */}
<div className="absolute left-[21px] top-0 bottom-0 w-px bg-border/50 pointer-events-none" /> <div className="absolute left-[21px] top-0 bottom-0 w-px bg-border/50 pointer-events-none" />
{/* Horizontal branch line */} {/* Horizontal branch line */}
<div className="absolute left-[21px] top-1/2 w-4 h-px bg-border/50 pointer-events-none" /> <div className="absolute left-[21px] top-1/2 w-[11px] h-px bg-border/50 pointer-events-none z-10" />
<Plus className="w-3.5 h-3.5" />
Add level <div className="flex items-center pl-[38px] pr-1 z-10 relative">
<Plus className="w-3.5 h-3.5" />
</div>
<span className="truncate">Add level</span>
</button> </button>
{levels.length === 0 && ( {levels.length === 0 && (
<div className="text-xs text-muted-foreground pl-10 pr-2 py-2 relative border-b border-border/50"> <div className="text-xs text-muted-foreground pl-[38px] pr-2 py-0 relative border-b border-border/50 h-8 flex items-center select-none">
{/* Vertical tree line */} {/* Vertical tree line */}
<div className="absolute left-[21px] top-0 bottom-1/2 w-px bg-border/50 pointer-events-none" /> <div className="absolute left-[21px] top-0 bottom-1/2 w-px bg-border/50 pointer-events-none" />
{/* Horizontal branch line */} {/* Horizontal branch line */}
<div className="absolute left-[21px] top-1/2 w-4 h-px bg-border/50 pointer-events-none" /> <div className="absolute left-[21px] top-1/2 w-[11px] h-px bg-border/50 pointer-events-none" />
No levels yet No levels yet
</div> </div>
)} )}
@@ -538,24 +767,12 @@ function LevelsSection() {
level={level} level={level}
selectedLevelId={selectedLevelId} selectedLevelId={selectedLevelId}
setSelection={setSelection} setSelection={setSelection}
setReferencesLevelId={setReferencesLevelId}
deleteNode={deleteNode} deleteNode={deleteNode}
updateNode={updateNode} updateNode={updateNode}
isLast={index === levels.length - 1} isLast={index === levels.length - 1}
/> />
))} ))}
</div> </div>
{/* References dialog */}
{referencesLevelId && (
<ReferencesDialog
levelId={referencesLevelId}
open={!!referencesLevelId}
onOpenChange={(open) => {
if (!open) setReferencesLevelId(null);
}}
/>
)}
</div> </div>
); );
} }
@@ -566,13 +783,18 @@ function LayerToggle() {
const phase = useEditor((state) => state.phase); const phase = useEditor((state) => state.phase);
const setPhase = useEditor((state) => state.setPhase); const setPhase = useEditor((state) => state.setPhase);
const activeTab =
phase === "structure" && structureLayer === "elements" ? "structure" :
phase === "furnish" ? "furnish" :
phase === "structure" && structureLayer === "zones" ? "zones" : "none";
return ( return (
<div className="flex items-center p-1 bg-accent/20 gap-1 border-b border-border/50"> <div className="flex items-center p-1 bg-accent/20 gap-1 border-b border-border/50 relative">
<button <button
className={cn( className={cn(
"flex-1 flex flex-col items-center justify-center py-2 rounded-md text-[10px] font-medium transition-all duration-200 cursor-pointer", "relative flex-1 flex flex-col items-center justify-center py-2 rounded-md text-[10px] font-medium transition-all duration-200 cursor-pointer",
phase === "structure" && structureLayer === "elements" activeTab === "structure"
? "bg-white dark:bg-background shadow-sm ring-1 ring-black/5 dark:ring-white/10 text-foreground" ? "text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-accent/50" : "text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-accent/50"
)} )}
onClick={() => { onClick={() => {
@@ -580,36 +802,56 @@ function LayerToggle() {
setStructureLayer("elements"); setStructureLayer("elements");
}} }}
> >
<img {activeTab === "structure" && (
src="/icons/room.png" <motion.div
alt="Structure" layoutId="layerToggleActiveBg"
className={cn("w-6 h-6 mb-1", !(phase === "structure" && structureLayer === "elements") && "opacity-50 grayscale")} className="absolute inset-0 bg-white dark:bg-background shadow-sm ring-1 ring-black/5 dark:ring-white/10 rounded-md"
/> transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
Structure />
)}
<div className="relative z-10 flex flex-col items-center">
<img
src="/icons/room.png"
alt="Structure"
className={cn("w-6 h-6 mb-1 transition-all", activeTab !== "structure" && "opacity-50 grayscale")}
/>
Structure
</div>
</button> </button>
<button <button
className={cn( className={cn(
"flex-1 flex flex-col items-center justify-center py-2 rounded-md text-[10px] font-medium transition-all duration-200 cursor-pointer", "relative flex-1 flex flex-col items-center justify-center py-2 rounded-md text-[10px] font-medium transition-all duration-200 cursor-pointer",
phase === "furnish" activeTab === "furnish"
? "bg-white dark:bg-background shadow-sm ring-1 ring-black/5 dark:ring-white/10 text-foreground" ? "text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-accent/50" : "text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-accent/50"
)} )}
onClick={() => { onClick={() => {
setPhase("furnish"); setPhase("furnish");
}} }}
> >
<img {activeTab === "furnish" && (
src="/icons/couch.png" <motion.div
alt="Furnish" layoutId="layerToggleActiveBg"
className={cn("w-6 h-6 mb-1", phase !== "furnish" && "opacity-50 grayscale")} className="absolute inset-0 bg-white dark:bg-background shadow-sm ring-1 ring-black/5 dark:ring-white/10 rounded-md"
/> transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
Furnish />
)}
<div className="relative z-10 flex flex-col items-center">
<img
src="/icons/couch.png"
alt="Furnish"
className={cn("w-6 h-6 mb-1 transition-all", activeTab !== "furnish" && "opacity-50 grayscale")}
/>
Furnish
</div>
</button> </button>
<button <button
className={cn( className={cn(
"flex-1 flex flex-col items-center justify-center py-2 rounded-md text-[10px] font-medium transition-all duration-200 cursor-pointer", "relative flex-1 flex flex-col items-center justify-center py-2 rounded-md text-[10px] font-medium transition-all duration-200 cursor-pointer",
phase === "structure" && structureLayer === "zones" activeTab === "zones"
? "bg-white dark:bg-background shadow-sm ring-1 ring-black/5 dark:ring-white/10 text-foreground" ? "text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-accent/50" : "text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-accent/50"
)} )}
onClick={() => { onClick={() => {
@@ -617,18 +859,27 @@ function LayerToggle() {
setStructureLayer("zones"); setStructureLayer("zones");
}} }}
> >
<img {activeTab === "zones" && (
src="/icons/kitchen.png" <motion.div
alt="Zones" layoutId="layerToggleActiveBg"
className={cn("w-6 h-6 mb-1", !(phase === "structure" && structureLayer === "zones") && "opacity-50 grayscale")} className="absolute inset-0 bg-white dark:bg-background shadow-sm ring-1 ring-black/5 dark:ring-white/10 rounded-md"
/> transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
Zones />
)}
<div className="relative z-10 flex flex-col items-center">
<img
src="/icons/kitchen.png"
alt="Zones"
className={cn("w-6 h-6 mb-1 transition-all", activeTab !== "zones" && "opacity-50 grayscale")}
/>
Zones
</div>
</button> </button>
</div> </div>
); );
} }
function ZoneItem({ zone }: { zone: ZoneNode }) { function ZoneItem({ zone, isLast }: { zone: ZoneNode, isLast?: boolean }) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false); const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false);
const deleteNode = useScene((state) => state.deleteNode); const deleteNode = useScene((state) => state.deleteNode);
@@ -680,7 +931,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
<div <div
ref={itemRef} ref={itemRef}
className={cn( className={cn(
"flex items-center h-8 cursor-pointer group/row text-sm px-3 select-none border-b border-border/50 transition-all duration-200", "relative flex items-center h-8 cursor-pointer group/row text-sm px-3 select-none border-b border-border/50 transition-all duration-200",
isSelected isSelected
? "bg-accent/50 text-foreground" ? "bg-accent/50 text-foreground"
: isHovered : isHovered
@@ -692,6 +943,11 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
onMouseEnter={() => setHoveredId(zone.id)} onMouseEnter={() => setHoveredId(zone.id)}
onMouseLeave={() => setHoveredId(null)} onMouseLeave={() => setHoveredId(null)}
> >
{/* Vertical tree line */}
<div className={cn("absolute w-px bg-border/50 pointer-events-none", isLast ? "top-0 bottom-1/2" : "top-0 bottom-0")} style={{ left: 8 }} />
{/* Horizontal branch line */}
<div className="absolute top-1/2 h-px bg-border/50 pointer-events-none" style={{ left: 8, width: 4 }} />
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<button <button
@@ -805,6 +1061,28 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
); );
} }
function MultiSelectionBadge() {
const selectedIds = useViewer((state) => state.selection.selectedIds);
const setSelection = useViewer((state) => state.setSelection);
if (selectedIds.length <= 1) return null;
return (
<div className="sticky top-4 z-50 pointer-events-none flex justify-center w-full h-0 overflow-visible">
<div className="pointer-events-auto flex items-center gap-2.5 px-0.5 pl-2 py-4 bg-primary text-primary-foreground text-xs font-medium rounded-full shadow-lg shadow-black/10 border border-primary/20 backdrop-blur-md">
<span>{selectedIds.length} objects selected</span>
<button
onClick={() => setSelection({ selectedIds: [] })}
className="hover:bg-primary-foreground/20 p-1.5 rounded-full transition-colors cursor-pointer"
title="Clear selection"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
);
}
function ContentSection() { function ContentSection() {
const nodes = useScene((state) => state.nodes); const nodes = useScene((state) => state.nodes);
const selectedLevelId = useViewer((state) => state.selection.levelId); const selectedLevelId = useViewer((state) => state.selection.levelId);
@@ -853,8 +1131,8 @@ function ContentSection() {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
{levelZones.map((zone) => ( {levelZones.map((zone, index) => (
<ZoneItem key={zone.id} zone={zone} /> <ZoneItem key={zone.id} zone={zone} isLast={index === levelZones.length - 1} />
))} ))}
</div> </div>
); );
@@ -865,28 +1143,9 @@ function ContentSection() {
const childNode = nodes[childId]; const childNode = nodes[childId];
if (!childNode || childNode.type === "zone") return false; if (!childNode || childNode.type === "zone") return false;
// In structure mode, show structural elements (walls, slabs, etc.) and doors/windows // We no longer filter out structural nodes in furnish mode or furnish nodes in structure mode
if (phase === "structure") { // This allows nested items (like lights in a ceiling or cabinetry on a wall) to remain visible
if (childNode.type === "item") { // and selectable in both modes, ensuring seamless transition in the tree view.
const category = childNode.asset?.category?.toLowerCase() || "";
// Only show doors and windows in structure mode
return category === "door" || category === "window";
}
// Show all other structural elements (walls, slabs, ceiling, roof)
return true;
}
// In furnish mode, only show items that are NOT doors or windows
if (phase === "furnish") {
if (childNode.type === "item") {
const category = childNode.asset?.category?.toLowerCase() || "";
// Hide doors and windows in furnish mode
return category !== "door" && category !== "window";
}
// Hide structural elements in furnish mode
return false;
}
return true; return true;
}); });
@@ -900,8 +1159,8 @@ function ContentSection() {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
{elementChildren.map((childId) => ( {elementChildren.map((childId, index) => (
<TreeNode key={childId} nodeId={childId} depth={0} /> <TreeNode key={childId} nodeId={childId} depth={0} isLast={index === elementChildren.length - 1} />
))} ))}
</div> </div>
); );
@@ -931,8 +1190,13 @@ function BuildingItem({
}, [isBuildingActive]); }, [isBuildingActive]);
return ( return (
<div className={cn("flex flex-col", isBuildingActive && "flex-1 min-h-0")}> <motion.div
<div layout
transition={{ type: "spring", bounce: 0, duration: 0.4 }}
className={cn("flex flex-col shrink-0 overflow-hidden", isBuildingActive && "flex-1 min-h-0")}
>
<motion.div
layout="position"
ref={itemRef} ref={itemRef}
className={cn( className={cn(
"group/building flex items-center h-10 border-b border-border/50 pr-2 transition-all duration-200 shrink-0", "group/building flex items-center h-10 border-b border-border/50 pr-2 transition-all duration-200 shrink-0",
@@ -1025,21 +1289,32 @@ function BuildingItem({
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </motion.div>
{/* Tools and content for the active building */} {/* Tools and content for the active building */}
{isBuildingActive && ( <AnimatePresence initial={false}>
<div className="flex flex-col flex-1 min-h-0 animate-in fade-in slide-in-from-top-2 duration-200"> {isBuildingActive && (
<div className="shrink-0 flex flex-col"> <motion.div
<LevelsSection /> initial={{ opacity: 0, flex: 0 }}
<LayerToggle /> animate={{ opacity: 1, flex: "1 1 0%" }}
</div> exit={{ opacity: 0, flex: "0 0 0px" }}
<div className="flex-1 overflow-y-auto overflow-x-hidden min-h-0"> transition={{ type: "spring", bounce: 0, duration: 0.4 }}
<ContentSection /> className="flex flex-col w-full overflow-hidden"
</div> >
</div> <div className="flex flex-col flex-1 min-h-0 w-full">
)} <div className="shrink-0 flex flex-col">
</div> <LevelsSection />
<LayerToggle />
</div>
<div className="flex-1 overflow-y-auto overflow-x-hidden min-h-0 relative">
<MultiSelectionBadge />
<ContentSection />
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
); );
} }
@@ -1064,61 +1339,77 @@ export function SitePanel() {
.filter((node): node is BuildingNode => node?.type === "building"); .filter((node): node is BuildingNode => node?.type === "building");
return ( return (
<div className="flex flex-col h-full"> <LayoutGroup>
{/* Site Header */} <div className="flex flex-col h-full">
{siteNode && ( {/* Site Header */}
<div {siteNode && (
className={cn( <motion.div
"flex items-center justify-between px-3 py-3 border-b border-border/50 cursor-pointer transition-colors shrink-0", layout="position"
phase === "site" ? "bg-accent/50 text-foreground" : "hover:bg-accent/30 text-muted-foreground hover:text-foreground" className={cn(
)} "flex items-center justify-between px-3 py-3 border-b border-border/50 cursor-pointer transition-colors shrink-0",
onClick={() => setPhase("site")} phase === "site" ? "bg-accent/50 text-foreground" : "hover:bg-accent/30 text-muted-foreground hover:text-foreground"
> )}
<div className="flex items-center gap-2"> onClick={() => setPhase("site")}
<img >
src="/icons/site.png" <div className="flex items-center gap-2">
className={cn("w-5 h-5 object-contain transition-all", phase !== "site" && "opacity-60 grayscale")} <img
alt="Site" src="/icons/site.png"
className={cn("w-5 h-5 object-contain transition-all", phase !== "site" && "opacity-60 grayscale")}
alt="Site"
/>
<span className="text-sm font-medium">{siteNode.name || "Site"}</span>
</div>
<CameraPopover
nodeId={siteNode.id as AnyNodeId}
hasCamera={!!siteNode.camera}
open={siteCameraOpen}
onOpenChange={setSiteCameraOpen}
buttonClassName={cn("transition-colors", phase === "site" ? "hover:bg-black/5 dark:hover:bg-white/10" : "hover:bg-accent")}
/> />
<span className="text-sm font-medium">{siteNode.name || "Site"}</span> </motion.div>
</div>
<CameraPopover
nodeId={siteNode.id as AnyNodeId}
hasCamera={!!siteNode.camera}
open={siteCameraOpen}
onOpenChange={setSiteCameraOpen}
buttonClassName={cn("transition-colors", phase === "site" ? "hover:bg-black/5 dark:hover:bg-white/10" : "hover:bg-accent")}
/>
</div>
)}
<div className={cn("flex-1 flex flex-col min-h-0", phase === "site" && "overflow-y-auto")}>
{/* When phase is site, show property line immediately under site header */}
{phase === "site" && <div className="shrink-0"><PropertyLineSection /></div>}
{/* Buildings List */}
{buildings.length === 0 ? (
<div className="px-3 py-4 text-sm text-muted-foreground">
No buildings yet
</div>
) : (
<div className="flex flex-col flex-1 min-h-0">
{buildings.map((building) => {
const isBuildingActive = (phase === "structure" || phase === "furnish") && selectedBuildingId === building.id;
return (
<BuildingItem
key={building.id}
building={building}
isBuildingActive={isBuildingActive}
buildingCameraOpen={buildingCameraOpen}
setBuildingCameraOpen={setBuildingCameraOpen}
/>
);
})}
</div>
)} )}
<motion.div layout className={cn("flex-1 flex flex-col min-h-0", phase === "site" && "overflow-y-auto")}>
{/* When phase is site, show property line immediately under site header */}
<AnimatePresence initial={false}>
{phase === "site" && (
<motion.div
layout="position"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ type: "spring", bounce: 0, duration: 0.4 }}
className="shrink-0 overflow-hidden"
>
<PropertyLineSection />
</motion.div>
)}
</AnimatePresence>
{/* Buildings List */}
{buildings.length === 0 ? (
<motion.div layout="position" className="px-3 py-4 text-sm text-muted-foreground">
No buildings yet
</motion.div>
) : (
<motion.div layout className="flex flex-col flex-1 min-h-0">
{buildings.map((building) => {
const isBuildingActive = (phase === "structure" || phase === "furnish") && selectedBuildingId === building.id;
return (
<BuildingItem
key={building.id}
building={building}
isBuildingActive={isBuildingActive}
buildingCameraOpen={buildingCameraOpen}
setBuildingCameraOpen={setBuildingCameraOpen}
/>
);
})}
</motion.div>
)}
</motion.div>
</div> </div>
</div> </LayoutGroup>
); );
} }
@@ -2,8 +2,9 @@ import { type AnyNodeId, ItemNode, useScene } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import Image from "next/image"; import Image from "next/image";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import useEditor from "@/store/use-editor";
import { InlineRenameInput } from "./inline-rename-input"; import { InlineRenameInput } from "./inline-rename-input";
import { TreeNode, TreeNodeWrapper } from "./tree-node"; import { TreeNode, TreeNodeWrapper, handleTreeSelection } from "./tree-node";
import { TreeNodeActions } from "./tree-node-actions"; import { TreeNodeActions } from "./tree-node-actions";
const CATEGORY_ICONS: Record<string, string> = { const CATEGORY_ICONS: Record<string, string> = {
@@ -19,9 +20,10 @@ const CATEGORY_ICONS: Record<string, string> = {
interface ItemTreeNodeProps { interface ItemTreeNodeProps {
node: ItemNode; node: ItemNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) { export function ItemTreeNode({ node, depth, isLast }: ItemTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [expanded, setExpanded] = useState(true); const [expanded, setExpanded] = useState(true);
const iconSrc = CATEGORY_ICONS[node.asset.category] || "/icons/couch.png"; const iconSrc = CATEGORY_ICONS[node.asset.category] || "/icons/couch.png";
@@ -51,8 +53,12 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
} }
}, [selectedIds, node.id]); }, [selectedIds, node.id]);
const handleClick = () => { const handleClick = (e: React.MouseEvent) => {
setSelection({ selectedIds: [node.id] }); e.stopPropagation();
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection);
if (!handled && useEditor.getState().phase === "structure") {
useEditor.getState().setPhase("furnish");
}
}; };
const handleDoubleClick = () => { const handleDoubleClick = () => {
@@ -72,6 +78,7 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
return ( return (
<TreeNodeWrapper <TreeNodeWrapper
nodeId={node.id}
icon={<Image src={iconSrc} alt="" width={14} height={14} className="object-contain" />} icon={<Image src={iconSrc} alt="" width={14} height={14} className="object-contain" />}
label={ label={
<InlineRenameInput <InlineRenameInput
@@ -93,10 +100,11 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isVisible={node.visible !== false} isVisible={node.visible !== false}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
> >
{hasChildren && node.children.map((childId) => ( {hasChildren && node.children.map((childId, index) => (
<TreeNode key={childId} nodeId={childId} depth={depth + 1} /> <TreeNode key={childId} nodeId={childId} depth={depth + 1} isLast={index === node.children.length - 1} />
))} ))}
</TreeNodeWrapper> </TreeNodeWrapper>
); );
@@ -9,9 +9,10 @@ import { TreeNodeActions } from "./tree-node-actions";
interface LevelTreeNodeProps { interface LevelTreeNodeProps {
node: LevelNode; node: LevelNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) { export function LevelTreeNode({ node, depth, isLast }: LevelTreeNodeProps) {
const [expanded, setExpanded] = useState(true); const [expanded, setExpanded] = useState(true);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const isSelected = useViewer((state) => state.selection.levelId === node.id); const isSelected = useViewer((state) => state.selection.levelId === node.id);
@@ -48,10 +49,11 @@ export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
> >
{node.children.map((childId) => ( {node.children.map((childId, index) => (
<TreeNode key={childId} nodeId={childId} depth={depth + 1} /> <TreeNode key={childId} nodeId={childId} depth={depth + 1} isLast={index === node.children.length - 1} />
))} ))}
</TreeNodeWrapper> </TreeNodeWrapper>
); );
@@ -1,265 +0,0 @@
'use client'
import {
type AnyNodeId,
type GuideNode,
GuideNode as GuideNodeSchema,
type LevelNode,
type ScanNode,
ScanNode as ScanNodeSchema,
useScene,
} from '@pascal-app/core'
import { Box, Image, Pencil, Plus, Trash2 } from 'lucide-react'
import { useCallback, useRef, useState } from 'react'
import useEditor from '@/store/use-editor'
import { deleteProjectAssetByUrl, uploadProjectAsset } from '@/features/community/lib/assets/actions'
import { useProjectStore } from '@/features/community/lib/projects/store'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/primitives/dialog'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/primitives/popover'
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB — matches server action bodySizeLimit
interface ReferencesDialogProps {
levelId: string
open: boolean
onOpenChange: (open: boolean) => void
}
export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDialogProps) {
const nodes = useScene((s) => s.nodes)
const createNode = useScene((s) => s.createNode)
const deleteNode = useScene((s) => s.deleteNode)
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
const activeProject = useProjectStore((s) => s.activeProject)
const [uploadError, setUploadError] = useState<string | null>(null)
const [uploading, setUploading] = useState(false)
const scanInputRef = useRef<HTMLInputElement>(null)
const guideInputRef = useRef<HTMLInputElement>(null)
const handleAddScan = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
e.target.value = ''
const projectId = activeProject?.id
if (!projectId) {
setUploadError('No active project. Please open a project first.')
return
}
if (file.size > MAX_FILE_SIZE) {
setUploadError(`File is too large (${(file.size / 1024 / 1024).toFixed(0)} MB). Maximum size is 100 MB.`)
return
}
setUploadError(null)
setUploading(true)
const result = await uploadProjectAsset(projectId, file, 'scan')
setUploading(false)
if (!result.success) {
setUploadError(result.error)
return
}
const node = ScanNodeSchema.parse({
url: result.url,
name: file.name,
parentId: levelId,
})
createNode(node, levelId as AnyNodeId)
// Auto-select and close dialog
setSelectedReferenceId(node.id)
onOpenChange(false)
},
[levelId, createNode, setSelectedReferenceId, onOpenChange, activeProject],
)
const handleAddGuide = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
e.target.value = ''
const projectId = activeProject?.id
if (!projectId) {
setUploadError('No active project. Please open a project first.')
return
}
if (file.size > MAX_FILE_SIZE) {
setUploadError(`File is too large (${(file.size / 1024 / 1024).toFixed(0)} MB). Maximum size is 100 MB.`)
return
}
setUploadError(null)
setUploading(true)
const result = await uploadProjectAsset(projectId, file, 'guide')
setUploading(false)
if (!result.success) {
setUploadError(result.error)
return
}
const node = GuideNodeSchema.parse({
url: result.url,
name: file.name,
parentId: levelId,
})
createNode(node, levelId as AnyNodeId)
// Auto-select and close dialog
setSelectedReferenceId(node.id)
onOpenChange(false)
},
[levelId, createNode, setSelectedReferenceId, onOpenChange, activeProject],
)
const handleEdit = useCallback(
(nodeId: string) => {
setSelectedReferenceId(nodeId)
onOpenChange(false)
},
[setSelectedReferenceId, onOpenChange],
)
const handleDelete = useCallback(
async (nodeId: string) => {
const refNode = nodes[nodeId as AnyNodeId] as ScanNode | GuideNode | undefined
const projectId = activeProject?.id
// Delete storage asset first (before removing from scene)
if (
projectId &&
refNode?.url &&
(refNode.url.startsWith('http://') || refNode.url.startsWith('https://'))
) {
const result = await deleteProjectAssetByUrl(projectId, refNode.url)
if (!result.success) {
setUploadError(`Failed to delete asset: ${result.error}`)
return
}
}
deleteNode(nodeId as AnyNodeId)
},
[deleteNode, nodes, activeProject],
)
const level = nodes[levelId as AnyNodeId] as LevelNode | undefined
if (!level) return null
// Find all scan and guide children of this level
const references = Object.values(nodes).filter(
(node): node is ScanNode | GuideNode =>
(node.type === 'scan' || node.type === 'guide') && node.parentId === levelId,
)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>References {level.name || `Level ${level.level}`}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-1 max-h-64 overflow-y-auto">
{references.length === 0 && (
<p className="text-sm text-muted-foreground py-4 text-center">
No references yet. Add a 3D scan or guide image.
</p>
)}
{references.map((ref) => (
<div
key={ref.id}
className="flex items-center gap-2 px-2 py-1.5 rounded text-sm hover:bg-accent/50 group"
>
{ref.type === 'scan' ? (
<Box className="w-4 h-4 shrink-0 text-muted-foreground" />
) : (
<Image className="w-4 h-4 shrink-0 text-muted-foreground" />
)}
<span className="flex-1 truncate">
{ref.name || (ref.type === 'scan' ? '3D Scan' : 'Guide Image')}
</span>
<button
className="opacity-0 group-hover:opacity-100 w-6 h-6 flex items-center justify-center rounded hover:bg-accent cursor-pointer"
onClick={() => handleEdit(ref.id)}
title="Edit"
>
<Pencil className="w-3 h-3" />
</button>
<button
className="opacity-0 group-hover:opacity-100 w-6 h-6 flex items-center justify-center rounded hover:bg-destructive/10 text-destructive cursor-pointer"
onClick={() => handleDelete(ref.id)}
title="Delete"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
))}
</div>
{uploadError && (
<p className="text-xs text-destructive px-1 pb-1">{uploadError}</p>
)}
<div className="flex justify-end pt-2 border-t border-border/50">
<input
ref={scanInputRef}
type="file"
accept=".glb,.gltf"
className="hidden"
onChange={handleAddScan}
/>
<input
ref={guideInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleAddGuide}
/>
<Popover>
<PopoverTrigger asChild>
<button
disabled={uploading}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm bg-primary text-primary-foreground hover:bg-primary/90 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="w-3.5 h-3.5" />
{uploading ? 'Uploading…' : 'Add'}
</button>
</PopoverTrigger>
<PopoverContent align="end" className="w-44 p-1">
<button
className="flex items-center gap-2 w-full px-3 py-2 rounded-md text-sm hover:bg-accent cursor-pointer"
onClick={() => scanInputRef.current?.click()}
>
<Box className="w-4 h-4" />
3D Scan
</button>
<button
className="flex items-center gap-2 w-full px-3 py-2 rounded-md text-sm hover:bg-accent cursor-pointer"
onClick={() => guideInputRef.current?.click()}
>
<Image className="w-4 h-4" />
Guide Image
</button>
</PopoverContent>
</Popover>
</div>
</DialogContent>
</Dialog>
)
}
@@ -2,24 +2,31 @@ import { RoofNode } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import useEditor from "@/store/use-editor";
import { InlineRenameInput } from "./inline-rename-input"; import { InlineRenameInput } from "./inline-rename-input";
import { TreeNodeWrapper } from "./tree-node"; import { TreeNodeWrapper, handleTreeSelection } from "./tree-node";
import { TreeNodeActions } from "./tree-node-actions"; import { TreeNodeActions } from "./tree-node-actions";
interface RoofTreeNodeProps { interface RoofTreeNodeProps {
node: RoofNode; node: RoofNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) { export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id)); const selectedIds = useViewer((state) => state.selection.selectedIds);
const isSelected = selectedIds.includes(node.id);
const isHovered = useViewer((state) => state.hoveredId === node.id); const isHovered = useViewer((state) => state.hoveredId === node.id);
const setSelection = useViewer((state) => state.setSelection); const setSelection = useViewer((state) => state.setSelection);
const setHoveredId = useViewer((state) => state.setHoveredId); const setHoveredId = useViewer((state) => state.setHoveredId);
const handleClick = () => { const handleClick = (e: React.MouseEvent) => {
setSelection({ selectedIds: [node.id] }); e.stopPropagation();
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection);
if (!handled && useEditor.getState().phase === "furnish") {
useEditor.getState().setPhase("structure");
}
}; };
const handleDoubleClick = () => { const handleDoubleClick = () => {
@@ -41,6 +48,7 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) {
return ( return (
<TreeNodeWrapper <TreeNodeWrapper
nodeId={node.id}
icon={<Image src="/icons/roof.png" alt="" width={14} height={14} className="object-contain" />} icon={<Image src="/icons/roof.png" alt="" width={14} height={14} className="object-contain" />}
label={ label={
<InlineRenameInput <InlineRenameInput
@@ -62,6 +70,7 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) {
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isVisible={node.visible !== false} isVisible={node.visible !== false}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
/> />
); );
@@ -2,24 +2,31 @@ import { SlabNode } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import useEditor from "@/store/use-editor";
import { InlineRenameInput } from "./inline-rename-input"; import { InlineRenameInput } from "./inline-rename-input";
import { TreeNodeWrapper } from "./tree-node"; import { TreeNodeWrapper, handleTreeSelection } from "./tree-node";
import { TreeNodeActions } from "./tree-node-actions"; import { TreeNodeActions } from "./tree-node-actions";
interface SlabTreeNodeProps { interface SlabTreeNodeProps {
node: SlabNode; node: SlabNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { export function SlabTreeNode({ node, depth, isLast }: SlabTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id)); const selectedIds = useViewer((state) => state.selection.selectedIds);
const isSelected = selectedIds.includes(node.id);
const isHovered = useViewer((state) => state.hoveredId === node.id); const isHovered = useViewer((state) => state.hoveredId === node.id);
const setSelection = useViewer((state) => state.setSelection); const setSelection = useViewer((state) => state.setSelection);
const setHoveredId = useViewer((state) => state.setHoveredId); const setHoveredId = useViewer((state) => state.setHoveredId);
const handleClick = () => { const handleClick = (e: React.MouseEvent) => {
setSelection({ selectedIds: [node.id] }); e.stopPropagation();
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection);
if (!handled && useEditor.getState().phase === "furnish") {
useEditor.getState().setPhase("structure");
}
}; };
const handleDoubleClick = () => { const handleDoubleClick = () => {
@@ -40,6 +47,7 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) {
return ( return (
<TreeNodeWrapper <TreeNodeWrapper
nodeId={node.id}
icon={<Image src="/icons/floor.png" alt="" width={14} height={14} className="object-contain" />} icon={<Image src="/icons/floor.png" alt="" width={14} height={14} className="object-contain" />}
label={ label={
<InlineRenameInput <InlineRenameInput
@@ -61,6 +69,7 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) {
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isVisible={node.visible !== false} isVisible={node.visible !== false}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
/> />
); );
@@ -2,6 +2,53 @@ import { AnyNodeId, useScene } from "@pascal-app/core";
import { ChevronDown, ChevronRight } from "lucide-react"; import { ChevronDown, ChevronRight } from "lucide-react";
import { forwardRef, useEffect, useRef } from "react"; import { forwardRef, useEffect, useRef } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "motion/react";
export function handleTreeSelection(
e: React.MouseEvent,
nodeId: string,
selectedIds: string[],
setSelection: (s: any) => void
) {
if (e.metaKey || e.ctrlKey) {
if (selectedIds.includes(nodeId)) {
setSelection({ selectedIds: selectedIds.filter((id) => id !== nodeId) });
} else {
setSelection({ selectedIds: [...selectedIds, nodeId] });
}
return true;
}
if (e.shiftKey && selectedIds.length > 0) {
const lastSelectedId = selectedIds[selectedIds.length - 1];
if (lastSelectedId) {
const nodes = Array.from(document.querySelectorAll('[data-treenode-id]'));
const nodeIds = nodes.map(n => n.getAttribute('data-treenode-id') as string);
const startIndex = nodeIds.indexOf(lastSelectedId);
const endIndex = nodeIds.indexOf(nodeId);
if (startIndex !== -1 && endIndex !== -1) {
const start = Math.min(startIndex, endIndex);
const end = Math.max(startIndex, endIndex);
const range = nodeIds.slice(start, end + 1);
// We can keep the previous selections that were outside the range if we want,
// but standard file system shift-click replaces the selection with the range.
setSelection({ selectedIds: range });
return true;
}
}
// Fallback: if range selection fails (e.g. node not visible in tree), just add to selection
if (!selectedIds.includes(nodeId)) {
setSelection({ selectedIds: [...selectedIds, nodeId] });
return true;
}
}
setSelection({ selectedIds: [nodeId] });
return false;
}
import { BuildingTreeNode } from "./building-tree-node"; import { BuildingTreeNode } from "./building-tree-node";
import { CeilingTreeNode } from "./ceiling-tree-node"; import { CeilingTreeNode } from "./ceiling-tree-node";
import { DoorTreeNode } from "./door-tree-node"; import { DoorTreeNode } from "./door-tree-node";
@@ -16,47 +63,49 @@ import { ZoneTreeNode } from "./zone-tree-node";
interface TreeNodeProps { interface TreeNodeProps {
nodeId: AnyNodeId; nodeId: AnyNodeId;
depth?: number; depth?: number;
isLast?: boolean;
} }
export function TreeNode({ nodeId, depth = 0 }: TreeNodeProps) { export function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
const node = useScene((state) => state.nodes[nodeId]); const node = useScene((state) => state.nodes[nodeId]);
if (!node) return null; if (!node) return null;
switch (node.type) { switch (node.type) {
case "building": case "building":
return <BuildingTreeNode node={node} depth={depth} />; return <BuildingTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "ceiling": case "ceiling":
return <CeilingTreeNode node={node} depth={depth} />; return <CeilingTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "level": case "level":
return <LevelTreeNode node={node} depth={depth} />; return <LevelTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "slab": case "slab":
return <SlabTreeNode node={node} depth={depth} />; return <SlabTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "wall": case "wall":
return <WallTreeNode node={node} depth={depth} />; return <WallTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "roof": case "roof":
return <RoofTreeNode node={node} depth={depth} />; return <RoofTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "item": case "item":
return <ItemTreeNode node={node} depth={depth} />; return <ItemTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "door": case "door":
return <DoorTreeNode node={node} depth={depth} />; return <DoorTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "window": case "window":
return <WindowTreeNode node={node} depth={depth} />; return <WindowTreeNode node={node as any} depth={depth} isLast={isLast} />;
case "zone": case "zone":
return <ZoneTreeNode node={node} depth={depth} />; return <ZoneTreeNode node={node as any} depth={depth} isLast={isLast} />;
default: default:
return null; return null;
} }
} }
interface TreeNodeWrapperProps { interface TreeNodeWrapperProps {
nodeId?: string;
icon: React.ReactNode; icon: React.ReactNode;
label: React.ReactNode; label: React.ReactNode;
depth: number; depth: number;
hasChildren: boolean; hasChildren: boolean;
expanded: boolean; expanded: boolean;
onToggle: () => void; onToggle: () => void;
onClick: () => void; onClick: (e: React.MouseEvent) => void;
onDoubleClick?: () => void; onDoubleClick?: () => void;
onMouseEnter?: () => void; onMouseEnter?: () => void;
onMouseLeave?: () => void; onMouseLeave?: () => void;
@@ -65,56 +114,80 @@ interface TreeNodeWrapperProps {
isSelected?: boolean; isSelected?: boolean;
isHovered?: boolean; isHovered?: boolean;
isVisible?: boolean; isVisible?: boolean;
isLast?: boolean;
} }
export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>( export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
function TreeNodeWrapper( function TreeNodeWrapper(
{ {
icon, nodeId,
label, icon,
depth, label,
hasChildren, depth,
expanded, hasChildren,
onToggle, expanded,
onClick, onToggle,
onDoubleClick, onClick,
onMouseEnter, onDoubleClick,
onMouseLeave, onMouseEnter,
actions, onMouseLeave,
children, actions,
isSelected, children,
isHovered, isSelected,
isVisible = true, isHovered,
}, isVisible = true,
ref isLast,
) { },
const rowRef = useRef<HTMLDivElement>(null); ref
) {
const rowRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (isSelected && rowRef.current) { if (isSelected && rowRef.current) {
rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} }
}, [isSelected]); }, [isSelected]);
return ( return (
<div ref={ref}> <div ref={ref} data-treenode-id={nodeId}>
<div <div
ref={rowRef} ref={rowRef}
className={cn( className={cn(
"flex items-center h-8 cursor-pointer group/row text-sm select-none border-b border-border/50 transition-all duration-200", "relative flex items-center h-8 cursor-pointer group/row text-sm select-none border-b border-r border-border/50 border-r-transparent transition-all duration-200",
isSelected isSelected
? "bg-accent/50 text-foreground" ? "bg-accent/50 text-foreground border-r-white border-r-3"
: isHovered : isHovered
? "bg-accent/30 text-foreground" ? "bg-accent/30 text-foreground"
: "text-muted-foreground hover:bg-accent/30 hover:text-foreground", : "text-muted-foreground hover:bg-accent/30 hover:text-foreground",
!isVisible && "opacity-50" !isVisible && "opacity-50"
)} )}
style={{ paddingLeft: depth * 12 + 12, paddingRight: 12 }} style={{ paddingLeft: depth * 12 + 12, paddingRight: 12 }}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
<button {/* Vertical tree line */}
className="w-4 h-4 flex items-center justify-center shrink-0" <div
className={cn(
"absolute w-px bg-border/50 pointer-events-none",
isLast ? "top-0 bottom-1/2" : "top-0 bottom-0"
)}
style={{ left: (depth - 1) * 12 + 20 }}
/>
{/* Horizontal branch line */}
<div
className="absolute top-1/2 h-px bg-border/50 pointer-events-none"
style={{ left: (depth - 1) * 12 + 20, width: 4 }}
/>
{/* Line down to children */}
{hasChildren && expanded && (
<div
className="absolute top-1/2 bottom-0 w-px bg-border/50 pointer-events-none"
style={{ left: depth * 12 + 20 }}
/>
)}
<button
className="w-4 h-4 flex items-center justify-center shrink-0 z-10 bg-inherit"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onToggle(); onToggle();
@@ -149,7 +222,19 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
</div> </div>
)} )}
</div> </div>
{expanded && children} <AnimatePresence initial={false}>
{expanded && children && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ type: "spring", bounce: 0, duration: 0.3 }}
className="overflow-hidden"
>
{children}
</motion.div>
)}
</AnimatePresence>
</div> </div>
); );
} }
@@ -2,16 +2,18 @@ import { type AnyNodeId, WallNode, useScene } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from "@pascal-app/viewer";
import Image from "next/image"; import Image from "next/image";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import useEditor from "@/store/use-editor";
import { InlineRenameInput } from "./inline-rename-input"; import { InlineRenameInput } from "./inline-rename-input";
import { TreeNode, TreeNodeWrapper } from "./tree-node"; import { TreeNode, TreeNodeWrapper, handleTreeSelection } from "./tree-node";
import { TreeNodeActions } from "./tree-node-actions"; import { TreeNodeActions } from "./tree-node-actions";
interface WallTreeNodeProps { interface WallTreeNodeProps {
node: WallNode; node: WallNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function WallTreeNode({ node, depth }: WallTreeNodeProps) { export function WallTreeNode({ node, depth, isLast }: WallTreeNodeProps) {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const selectedIds = useViewer((state) => state.selection.selectedIds); const selectedIds = useViewer((state) => state.selection.selectedIds);
@@ -40,8 +42,12 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
} }
}, [selectedIds, node.id]); }, [selectedIds, node.id]);
const handleClick = () => { const handleClick = (e: React.MouseEvent) => {
setSelection({ selectedIds: [node.id] }); e.stopPropagation();
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection);
if (!handled && useEditor.getState().phase === "furnish") {
useEditor.getState().setPhase("structure");
}
}; };
const handleDoubleClick = () => { const handleDoubleClick = () => {
@@ -60,6 +66,7 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
return ( return (
<TreeNodeWrapper <TreeNodeWrapper
nodeId={node.id}
icon={<Image src="/icons/wall.png" alt="" width={14} height={14} className="object-contain" />} icon={<Image src="/icons/wall.png" alt="" width={14} height={14} className="object-contain" />}
label={ label={
<InlineRenameInput <InlineRenameInput
@@ -81,10 +88,11 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isVisible={node.visible !== false} isVisible={node.visible !== false}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
> >
{node.children.map((childId) => ( {node.children.map((childId, index) => (
<TreeNode key={childId} nodeId={childId} depth={depth + 1} /> <TreeNode key={childId} nodeId={childId} depth={depth + 1} isLast={index === node.children.length - 1} />
))} ))}
</TreeNodeWrapper> </TreeNodeWrapper>
); );
@@ -4,18 +4,21 @@ import { WindowNode } from "@pascal-app/core"
import { useViewer } from "@pascal-app/viewer" import { useViewer } from "@pascal-app/viewer"
import Image from "next/image" import Image from "next/image"
import { useState } from "react" import { useState } from "react"
import useEditor from "@/store/use-editor"
import { InlineRenameInput } from "./inline-rename-input" import { InlineRenameInput } from "./inline-rename-input"
import { TreeNodeWrapper } from "./tree-node" import { TreeNodeWrapper, handleTreeSelection } from "./tree-node"
import { TreeNodeActions } from "./tree-node-actions" import { TreeNodeActions } from "./tree-node-actions"
interface WindowTreeNodeProps { interface WindowTreeNodeProps {
node: WindowNode node: WindowNode
depth: number depth: number
isLast?: boolean
} }
export function WindowTreeNode({ node, depth }: WindowTreeNodeProps) { export function WindowTreeNode({ node, depth, isLast }: WindowTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id)) const selectedIds = useViewer((state) => state.selection.selectedIds)
const isSelected = selectedIds.includes(node.id)
const isHovered = useViewer((state) => state.hoveredId === node.id) const isHovered = useViewer((state) => state.hoveredId === node.id)
const setSelection = useViewer((state) => state.setSelection) const setSelection = useViewer((state) => state.setSelection)
const setHoveredId = useViewer((state) => state.setHoveredId) const setHoveredId = useViewer((state) => state.setHoveredId)
@@ -24,6 +27,7 @@ export function WindowTreeNode({ node, depth }: WindowTreeNodeProps) {
return ( return (
<TreeNodeWrapper <TreeNodeWrapper
nodeId={node.id}
icon={<Image src="/icons/window.png" alt="" width={14} height={14} className="object-contain" />} icon={<Image src="/icons/window.png" alt="" width={14} height={14} className="object-contain" />}
label={ label={
<InlineRenameInput <InlineRenameInput
@@ -38,13 +42,20 @@ export function WindowTreeNode({ node, depth }: WindowTreeNodeProps) {
hasChildren={false} hasChildren={false}
expanded={false} expanded={false}
onToggle={() => {}} onToggle={() => {}}
onClick={() => setSelection({ selectedIds: [node.id] })} onClick={(e: React.MouseEvent) => {
e.stopPropagation()
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection)
if (!handled && useEditor.getState().phase === "furnish") {
useEditor.getState().setPhase("structure")
}
}}
onDoubleClick={() => setIsEditing(true)} onDoubleClick={() => setIsEditing(true)}
onMouseEnter={() => setHoveredId(node.id)} onMouseEnter={() => setHoveredId(node.id)}
onMouseLeave={() => setHoveredId(null)} onMouseLeave={() => setHoveredId(null)}
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isVisible={node.visible !== false} isVisible={node.visible !== false}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
/> />
) )
@@ -8,9 +8,10 @@ import { TreeNodeActions } from "./tree-node-actions";
interface ZoneTreeNodeProps { interface ZoneTreeNodeProps {
node: ZoneNode; node: ZoneNode;
depth: number; depth: number;
isLast?: boolean;
} }
export function ZoneTreeNode({ node, depth }: ZoneTreeNodeProps) { export function ZoneTreeNode({ node, depth, isLast }: ZoneTreeNodeProps) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const isSelected = useViewer((state) => state.selection.zoneId === node.id); const isSelected = useViewer((state) => state.selection.zoneId === node.id);
const isHovered = useViewer((state) => state.hoveredId === node.id); const isHovered = useViewer((state) => state.hoveredId === node.id);
@@ -64,6 +65,7 @@ export function ZoneTreeNode({ node, depth }: ZoneTreeNodeProps) {
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
isSelected={isSelected} isSelected={isSelected}
isHovered={isHovered} isHovered={isHovered}
isLast={isLast}
actions={<TreeNodeActions node={node} />} actions={<TreeNodeActions node={node} />}
/> />
); );
@@ -46,11 +46,7 @@ export default function CommunityHub() {
} }
}, [isAuthenticated, authLoading]) }, [isAuthenticated, authLoading])
const handleProjectCreated = async (projectId: string) => { const handleProjectCreated = (projectId: string) => {
const result = await getUserProjects()
if (result.success) {
setUserProjects(result.data || [])
}
router.push(`/editor/${projectId}`) router.push(`/editor/${projectId}`)
} }
@@ -4,6 +4,7 @@ import { X } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog'
import { Switch } from '@/components/ui/primitives/switch' import { Switch } from '@/components/ui/primitives/switch'
import { useScene } from '@pascal-app/core'
import { createProject } from '../lib/projects/actions' import { createProject } from '../lib/projects/actions'
interface NewProjectDialogProps { interface NewProjectDialogProps {
@@ -30,7 +31,12 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess }: NewProjectDi
setIsCreating(true) setIsCreating(true)
try { try {
const result = await createProject({ name, isPrivate }) // Get the default scene graph
useScene.getState().clearScene()
const { nodes, rootNodeIds } = useScene.getState()
const sceneGraph = { nodes, rootNodeIds }
const result = await createProject({ name, isPrivate, sceneGraph })
if (result.success && result.data) { if (result.success && result.data) {
onOpenChange(false) onOpenChange(false)
@@ -62,43 +62,47 @@ export function useProjectScene() {
// Suppress auto-save for the store update caused by setScene/clearScene // Suppress auto-save for the store update caused by setScene/clearScene
isLoadingSceneRef.current = true isLoadingSceneRef.current = true
useProjectStore.getState().setIsSceneLoading(true)
try { try {
useScene.getState().clearScene()
const result = await getProjectModel(projectId || '') const result = await getProjectModel(projectId || '')
if (result.success && result.data?.scene_graph) { if (result.success && result.data?.scene_graph) {
// Load the scene graph into the store // Load the scene graph into the store
const { nodes, rootNodeIds } = result.data.scene_graph const { nodes, rootNodeIds } = result.data.scene_graph
useScene.getState().setScene(nodes, rootNodeIds) useScene.getState().setScene(nodes, rootNodeIds)
// Auto-select the first building + level after store is updated
const sceneNodes = useScene.getState().nodes as Record<string, any>
const sceneRootIds = useScene.getState().rootNodeIds
const siteNode = sceneRootIds[0] ? sceneNodes[sceneRootIds[0]] : null
const resolve = (child: any) =>
typeof child === 'string' ? sceneNodes[child] : child
const firstBuilding = siteNode?.children?.map(resolve).find((n: any) => n?.type === 'building')
const firstLevel = firstBuilding?.children?.map(resolve).find((n: any) => n?.type === 'level')
if (firstBuilding && firstLevel) {
useViewer.getState().setSelection({
buildingId: firstBuilding.id,
levelId: firstLevel.id,
selectedIds: [],
zoneId: null,
})
useEditor.getState().setPhase('structure')
} else {
useEditor.getState().setPhase('site')
useViewer.getState().setSelection({
buildingId: null,
levelId: null,
selectedIds: [],
zoneId: null,
})
}
} else { } else {
// No scene found - clear the scene // No scene found - clear the scene
useScene.getState().clearScene() useScene.getState().clearScene()
}
// Auto-select the first building + level after store is updated
const sceneNodes = useScene.getState().nodes as Record<string, any>
const sceneRootIds = useScene.getState().rootNodeIds
const siteNode = sceneRootIds[0] ? sceneNodes[sceneRootIds[0]] : null
const resolve = (child: any) =>
typeof child === 'string' ? sceneNodes[child] : child
const firstBuilding = siteNode?.children?.map(resolve).find((n: any) => n?.type === 'building')
const firstLevel = firstBuilding?.children?.map(resolve).find((n: any) => n?.type === 'level')
if (firstBuilding && firstLevel) {
useViewer.getState().setSelection({
buildingId: firstBuilding.id,
levelId: firstLevel.id,
selectedIds: [],
zoneId: null,
})
useEditor.getState().setPhase('structure')
useEditor.getState().setStructureLayer('elements')
// Auto-select the wall tool if the level is empty (e.g., brand new project)
if (!firstLevel.children || firstLevel.children.length === 0) {
useEditor.getState().setMode('build')
useEditor.getState().setTool('wall')
}
} else {
useEditor.getState().setPhase('site') useEditor.getState().setPhase('site')
useViewer.getState().setSelection({ useViewer.getState().setSelection({
buildingId: null, buildingId: null,
@@ -110,13 +114,42 @@ export function useProjectScene() {
} catch (error) { } catch (error) {
// Fall back to clear scene // Fall back to clear scene
useScene.getState().clearScene() useScene.getState().clearScene()
useEditor.getState().setPhase('site')
useViewer.getState().setSelection({ // Auto-select the first building + level from the cleared scene
buildingId: null, const sceneNodes = useScene.getState().nodes as Record<string, any>
levelId: null, const sceneRootIds = useScene.getState().rootNodeIds
selectedIds: [], const siteNode = sceneRootIds[0] ? sceneNodes[sceneRootIds[0]] : null
zoneId: null, const resolve = (child: any) =>
}) typeof child === 'string' ? sceneNodes[child] : child
const firstBuilding = siteNode?.children?.map(resolve).find((n: any) => n?.type === 'building')
const firstLevel = firstBuilding?.children?.map(resolve).find((n: any) => n?.type === 'level')
if (firstBuilding && firstLevel) {
useViewer.getState().setSelection({
buildingId: firstBuilding.id,
levelId: firstLevel.id,
selectedIds: [],
zoneId: null,
})
useEditor.getState().setPhase('structure')
useEditor.getState().setStructureLayer('elements')
// Auto-select the wall tool if the level is empty (e.g., brand new project)
if (!firstLevel.children || firstLevel.children.length === 0) {
useEditor.getState().setMode('build')
useEditor.getState().setTool('wall')
}
} else {
useEditor.getState().setPhase('site')
useViewer.getState().setSelection({
buildingId: null,
levelId: null,
selectedIds: [],
zoneId: null,
})
}
} finally {
useProjectStore.getState().setIsSceneLoading(false)
} }
// Allow auto-save again after a tick (let the store update propagate) // Allow auto-save again after a tick (let the store update propagate)
@@ -15,12 +15,14 @@ interface ProjectStore {
activeProject: Project | null activeProject: Project | null
projects: Project[] projects: Project[]
isLoading: boolean isLoading: boolean
isSceneLoading: boolean
error: string | null error: string | null
// Actions // Actions
fetchProjects: () => Promise<void> fetchProjects: () => Promise<void>
fetchActiveProject: () => Promise<void> fetchActiveProject: () => Promise<void>
setActiveProject: (projectId: string) => Promise<void> setActiveProject: (projectId: string) => Promise<void>
setIsSceneLoading: (loading: boolean) => void
initialize: () => Promise<void> initialize: () => Promise<void>
updateActiveThumbnail: (thumbnailUrl: string) => void updateActiveThumbnail: (thumbnailUrl: string) => void
} }
@@ -30,6 +32,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
activeProject: null, activeProject: null,
projects: [], projects: [],
isLoading: true, isLoading: true,
isSceneLoading: false,
error: null, error: null,
// Fetch all projects // Fetch all projects
@@ -79,6 +82,10 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
} }
}, },
setIsSceneLoading: (loading: boolean) => {
set({ isSceneLoading: loading })
},
// Patch the active project's thumbnail URL in place (no refetch) // Patch the active project's thumbnail URL in place (no refetch)
updateActiveThumbnail: (thumbnailUrl: string) => { updateActiveThumbnail: (thumbnailUrl: string) => {
set((state) => ({ set((state) => ({
+34
View File
@@ -16,6 +16,10 @@ export const useKeyboard = () => {
e.preventDefault() e.preventDefault()
emitter.emit('tool:cancel') emitter.emit('tool:cancel')
// Clear selections to close UI panels, but KEEP the active building and level context
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
useEditor.getState().setSelectedReferenceId(null)
// If in build mode, switch back to select mode // If in build mode, switch back to select mode
const { mode } = useEditor.getState() const { mode } = useEditor.getState()
if (mode === 'build') { if (mode === 'build') {
@@ -46,6 +50,36 @@ export const useKeyboard = () => {
} else if (e.key === 'Z' && e.shiftKey && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'Z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault()
useScene.temporal.getState().redo() useScene.temporal.getState().redo()
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
const { buildingId, levelId } = useViewer.getState().selection
if (buildingId) {
const building = useScene.getState().nodes[buildingId]
if (building && building.type === 'building' && building.children.length > 0) {
const currentIdx = levelId ? building.children.indexOf(levelId as any) : -1
const nextIdx = currentIdx < building.children.length - 1 ? currentIdx + 1 : currentIdx
if (nextIdx !== -1 && nextIdx !== currentIdx) {
useViewer.getState().setSelection({ levelId: building.children[nextIdx] as any })
} else if (currentIdx === -1) {
useViewer.getState().setSelection({ levelId: building.children[0] as any })
}
}
}
} else if (e.key === 'ArrowDown' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
const { buildingId, levelId } = useViewer.getState().selection
if (buildingId) {
const building = useScene.getState().nodes[buildingId]
if (building && building.type === 'building' && building.children.length > 0) {
const currentIdx = levelId ? building.children.indexOf(levelId as any) : -1
const prevIdx = currentIdx > 0 ? currentIdx - 1 : currentIdx
if (prevIdx !== -1 && prevIdx !== currentIdx) {
useViewer.getState().setSelection({ levelId: building.children[prevIdx] as any })
} else if (currentIdx === -1) {
useViewer.getState().setSelection({ levelId: building.children[building.children.length - 1] as any })
}
}
}
} else if (e.key === 'Delete' || e.key === 'Backspace') { } else if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault() e.preventDefault()
+18 -14
View File
@@ -29,38 +29,42 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@react-google-maps/api": "^2.20.8", "@react-google-maps/api": "^2.20.8",
"@react-three/uikit-lucide": "^1.0.60", "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@react-three/uikit-lucide": "^1.0.62",
"@repo/ui": "*", "@repo/ui": "*",
"@supabase/supabase-js": "^2.95.3", "@supabase/supabase-js": "^2.98.0",
"@t3-oss/env-nextjs": "^0.13.10", "@t3-oss/env-nextjs": "^0.13.10",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.2.1",
"@types/three": "^0.183.0", "@types/three": "^0.183.1",
"@vercel/analytics": "^1.6.1", "@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1", "@vercel/speed-insights": "^1.3.1",
"@vercel/toolbar": "^0.2.2", "@vercel/toolbar": "^0.2.2",
"@visual-json/react": "latest", "@visual-json/react": "latest",
"better-auth": "^1.4.18", "better-auth": "^1.4.19",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"howler": "^2.2.4", "howler": "^2.2.4",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.26.2", "motion": "^12.34.3",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"next": "16.1.0", "next": "16.1.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.2.0", "react": "^19.2.4",
"react-dom": "^19.2.0", "react-dom": "^19.2.4",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.2.1",
"zod": "^4.3.6" "three": "^0.183.1",
"zod": "^4.3.6",
"zustand": "^5.0.11"
}, },
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"@types/howler": "^2.2.12", "@types/howler": "^2.2.12",
"@types/node": "^22.15.3", "@types/node": "^22.19.12",
"@types/react": "19.2.2", "@types/react": "19.2.2",
"@types/react-dom": "19.2.2", "@types/react-dom": "19.2.2",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "5.9.2" "typescript": "5.9.3"
} }
} }
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5056 10.7754C21.1225 10.5355 21.431 10.4155 21.5176 10.2459C21.5926 10.099 21.5903 9.92446 21.5115 9.77954C21.4205 9.61226 21.109 9.50044 20.486 9.2768L4.59629 3.5728C4.0866 3.38983 3.83175 3.29835 3.66514 3.35605C3.52029 3.40621 3.40645 3.52004 3.35629 3.6649C3.29859 3.8315 3.39008 4.08635 3.57304 4.59605L9.277 20.4858C9.50064 21.1088 9.61246 21.4203 9.77973 21.5113C9.92465 21.5901 10.0991 21.5924 10.2461 21.5174C10.4157 21.4308 10.5356 21.1223 10.7756 20.5054L13.3724 13.8278C13.4194 13.707 13.4429 13.6466 13.4792 13.5957C13.5114 13.5506 13.5508 13.5112 13.5959 13.479C13.6468 13.4427 13.7072 13.4192 13.828 13.3722L20.5056 10.7754Z" stroke="#D3DAF0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" fill="#202022"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+151 -149
View File
@@ -5,20 +5,14 @@
"": { "": {
"name": "editor", "name": "editor",
"dependencies": { "dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"portless": "^0.4.2", "portless": "^0.4.2",
"three": "^0.183.0",
"three-bvh-csg": "^0.0.17",
"three-mesh-bvh": "^0.9.8",
"zustand": "^5.0.11",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.2", "@biomejs/biome": "^2.4.4",
"supabase": "2.75.3", "supabase": "2.76.15",
"turbo": "^2.8.10", "turbo": "^2.8.11",
"typescript": "5.9.2", "typescript": "5.9.3",
"ultracite": "^7.2.3", "ultracite": "^7.2.4",
}, },
}, },
"apps/editor": { "apps/editor": {
@@ -43,39 +37,43 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@react-google-maps/api": "^2.20.8", "@react-google-maps/api": "^2.20.8",
"@react-three/uikit-lucide": "^1.0.60", "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@react-three/uikit-lucide": "^1.0.62",
"@repo/ui": "*", "@repo/ui": "*",
"@supabase/supabase-js": "^2.95.3", "@supabase/supabase-js": "^2.98.0",
"@t3-oss/env-nextjs": "^0.13.10", "@t3-oss/env-nextjs": "^0.13.10",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.2.1",
"@types/three": "^0.183.0", "@types/three": "^0.183.1",
"@vercel/analytics": "^1.6.1", "@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1", "@vercel/speed-insights": "^1.3.1",
"@vercel/toolbar": "^0.2.2", "@vercel/toolbar": "^0.2.2",
"@visual-json/react": "latest", "@visual-json/react": "latest",
"better-auth": "^1.4.18", "better-auth": "^1.4.19",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"howler": "^2.2.4", "howler": "^2.2.4",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.26.2", "motion": "^12.34.3",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"next": "16.1.0", "next": "16.1.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.2.0", "react": "^19.2.4",
"react-dom": "^19.2.0", "react-dom": "^19.2.4",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.2.1",
"three": "^0.183.1",
"zod": "^4.3.6", "zod": "^4.3.6",
"zustand": "^5.0.11",
}, },
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"@types/howler": "^2.2.12", "@types/howler": "^2.2.12",
"@types/node": "^22.15.3", "@types/node": "^22.19.12",
"@types/react": "19.2.2", "@types/react": "19.2.2",
"@types/react-dom": "19.2.2", "@types/react-dom": "19.2.2",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "5.9.2", "typescript": "5.9.3",
}, },
}, },
"packages/auth": { "packages/auth": {
@@ -88,7 +86,7 @@
}, },
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"typescript": "5.9.2", "typescript": "5.9.3",
}, },
}, },
"packages/core": { "packages/core": {
@@ -99,7 +97,7 @@
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"three-bvh-csg": "^0.0.17", "three-bvh-csg": "^0.0.18",
"three-mesh-bvh": "^0.9.8", "three-mesh-bvh": "^0.9.8",
"zod": "^4.3.5", "zod": "^4.3.5",
"zundo": "^2.3.0", "zundo": "^2.3.0",
@@ -109,7 +107,7 @@
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/three": "^0.183.0", "@types/three": "^0.183.0",
"typescript": "5.9.2", "typescript": "5.9.3",
}, },
"peerDependencies": { "peerDependencies": {
"@react-three/drei": "^10", "@react-three/drei": "^10",
@@ -123,15 +121,15 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.95.3", "@supabase/supabase-js": "^2.95.3",
"drizzle-orm": "^0.39.0", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.8.3",
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"postgres": "^3.4.5", "postgres": "^3.4.5",
}, },
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"drizzle-kit": "^0.30.0", "drizzle-kit": "^0.31.9",
"typescript": "5.9.2", "typescript": "5.9.3",
}, },
}, },
"packages/eslint-config": { "packages/eslint-config": {
@@ -169,7 +167,7 @@
"@types/react": "19.2.2", "@types/react": "19.2.2",
"@types/react-dom": "19.2.2", "@types/react-dom": "19.2.2",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "5.9.2", "typescript": "5.9.3",
}, },
}, },
"packages/viewer": { "packages/viewer": {
@@ -182,7 +180,7 @@
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/three": "^0.183.0", "@types/three": "^0.183.0",
"typescript": "5.9.2", "typescript": "5.9.3",
}, },
"peerDependencies": { "peerDependencies": {
"@pascal-app/core": "^0.1.4", "@pascal-app/core": "^0.1.4",
@@ -198,31 +196,31 @@
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="], "@better-auth/core": ["@better-auth/core@1.4.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.18", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.18" } }, "sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ=="], "@better-auth/telemetry": ["@better-auth/telemetry@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.19" } }, "sha512-ApGNS7olCTtDpKF8Ow3Z+jvFAirOj7c4RyFUpu8axklh3mH57ndpfUAUjhgA8UVoaaH/mnm/Tl884BlqiewLyw=="],
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
"@biomejs/biome": ["@biomejs/biome@2.4.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.2", "@biomejs/cli-darwin-x64": "2.4.2", "@biomejs/cli-linux-arm64": "2.4.2", "@biomejs/cli-linux-arm64-musl": "2.4.2", "@biomejs/cli-linux-x64": "2.4.2", "@biomejs/cli-linux-x64-musl": "2.4.2", "@biomejs/cli-win32-arm64": "2.4.2", "@biomejs/cli-win32-x64": "2.4.2" }, "bin": { "biome": "bin/biome" } }, "sha512-vVE/FqLxNLbvYnFDYg3Xfrh1UdFhmPT5i+yPT9GE2nTUgI4rkqo5krw5wK19YHBd7aE7J6r91RRmb8RWwkjy6w=="], "@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3pEcKCP/1POKyaZZhXcxFl3+d9njmeAihZ17k8lL/1vk+6e0Cbf0yPzKItFiT+5Yh6TQA4uKvnlqe0oVZwRxCA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-P7hK1jLVny+0R9UwyGcECxO6sjETxfPyBm/1dmFjnDOHgdDPjPqozByunrwh4xPKld8sxOr5eAsSqal5uKgeBg=="], "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh1a/+W+SUCXhEdL7TiX3ArPTFCQKJTI1mGncZNWfO+6suk+gYA4lNyJcBB+pwvF49uw0pEbUS49BgYOY4hzUg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-DI3Mi7GT2zYNgUTDEbSjl3e1KhoP76OjQdm8JpvZYZWtVDRyLd3w8llSr2TWk1z+U3P44kUBWY3X7H9MD1/DGQ=="], "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-/x04YK9+7erw6tYEcJv9WXoBHcULI/wMOvNdAyE9S3JStZZ9yJyV67sWAI+90UHuDo/BDhq0d96LDqGlSVv7WA=="], "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+sPAXq3bxmFwhVFJnSwkSF5Rw2ZAJMH3MF6C9IveAEOdSpgajPhoQhbbAK12SehN9j2QrHpk4J/cHsa/HqWaYQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-GK2ErnrKpWFigYP68cXiCHK4RTL4IUWhK92AFS3U28X/nuAL5+hTuy6hyobc8JZRSt+upXt1nXChK+tuHHx4mA=="], "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-wbBmTkeAoAYbOQ33f6sfKG7pcRSydQiF+dTYOBjJsnXO2mWEOQHllKlC2YVnedqZFERp2WZhFUoO7TNRwnwEHQ=="], "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-k2uqwLYrNNxnaoiW3RJxoMGnbKda8FuCmtYG3cOtVljs3CzWxaTR+AoXwKGHscC9thax9R4kOrtWqWN0+KdPTw=="], "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9ma7C4g8Sq3cBlRJD2yrsHXB1mnnEBdpy7PhvFrylQWQb4PoyCmPucdX7frvsSBQuFtIiKCrolPl/8tCZrKvgQ=="], "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A=="],
"@clack/core": ["@clack/core@1.0.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g=="], "@clack/core": ["@clack/core@1.0.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g=="],
@@ -238,51 +236,57 @@
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
@@ -390,25 +394,25 @@
"@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="], "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="],
"@next/env": ["@next/env@16.1.0", "", {}, "sha512-Dd23XQeFHmhf3KBW76leYVkejHlCdB7erakC2At2apL1N08Bm+dLYNP+nNHh0tzUXfPQcNcXiQyacw0PG4Fcpw=="], "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.9", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.9", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-onHq8dl8KjDb8taANQdzs3XmIqQWV3fYdslkGENuvVInFQzZnuBYYOG2HGHqqtvgmEU7xWzhgndXXxnhk4Z3fQ=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Am6VJTp8KhLuAH13tPrAoVIXzuComlZlMwGr++o2KDjWiKPe3VwpxYhgV6I4gKls2EnsIMggL4y7GdXyDdJcFA=="], "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fVicfaJT6QfghNyg8JErZ+EMNQ812IS0lmKfbmC01LF1nFBcKfcs4Q75Yy8IqnsCqH/hZwGhqzj3IGVfWV6vpA=="], "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TojQnDRoX7wJWXEEwdfuJtakMDW64Q7NrxQPviUnfYJvAx5/5wcGE+1vZzQ9F17m+SdpFeeXuOr6v3jbyusYMQ=="], "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-quhNFVySW4QwXiZkZ34SbfzNBm27vLrxZ2HwTfFFO1BBP0OY1+pI0nbyewKeq1FriqU+LZrob/cm26lwsiAi8Q=="], "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-6JW0z2FZUK5iOVhUIWqE4RblAhUj1EwhZ/MwteGb//SpFTOHydnhbp3868gxalwea+mbOLWO6xgxj9wA9wNvNw=="], "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+DK/akkAvvXn5RdYN84IOmLkSy87SCmpofJPdB8vbLmf01BzntPBSYXnMvnEEv/Vcf3HYJwt24QZ/s6sWAwOMQ=="], "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Tr0j94MphimCCks+1rtYPzQFK+faJuhHWCegU9S9gDlgyOk8Y3kPmO64UcjyzZAlligeBtYZ/2bEyrKq0d2wqQ=="], "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
@@ -432,13 +436,15 @@
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
"@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.61", "", {}, "sha512-Nxk+3dAdsXFdx5pq1IHNe1j4zxogkAfhmBKUIHS+aJUs45q/XMtlADVrHLooiQ/Amlp9tXAvbvFYUOGd897POw=="], "@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.62", "", {}, "sha512-TGmAo0mFbdzPwwNCDzJoy6Khr3T/Z613ApwegYsRs6S+P+AeOSIS4+4aB5bL4G7hY6uNt5cyZVClESuEjYXt9g=="],
"@pmndrs/uikit": ["@pmndrs/uikit@1.0.61", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.61", "@pmndrs/uikit-pub-sub": "^1.0.61", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-xoNp2jKRoHa7INC3QaAL2ddpGmKy+eORMh4zoNds17EndqyBrqYzfWJK+PVmei/5MQNBTi0KrY2qxBGj6hB0cg=="], "@pmndrs/pointer-events": ["@pmndrs/pointer-events@6.6.29", "", {}, "sha512-o4YD6VfJgDYjFgde/YyAw2X5KY454tdmOXrHGOvKTWJBHzkL90B5vH4rqmexwRVvaDfT3YLvVh/Dm5cBbgZXMg=="],
"@pmndrs/uikit-lucide": ["@pmndrs/uikit-lucide@1.0.61", "", { "dependencies": { "@pmndrs/uikit": "^1.0.61" } }, "sha512-pToRVLnOIuxs0RUm6UTa5HWgcDfhLATq1FtgKmzxiUuHVvYf4h8PIrFJswP8e/KeotzHvr5sTg/HIYvbvSo0/A=="], "@pmndrs/uikit": ["@pmndrs/uikit@1.0.62", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.62", "@pmndrs/uikit-pub-sub": "^1.0.62", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-TRV7Q1hKmGIFEzhJYF4xaXowdgqGUQd+XKcpEfyvwShOWEWqiDAYoI5OYjDQPhVVshI2frb5XVPr6acGf2laFQ=="],
"@pmndrs/uikit-pub-sub": ["@pmndrs/uikit-pub-sub@1.0.61", "", { "dependencies": { "@preact/signals-core": "^1.8.0" } }, "sha512-J1r5faMzwoWgAsDTW/0B+M96BuZgCyrvpM91/+QENnBfFCRR6xM2LIqI3KB/HlaO/1L8zQQHQE5g8HA2tVT4xw=="], "@pmndrs/uikit-lucide": ["@pmndrs/uikit-lucide@1.0.62", "", { "dependencies": { "@pmndrs/uikit": "^1.0.62" } }, "sha512-aGgFJixWnYf+Gb3anQr04G3MaDYf82AQSZZFYO/Xzo1Y8QxaAjeiQdIgZtq7rrEWWxzJbVepHN9YgHyfLmBPMg=="],
"@pmndrs/uikit-pub-sub": ["@pmndrs/uikit-pub-sub@1.0.62", "", { "dependencies": { "@preact/signals-core": "^1.8.0" } }, "sha512-E/8z5ZnqGfHgsgyUtMujJwRM11JSsZpxYnSxDDOZ+Q5FcK7aLSBCfT80Nm32EdytpN+xQ76mDI8ROvWYg3abxg=="],
"@preact/signals-core": ["@preact/signals-core@1.12.1", "", {}, "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA=="], "@preact/signals-core": ["@preact/signals-core@1.12.1", "", {}, "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA=="],
@@ -532,9 +538,9 @@
"@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="], "@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="],
"@react-three/uikit": ["@react-three/uikit@1.0.61", "", { "dependencies": { "@pmndrs/uikit": "^1.0.61", "@preact/signals-core": "^1.5.1", "suspend-react": "^0.1.3", "zustand": "^5.0.6" }, "peerDependencies": { "@react-three/fiber": ">=8", "react": ">=18" } }, "sha512-rZrgev90X0UwXQ9CvBVYvoiMDW2UrIlQEoBERDG7BO3pdvXGENuMG9hmodGBGe9c8IC0REO9u7xerMWPBz8zyQ=="], "@react-three/uikit": ["@react-three/uikit@1.0.62", "", { "dependencies": { "@pmndrs/pointer-events": "^6.6.29", "@pmndrs/uikit": "^1.0.62", "@preact/signals-core": "^1.5.1", "suspend-react": "^0.1.3", "zustand": "^5.0.6" }, "peerDependencies": { "@react-three/fiber": ">=8", "react": ">=18" } }, "sha512-2kvZSsIp/S6aTkEl9RT1rDV+p2S4fmIoUZTvHIWwpiENorQfARC/FZEC4tFzIRKty8+7Pa8J7h8twvkF3vWZ4A=="],
"@react-three/uikit-lucide": ["@react-three/uikit-lucide@1.0.61", "", { "dependencies": { "@pmndrs/uikit-lucide": "^1.0.61", "@react-three/uikit": "^1.0.61" } }, "sha512-iZc71VDl90sL14JjL6DqX6a0vJ0HKotFRHU4plIfauMcyLxmjhgQ9jQHKXUNwyvJtJhzNm+We7DUz5hCPODntQ=="], "@react-three/uikit-lucide": ["@react-three/uikit-lucide@1.0.62", "", { "dependencies": { "@pmndrs/uikit-lucide": "^1.0.62", "@react-three/uikit": "^1.0.62" } }, "sha512-/iC9yjUwEHNFRPvTIIXiMIvek39XA2QpFZjVsr3XOwF3jmZ3IFlEOtjxZz0OYpxVZQSMAIqolPE78Ndim7pSZg=="],
"@repo/eslint-config": ["@repo/eslint-config@workspace:packages/eslint-config"], "@repo/eslint-config": ["@repo/eslint-config@workspace:packages/eslint-config"],
@@ -546,17 +552,17 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@supabase/auth-js": ["@supabase/auth-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg=="], "@supabase/auth-js": ["@supabase/auth-js@2.98.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg=="],
"@supabase/functions-js": ["@supabase/functions-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig=="], "@supabase/functions-js": ["@supabase/functions-js@2.98.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg=="],
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg=="], "@supabase/postgrest-js": ["@supabase/postgrest-js@2.98.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg=="],
"@supabase/realtime-js": ["@supabase/realtime-js@2.95.3", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw=="], "@supabase/realtime-js": ["@supabase/realtime-js@2.98.0", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw=="],
"@supabase/storage-js": ["@supabase/storage-js@2.95.3", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A=="], "@supabase/storage-js": ["@supabase/storage-js@2.98.0", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ=="],
"@supabase/supabase-js": ["@supabase/supabase-js@2.95.3", "", { "dependencies": { "@supabase/auth-js": "2.95.3", "@supabase/functions-js": "2.95.3", "@supabase/postgrest-js": "2.95.3", "@supabase/realtime-js": "2.95.3", "@supabase/storage-js": "2.95.3" } }, "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg=="], "@supabase/supabase-js": ["@supabase/supabase-js@2.98.0", "", { "dependencies": { "@supabase/auth-js": "2.98.0", "@supabase/functions-js": "2.98.0", "@supabase/postgrest-js": "2.98.0", "@supabase/realtime-js": "2.98.0", "@supabase/storage-js": "2.98.0" } }, "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
@@ -564,35 +570,35 @@
"@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.10", "", { "dependencies": { "@t3-oss/env-core": "0.13.10" }, "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ=="], "@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.10", "", { "dependencies": { "@t3-oss/env-core": "0.13.10" }, "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="], "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
"@tinyhttp/accepts": ["@tinyhttp/accepts@1.3.0", "", { "dependencies": { "es-mime-types": "^0.0.16", "negotiator": "^0.6.2" } }, "sha512-YaJ4EMgVUI6JHzWO14lr6vn/BLJEoFN4Sqd20l0/oBcLLENkP8gnPtX1jB7OhIu0AE40VCweAqvSP+0/pgzB1g=="], "@tinyhttp/accepts": ["@tinyhttp/accepts@1.3.0", "", { "dependencies": { "es-mime-types": "^0.0.16", "negotiator": "^0.6.2" } }, "sha512-YaJ4EMgVUI6JHzWO14lr6vn/BLJEoFN4Sqd20l0/oBcLLENkP8gnPtX1jB7OhIu0AE40VCweAqvSP+0/pgzB1g=="],
@@ -638,7 +644,7 @@
"@types/md5": ["@types/md5@2.3.6", "", {}, "sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA=="], "@types/md5": ["@types/md5@2.3.6", "", {}, "sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA=="],
"@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], "@types/node": ["@types/node@22.19.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-0QEp0aPJYSyf6RrTjDB7HlKgNMTY+V2C7ESTaVt6G9gQ0rPLzTGz7OF2NXTLR5vcy7HJEtIUsyWLsfX0kTqJBA=="],
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
@@ -740,7 +746,7 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="],
"better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="], "better-auth": ["better-auth@1.4.19", "", { "dependencies": { "@better-auth/core": "1.4.19", "@better-auth/telemetry": "1.4.19", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ=="],
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
@@ -850,15 +856,15 @@
"draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="], "draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="],
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="], "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="],
"drizzle-orm": ["drizzle-orm@0.39.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw=="], "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
"drizzle-zod": ["drizzle-zod@0.5.1", "", { "peerDependencies": { "drizzle-orm": ">=0.23.13", "zod": "*" } }, "sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A=="], "drizzle-zod": ["drizzle-zod@0.8.3", "", { "peerDependencies": { "drizzle-orm": ">=0.36.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -888,7 +894,7 @@
"es-vary": ["es-vary@0.0.8", "", {}, "sha512-fiERjQiCHrXUAToNRT/sh7MtXnfei9n7cF9oVQRUEp9L5BGXsTKSPaXq8L+4v0c/ezfvuTWd/f0JSl5IBRUvSg=="], "es-vary": ["es-vary@0.0.8", "", {}, "sha512-fiERjQiCHrXUAToNRT/sh7MtXnfei9n7cF9oVQRUEp9L5BGXsTKSPaXq8L+4v0c/ezfvuTWd/f0JSl5IBRUvSg=="],
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
@@ -962,7 +968,7 @@
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"framer-motion": ["framer-motion@12.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="], "framer-motion": ["framer-motion@12.34.3", "", { "dependencies": { "motion-dom": "^12.34.3", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -1150,29 +1156,29 @@
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@@ -1214,11 +1220,11 @@
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"motion": ["motion@12.26.2", "", { "dependencies": { "framer-motion": "^12.26.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw=="], "motion": ["motion@12.34.3", "", { "dependencies": { "framer-motion": "^12.34.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw=="],
"motion-dom": ["motion-dom@12.26.2", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw=="], "motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="],
"motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="], "motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -1230,7 +1236,7 @@
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"next": ["next@16.1.0", "", { "dependencies": { "@next/env": "16.1.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.0", "@next/swc-darwin-x64": "16.1.0", "@next/swc-linux-arm64-gnu": "16.1.0", "@next/swc-linux-arm64-musl": "16.1.0", "@next/swc-linux-x64-gnu": "16.1.0", "@next/swc-linux-x64-musl": "16.1.0", "@next/swc-win32-arm64-msvc": "16.1.0", "@next/swc-win32-x64-msvc": "16.1.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Y+KbmDbefYtHDDQKLNrmzE/YYzG2msqo2VXhzh5yrJ54tx/6TmGdkR5+kP9ma7i7LwZpZMfoY3m/AoPPPKxtVw=="], "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
@@ -1342,9 +1348,9 @@
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
@@ -1458,7 +1464,7 @@
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"supabase": ["supabase@2.75.3", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.7" }, "bin": { "supabase": "bin/supabase" } }, "sha512-j2uHJfK8TE7SxL6cd+kQixwkl8H5ASBkQfOEVjf3d3XS1Wixy4UO8WudDYNVriu1KuKzsNnkkSC/jTyHNXNrcQ=="], "supabase": ["supabase@2.76.15", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.9" }, "bin": { "supabase": "bin/supabase" } }, "sha512-m69o1XPAzZaIWfQiEeT+KY/Ci3OSA663RyoH9xECbXSxhr7dsipLCpCqT1E4MCob0mMhHh/7A+Eltx4y1qSwiQ=="],
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="], "supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
@@ -1468,17 +1474,17 @@
"suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="], "suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="], "tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="],
"three": ["three@0.183.1", "", {}, "sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ=="], "three": ["three@0.183.1", "", {}, "sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ=="],
"three-bvh-csg": ["three-bvh-csg@0.0.17", "", { "peerDependencies": { "three": ">=0.151.0", "three-mesh-bvh": ">=0.6.6" } }, "sha512-iEkHDF8GRfGM6593Cuw8SnF1vfENCp46gIAtRzuL4nGXGWPcR1sbTBwM9ptDONb5twqlZp5WkAKya5aBKe2qcA=="], "three-bvh-csg": ["three-bvh-csg@0.0.18", "", { "peerDependencies": { "three": ">=0.179.0", "three-mesh-bvh": ">=0.9.7" } }, "sha512-M3GCZMmGFgASGuDf+YMamM83nVlD/vdwzVHcYbFxgW+g1S7/nKPiuY00YVHOMbjmJPh8mLevGZL65ItHUuGt2w=="],
"three-mesh-bvh": ["three-mesh-bvh@0.9.8", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-YphYvdXEZSXdz6iNdWJo1RB6qvSCRyiXPEVSvNU6xVWbLDOdSrfEIsJOpgFOnefdmVEvZ6M+sY0cjh9gl7MvdA=="], "three-mesh-bvh": ["three-mesh-bvh@0.9.8", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-YphYvdXEZSXdz6iNdWJo1RB6qvSCRyiXPEVSvNU6xVWbLDOdSrfEIsJOpgFOnefdmVEvZ6M+sY0cjh9gl7MvdA=="],
@@ -1502,19 +1508,19 @@
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="], "tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
"turbo": ["turbo@2.8.10", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.10", "turbo-darwin-arm64": "2.8.10", "turbo-linux-64": "2.8.10", "turbo-linux-arm64": "2.8.10", "turbo-windows-64": "2.8.10", "turbo-windows-arm64": "2.8.10" }, "bin": { "turbo": "bin/turbo" } }, "sha512-OxbzDES66+x7nnKGg2MwBA1ypVsZoDTLHpeaP4giyiHSixbsiTaMyeJqbEyvBdp5Cm28fc+8GG6RdQtic0ijwQ=="], "turbo": ["turbo@2.8.11", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.11", "turbo-darwin-arm64": "2.8.11", "turbo-linux-64": "2.8.11", "turbo-linux-arm64": "2.8.11", "turbo-windows-64": "2.8.11", "turbo-windows-arm64": "2.8.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-H+rwSHHPLoyPOSoHdmI1zY0zy0GGj1Dmr7SeJW+nZiWLz2nex8EJ+fkdVabxXFMNEux+aywI4Sae8EqhmnOv4A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g=="], "turbo-darwin-64": ["turbo-darwin-64@2.8.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-XKaCWaz4OCt77oYYvGCIRpvYD4c/aNaKjRkUpv+e8rN3RZb+5Xsyew4yRO+gaHdMIUhQznXNXfHlhs+/p7lIhA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sidzowgWL3s5xCHLeqwC9M3s9M0i16W1nuQF3Mc7fPHpZ+YPohvcbVFBB2uoRRHYZg6yBnwD4gyUHKTeXfwtXA=="], "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VvynLHGUNvQ9k7GZjRPSsRcK4VkioTfFb7O7liAk4nHKjEcMdls7GqxzjVWgJiKz3hWmQGaP9hRa9UUnhVWCxA=="],
"turbo-linux-64": ["turbo-linux-64@2.8.10", "", { "os": "linux", "cpu": "x64" }, "sha512-YK9vcpL3TVtqonB021XwgaQhY9hJJbKKUhLv16osxV0HkcQASQWUqR56yMge7puh6nxU67rQlTq1b7ksR1T3KA=="], "turbo-linux-64": ["turbo-linux-64@2.8.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cbSn37dcm+EmkQ7DD0euy7xV7o2el4GAOr1XujvkAyKjjNvQ+6QIUeDgQcwAx3D17zPpDvfDMJY2dLQadWnkmQ=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-3+j2tL0sG95iBJTm+6J8/45JsETQABPqtFyYjVjBbi6eVGdtNTiBmHNKrbvXRlQ3ZbUG75bKLaSSDHSEEN+btQ=="], "turbo-linux-arm64": ["turbo-linux-arm64@2.8.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-+trymp2s2aBrhS04l6qFxcExzZ8ffndevuUB9c5RCeqsVpZeiWuGQlWNm5XjOmzoMayxRARZ5ma7yiWbGMiLqQ=="],
"turbo-windows-64": ["turbo-windows-64@2.8.10", "", { "os": "win32", "cpu": "x64" }, "sha512-hdeF5qmVY/NFgiucf8FW0CWJWtyT2QPm5mIsX0W1DXAVzqKVXGq+Zf+dg4EUngAFKjDzoBeN6ec2Fhajwfztkw=="], "turbo-windows-64": ["turbo-windows-64@2.8.11", "", { "os": "win32", "cpu": "x64" }, "sha512-3kJjFSM4yw1n9Uzmi+XkAUgCae19l/bH6RJ442xo7mnZm0tpOjo33F+FYHoSVpIWVMd0HG0LDccyafPSdylQbA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-QGdr/Q8LWmj+ITMkSvfiz2glf0d7JG0oXVzGL3jxkGqiBI1zXFj20oqVY0qWi+112LO9SVrYdpHS0E/oGFrMbQ=="], "turbo-windows-arm64": ["turbo-windows-arm64@2.8.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-JOM4uF2vuLsJUvibdR6X9QqdZr6BhC6Nhlrw4LKFPsXZZI/9HHLoqAiYRpE4MuzIwldCH/jVySnWXrI1SKto0g=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
@@ -1528,11 +1534,11 @@
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="], "typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="],
"ultracite": ["ultracite@7.2.3", "", { "dependencies": { "@clack/prompts": "^1.0.1", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.3", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-WKNS2sKAZe4BHu+JGbZebXvy/A1QagDaBnndrK/zwOJAze/mQ8jeHfdG2bPlv3qcJ5fdS3w2Kd7c/eIcH78HvA=="], "ultracite": ["ultracite@7.2.4", "", { "dependencies": { "@clack/prompts": "^1.0.1", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.3", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-3b2g2pwZMDSd+PBK8pxYCjEoYaqVSgXD22HS22BxR8GfbmRXJ3VpNu5Z5lNywIxZXsJleWdpGPHibOPYr/xmKg=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
@@ -1586,8 +1592,6 @@
"zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="], "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
"@better-auth/core/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -1618,8 +1622,6 @@
"@react-three/fiber/zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="], "@react-three/fiber/zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="],
"@react-three/uikit/zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="],
"@repo/ui/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], "@repo/ui/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
@@ -1634,6 +1636,8 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/ws/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -1654,8 +1658,6 @@
"@vercel/toolbar/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "@vercel/toolbar/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+6 -12
View File
@@ -27,20 +27,14 @@
"release:major": "gh workflow run release.yml -f package=both -f bump=major" "release:major": "gh workflow run release.yml -f package=both -f bump=major"
}, },
"dependencies": { "dependencies": {
"@react-three/drei": "^10.7.7", "portless": "^0.4.2"
"@react-three/fiber": "^9.5.0",
"portless": "^0.4.2",
"three": "^0.183.0",
"three-bvh-csg": "^0.0.17",
"three-mesh-bvh": "^0.9.8",
"zustand": "^5.0.11"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.2", "@biomejs/biome": "^2.4.4",
"supabase": "2.75.3", "supabase": "2.76.15",
"turbo": "^2.8.10", "turbo": "^2.8.11",
"typescript": "5.9.2", "typescript": "5.9.3",
"ultracite": "^7.2.3" "ultracite": "^7.2.4"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
+1 -1
View File
@@ -19,6 +19,6 @@
}, },
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"typescript": "5.9.2" "typescript": "5.9.3"
} }
} }
+2 -2
View File
@@ -32,7 +32,7 @@
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"three-bvh-csg": "^0.0.17", "three-bvh-csg": "^0.0.18",
"three-mesh-bvh": "^0.9.8", "three-mesh-bvh": "^0.9.8",
"zod": "^4.3.5", "zod": "^4.3.5",
"zundo": "^2.3.0", "zundo": "^2.3.0",
@@ -41,7 +41,7 @@
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"typescript": "5.9.2", "typescript": "5.9.3",
"@types/three": "^0.183.0" "@types/three": "^0.183.0"
}, },
"keywords": [ "keywords": [
+4 -4
View File
@@ -20,14 +20,14 @@
}, },
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.95.3", "@supabase/supabase-js": "^2.95.3",
"drizzle-orm": "^0.39.0", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.8.3",
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"postgres": "^3.4.5" "postgres": "^3.4.5"
}, },
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"drizzle-kit": "^0.30.0", "drizzle-kit": "^0.31.9",
"typescript": "5.9.2" "typescript": "5.9.3"
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@
"@types/react": "19.2.2", "@types/react": "19.2.2",
"@types/react-dom": "19.2.2", "@types/react-dom": "19.2.2",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "5.9.2" "typescript": "5.9.3"
}, },
"dependencies": { "dependencies": {
"react": "^19.2.0", "react": "^19.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"typescript": "5.9.2", "typescript": "5.9.3",
"@types/three": "^0.183.0" "@types/three": "^0.183.0"
}, },
"keywords": [ "keywords": [
@@ -75,7 +75,7 @@ const pointInPolygonWithTolerance = (
interface SelectionStrategy { interface SelectionStrategy {
types: SelectableNodeType[] types: SelectableNodeType[]
handleClick: (node: AnyNode) => void handleClick: (node: AnyNode, nativeEvent?: MouseEvent) => void
handleDeselect: () => void handleDeselect: () => void
isValid: (node: AnyNode) => boolean isValid: (node: AnyNode) => boolean
} }
@@ -159,6 +159,21 @@ const isNodeInZone = (node: AnyNode, levelId: string, zoneId: string): boolean =
const getStrategy = (): SelectionStrategy | null => { const getStrategy = (): SelectionStrategy | null => {
const { buildingId, levelId, zoneId } = useViewer.getState().selection const { buildingId, levelId, zoneId } = useViewer.getState().selection
const computeNextIds = (node: AnyNode, selectedIds: string[], event?: any): string[] => {
const isMeta = event?.metaKey || event?.nativeEvent?.metaKey;
const isCtrl = event?.ctrlKey || event?.nativeEvent?.ctrlKey;
if (isMeta || isCtrl) {
if (selectedIds.includes(node.id)) {
return selectedIds.filter((id) => id !== node.id);
} else {
return [...selectedIds, node.id];
}
}
return [node.id];
};
// No building selected -> can select buildings // No building selected -> can select buildings
if (!buildingId) { if (!buildingId) {
return { return {
@@ -204,16 +219,9 @@ const getStrategy = (): SelectionStrategy | null => {
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows, doors) // Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows, doors)
return { return {
types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'window', 'door'], types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'window', 'door'],
handleClick: (node) => { handleClick: (node, nativeEvent) => {
const { selectedIds } = useViewer.getState().selection const { selectedIds } = useViewer.getState().selection
// Toggle selection - if already selected, deselect; otherwise select useViewer.getState().setSelection({ selectedIds: computeNextIds(node, selectedIds, nativeEvent) })
if (selectedIds.includes(node.id)) {
useViewer
.getState()
.setSelection({ selectedIds: selectedIds.filter((id) => id !== node.id) })
} else {
useViewer.getState().setSelection({ selectedIds: [node.id] })
}
}, },
handleDeselect: () => { handleDeselect: () => {
const { selectedIds } = useViewer.getState().selection const { selectedIds } = useViewer.getState().selection
@@ -262,7 +270,7 @@ export const SelectionManager = () => {
event.stopPropagation() event.stopPropagation()
clickHandledRef.current = true clickHandledRef.current = true
strategy.handleClick(event.node) strategy.handleClick(event.node, event.nativeEvent as unknown as MouseEvent)
// Clear hover immediately after clicking on building/level/zone // Clear hover immediately after clicking on building/level/zone
useViewer.setState({ hoveredId: null }) useViewer.setState({ hoveredId: null })
} }
+5 -3
View File
@@ -69,11 +69,13 @@ export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], t
if (useViewer.getState().cameraDragging) return if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return if (e.button !== 0) return
emit('pointerup', e) emit('pointerup', e)
// Synthesize a click event on pointer up to be more forgiving than R3F's default onClick
// which often fails if the mouse moves even 1 pixel.
emit('click', e)
}, },
onClick: (e: ThreeEvent<PointerEvent>) => { onClick: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return // Disable default R3F click since we synthesize it on pointerup
if (e.button !== 0) return // This prevents double-clicks from firing twice.
emit('click', e)
}, },
onPointerEnter: (e: ThreeEvent<PointerEvent>) => { onPointerEnter: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return if (useViewer.getState().cameraDragging) return