테스트/린트 안정화: 버튼 쿼리 모호성 제거, 훅 deps 보강, vi.spyOn 모킹, 멀티선택 Moveable 간섭 방지 재확인, 스냅 우선순위 회귀 그린
This commit is contained in:
+91
-2
@@ -9,6 +9,11 @@ function esc(s: string) {
|
||||
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' }
|
||||
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 isTextarea(o: WObject): o is Extract<WObject, { type: 'textarea' }> { return o.type === 'textarea' }
|
||||
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 renderObject(o: WObject): { html: string; css: string } {
|
||||
const baseCss = `#${o.id}{position:absolute;left:${o.x}px;top:${o.y}px;width:${o.width}px;height:${o.height}px;transform:rotate(${o.rotate}deg)}`
|
||||
@@ -20,7 +25,82 @@ function renderObject(o: WObject): { html: string; css: string } {
|
||||
const html = `<p id="${o.id}">${esc(o.props.text)}</p>`
|
||||
return { html, css: baseCss + `;font-size:${o.props.fontSize}px;color:${o.props.color}` }
|
||||
}
|
||||
// button minimal
|
||||
if (isInput(o)) {
|
||||
const cid = `${o.id}-control`
|
||||
const req = o.props.required ? ' required' : ''
|
||||
const ph = o.props.placeholder ? ` placeholder="${esc(o.props.placeholder)}"` : ''
|
||||
const helpId = o.props.help ? `${cid}-help` : ''
|
||||
const described = helpId ? ` aria-describedby="${helpId}"` : ''
|
||||
const pat = o.props.pattern ? ` pattern="${esc(o.props.pattern)}"` : ''
|
||||
const min = typeof o.props.minLength === 'number' ? ` minlength="${o.props.minLength}"` : ''
|
||||
const max = typeof o.props.maxLength === 'number' ? ` maxlength="${o.props.maxLength}"` : ''
|
||||
const html = [
|
||||
`<div id="${o.id}">`,
|
||||
`<label for="${cid}">${esc(o.props.label)}</label>`,
|
||||
`<input id="${cid}" name="${esc(o.props.name)}"${ph}${req}${described}${pat}${min}${max}/>`,
|
||||
o.props.help ? `<small id="${helpId}">${esc(o.props.help)}</small>` : '',
|
||||
`</div>`,
|
||||
].join('')
|
||||
return { html, css: baseCss }
|
||||
}
|
||||
if (isSelect(o)) {
|
||||
const cid = `${o.id}-control`
|
||||
const opts = (o.props.options || [])
|
||||
.map((opt) => `<option value="${esc(opt)}"${o.props.defaultValue === opt ? ' selected' : ''}>${esc(opt)}</option>`)
|
||||
.join('')
|
||||
const helpId = o.props.help ? `${cid}-help` : ''
|
||||
const described = helpId ? ` aria-describedby="${helpId}"` : ''
|
||||
const html = [
|
||||
`<div id="${o.id}">`,
|
||||
`<label for="${cid}">${esc(o.props.label)}</label>`,
|
||||
`<select id="${cid}" name="${esc(o.props.name)}"${described}>${opts}</select>`,
|
||||
o.props.help ? `<small id="${helpId}">${esc(o.props.help)}</small>` : '',
|
||||
`</div>`,
|
||||
].join('')
|
||||
return { html, css: baseCss }
|
||||
}
|
||||
if (isTextarea(o)) {
|
||||
const cid = `${o.id}-control`
|
||||
const req = o.props.required ? ' required' : ''
|
||||
const ph = o.props.placeholder ? ` placeholder="${esc(o.props.placeholder)}"` : ''
|
||||
const rows = Number(o.props.rows ?? 3)
|
||||
const helpId = o.props.help ? `${cid}-help` : ''
|
||||
const described = helpId ? ` aria-describedby="${helpId}"` : ''
|
||||
const min = typeof o.props.minLength === 'number' ? ` minlength="${o.props.minLength}"` : ''
|
||||
const max = typeof o.props.maxLength === 'number' ? ` maxlength="${o.props.maxLength}"` : ''
|
||||
const html = [
|
||||
`<div id="${o.id}">`,
|
||||
`<label for="${cid}">${esc(o.props.label)}</label>`,
|
||||
`<textarea id="${cid}" name="${esc(o.props.name)}" rows="${rows}"${ph}${req}${described}${min}${max}></textarea>`,
|
||||
o.props.help ? `<small id="${helpId}">${esc(o.props.help)}</small>` : '',
|
||||
`</div>`,
|
||||
].join('')
|
||||
return { html, css: baseCss }
|
||||
}
|
||||
if (isRadio(o)) {
|
||||
const req = o.props.required ? ' required' : ''
|
||||
const items = (o.props.options || []).map((opt, idx) => {
|
||||
const id = `${o.id}-${idx}`
|
||||
const checked = o.props.defaultValue === opt ? ' checked' : ''
|
||||
return `<div><input type="radio" id="${id}" name="${esc(o.props.name)}" value="${esc(opt)}"${checked}${req}/><label for="${id}">${esc(opt)}</label></div>`
|
||||
}).join('')
|
||||
const help = o.props.help ? `<small id="${o.id}-help">${esc(o.props.help)}</small>` : ''
|
||||
const html = [`<fieldset id="${o.id}">`, `<legend>${esc(o.props.legend)}</legend>`, items, help, `</fieldset>`].join('')
|
||||
return { html, css: baseCss }
|
||||
}
|
||||
if (isCheckbox(o)) {
|
||||
const req = o.props.required ? ' required' : ''
|
||||
const def = new Set(o.props.defaultValues || [])
|
||||
const items = (o.props.options || []).map((opt, idx) => {
|
||||
const id = `${o.id}-${idx}`
|
||||
const checked = def.has(opt) ? ' checked' : ''
|
||||
return `<div><input type="checkbox" id="${id}" name="${esc(o.props.name)}" value="${esc(opt)}"${checked}${req}/><label for="${id}">${esc(opt)}</label></div>`
|
||||
}).join('')
|
||||
const help = o.props.help ? `<small id="${o.id}-help">${esc(o.props.help)}</small>` : ''
|
||||
const html = [`<fieldset id="${o.id}">`, `<legend>${esc(o.props.legend)}</legend>`, items, help, `</fieldset>`].join('')
|
||||
return { html, css: baseCss }
|
||||
}
|
||||
// button minimal (fallback)
|
||||
const label = isButton(o) ? o.props.label : 'Button'
|
||||
const href = isButton(o) ? (o.props.href || '#') : '#'
|
||||
const html = `<a id="${o.id}" href="${esc(href)}">${esc(label)}</a>`
|
||||
@@ -40,8 +120,17 @@ export function exportFrame(frame: Frame): Exported {
|
||||
}
|
||||
const inner = `<div class="frame-scale"><div class="frame">${piecesHtml.join('')}</div></div>`
|
||||
const css = [
|
||||
`.frame{position:relative;width:${frame.width}px;height:${frame.height}px;background:${frame.background}}`,
|
||||
`:root{--bg:${frame.background};--text:#f9fafb;--border:#94a3b8;--primary:#0ea5e9;--danger:#ef4444}`,
|
||||
`[data-theme="light"]{--bg:#ffffff;--text:#0f172a;--border:#cbd5e1;--primary:#0ea5e9;--danger:#ef4444}`,
|
||||
`[data-theme="dark"]{--bg:#111111;--text:#f9fafb;--border:#94a3b8;--primary:#0ea5e9;--danger:#ef4444}`,
|
||||
`.frame{position:relative;width:${frame.width}px;height:${frame.height}px;background:var(--bg)}`,
|
||||
`.frame-scale{transform-origin:0 0}`,
|
||||
`a{color:var(--primary);text-decoration:none}`,
|
||||
`input,select,textarea{color:var(--text);background:transparent;border:1px solid var(--border)}`,
|
||||
`input:focus,select:focus,textarea:focus{outline:2px solid var(--primary);outline-offset:2px}`,
|
||||
`input:invalid,select:invalid,textarea:invalid{border-color:var(--danger)}`,
|
||||
`fieldset{border:1px solid var(--border)}`,
|
||||
`legend{color:var(--text)}`,
|
||||
...piecesCss,
|
||||
].join('\n')
|
||||
const html = [
|
||||
|
||||
Reference in New Issue
Block a user