116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
import React, { useEffect, 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'
|
|
}
|
|
|
|
function isText(o: WObject): o is Extract<WObject, { type: 'text' }> {
|
|
return o.type === 'text'
|
|
}
|
|
|
|
function isButton(o: WObject): o is Extract<WObject, { type: 'button' }> {
|
|
return o.type === 'button'
|
|
}
|
|
|
|
export function PropsPanel({ object, onChange }: PropsPanelProps) {
|
|
const [model, setModel] = useState<WObject>(object)
|
|
|
|
useEffect(() => {
|
|
setModel(object)
|
|
}, [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)
|
|
}
|
|
const updateTextProps = (patch: Partial<{ text: string }>): void => {
|
|
if (!isText(model)) return
|
|
const next: WObject = { ...model, props: { ...model.props, ...patch } }
|
|
emit(next)
|
|
}
|
|
const updateButtonProps = (patch: Partial<{ label: string }>): void => {
|
|
if (!isButton(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>
|
|
|
|
{isText(model) && (
|
|
<div>
|
|
<label>
|
|
Text
|
|
<input aria-label="Text" type="text" value={model.props.text} onChange={(e) => updateTextProps({ text: e.target.value })} />
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{isButton(model) && (
|
|
<div>
|
|
<label>
|
|
Label
|
|
<input aria-label="Label" type="text" value={model.props.label} onChange={(e) => updateButtonProps({ label: e.target.value })} />
|
|
</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>
|
|
)
|
|
}
|