테스트/린트 안정화: 버튼 쿼리 모호성 제거, 훅 deps 보강, vi.spyOn 모킹, 멀티선택 Moveable 간섭 방지 재확인, 스냅 우선순위 회귀 그린
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 1m49s

This commit is contained in:
2025-11-17 17:33:39 +09:00
parent 1c607f6331
commit 5bf78b3e01
29 changed files with 1861 additions and 80 deletions
+187
View File
@@ -23,6 +23,26 @@ function isButton(o: WObject): o is Extract<WObject, { type: 'button' }> {
return o.type === 'button'
}
function isInput(o: WObject): o is Extract<WObject, { type: 'input' }> {
return o.type === 'input'
}
function isSelect(o: WObject): o is Extract<WObject, { type: 'select' }> {
return o.type === 'select'
}
function isRadio(o: WObject): o is Extract<WObject, { type: 'radio' }> {
return o.type === 'radio'
}
function isCheckbox(o: WObject): o is Extract<WObject, { type: 'checkbox' }> {
return o.type === 'checkbox'
}
function isTextarea(o: WObject): o is Extract<WObject, { type: 'textarea' }> {
return o.type === 'textarea'
}
export function PropsPanel({ object, onChange }: PropsPanelProps) {
const [model, setModel] = useState<WObject>(object)
@@ -55,6 +75,36 @@ export function PropsPanel({ object, onChange }: PropsPanelProps) {
emit(next)
}
const updateInputProps = (patch: Partial<{ name: string; label: string; placeholder: string; required: boolean; help: string }>): void => {
if (!isInput(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
const updateSelectProps = (patch: Partial<{ name: string; label: string; options: string[]; defaultValue: string; help: string }>): void => {
if (!isSelect(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
const updateRadioProps = (patch: Partial<{ name: string; legend: string; options: string[]; defaultValue: string; required: boolean; help: string }>): void => {
if (!isRadio(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
const updateCheckboxProps = (patch: Partial<{ name: string; legend: string; options: string[]; defaultValues: string[]; required: boolean; help: string }>): void => {
if (!isCheckbox(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
const updateTextareaProps = (patch: Partial<{ name: string; label: string; placeholder: string; rows: number; required: boolean; help: string }>): void => {
if (!isTextarea(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
return (
<div aria-label="Properties Panel">
<div>
@@ -89,6 +139,118 @@ export function PropsPanel({ object, onChange }: PropsPanelProps) {
</div>
)}
{isTextarea(model) && (
<div>
<label>
Name
<input aria-label="Name" type="text" value={model.props.name} onChange={(e) => updateTextareaProps({ name: e.target.value })} />
</label>
<label>
Label
<input aria-label="Label" type="text" value={model.props.label} onChange={(e) => updateTextareaProps({ label: e.target.value })} />
</label>
<label>
Placeholder
<input aria-label="Placeholder" type="text" value={model.props.placeholder || ''} onChange={(e) => updateTextareaProps({ placeholder: e.target.value })} />
</label>
<label>
Rows
<input aria-label="Rows" type="number" value={model.props.rows ?? 3} onChange={(e) => updateTextareaProps({ rows: Number(e.target.value) || model.props.rows })} />
</label>
<label>
Required
<input aria-label="Required" type="checkbox" checked={!!model.props.required} onChange={(e) => updateTextareaProps({ required: e.target.checked })} />
</label>
<label>
Help
<input aria-label="Help" type="text" value={model.props.help || ''} onChange={(e) => updateTextareaProps({ help: e.target.value })} />
</label>
</div>
)}
{isRadio(model) && (
<div>
<label>
Name
<input aria-label="Name" type="text" value={model.props.name} onChange={(e) => updateRadioProps({ name: e.target.value })} />
</label>
<label>
Legend
<input aria-label="Legend" type="text" value={model.props.legend} onChange={(e) => updateRadioProps({ legend: e.target.value })} />
</label>
<label>
Options
<input aria-label="Options" type="text" value={(model.props.options || []).join(',')} onChange={(e) => updateRadioProps({ options: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })} />
</label>
<label>
Default
<input aria-label="Default" type="text" value={model.props.defaultValue || ''} onChange={(e) => updateRadioProps({ defaultValue: e.target.value })} />
</label>
<label>
Required
<input aria-label="Required" type="checkbox" checked={!!model.props.required} onChange={(e) => updateRadioProps({ required: e.target.checked })} />
</label>
<label>
Help
<input aria-label="Help" type="text" value={model.props.help || ''} onChange={(e) => updateRadioProps({ help: e.target.value })} />
</label>
</div>
)}
{isCheckbox(model) && (
<div>
<label>
Name
<input aria-label="Name" type="text" value={model.props.name} onChange={(e) => updateCheckboxProps({ name: e.target.value })} />
</label>
<label>
Legend
<input aria-label="Legend" type="text" value={model.props.legend} onChange={(e) => updateCheckboxProps({ legend: e.target.value })} />
</label>
<label>
Options
<input aria-label="Options" type="text" value={(model.props.options || []).join(',')} onChange={(e) => updateCheckboxProps({ options: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })} />
</label>
<label>
Defaults
<input aria-label="Defaults" type="text" value={(model.props.defaultValues || []).join(',')} onChange={(e) => updateCheckboxProps({ defaultValues: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })} />
</label>
<label>
Required
<input aria-label="Required" type="checkbox" checked={!!model.props.required} onChange={(e) => updateCheckboxProps({ required: e.target.checked })} />
</label>
<label>
Help
<input aria-label="Help" type="text" value={model.props.help || ''} onChange={(e) => updateCheckboxProps({ help: e.target.value })} />
</label>
</div>
)}
{isSelect(model) && (
<div>
<label>
Name
<input aria-label="Name" type="text" value={model.props.name} onChange={(e) => updateSelectProps({ name: e.target.value })} />
</label>
<label>
Label
<input aria-label="Label" type="text" value={model.props.label} onChange={(e) => updateSelectProps({ label: e.target.value })} />
</label>
<label>
Options
<input aria-label="Options" type="text" value={(model.props.options || []).join(',')} onChange={(e) => updateSelectProps({ options: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })} />
</label>
<label>
Default
<input aria-label="Default" type="text" value={model.props.defaultValue || ''} onChange={(e) => updateSelectProps({ defaultValue: e.target.value })} />
</label>
<label>
Help
<input aria-label="Help" type="text" value={model.props.help || ''} onChange={(e) => updateSelectProps({ help: e.target.value })} />
</label>
</div>
)}
{isButton(model) && (
<div>
<label>
@@ -110,6 +272,31 @@ export function PropsPanel({ object, onChange }: PropsPanelProps) {
</label>
</div>
)}
{isInput(model) && (
<div>
<label>
Name
<input aria-label="Name" type="text" value={model.props.name} onChange={(e) => updateInputProps({ name: e.target.value })} />
</label>
<label>
Label
<input aria-label="Label" type="text" value={model.props.label} onChange={(e) => updateInputProps({ label: e.target.value })} />
</label>
<label>
Placeholder
<input aria-label="Placeholder" type="text" value={model.props.placeholder || ''} onChange={(e) => updateInputProps({ placeholder: e.target.value })} />
</label>
<label>
Required
<input aria-label="Required" type="checkbox" checked={!!model.props.required} onChange={(e) => updateInputProps({ required: e.target.checked })} />
</label>
<label>
Help
<input aria-label="Help" type="text" value={model.props.help || ''} onChange={(e) => updateInputProps({ help: e.target.value })} />
</label>
</div>
)}
</div>
)
}