feat(panels): add component/instance UI to layer and property panels
layer panel shows diamond icon and purple/#9281f7 styling for components/instances with context menu actions; property panel resolves RefNode display, routes instance overrides, adds header with go-to and detach buttons, and supports inline name editing on click
This commit is contained in:
parent
a4d6e11aad
commit
0c0ed93ae8
|
|
@ -5,6 +5,8 @@ import {
|
|||
Group,
|
||||
Lock,
|
||||
EyeOff,
|
||||
Component,
|
||||
Unlink,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface LayerContextMenuProps {
|
||||
|
|
@ -12,6 +14,9 @@ interface LayerContextMenuProps {
|
|||
y: number
|
||||
nodeId: string
|
||||
canGroup: boolean
|
||||
canCreateComponent: boolean
|
||||
isReusable: boolean
|
||||
isInstance: boolean
|
||||
onAction: (action: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
|
@ -20,6 +25,9 @@ const MENU_ITEMS = [
|
|||
{ action: 'duplicate', label: 'Duplicate', icon: Copy },
|
||||
{ action: 'delete', label: 'Delete', icon: Trash2 },
|
||||
{ action: 'group', label: 'Group Selection', icon: Group, requireGroup: true },
|
||||
{ action: 'make-component', label: 'Create Component', icon: Component, requireCreateComponent: true },
|
||||
{ action: 'detach-component', label: 'Detach Component', icon: Unlink, requireReusable: true },
|
||||
{ action: 'detach-component', label: 'Detach Instance', icon: Unlink, requireInstance: true },
|
||||
{ action: 'lock', label: 'Toggle Lock', icon: Lock },
|
||||
{ action: 'hide', label: 'Toggle Visibility', icon: EyeOff },
|
||||
]
|
||||
|
|
@ -28,6 +36,9 @@ export default function LayerContextMenu({
|
|||
x,
|
||||
y,
|
||||
canGroup,
|
||||
canCreateComponent,
|
||||
isReusable,
|
||||
isInstance,
|
||||
onAction,
|
||||
onClose,
|
||||
}: LayerContextMenuProps) {
|
||||
|
|
@ -57,7 +68,11 @@ export default function LayerContextMenu({
|
|||
style={{ left: x, top: y }}
|
||||
>
|
||||
{MENU_ITEMS.filter(
|
||||
(item) => !item.requireGroup || canGroup,
|
||||
(item) =>
|
||||
(!item.requireGroup || canGroup) &&
|
||||
(!('requireCreateComponent' in item) || canCreateComponent) &&
|
||||
(!('requireReusable' in item) || isReusable) &&
|
||||
(!('requireInstance' in item) || isInstance),
|
||||
).map((item) => (
|
||||
<button
|
||||
key={item.action}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
ImageIcon,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Diamond,
|
||||
} from 'lucide-react'
|
||||
import type { PenNodeType } from '@/types/pen'
|
||||
|
||||
|
|
@ -44,6 +45,8 @@ interface LayerItemProps {
|
|||
locked: boolean
|
||||
hasChildren: boolean
|
||||
expanded: boolean
|
||||
isReusable: boolean
|
||||
isInstance: boolean
|
||||
dropPosition: DropPosition
|
||||
onSelect: (id: string) => void
|
||||
onRename: (id: string, name: string) => void
|
||||
|
|
@ -66,6 +69,8 @@ export default function LayerItem({
|
|||
locked,
|
||||
hasChildren,
|
||||
expanded,
|
||||
isReusable,
|
||||
isInstance,
|
||||
dropPosition,
|
||||
onSelect,
|
||||
onRename,
|
||||
|
|
@ -80,7 +85,7 @@ export default function LayerItem({
|
|||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editName, setEditName] = useState(name)
|
||||
|
||||
const Icon = TYPE_ICONS[type] ?? Square
|
||||
const Icon = isReusable || isInstance ? Diamond : (TYPE_ICONS[type] ?? Square)
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
setEditName(name)
|
||||
|
|
@ -120,8 +125,16 @@ export default function LayerItem({
|
|||
<div
|
||||
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'
|
||||
? isReusable
|
||||
? 'bg-purple-500/15 text-purple-400'
|
||||
: isInstance
|
||||
? 'bg-[#9281f7]/10 text-[#9281f7]'
|
||||
: 'bg-primary/15 text-primary'
|
||||
: isReusable
|
||||
? 'text-purple-400 hover:bg-purple-500/10'
|
||||
: isInstance
|
||||
? 'text-[#9281f7] hover:bg-[#9281f7]/10'
|
||||
: 'text-muted-foreground hover:bg-accent/50'
|
||||
} ${!visible ? 'opacity-40' : ''} ${dropInsideHighlight}`}
|
||||
style={{ paddingLeft: `${depth * 12 + 4}px` }}
|
||||
onClick={() => onSelect(id)}
|
||||
|
|
@ -146,7 +159,7 @@ export default function LayerItem({
|
|||
<span className="shrink-0 w-3" />
|
||||
)}
|
||||
|
||||
<Icon size={12} className="shrink-0 opacity-60" />
|
||||
<Icon size={12} className={`shrink-0 ${isReusable ? 'text-purple-400' : isInstance ? 'text-[#9281f7]' : 'opacity-60'}`} />
|
||||
|
||||
{isEditing ? (
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef, useCallback } from 'react'
|
||||
import { useDocumentStore } from '@/stores/document-store'
|
||||
import { useDocumentStore, findNodeInTree } from '@/stores/document-store'
|
||||
import { useCanvasStore } from '@/stores/canvas-store'
|
||||
import type { FabricObjectWithPenId } from '@/canvas/canvas-object-factory'
|
||||
import type { PenNode } from '@/types/pen'
|
||||
|
|
@ -15,6 +15,28 @@ interface DragState {
|
|||
dropPosition: DropPosition
|
||||
}
|
||||
|
||||
function isNodeReusable(node: PenNode, parentReusable: boolean): boolean {
|
||||
if (parentReusable) return true
|
||||
return 'reusable' in node && node.reusable === true
|
||||
}
|
||||
|
||||
/** Get effective children for a node, resolving RefNode instances. */
|
||||
function getEffectiveChildren(
|
||||
node: PenNode,
|
||||
allChildren: PenNode[],
|
||||
): PenNode[] | null {
|
||||
if (node.type === 'ref') {
|
||||
const component = findNodeInTree(allChildren, node.ref)
|
||||
if (component && 'children' in component && component.children?.length) {
|
||||
return component.children
|
||||
}
|
||||
return null
|
||||
}
|
||||
return 'children' in node && node.children && node.children.length > 0
|
||||
? node.children
|
||||
: null
|
||||
}
|
||||
|
||||
function renderLayerTree(
|
||||
nodes: PenNode[],
|
||||
depth: number,
|
||||
|
|
@ -33,14 +55,16 @@ function renderLayerTree(
|
|||
dragOverId: string | null,
|
||||
dropPosition: DropPosition,
|
||||
collapsedIds: Set<string>,
|
||||
allChildren: PenNode[],
|
||||
parentReusable = false,
|
||||
parentIsInstance = false,
|
||||
) {
|
||||
return [...nodes].reverse().map((node) => {
|
||||
const nodeChildren =
|
||||
'children' in node && node.children && node.children.length > 0
|
||||
? node.children
|
||||
: null
|
||||
const nodeChildren = getEffectiveChildren(node, allChildren)
|
||||
const isExpanded = !collapsedIds.has(node.id)
|
||||
const isDropTarget = dragOverId === node.id
|
||||
const isInstance = node.type === 'ref' || parentIsInstance
|
||||
const reusable = isNodeReusable(node, parentReusable)
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
|
|
@ -54,6 +78,8 @@ function renderLayerTree(
|
|||
locked={node.locked === true}
|
||||
hasChildren={nodeChildren !== null}
|
||||
expanded={isExpanded}
|
||||
isReusable={reusable}
|
||||
isInstance={isInstance}
|
||||
dropPosition={isDropTarget ? dropPosition : null}
|
||||
{...handlers}
|
||||
/>
|
||||
|
|
@ -67,6 +93,9 @@ function renderLayerTree(
|
|||
dragOverId,
|
||||
dropPosition,
|
||||
collapsedIds,
|
||||
allChildren,
|
||||
reusable,
|
||||
isInstance,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -214,6 +243,9 @@ export default function LayerPanel() {
|
|||
setDropPosition(null)
|
||||
}, [children, getParentOf, moveNode])
|
||||
|
||||
const makeReusable = useDocumentStore((s) => s.makeReusable)
|
||||
const detachComponent = useDocumentStore((s) => s.detachComponent)
|
||||
|
||||
const handleContextAction = useCallback(
|
||||
(action: string) => {
|
||||
if (!contextMenu) return
|
||||
|
|
@ -239,6 +271,12 @@ export default function LayerPanel() {
|
|||
case 'hide':
|
||||
toggleVisibility(nodeId)
|
||||
break
|
||||
case 'make-component':
|
||||
makeReusable(nodeId)
|
||||
break
|
||||
case 'detach-component':
|
||||
detachComponent(nodeId)
|
||||
break
|
||||
}
|
||||
setContextMenu(null)
|
||||
},
|
||||
|
|
@ -251,6 +289,8 @@ export default function LayerPanel() {
|
|||
toggleLock,
|
||||
toggleVisibility,
|
||||
setSelection,
|
||||
makeReusable,
|
||||
detachComponent,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -287,20 +327,34 @@ export default function LayerPanel() {
|
|||
dragOverId,
|
||||
dropPosition,
|
||||
collapsedIds,
|
||||
children,
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{contextMenu && (
|
||||
<LayerContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
nodeId={contextMenu.nodeId}
|
||||
canGroup={selectedIds.length >= 2}
|
||||
onAction={handleContextAction}
|
||||
onClose={() => setContextMenu(null)}
|
||||
/>
|
||||
)}
|
||||
{contextMenu && (() => {
|
||||
const contextNode = getNodeById(contextMenu.nodeId)
|
||||
const isContainer = contextNode
|
||||
? contextNode.type === 'frame' || contextNode.type === 'group' || contextNode.type === 'rectangle'
|
||||
: false
|
||||
const nodeIsReusable = contextNode
|
||||
? 'reusable' in contextNode && contextNode.reusable === true
|
||||
: false
|
||||
const nodeIsInstance = contextNode?.type === 'ref'
|
||||
return (
|
||||
<LayerContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
nodeId={contextMenu.nodeId}
|
||||
canGroup={selectedIds.length >= 2}
|
||||
canCreateComponent={isContainer && !nodeIsReusable}
|
||||
isReusable={nodeIsReusable}
|
||||
isInstance={nodeIsInstance}
|
||||
onAction={handleContextAction}
|
||||
onClose={() => setContextMenu(null)}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useCanvasStore } from '@/stores/canvas-store'
|
||||
import { useDocumentStore } from '@/stores/document-store'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import type { PenNode, ContainerProps } from '@/types/pen'
|
||||
import type { PenNode, ContainerProps, RefNode } from '@/types/pen'
|
||||
import { Component, Diamond, ArrowUpRight, Unlink } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { FabricObjectWithPenId } from '@/canvas/canvas-object-factory'
|
||||
import SizeSection from './size-section'
|
||||
import LayoutSection from './layout-section'
|
||||
import FillSection from './fill-section'
|
||||
|
|
@ -10,51 +14,245 @@ import AppearanceSection from './appearance-section'
|
|||
import TextSection from './text-section'
|
||||
import EffectsSection from './effects-section'
|
||||
|
||||
/** Properties stored directly on the RefNode (instance-level), not as overrides. */
|
||||
const INSTANCE_DIRECT_PROPS = new Set([
|
||||
'x', 'y', 'width', 'height', 'name', 'visible', 'locked', 'rotation', 'opacity', 'flipX', 'flipY', 'enabled', 'theme',
|
||||
])
|
||||
|
||||
export default function PropertyPanel() {
|
||||
const activeId = useCanvasStore((s) => s.selection.activeId)
|
||||
const setSelection = useCanvasStore((s) => s.setSelection)
|
||||
const fabricCanvas = useCanvasStore((s) => s.fabricCanvas)
|
||||
const children = useDocumentStore((s) => s.document.children)
|
||||
const getNodeById = useDocumentStore((s) => s.getNodeById)
|
||||
const updateNode = useDocumentStore((s) => s.updateNode)
|
||||
const makeReusable = useDocumentStore((s) => s.makeReusable)
|
||||
const detachComponent = useDocumentStore((s) => s.detachComponent)
|
||||
|
||||
// Subscribe to `children` so we re-render when nodes change
|
||||
void children
|
||||
const node = activeId ? getNodeById(activeId) : undefined
|
||||
|
||||
const handleUpdate = (updates: Partial<PenNode>) => {
|
||||
if (activeId) {
|
||||
updateNode(activeId, updates)
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasLayout =
|
||||
node.type === 'frame' || node.type === 'group' || node.type === 'rectangle'
|
||||
const hasFill =
|
||||
node.type !== 'line' && node.type !== 'ref'
|
||||
const hasStroke = node.type !== 'ref'
|
||||
const nodeIsReusable = 'reusable' in node && node.reusable === true
|
||||
const nodeIsInstance = node.type === 'ref'
|
||||
|
||||
// For RefNodes, resolve the referenced component to get visual properties.
|
||||
// The display node merges: component base → instance overrides → RefNode position/meta.
|
||||
let displayNode = node
|
||||
if (nodeIsInstance) {
|
||||
const refNode = node as RefNode
|
||||
const component = getNodeById(refNode.ref)
|
||||
if (component) {
|
||||
const topOverrides = refNode.descendants?.[refNode.ref] ?? {}
|
||||
const merged: Record<string, unknown> = { ...component, ...topOverrides }
|
||||
// Apply RefNode's own explicitly defined properties
|
||||
for (const [key, val] of Object.entries(node)) {
|
||||
if (key === 'type' || key === 'ref' || key === 'descendants' || key === 'children') continue
|
||||
if (val !== undefined) {
|
||||
merged[key] = val
|
||||
}
|
||||
}
|
||||
// Use component's type (frame/rect/etc.) not 'ref'
|
||||
merged.type = component.type
|
||||
if (!merged.name) merged.name = component.name
|
||||
displayNode = merged as unknown as PenNode
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = (updates: Partial<PenNode>) => {
|
||||
if (!activeId) return
|
||||
if (nodeIsInstance && node.type === 'ref') {
|
||||
const refNode = node as RefNode
|
||||
const refNodeUpdate: Record<string, unknown> = {}
|
||||
const overrideProps: Record<string, unknown> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (INSTANCE_DIRECT_PROPS.has(key)) {
|
||||
refNodeUpdate[key] = value
|
||||
} else {
|
||||
overrideProps[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Store visual properties as overrides in descendants[ref]
|
||||
if (Object.keys(overrideProps).length > 0) {
|
||||
const currentDescendants = refNode.descendants ?? {}
|
||||
const existing = currentDescendants[refNode.ref] ?? {}
|
||||
refNodeUpdate.descendants = {
|
||||
...currentDescendants,
|
||||
[refNode.ref]: { ...existing, ...overrideProps },
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(refNodeUpdate).length > 0) {
|
||||
updateNode(activeId, refNodeUpdate as Partial<PenNode>)
|
||||
}
|
||||
} else {
|
||||
updateNode(activeId, updates)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoToComponent = () => {
|
||||
if (!nodeIsInstance || node.type !== 'ref') return
|
||||
const refId = (node as RefNode).ref
|
||||
setSelection([refId], refId)
|
||||
if (fabricCanvas) {
|
||||
const objects = fabricCanvas.getObjects() as FabricObjectWithPenId[]
|
||||
const target = objects.find((o) => (o as FabricObjectWithPenId).penNodeId === refId)
|
||||
if (target) {
|
||||
fabricCanvas.setActiveObject(target)
|
||||
fabricCanvas.requestRenderAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isContainer =
|
||||
displayNode.type === 'frame' || displayNode.type === 'group' || displayNode.type === 'rectangle'
|
||||
const hasLayout = isContainer
|
||||
const hasFill = displayNode.type !== 'line'
|
||||
const hasStroke = true
|
||||
const hasCornerRadius =
|
||||
node.type === 'rectangle' || node.type === 'frame'
|
||||
const hasEffects = node.type !== 'ref'
|
||||
const isText = node.type === 'text'
|
||||
displayNode.type === 'rectangle' || displayNode.type === 'frame'
|
||||
const hasEffects = true
|
||||
const isText = displayNode.type === 'text'
|
||||
|
||||
const [isEditingName, setIsEditingName] = useState(false)
|
||||
const [editName, setEditName] = useState('')
|
||||
|
||||
// Reset editing state when selection changes
|
||||
useEffect(() => {
|
||||
setIsEditingName(false)
|
||||
}, [activeId])
|
||||
|
||||
const handleNameClick = () => {
|
||||
setEditName(node.name ?? node.type)
|
||||
setIsEditingName(true)
|
||||
}
|
||||
|
||||
const handleNameBlur = () => {
|
||||
setIsEditingName(false)
|
||||
const trimmed = editName.trim()
|
||||
if (trimmed && trimmed !== (node.name ?? node.type)) {
|
||||
updateNode(activeId!, { name: trimmed } as Partial<PenNode>)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNameKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') (e.target as HTMLInputElement).blur()
|
||||
if (e.key === 'Escape') {
|
||||
setIsEditingName(false)
|
||||
setEditName(node.name ?? node.type)
|
||||
}
|
||||
}
|
||||
|
||||
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-foreground">
|
||||
{node.name ?? node.type}
|
||||
</span>
|
||||
{/* Header */}
|
||||
<div className="h-8 flex items-center px-2 border-b border-border gap-1">
|
||||
{(nodeIsReusable || nodeIsInstance) && (
|
||||
<Diamond size={12} className={`shrink-0 ${nodeIsReusable ? 'text-purple-400' : 'text-[#9281f7]'}`} />
|
||||
)}
|
||||
{isEditingName ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onBlur={handleNameBlur}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
className={`text-[11px] font-medium flex-1 min-w-0 bg-secondary rounded px-1.5 py-0.5 border border-ring focus:outline-none ${
|
||||
nodeIsReusable
|
||||
? 'text-purple-400'
|
||||
: nodeIsInstance
|
||||
? 'text-[#9281f7]'
|
||||
: 'text-foreground'
|
||||
}`}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`text-[11px] font-medium flex-1 truncate cursor-text ${
|
||||
nodeIsReusable
|
||||
? 'text-purple-400 border border-purple-400/50 rounded px-1.5 py-0.5'
|
||||
: nodeIsInstance
|
||||
? 'text-[#9281f7] border border-dashed border-[#9281f7]/50 rounded px-1.5 py-0.5'
|
||||
: 'text-foreground px-1'
|
||||
}`}
|
||||
onClick={handleNameClick}
|
||||
>
|
||||
{node.name ?? node.type}
|
||||
</span>
|
||||
)}
|
||||
{nodeIsInstance && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
title="Go to component"
|
||||
className="p-1 rounded hover:bg-accent/50 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={handleGoToComponent}
|
||||
>
|
||||
<ArrowUpRight size={12} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Detach instance"
|
||||
className="p-1 rounded hover:bg-accent/50 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={() => activeId && detachComponent(activeId)}
|
||||
>
|
||||
<Unlink size={12} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{(isContainer || nodeIsInstance) && (
|
||||
<div className="px-3 py-2">
|
||||
{nodeIsReusable ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-7 text-xs gap-1.5 text-purple-400 border-purple-500/30 hover:bg-purple-500/10"
|
||||
onClick={() => activeId && detachComponent(activeId)}
|
||||
>
|
||||
<Unlink size={12} />
|
||||
Detach Component
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-7 text-xs gap-1.5"
|
||||
onClick={() => {
|
||||
if (!activeId) return
|
||||
if (nodeIsInstance) {
|
||||
const newId = detachComponent(activeId)
|
||||
if (newId) {
|
||||
makeReusable(newId)
|
||||
setSelection([newId], newId)
|
||||
}
|
||||
return
|
||||
}
|
||||
makeReusable(activeId)
|
||||
}}
|
||||
>
|
||||
<Component size={12} />
|
||||
Create Component
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="px-3 py-2">
|
||||
<SizeSection
|
||||
node={node}
|
||||
node={displayNode}
|
||||
onUpdate={handleUpdate}
|
||||
hasCornerRadius={hasCornerRadius}
|
||||
cornerRadius={
|
||||
'cornerRadius' in node ? node.cornerRadius : undefined
|
||||
'cornerRadius' in displayNode ? displayNode.cornerRadius : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -63,7 +261,7 @@ export default function PropertyPanel() {
|
|||
<>
|
||||
<Separator />
|
||||
<div className="px-3 py-2">
|
||||
<LayoutSection node={node as PenNode & ContainerProps} onUpdate={handleUpdate} />
|
||||
<LayoutSection node={displayNode as PenNode & ContainerProps} onUpdate={handleUpdate} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -74,7 +272,7 @@ export default function PropertyPanel() {
|
|||
<>
|
||||
<div className="px-3 py-2">
|
||||
<FillSection
|
||||
fills={'fill' in node ? node.fill : undefined}
|
||||
fills={'fill' in displayNode ? displayNode.fill : undefined}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -86,7 +284,7 @@ export default function PropertyPanel() {
|
|||
<>
|
||||
<div className="px-3 py-2">
|
||||
<StrokeSection
|
||||
stroke={'stroke' in node ? node.stroke : undefined}
|
||||
stroke={'stroke' in displayNode ? displayNode.stroke : undefined}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -95,7 +293,7 @@ export default function PropertyPanel() {
|
|||
)}
|
||||
|
||||
<div className="px-3 py-2">
|
||||
<AppearanceSection node={node} onUpdate={handleUpdate} />
|
||||
<AppearanceSection node={displayNode} onUpdate={handleUpdate} />
|
||||
</div>
|
||||
|
||||
{hasEffects && (
|
||||
|
|
@ -103,18 +301,18 @@ export default function PropertyPanel() {
|
|||
<Separator />
|
||||
<div className="px-3 py-2">
|
||||
<EffectsSection
|
||||
effects={'effects' in node ? node.effects : undefined}
|
||||
effects={'effects' in displayNode ? displayNode.effects : undefined}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isText && node.type === 'text' && (
|
||||
{isText && displayNode.type === 'text' && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="px-3 py-2">
|
||||
<TextSection node={node} onUpdate={handleUpdate} />
|
||||
<TextSection node={displayNode} onUpdate={handleUpdate} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue