Files
fabrikanabytok/apps/fabrikanabytok/components/planner/layers-panel.tsx
2025-11-28 20:48:15 +01:00

386 lines
11 KiB
TypeScript

"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
Eye,
EyeOff,
Lock,
Unlock,
Plus,
Trash2,
ChevronRight,
ChevronDown,
Folder,
Box,
Circle,
} from "lucide-react"
import { usePlannerStore } from "@/lib/store/planner-store"
import { cn } from "@/lib/utils"
import type { PlacedObject } from "@/lib/types/planner.types"
interface TreeNodeProps {
obj: PlacedObject
level: number
onSelect: (id: string) => void
isSelected: boolean
}
function TreeNode({ obj, level, onSelect, isSelected }: TreeNodeProps) {
const [isExpanded, setIsExpanded] = useState(true)
const {
getChildObjects,
toggleVisibility,
lockObject,
removeObject,
ungroupObject,
selectedElementId,
selectElement,
selectPart,
} = usePlannerStore()
const children = getChildObjects(obj.id)
const hasChildren = children.length > 0 || obj.elements.length > 0
return (
<div>
{/* Object row */}
<div
className={cn(
"group flex items-center gap-1 py-1 px-2 hover:bg-muted/50 rounded cursor-pointer transition-colors",
isSelected && "bg-brand-50 border-l-2 border-brand-600",
!obj.visible && "opacity-50"
)}
style={{ paddingLeft: `${level * 16 + 8}px` }}
onClick={() => onSelect(obj.id)}
>
{/* Expand/collapse */}
{hasChildren ? (
<button
onClick={(e) => {
e.stopPropagation()
setIsExpanded(!isExpanded)
}}
className="p-0.5 hover:bg-muted rounded"
>
{isExpanded ? (
<ChevronDown className="w-3 h-3" />
) : (
<ChevronRight className="w-3 h-3" />
)}
</button>
) : (
<div className="w-4" />
)}
{/* Icon */}
{obj.isGroup ? (
<Folder className="w-3.5 h-3.5 text-amber-600" />
) : (
<Box className="w-3.5 h-3.5 text-blue-600" />
)}
{/* Name */}
<span className="flex-1 text-sm truncate">{obj.name}</span>
{/* Layer color indicator */}
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: obj.layer }}
/>
{/* Actions */}
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => {
e.stopPropagation()
toggleVisibility(obj.id)
}}
className="p-1 hover:bg-muted rounded"
title={obj.visible ? "Hide" : "Show"}
>
{obj.visible ? (
<Eye className="w-3 h-3" />
) : (
<EyeOff className="w-3 h-3" />
)}
</button>
<button
onClick={(e) => {
e.stopPropagation()
lockObject(obj.id, !obj.locked)
}}
className="p-1 hover:bg-muted rounded"
title={obj.locked ? "Unlock" : "Lock"}
>
{obj.locked ? (
<Lock className="w-3 h-3" />
) : (
<Unlock className="w-3 h-3" />
)}
</button>
{obj.isGroup && (
<button
onClick={(e) => {
e.stopPropagation()
ungroupObject(obj.id)
}}
className="p-1 hover:bg-muted rounded"
title="Ungroup"
>
<Circle className="w-3 h-3" />
</button>
)}
<button
onClick={(e) => {
e.stopPropagation()
removeObject(obj.id)
}}
className="p-1 hover:bg-destructive/10 rounded text-destructive"
title="Delete"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
{/* Children and Elements */}
{isExpanded && hasChildren && (
<div>
{/* Render elements */}
{obj.elements.map((element) => (
<div key={element.id}>
<div
className={cn(
"group flex items-center gap-1 py-1 px-2 hover:bg-muted/50 rounded cursor-pointer transition-colors",
selectedElementId === element.id && "bg-blue-50 border-l-2 border-blue-600",
!element.visible && "opacity-50"
)}
style={{ paddingLeft: `${(level + 1) * 16 + 8}px` }}
onClick={(e) => {
e.stopPropagation()
selectElement(obj.id, element.id)
}}
>
<div className="w-4" />
<Circle className="w-3 h-3 text-blue-500" />
<span className="flex-1 text-xs truncate">{element.name}</span>
<span className="text-xs text-muted-foreground">Element</span>
</div>
{/* Render parts */}
{element.parts.map((part) => (
<div
key={part.id}
className={cn(
"group flex items-center gap-1 py-0.5 px-2 hover:bg-muted/50 rounded cursor-pointer transition-colors",
!part.visible && "opacity-50"
)}
style={{ paddingLeft: `${(level + 2) * 16 + 8}px` }}
onClick={(e) => {
e.stopPropagation()
selectPart(obj.id, element.id, part.id)
}}
>
<div className="w-4" />
<Circle className="w-2.5 h-2.5 text-purple-500" />
<span className="flex-1 text-xs truncate">{part.name}</span>
<span className="text-xs text-muted-foreground">{part.type}</span>
</div>
))}
</div>
))}
{/* Render child objects */}
{children.map((child) => (
<TreeNode
key={child.id}
obj={child}
level={level + 1}
onSelect={onSelect}
isSelected={false}
/>
))}
</div>
)}
</div>
)
}
export function LayersPanel() {
const [newLayerName, setNewLayerName] = useState("")
const [showNewLayerInput, setShowNewLayerInput] = useState(false)
const {
placedObjects,
layers,
activeLayerId,
selectedObjectId,
selectObject,
addLayer,
removeLayer,
setActiveLayer,
toggleLayerVisibility,
toggleLayerLock,
groupObjects,
selectedObjectIds,
} = usePlannerStore()
// Get root level objects (no parent)
const rootObjects = placedObjects.filter((obj) => !obj.parentId)
const handleAddLayer = () => {
if (newLayerName.trim()) {
addLayer(newLayerName.trim())
setNewLayerName("")
setShowNewLayerInput(false)
}
}
const handleGroupSelected = () => {
if (selectedObjectIds.length >= 2) {
groupObjects(selectedObjectIds)
}
}
return (
<div className="flex flex-col h-full">
{/* Layers header */}
<div className="p-3 border-b space-y-2">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-sm">Layers</h3>
<Button
size="sm"
variant="ghost"
onClick={() => setShowNewLayerInput(true)}
>
<Plus className="w-3 h-3" />
</Button>
</div>
{showNewLayerInput && (
<div className="flex gap-1">
<Input
placeholder="Layer name"
value={newLayerName}
onChange={(e) => setNewLayerName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAddLayer()}
className="h-7 text-xs"
autoFocus
/>
<Button size="sm" onClick={handleAddLayer} className="h-7">
Add
</Button>
</div>
)}
{selectedObjectIds.length >= 2 && (
<Button
size="sm"
variant="outline"
onClick={handleGroupSelected}
className="w-full"
>
<Folder className="w-3 h-3 mr-1" />
Group Selected ({selectedObjectIds.length})
</Button>
)}
</div>
{/* Layers list */}
<div className="border-b">
{layers.map((layer) => (
<div
key={layer.id}
className={cn(
"flex items-center gap-2 px-3 py-2 hover:bg-muted/50 cursor-pointer transition-colors border-l-2",
activeLayerId === layer.id ? "border-brand-600 bg-brand-50" : "border-transparent"
)}
onClick={() => setActiveLayer(layer.id)}
>
<div
className="w-3 h-3 rounded"
style={{ backgroundColor: layer.color }}
/>
<span className="flex-1 text-sm font-medium">{layer.name}</span>
<span className="text-xs text-muted-foreground">
{layer.objectIds.length}
</span>
<button
onClick={(e) => {
e.stopPropagation()
toggleLayerVisibility(layer.id)
}}
className="p-1 hover:bg-muted rounded"
>
{layer.visible ? (
<Eye className="w-3 h-3" />
) : (
<EyeOff className="w-3 h-3" />
)}
</button>
<button
onClick={(e) => {
e.stopPropagation()
toggleLayerLock(layer.id)
}}
className="p-1 hover:bg-muted rounded"
>
{layer.locked ? (
<Lock className="w-3 h-3" />
) : (
<Unlock className="w-3 h-3" />
)}
</button>
{layer.id !== "default" && (
<button
onClick={(e) => {
e.stopPropagation()
removeLayer(layer.id)
}}
className="p-1 hover:bg-destructive/10 rounded text-destructive"
>
<Trash2 className="w-3 h-3" />
</button>
)}
</div>
))}
</div>
{/* Objects tree */}
<div className="flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="p-2 space-y-0.5">
{rootObjects.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-xs">
No objects in scene.
<br />
Drag products from the sidebar!
</div>
) : (
rootObjects.map((obj) => (
<TreeNode
key={obj.id}
obj={obj}
level={0}
onSelect={selectObject}
isSelected={selectedObjectId === obj.id}
/>
))
)}
</div>
</ScrollArea>
</div>
</div>
)
}