Files
landing-builder/components/wysiwyg/PropsPanel.tsx
jaybe 98d33fc0d0
Auto PR / open-pr (push) Successful in 21s
Auto Label PR / add-automerge-label (pull_request) Successful in 6s
CI / test (pull_request) Successful in 1m16s
feat(wysiwyg): 레이어/속성 패널 및 Export(절대배치) 1차 TDD/구현
- LayersPanel: 순서 변경/잠금/숨김 토글
- PropsPanel: 위치/크기/회전 및 이미지 src/alt 편집
- exportFrame: frame-scale 토큰 및 오브젝트 절대배치 HTML/CSS 출력
- 테스트 추가 및 전체 그린 유지
2025-11-16 21:35:28 +09:00

76 lines
2.4 KiB
TypeScript

import React, { useState } from 'react'
import type { WObject } from '@/lib/wysiwyg/schema'
export type PropsPanelProps = {
object: WObject
onChange?: (next: WObject) => void
}
function num(v: string, fallback: number) {
const n = Number(v)
return Number.isFinite(n) ? n : fallback
}
function isImage(o: WObject): o is Extract<WObject, { type: 'image' }> {
return o.type === 'image'
}
export function PropsPanel({ object, onChange }: PropsPanelProps) {
const [model, setModel] = useState<WObject>(object)
const emit = (next: WObject) => {
setModel(next)
onChange?.(next)
}
const update = (patch: Partial<Pick<WObject, 'x' | 'y' | 'width' | 'height' | 'rotate'>>): void => {
const next: WObject = { ...model, ...patch }
emit(next)
}
const updateImageProps = (patch: Partial<{ src: string; alt?: string }>): void => {
if (!isImage(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
return (
<div aria-label="Properties Panel">
<div>
<label>
X
<input aria-label="X" type="number" value={model.x} onChange={(e) => update({ x: num(e.target.value, model.x) })} />
</label>
<label>
Y
<input aria-label="Y" type="number" value={model.y} onChange={(e) => update({ y: num(e.target.value, model.y) })} />
</label>
<label>
Width
<input aria-label="Width" type="number" value={model.width} onChange={(e) => update({ width: num(e.target.value, model.width) })} />
</label>
<label>
Height
<input aria-label="Height" type="number" value={model.height} onChange={(e) => update({ height: num(e.target.value, model.height) })} />
</label>
<label>
Rotate
<input aria-label="Rotate" type="number" value={model.rotate} onChange={(e) => update({ rotate: num(e.target.value, model.rotate) })} />
</label>
</div>
{isImage(model) && (
<div>
<label>
Src
<input aria-label="Src" type="text" value={model.props.src} onChange={(e) => updateImageProps({ src: e.target.value })} />
</label>
<label>
Alt
<input aria-label="Alt" type="text" value={model.props.alt || ''} onChange={(e) => updateImageProps({ alt: e.target.value })} />
</label>
</div>
)}
</div>
)
}