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:
co-authored by
Claude Opus 4.6
parent
441517424d
commit
5ecefbdc06
@@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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%}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -61,44 +61,48 @@ export function useProjectScene() {
|
|||||||
async function loadScene() {
|
async function loadScene() {
|
||||||
// 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) => ({
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -19,6 +19,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"typescript": "5.9.2"
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user