feat(wysiwyg): 레이어/속성 패널 및 Export(절대배치) 1차 TDD/구현
- LayersPanel: 순서 변경/잠금/숨김 토글 - PropsPanel: 위치/크기/회전 및 이미지 src/alt 편집 - exportFrame: frame-scale 토큰 및 오브젝트 절대배치 HTML/CSS 출력 - 테스트 추가 및 전체 그린 유지
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import type { Frame } from '@/lib/wysiwyg/schema'
|
||||
|
||||
export type LayersPanelProps = {
|
||||
frame: Frame
|
||||
onChange?: (next: Frame) => void
|
||||
}
|
||||
|
||||
export function LayersPanel({ frame, onChange }: LayersPanelProps) {
|
||||
const [model, setModel] = useState<Frame>(frame)
|
||||
|
||||
const move = useCallback((id: string, dir: 1 | -1) => {
|
||||
const idx = model.layers.findIndex((l) => l.id === id)
|
||||
if (idx < 0) return
|
||||
const j = idx + dir
|
||||
if (j < 0 || j >= model.layers.length) return
|
||||
const nextLayers = model.layers.slice()
|
||||
const [it] = nextLayers.splice(idx, 1)
|
||||
nextLayers.splice(j, 0, it)
|
||||
const next = { ...model, layers: nextLayers }
|
||||
setModel(next)
|
||||
onChange?.(next)
|
||||
}, [model, onChange])
|
||||
|
||||
const toggle = useCallback((id: string, key: 'locked' | 'visible') => {
|
||||
const nextLayers = model.layers.map((l) => l.id === id ? { ...l, [key]: key === 'visible' ? !l.visible : !l.locked } : l)
|
||||
const next = { ...model, layers: nextLayers }
|
||||
setModel(next)
|
||||
onChange?.(next)
|
||||
}, [model, onChange])
|
||||
|
||||
return (
|
||||
<div aria-label="Layers Panel">
|
||||
{model.layers.map((l) => (
|
||||
<div key={l.id} data-testid="layer-item" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<span>{l.name}</span>
|
||||
<button type="button" data-testid={`layer-up-${l.id}`} onClick={() => move(l.id, -1)} aria-label={`move up ${l.name}`}>
|
||||
↑
|
||||
</button>
|
||||
<button type="button" data-testid={`layer-down-${l.id}`} onClick={() => move(l.id, 1)} aria-label={`move down ${l.name}`}>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`layer-lock-${l.id}`}
|
||||
aria-pressed={l.locked ? 'true' : 'false'}
|
||||
onClick={() => toggle(l.id, 'locked')}
|
||||
aria-label={`lock ${l.name}`}
|
||||
>
|
||||
🔒
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`layer-hide-${l.id}`}
|
||||
aria-pressed={l.visible ? 'false' : 'true'}
|
||||
onClick={() => toggle(l.id, 'visible')}
|
||||
aria-label={`toggle ${l.name} visibility`}
|
||||
>
|
||||
👁
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user