mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-06-01 03:14:29 +07:00
feat(panels): conditional property panel, layer expand/collapse and scoped hover
- property panel only renders when an element is selected - layer items show eye/lock icons only on the hovered row (group/layer scope) - replace drag handle with chevron expand/collapse for nodes with children - track collapsed state in LayerPanel, default all expanded - remove uppercase from panel section headers
This commit is contained in:
parent
f85bbe69dc
commit
892cc789e1
4 changed files with 88 additions and 62 deletions
|
|
@ -19,7 +19,7 @@ export default function CornerRadiusSection({
|
|||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
<h4 className="text-xs font-medium text-muted-foreground tracking-wider">
|
||||
Corner Radius
|
||||
</h4>
|
||||
<NumberInput
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ import {
|
|||
Hexagon,
|
||||
Spline,
|
||||
Link,
|
||||
GripVertical,
|
||||
ImageIcon,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import type { PenNodeType } from '@/types/pen'
|
||||
|
||||
|
|
@ -39,10 +40,13 @@ interface LayerItemProps {
|
|||
selected: boolean
|
||||
visible: boolean
|
||||
locked: boolean
|
||||
hasChildren: boolean
|
||||
expanded: boolean
|
||||
onSelect: (id: string) => void
|
||||
onRename: (id: string, name: string) => void
|
||||
onToggleVisibility: (id: string) => void
|
||||
onToggleLock: (id: string) => void
|
||||
onToggleExpand: (id: string) => void
|
||||
onContextMenu: (e: React.MouseEvent, id: string) => void
|
||||
onDragStart: (id: string) => void
|
||||
onDragOver: (id: string) => void
|
||||
|
|
@ -57,10 +61,13 @@ export default function LayerItem({
|
|||
selected,
|
||||
visible,
|
||||
locked,
|
||||
hasChildren,
|
||||
expanded,
|
||||
onSelect,
|
||||
onRename,
|
||||
onToggleVisibility,
|
||||
onToggleLock,
|
||||
onToggleExpand,
|
||||
onContextMenu,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
|
|
@ -91,15 +98,13 @@ export default function LayerItem({
|
|||
}
|
||||
}
|
||||
|
||||
const handlePointerDown = (e: React.PointerEvent) => {
|
||||
if ((e.target as HTMLElement).closest('[data-drag-handle]')) {
|
||||
onDragStart(id)
|
||||
}
|
||||
const handlePointerDown = () => {
|
||||
onDragStart(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center h-7 px-1 gap-1 cursor-pointer rounded text-xs transition-colors ${
|
||||
className={`group/layer flex items-center h-7 px-1 gap-1 cursor-pointer rounded text-xs transition-colors ${
|
||||
selected
|
||||
? 'bg-primary/15 text-primary'
|
||||
: 'text-muted-foreground hover:bg-accent/50'
|
||||
|
|
@ -112,12 +117,20 @@ export default function LayerItem({
|
|||
onPointerEnter={() => onDragOver(id)}
|
||||
onPointerUp={onDragEnd}
|
||||
>
|
||||
<div
|
||||
data-drag-handle
|
||||
className="cursor-grab opacity-0 group-hover:opacity-60 hover:opacity-100 shrink-0"
|
||||
>
|
||||
<GripVertical size={10} />
|
||||
</div>
|
||||
{hasChildren ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggleExpand(id)
|
||||
}}
|
||||
className="shrink-0 opacity-60 hover:opacity-100"
|
||||
>
|
||||
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
</button>
|
||||
) : (
|
||||
<span className="shrink-0 w-3" />
|
||||
)}
|
||||
|
||||
<Icon size={12} className="shrink-0 opacity-60" />
|
||||
|
||||
|
|
@ -144,7 +157,7 @@ export default function LayerItem({
|
|||
className={`p-0.5 transition-opacity ${
|
||||
!visible
|
||||
? 'opacity-100 text-yellow-400'
|
||||
: 'opacity-0 group-hover:opacity-100'
|
||||
: 'opacity-0 group-hover/layer:opacity-100'
|
||||
}`}
|
||||
title={visible ? 'Hide' : 'Show'}
|
||||
>
|
||||
|
|
@ -159,7 +172,7 @@ export default function LayerItem({
|
|||
className={`p-0.5 transition-opacity ${
|
||||
locked
|
||||
? 'opacity-100 text-orange-400'
|
||||
: 'opacity-0 group-hover:opacity-100'
|
||||
: 'opacity-0 group-hover/layer:opacity-100'
|
||||
}`}
|
||||
title={locked ? 'Unlock' : 'Lock'}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -20,45 +20,57 @@ function renderLayerTree(
|
|||
onRename: (id: string, name: string) => void
|
||||
onToggleVisibility: (id: string) => void
|
||||
onToggleLock: (id: string) => void
|
||||
onToggleExpand: (id: string) => void
|
||||
onContextMenu: (e: React.MouseEvent, id: string) => void
|
||||
onDragStart: (id: string) => void
|
||||
onDragOver: (id: string) => void
|
||||
onDragEnd: () => void
|
||||
},
|
||||
dragOverId: string | null,
|
||||
collapsedIds: Set<string>,
|
||||
) {
|
||||
return [...nodes].reverse().map((node) => (
|
||||
<div key={node.id} className="group">
|
||||
<div
|
||||
className={
|
||||
dragOverId === node.id
|
||||
? 'border-t-2 border-blue-500'
|
||||
: 'border-t-2 border-transparent'
|
||||
}
|
||||
>
|
||||
<LayerItem
|
||||
id={node.id}
|
||||
name={node.name ?? node.type}
|
||||
type={node.type}
|
||||
depth={depth}
|
||||
selected={selectedIds.includes(node.id)}
|
||||
visible={node.visible !== false}
|
||||
locked={node.locked === true}
|
||||
{...handlers}
|
||||
/>
|
||||
return [...nodes].reverse().map((node) => {
|
||||
const nodeChildren =
|
||||
'children' in node && node.children && node.children.length > 0
|
||||
? node.children
|
||||
: null
|
||||
const isExpanded = !collapsedIds.has(node.id)
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<div
|
||||
className={
|
||||
dragOverId === node.id
|
||||
? 'border-t-2 border-blue-500'
|
||||
: 'border-t-2 border-transparent'
|
||||
}
|
||||
>
|
||||
<LayerItem
|
||||
id={node.id}
|
||||
name={node.name ?? node.type}
|
||||
type={node.type}
|
||||
depth={depth}
|
||||
selected={selectedIds.includes(node.id)}
|
||||
visible={node.visible !== false}
|
||||
locked={node.locked === true}
|
||||
hasChildren={nodeChildren !== null}
|
||||
expanded={isExpanded}
|
||||
{...handlers}
|
||||
/>
|
||||
</div>
|
||||
{nodeChildren &&
|
||||
isExpanded &&
|
||||
renderLayerTree(
|
||||
nodeChildren,
|
||||
depth + 1,
|
||||
selectedIds,
|
||||
handlers,
|
||||
dragOverId,
|
||||
collapsedIds,
|
||||
)}
|
||||
</div>
|
||||
{'children' in node &&
|
||||
node.children &&
|
||||
node.children.length > 0 &&
|
||||
renderLayerTree(
|
||||
node.children,
|
||||
depth + 1,
|
||||
selectedIds,
|
||||
handlers,
|
||||
dragOverId,
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default function LayerPanel() {
|
||||
|
|
@ -83,6 +95,19 @@ export default function LayerPanel() {
|
|||
|
||||
const dragRef = useRef<DragState>({ dragId: null, overId: null })
|
||||
const [dragOverId, setDragOverId] = useState<string | null>(null)
|
||||
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set())
|
||||
|
||||
const handleToggleExpand = useCallback((id: string) => {
|
||||
setCollapsedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(id: string) => {
|
||||
|
|
@ -190,6 +215,7 @@ export default function LayerPanel() {
|
|||
onRename: handleRename,
|
||||
onToggleVisibility: toggleVisibility,
|
||||
onToggleLock: toggleLock,
|
||||
onToggleExpand: handleToggleExpand,
|
||||
onContextMenu: handleContextMenu,
|
||||
onDragStart: handleDragStart,
|
||||
onDragOver: handleDragOver,
|
||||
|
|
@ -199,7 +225,7 @@ export default function LayerPanel() {
|
|||
return (
|
||||
<div className="w-56 bg-card border-r border-border flex flex-col shrink-0">
|
||||
<div className="h-8 flex items-center px-3 border-b border-border">
|
||||
<span className="text-xs font-medium text-muted-foreground tracking-wider">
|
||||
<span className="text-xs font-medium text-muted-foreground tracking-wider">
|
||||
Layers
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -209,7 +235,7 @@ export default function LayerPanel() {
|
|||
No layers yet. Use the toolbar to draw shapes.
|
||||
</p>
|
||||
) : (
|
||||
renderLayerTree(children, 0, selectedIds, handlers, dragOverId)
|
||||
renderLayerTree(children, 0, selectedIds, handlers, dragOverId, collapsedIds)
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,20 +26,7 @@ export default function PropertyPanel() {
|
|||
}
|
||||
|
||||
if (!node) {
|
||||
return (
|
||||
<div className="w-64 bg-card border-l border-border flex flex-col shrink-0">
|
||||
<div className="h-8 flex items-center px-3 border-b border-border">
|
||||
<span className="text-[11px] font-medium text-muted-foreground">
|
||||
Design
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p className="text-[11px] text-muted-foreground px-4 text-center">
|
||||
Select an element to view its properties.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const hasFill =
|
||||
|
|
|
|||
Loading…
Reference in a new issue