From dc5422635185822cf1ae2cf1f227d9874e3fbf76 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Mon, 17 Nov 2025 17:44:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=ED=9A=8C=EA=B7=80=20=EA=B0=95=ED=99=94:=20focus-vi?= =?UTF-8?q?sible=20=EB=A7=81/=EC=98=A4=ED=94=84=EC=85=8B,=20disabled/reado?= =?UTF-8?q?nly=20=ED=86=A0=ED=81=B0=20=EB=B0=8F=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MAIN_PLAN.md | 10 ++++++++ .../export.style.tokens.disabled.test.ts | 23 +++++++++++++++++++ .../export.style.tokens.focusVisible.test.ts | 22 ++++++++++++++++++ lib/wysiwyg/export.ts | 9 +++++--- 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 lib/wysiwyg/export.style.tokens.disabled.test.ts create mode 100644 lib/wysiwyg/export.style.tokens.focusVisible.test.ts diff --git a/MAIN_PLAN.md b/MAIN_PLAN.md index 45a74a2..4a80f26 100644 --- a/MAIN_PLAN.md +++ b/MAIN_PLAN.md @@ -199,6 +199,16 @@ - 오류 원인/디버깅 - 없음(스냅샷은 정규식 매칭으로 안정화) +### 스타일 토큰 회귀 강화(링/비활성) (2025-11-17) +- TDD + - `lib/wysiwyg/export.style.tokens.focusVisible.test.ts`: `--ring`, `--ring-offset` 토큰 존재 및 `:focus-visible`에서 outline/offset 참조 검증 + - `lib/wysiwyg/export.style.tokens.disabled.test.ts`: `--disabled-bg`, `--disabled-fg` 토큰 존재 및 `:disabled`/`[readonly]`에서 참조 검증 +- 구현: `lib/wysiwyg/export.ts` + - :root/테마 토큰에 `--ring`, `--ring-offset`, `--disabled-bg`, `--disabled-fg` 추가 + - 규칙 추가: `:focus-visible{outline:2px solid var(--ring);outline-offset:var(--ring-offset)}` + - 규칙 추가: `input,select,textarea:disabled{background:var(--disabled-bg);color:var(--disabled-fg)}` / `[readonly]{color:var(--disabled-fg)}` +- 결과: 전체 테스트 그린(166 파일 / 291 테스트) + ### Guides/그룹 액션 완성(드래그/리사이즈/회전 + 스냅) (2025-11-17) - TDD - `components/wysiwyg/WysiwygBuilder.guides.acceptance.test.tsx`: Guides 토글 ON/OFF 표시 확인 diff --git a/lib/wysiwyg/export.style.tokens.disabled.test.ts b/lib/wysiwyg/export.style.tokens.disabled.test.ts new file mode 100644 index 0000000..dc88c46 --- /dev/null +++ b/lib/wysiwyg/export.style.tokens.disabled.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest' +import { exportFrame } from '@/lib/wysiwyg/export' +import type { Frame } from '@/lib/wysiwyg/schema' + +function baseFrame(): Frame { + return { + id: 'f', width: 320, height: 200, background: '#111111', + layers: [{ id: 'l1', name: 'Base', visible: true, locked: false, objects: [] }], + } +} + +describe('wysiwyg export - disabled/read-only control tokens', () => { + it('defines disabled tokens and uses them for :disabled and [readonly]', () => { + const out = exportFrame(baseFrame()) + // tokens + expect(out.css).toMatch(/:root\s*{[^}]*--disabled-bg:/) + expect(out.css).toMatch(/:root\s*{[^}]*--disabled-fg:/) + // usage + expect(out.css).toMatch(/input:disabled,select:disabled,textarea:disabled\s*{[^}]*background:var\(--disabled-bg\)/) + expect(out.css).toMatch(/input:disabled,select:disabled,textarea:disabled\s*{[^}]*color:var\(--disabled-fg\)/) + expect(out.css).toMatch(/input\[readonly\],select\[readonly\],textarea\[readonly\]\s*{[^}]*color:var\(--disabled-fg\)/) + }) +}) diff --git a/lib/wysiwyg/export.style.tokens.focusVisible.test.ts b/lib/wysiwyg/export.style.tokens.focusVisible.test.ts new file mode 100644 index 0000000..427e7ec --- /dev/null +++ b/lib/wysiwyg/export.style.tokens.focusVisible.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest' +import { exportFrame } from '@/lib/wysiwyg/export' +import type { Frame } from '@/lib/wysiwyg/schema' + +function baseFrame(): Frame { + return { + id: 'f', width: 320, height: 200, background: '#111111', + layers: [{ id: 'l1', name: 'Base', visible: true, locked: false, objects: [] }], + } +} + +describe('wysiwyg export - focus-visible ring tokens', () => { + it('defines ring tokens and uses them in :focus-visible styles', () => { + const out = exportFrame(baseFrame()) + // tokens + expect(out.css).toMatch(/:root\s*{[^}]*--ring:/) + expect(out.css).toMatch(/:root\s*{[^}]*--ring-offset:/) + // usage (focus-visible across common focusables and controls) + expect(out.css).toMatch(/:focus-visible\s*{[^}]*outline:2px solid var\(--ring\)/) + expect(out.css).toMatch(/:focus-visible\s*{[^}]*outline-offset:var\(--ring-offset\)/) + }) +}) diff --git a/lib/wysiwyg/export.ts b/lib/wysiwyg/export.ts index 86ec61f..302fd62 100644 --- a/lib/wysiwyg/export.ts +++ b/lib/wysiwyg/export.ts @@ -120,15 +120,18 @@ export function exportFrame(frame: Frame): Exported { } const inner = `
${piecesHtml.join('')}
` const css = [ - `: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}`, + `:root{--bg:${frame.background};--text:#f9fafb;--border:#94a3b8;--primary:#0ea5e9;--danger:#ef4444;--ring:#22c55e;--ring-offset:2px;--disabled-bg:#e5e7eb;--disabled-fg:#9ca3af}`, + `[data-theme="light"]{--bg:#ffffff;--text:#0f172a;--border:#cbd5e1;--primary:#0ea5e9;--danger:#ef4444;--ring:#22c55e;--ring-offset:2px;--disabled-bg:#e5e7eb;--disabled-fg:#6b7280}`, + `[data-theme="dark"]{--bg:#111111;--text:#f9fafb;--border:#94a3b8;--primary:#0ea5e9;--danger:#ef4444;--ring:#22c55e;--ring-offset:2px;--disabled-bg:#1f2937;--disabled-fg:#9ca3af}`, `.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)}`, + `:focus-visible{outline:2px solid var(--ring);outline-offset:var(--ring-offset)}`, `input:focus,select:focus,textarea:focus{outline:2px solid var(--primary);outline-offset:2px}`, `input:invalid,select:invalid,textarea:invalid{border-color:var(--danger)}`, + `input:disabled,select:disabled,textarea:disabled{background:var(--disabled-bg);color:var(--disabled-fg)}`, + `input[readonly],select[readonly],textarea[readonly]{color:var(--disabled-fg)}`, `fieldset{border:1px solid var(--border)}`, `legend{color:var(--text)}`, ...piecesCss,