@su-record/vibe 2.8.1 → 2.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1514 @@
1
+ ---
2
+ description: Figma design to code — extract + generate in one step
3
+ argument-hint: "figma-url" ["figma-url-2"] [--standalone]
4
+ ---
5
+
6
+ # /vibe.figma
7
+
8
+ Extract Figma design data and generate production-ready component code, tailored to the project's tech stack.
9
+
10
+ ## Usage
11
+
12
+ ```
13
+ /vibe.figma "url" # Single design → project integrated (default)
14
+ /vibe.figma "mobile-url" "desktop-url" # Responsive — auto-detect viewport from frame width
15
+ /vibe.figma "url" --standalone # Self-contained output folder
16
+ /vibe.figma "url" --component LoginForm # Name the root component
17
+ /vibe.figma --local # Skip extraction, use existing figma-output/
18
+ ```
19
+
20
+ ### Generation Mode
21
+
22
+ | Flag | Behavior |
23
+ |------|----------|
24
+ | _(default)_ | **Project integration.** Use project's design system, existing tokens, component patterns. Place files in project's component directory. |
25
+ | `--standalone` | **Independent folder.** Create self-contained folder with own global styles, tokens, and components. No dependency on project's existing styles. Ready to copy-paste into any project. |
26
+ | _(multi URL)_ | **Responsive mode.** Auto-detected when 2+ URLs provided. Compares designs across viewports and generates responsive code with fluid scaling. |
27
+
28
+ ### Responsive Mode
29
+
30
+ When multiple URLs are provided:
31
+ 1. CLI extracts each URL → `figma-output/` with numbered files (`layers.1.json`, `frame.1.png`, etc.)
32
+ 2. Frame width auto-detects viewport: ≤480px = mobile, ≤1024px = tablet, >1024px = desktop
33
+ 3. `responsive.json` manifest maps each viewport to its files
34
+ 4. Code generation produces a **single component** with breakpoint-aware styles
35
+
36
+ ## File Reading Policy (Mandatory)
37
+
38
+ ### Single design
39
+ - **Screenshot first**: `get_design_context` 결과의 스크린샷을 먼저 분석
40
+ - **Then metadata**: `get_metadata` 결과로 레이어 구조 파악
41
+
42
+ ### Responsive mode (multiple URLs)
43
+ - **All screenshots**: 각 URL의 `get_screenshot` 결과를 side-by-side 비교
44
+ - **All metadata**: 각 URL의 `get_metadata` 결과에서 per-viewport 토큰 추출
45
+
46
+ ### Always
47
+ - **Project config**: Read `.claude/vibe/config.json` to determine tech stack
48
+ - **Design context**: Read `.claude/vibe/design-context.json` if it exists (brand, tokens, theme)
49
+ - **Existing code**: Scan project for existing component patterns, theme config, design system
50
+
51
+ ## Context Reset
52
+
53
+ **When this command runs, previous conversation is ignored.**
54
+ - Start fresh from the extracted Figma data
55
+ - Base all decisions on the design image + layer data + project stack
56
+
57
+ ---
58
+
59
+ > **⏱️ Timer**: Call `getCurrentTime` tool at the START. Record the result as `{start_time}`.
60
+
61
+ ## Phase 0: Figma Data Extraction
62
+
63
+ **Skip this phase if `--local` flag is provided and `figma-output/` already exists.**
64
+
65
+ ### 0-1. Extract via MCP (토큰 불필요)
66
+
67
+ Figma MCP 플러그인이 자체 인증을 처리하므로 별도 토큰 설정이 필요 없음.
68
+
69
+ **URL에서 fileKey와 nodeId 추출:**
70
+
71
+ ```
72
+ https://www.figma.com/design/:fileKey/:fileName?node-id=:nodeId
73
+ → fileKey, nodeId (하이픈을 콜론으로: "110-6231" → "110:6231")
74
+ ```
75
+
76
+ **Single URL:**
77
+
78
+ ```
79
+ 1. get_design_context(fileKey, nodeId) → 코드 + 스크린샷 + 메타데이터
80
+ 2. get_metadata(fileKey, nodeId) → 레이어 구조 (XML)
81
+ 3. get_screenshot(fileKey, nodeId) → 프레임 이미지
82
+ ```
83
+
84
+ **Multiple URLs (responsive mode):**
85
+
86
+ ```
87
+ 각 URL에 대해 위 과정 반복, 결과를 뷰포트별로 구분하여 분석.
88
+ 프레임 width로 mobile/tablet/desktop 자동 감지.
89
+ ```
90
+
91
+ ### 0-3. Verify Output & Detect Mode
92
+
93
+ ```
94
+ 1. Check figma-output/responsive.json exists → if yes, enter RESPONSIVE MODE
95
+ 2. If responsive mode:
96
+ - Read responsive.json → verify all listed files exist
97
+ - Confirm at least 2 viewports with different width classes
98
+ 3. If single mode:
99
+ - Check figma-output/layers.json exists → if not, report error and stop
100
+ - Check figma-output/frame.png exists → optional (only with node-id)
101
+ 4. Validate all layers JSON files have children array → warn if empty
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Phase 1: Design Analysis (Image-First)
107
+
108
+ ### Single mode
109
+
110
+ Read `figma-output/frame.png` and analyze:
111
+
112
+ | Aspect | What to Extract |
113
+ |--------|-----------------|
114
+ | Layout | Flex/Grid direction, alignment, wrapping |
115
+ | Components | Visual boundaries (cards, buttons, inputs, modals) |
116
+ | Spacing | Padding, margins, gaps between elements |
117
+ | Typography | Font sizes, weights, line heights, hierarchy |
118
+ | Colors | Background, text, border, accent colors |
119
+ | States | Hover/active/disabled indicators if visible |
120
+ | Responsive hints | Breakpoint indicators, fluid vs fixed widths |
121
+
122
+ ### Responsive mode
123
+
124
+ Read **ALL** frame images and analyze **side-by-side**:
125
+
126
+ | Aspect | What to Compare |
127
+ |--------|-----------------|
128
+ | Layout shift | Which elements reflow? (e.g., horizontal → vertical stack) |
129
+ | Visibility | Which elements hide/show per viewport? |
130
+ | Typography scale | Font size ratio between viewports |
131
+ | Spacing scale | Padding/gap ratio between viewports |
132
+ | Component shape | Does the component change form? (e.g., drawer → sidebar) |
133
+ | Navigation | Does nav change? (e.g., hamburger ↔ full nav bar) |
134
+
135
+ Build a **viewport diff table**:
136
+
137
+ ```
138
+ | Element | Mobile (375px) | Desktop (1440px) | Strategy |
139
+ |---------------|------------------------|------------------------|-------------------|
140
+ | Nav | hamburger + drawer | horizontal bar | component swap |
141
+ | Hero title | 24px | 48px | fluid: clamp() |
142
+ | Card grid | 1 column | 3 columns | grid auto-fit |
143
+ | Sidebar | hidden | visible | display toggle |
144
+ | Body text | 14px | 16px | fluid: clamp() |
145
+ | Padding | 16px | 48px | fluid: clamp() |
146
+ ```
147
+
148
+ ## Phase 2: Layer Data Extraction
149
+
150
+ ### Single mode
151
+
152
+ Read `figma-output/layers.json` and extract:
153
+
154
+ 1. **Component hierarchy** — Map nested layers to component tree
155
+ 2. **Design tokens** — Colors (fill, stroke), font properties, spacing values, border radius, shadows
156
+ 3. **Auto-layout** — Direction, gap, padding (maps directly to flex/grid)
157
+ 4. **Constraints** — Fixed vs fluid sizing
158
+ 5. **Component instances** — Identify reusable patterns
159
+ 6. **Image fills** — Identify layers with `type: "IMAGE"` fills (see Phase 2-A)
160
+
161
+ ### Responsive mode
162
+
163
+ Read **ALL** `layers.{N}.json` files and extract **per-viewport**:
164
+
165
+ 1. **Per-viewport tokens** — Record exact values for each viewport:
166
+ ```
167
+ { "mobile": { "h1": 24, "body": 14, "padding": 16 },
168
+ "desktop": { "h1": 48, "body": 16, "padding": 48 } }
169
+ ```
170
+ 2. **Layout differences** — Auto-layout direction changes (e.g., HORIZONTAL → VERTICAL)
171
+ 3. **Visibility map** — Which layers exist in one viewport but not the other
172
+ 4. **Shared tokens** — Values identical across all viewports (colors, border-radius, shadows are usually shared)
173
+
174
+ ### Phase 2-A: Image Fill Classification
175
+
176
+ Figma에서 이미지는 레이어의 `fills` 배열에 `type: "IMAGE"`로 들어옴. 이를 **용도별로 분류**해야 코드에서 올바른 패턴을 생성할 수 있음.
177
+
178
+ #### 감지 방법
179
+
180
+ `layers.json`에서 아래 패턴을 탐색:
181
+
182
+ ```
183
+ fills: [{ type: "IMAGE", scaleMode: "FILL" | "FIT" | "CROP" | "TILE", imageRef: "..." }]
184
+ ```
185
+
186
+ #### 분류 기준
187
+
188
+ | 판별 조건 | 분류 | 코드 패턴 |
189
+ |----------|------|----------|
190
+ | 레이어가 프레임/섹션의 **직계 배경**이고, 위에 텍스트/UI 요소가 겹침 | **Background Image** | `background-image` + `background-size` |
191
+ | 레이어가 독립적이고, 위에 겹치는 요소 없음 | **Content Image** | `<img>` 또는 `<picture>` |
192
+ | 레이어 이름에 `icon`, `logo`, `avatar` 포함 | **Inline Asset** | `<img>` (작은 크기) |
193
+ | 레이어가 반복 패턴(`scaleMode: "TILE"`) | **Pattern/Texture** | `background-image` + `background-repeat` |
194
+ | 레이어가 전체 프레임을 덮고 opacity < 1 또는 blendMode 적용 | **Overlay Image** | `background-image` + overlay `::before`/`::after` |
195
+
196
+ #### 이미지-텍스트 겹침 판별 (Background vs Content)
197
+
198
+ ```
199
+ frame의 fills에 IMAGE가 있고, children에 TEXT 레이어가 있으면:
200
+ → Background Image (텍스트 아래 깔리는 배경)
201
+
202
+ 독립 레이어의 fills에 IMAGE가 있고, 형제 레이어와 겹치지 않으면:
203
+ → Content Image
204
+ ```
205
+
206
+ #### 이미지 소스 추출
207
+
208
+ - `imageRef` 값으로 Figma API의 이미지 렌더링 사용 (`/images/{fileKey}`)
209
+ - 추출된 이미지는 `figma-output/assets/` 디렉토리에 저장
210
+ - 파일명은 레이어 이름 기반: `hero-bg.png`, `product-photo.jpg`
211
+
212
+ ### Responsive Scaling Calculation
213
+
214
+ Per-viewport 토큰 쌍에서 clamp() 값을 계산. 공식은 **Phase 4-3** 참조.
215
+
216
+ 핵심: Figma 디자인이 2x 스케일(2560px/720px)이므로, 반드시 타겟 해상도(1920px/480px)로 환산 후 clamp를 계산해야 함.
217
+
218
+ **Correction rule**: When image and JSON disagree, **image wins**. The image shows designer intent; JSON may have structural artifacts.
219
+
220
+ ## Phase 3: Project Stack Detection + Mode Resolution
221
+
222
+ ### 3-1. Detect Stack
223
+
224
+ 1. Read `.claude/vibe/config.json` → check `stacks` field
225
+ 2. If no config, detect from project files:
226
+ - `package.json` → React, Vue, Svelte, Angular, etc.
227
+ - `tailwind.config.*` → Tailwind CSS
228
+ - `next.config.*` → Next.js
229
+ - `nuxt.config.*` → Nuxt
230
+ - `*.module.css` → CSS Modules pattern
231
+ - `*.scss` / `sass` in deps → SCSS
232
+ - `styled-components` / `@emotion` in deps → CSS-in-JS
233
+
234
+ ### 3-2. Load Design Context (Design Skill Integration)
235
+
236
+ Read these files **in order** — later sources override earlier ones:
237
+
238
+ 1. **`.claude/vibe/design-context.json`** — brand personality, aesthetic direction, constraints
239
+ - `aesthetic.style` → guides visual weight (minimal vs bold)
240
+ - `aesthetic.colorMood` → warm/cool/vibrant tone for token selection
241
+ - `brand.personality` → preserve brand-expressive elements
242
+ - `constraints.accessibility` → AA or AAA level (affects contrast, focus, ARIA depth)
243
+ - `constraints.devices` → responsive breakpoints priority
244
+ 2. **`.claude/vibe/design-system/{project}/MASTER.md`** — authoritative token definitions
245
+ - If MASTER.md exists: **map Figma tokens to MASTER.md tokens first**, only create new tokens for values with no match
246
+ - If no MASTER.md: generate `figma-tokens.css` as standalone token source
247
+
248
+ **Decision rule**: When Figma token ≈ existing MASTER.md token (within 10% color distance or ±2px spacing), **use the existing token** — do not duplicate.
249
+
250
+ ### 3-3. Load Breakpoints
251
+
252
+ Breakpoints are loaded from multiple sources in priority order:
253
+
254
+ #### Source Priority
255
+
256
+ | Priority | Source | How |
257
+ |----------|--------|-----|
258
+ | 1 | **`~/.vibe/config.json`** | `figma.breakpoints` — user-customized via `vibe figma breakpoints --set` |
259
+ | 2 | **Project CSS/Tailwind** | Grep `tailwind.config.*` → `theme.screens`, or `@media.*min-width` patterns in codebase |
260
+ | 3 | **`responsive.json`** | Breakpoints embedded by CLI extract (from config at extraction time) |
261
+ | 4 | **Defaults** | Built-in values (breakpoint: 1024px, etc.) |
262
+
263
+ #### Default Breakpoints (built-in)
264
+
265
+ Based on game industry responsive storyboard standards:
266
+
267
+ ```
268
+ breakpoint: 1024px ← PC↔Mobile boundary (@media min-width)
269
+ pcTarget: 1920px ← PC main target resolution
270
+ mobilePortrait: 480px ← Mobile portrait max width
271
+ mobileMinimum: 360px ← Mobile minimum supported width
272
+ designPc: 2560px ← Figma PC artboard width (design is 2x scale)
273
+ designMobile: 720px ← Figma Mobile artboard width (design is 2x scale)
274
+ ```
275
+
276
+ #### How to use in code generation
277
+
278
+ ```
279
+ @media breakpoint:
280
+ - Single breakpoint model: @media (min-width: {breakpoint}px)
281
+ - Mobile-first: styles below breakpoint = mobile, above = PC
282
+
283
+ clamp() range:
284
+ - minVw = mobileMinimum (360px) — smallest supported viewport
285
+ - maxVw = pcTarget (1920px) — largest target viewport
286
+ - Values scale linearly between these bounds
287
+
288
+ Design scale factor:
289
+ - PC design at {designPc}px targets {pcTarget}px → scale = pcTarget/designPc
290
+ - Mobile design at {designMobile}px targets {mobilePortrait}px → scale = mobilePortrait/designMobile
291
+ - Apply scale to convert Figma pixel values to target pixel values
292
+ ```
293
+
294
+ #### User customization
295
+
296
+ Users can override any value via CLI:
297
+
298
+ ```bash
299
+ vibe figma breakpoints # Show current values
300
+ vibe figma breakpoints --set breakpoint=768 # Change PC↔Mobile boundary
301
+ vibe figma breakpoints --set mobileMinimum=320 # Change mobile minimum
302
+ ```
303
+
304
+ Stored in `~/.vibe/config.json` → `figma.breakpoints`. Partial overrides merge with defaults.
305
+
306
+ ### 3-4. Resolve Generation Mode
307
+
308
+ ```
309
+ if --standalone flag:
310
+ → create isolated output folder (default: figma-output/generated/)
311
+ → generate self-contained global styles + component styles
312
+ → no dependency on project's existing code
313
+
314
+ default (no flag):
315
+ → scan existing component directories, theme files, token definitions
316
+ → map output to project's conventions (file location, naming, imports)
317
+ → add only NEW tokens that don't exist yet
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Phase 4: Style Architecture
323
+
324
+ ### 4-1. Global Styles File
325
+
326
+ **Token resolution priority** (default mode):
327
+
328
+ 1. **MASTER.md tokens** — if `.claude/vibe/design-system/{project}/MASTER.md` exists, map Figma values to these tokens
329
+ 2. **design-context.json tokens** — if `detectedStack.fonts`, `aesthetic.colorMood` exist, align with these
330
+ 3. **New figma-tokens** — only for values that have no existing match
331
+
332
+ **Standalone mode**: Always generate a self-contained token file (no MASTER.md dependency).
333
+
334
+ #### --standalone mode output:
335
+
336
+ ```
337
+ figma-output/generated/
338
+ ├── styles/
339
+ │ ├── tokens.css ← CSS custom properties (colors, spacing, typography, shadows)
340
+ │ ├── global.css ← Reset + base typography + global layout
341
+ │ └── index.css ← Re-exports tokens.css + global.css
342
+ ├── components/
343
+ │ ├── ComponentName/
344
+ │ │ ├── ComponentName.tsx ← Component code
345
+ │ │ └── ComponentName.module.css (or .styles.ts)
346
+ │ └── ...
347
+ └── index.ts ← Barrel export
348
+ ```
349
+
350
+ #### Default (project integration) mode output:
351
+
352
+ ```
353
+ {project-component-dir}/ ← e.g., src/components/
354
+ ├── ComponentName/
355
+ │ ├── ComponentName.tsx
356
+ │ └── ComponentName.module.css (or .styles.ts)
357
+ └── ...
358
+
359
+ {project-style-dir}/ ← e.g., src/styles/ or extend existing
360
+ └── figma-tokens.css ← Only NEW tokens not already in project
361
+ ```
362
+
363
+ ### 4-2. Token File Format
364
+
365
+ **CSS Custom Properties (default):**
366
+
367
+ ```css
368
+ /* figma-tokens.css — Auto-generated from Figma. Do not edit manually. */
369
+ /* Source: https://www.figma.com/design/{fileKey} */
370
+
371
+ :root {
372
+ /* Colors */
373
+ --figma-primary: #3B82F6;
374
+ --figma-primary-hover: #2563EB;
375
+ --figma-surface: #FFFFFF;
376
+ --figma-surface-secondary: #F9FAFB;
377
+ --figma-text-primary: #111827;
378
+ --figma-text-secondary: #6B7280;
379
+ --figma-border: #E5E7EB;
380
+
381
+ /* Typography */
382
+ --figma-font-family: 'Inter', system-ui, sans-serif;
383
+ --figma-text-xs: 0.75rem; /* 12px */
384
+ --figma-text-sm: 0.875rem; /* 14px */
385
+ --figma-text-base: 1rem; /* 16px */
386
+ --figma-text-lg: 1.125rem; /* 18px */
387
+ --figma-text-xl: 1.25rem; /* 20px */
388
+ --figma-leading-tight: 1.25;
389
+ --figma-leading-normal: 1.5;
390
+
391
+ /* Spacing */
392
+ --figma-space-1: 0.25rem; /* 4px */
393
+ --figma-space-2: 0.5rem; /* 8px */
394
+ --figma-space-3: 0.75rem; /* 12px */
395
+ --figma-space-4: 1rem; /* 16px */
396
+ --figma-space-6: 1.5rem; /* 24px */
397
+ --figma-space-8: 2rem; /* 32px */
398
+
399
+ /* Shadows */
400
+ --figma-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
401
+ --figma-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
402
+
403
+ /* Border Radius */
404
+ --figma-radius-sm: 0.25rem; /* 4px */
405
+ --figma-radius-md: 0.5rem; /* 8px */
406
+ --figma-radius-lg: 0.75rem; /* 12px */
407
+ --figma-radius-full: 9999px;
408
+ }
409
+ ```
410
+
411
+ **Tailwind extend (if Tailwind detected):**
412
+
413
+ ```js
414
+ // figma.config.ts — merge into tailwind.config.ts theme.extend
415
+ export const figmaTokens = {
416
+ colors: {
417
+ figma: {
418
+ primary: '#3B82F6',
419
+ 'primary-hover': '#2563EB',
420
+ // ...
421
+ },
422
+ },
423
+ spacing: { /* ... */ },
424
+ borderRadius: { /* ... */ },
425
+ };
426
+ ```
427
+
428
+ **SCSS (if `*.scss` or `sass` detected):**
429
+
430
+ ```scss
431
+ // _figma-tokens.scss — Auto-generated from Figma. Do not edit manually.
432
+
433
+ // ── Variables ──
434
+ $figma-primary: #3B82F6;
435
+ $figma-primary-hover: #2563EB;
436
+ $figma-surface: #FFFFFF;
437
+ $figma-text-primary: #111827;
438
+ $figma-text-secondary: #6B7280;
439
+ $figma-border: #E5E7EB;
440
+
441
+ $figma-font-family: 'Inter', system-ui, sans-serif;
442
+ $figma-text-xs: 0.75rem;
443
+ $figma-text-sm: 0.875rem;
444
+ $figma-text-base: 1rem;
445
+ $figma-text-lg: 1.125rem;
446
+ $figma-text-xl: 1.25rem;
447
+
448
+ $figma-space-1: 0.25rem;
449
+ $figma-space-2: 0.5rem;
450
+ $figma-space-4: 1rem;
451
+ $figma-space-6: 1.5rem;
452
+ $figma-space-8: 2rem;
453
+
454
+ $figma-radius-sm: 0.25rem;
455
+ $figma-radius-md: 0.5rem;
456
+ $figma-radius-lg: 0.75rem;
457
+
458
+ $figma-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
459
+ $figma-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
460
+
461
+ // ── Breakpoints ──
462
+ $figma-bp: 1024px;
463
+ $figma-bp-mobile-min: 360px;
464
+ $figma-bp-pc-target: 1920px;
465
+
466
+ // ── Mixins ──
467
+ @mixin figma-pc {
468
+ @media (min-width: $figma-bp) { @content; }
469
+ }
470
+
471
+ @mixin figma-mobile-only {
472
+ @media (max-width: $figma-bp - 1px) { @content; }
473
+ }
474
+
475
+ // ── Functions ──
476
+ @function figma-fluid($mobile, $desktop, $min-vw: $figma-bp-mobile-min, $max-vw: $figma-bp-pc-target) {
477
+ $slope: ($desktop - $mobile) / ($max-vw - $min-vw);
478
+ $intercept: $mobile - $slope * $min-vw;
479
+ @return clamp(#{$mobile}, #{$intercept} + #{$slope * 100}vw, #{$desktop});
480
+ }
481
+
482
+ // Usage: font-size: figma-fluid(1rem, 2rem);
483
+ ```
484
+
485
+ **SCSS 사용 시 추가 규칙:**
486
+ - CSS custom properties 대신 `$변수` 사용 (프로젝트 컨벤션에 따라 둘 다 가능)
487
+ - `@mixin figma-pc` 로 breakpoint 일관성 유지 — `@media` 직접 사용 금지
488
+ - `figma-fluid()` 함수로 clamp() 계산 자동화 — 수동 계산 금지
489
+ - 파일명: `_figma-tokens.scss` (partial, `_` prefix)
490
+ - `@use 'figma-tokens' as figma;` 로 네임스페이스 import
491
+
492
+ ### 4-3. Responsive Token Format (responsive mode only)
493
+
494
+ When `responsive.json` exists, tokens that **differ across viewports** use `clamp()` for fluid scaling.
495
+ Tokens that are **identical** across viewports remain static.
496
+
497
+ **clamp() range uses breakpoints from Phase 3-3:**
498
+
499
+ ```
500
+ minVw = mobileMinimum (default: 360px)
501
+ maxVw = pcTarget (default: 1920px)
502
+ breakpoint = breakpoint (default: 1024px) ← used for @media
503
+
504
+ Design values must be scaled before clamp:
505
+ PC Figma value × (pcTarget / designPc) = target PC value
506
+ Mobile Figma value × (mobilePortrait / designMobile) = target mobile value
507
+ ```
508
+
509
+ **CSS Custom Properties (responsive):**
510
+
511
+ ```css
512
+ /* figma-tokens.css — Responsive tokens from Figma */
513
+ /* clamp range: {mobileMinimum}px → {pcTarget}px */
514
+ /* Breakpoint: {breakpoint}px (PC↔Mobile) */
515
+ /* Design scale: PC {designPc}→{pcTarget}, Mobile {designMobile}→{mobilePortrait} */
516
+
517
+ :root {
518
+ /* === Shared (same across all viewports) === */
519
+ --figma-primary: #3B82F6;
520
+ --figma-font-family: 'Inter', system-ui, sans-serif;
521
+ --figma-radius-md: 0.5rem;
522
+ --figma-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
523
+
524
+ /* === Fluid Typography (scales with viewport) === */
525
+ /* Figma PC 96px → target 36px, Figma Mobile 48px → target 32px */
526
+ --figma-text-h1: clamp(2rem, {intercept}rem + {slope}vw, 2.25rem);
527
+ --figma-text-body: clamp(0.875rem, {intercept}rem + {slope}vw, 1rem);
528
+
529
+ /* === Fluid Spacing (scales with viewport) === */
530
+ --figma-space-section: clamp(1rem, {intercept}rem + {slope}vw, 3rem);
531
+ --figma-space-content: clamp(0.75rem, {intercept}rem + {slope}vw, 1.5rem);
532
+
533
+ /* === Breakpoint (from config, user-customizable) === */
534
+ --figma-bp: 1024px;
535
+ }
536
+ ```
537
+
538
+ **clamp() calculation formula:**
539
+
540
+ ```
541
+ Step 1: Scale Figma values to target viewport
542
+ targetMobile = figmaMobileValue × (mobilePortrait / designMobile)
543
+ targetPc = figmaPcValue × (pcTarget / designPc)
544
+
545
+ Example (defaults): Figma PC h1=96px, Figma Mobile h1=48px
546
+ targetPc = 96 × (1920 / 2560) = 72px
547
+ targetMobile = 48 × (480 / 720) = 32px
548
+
549
+ Step 2: Calculate clamp()
550
+ minVw = mobileMinimum (360)
551
+ maxVw = pcTarget (1920)
552
+ min = targetMobile, max = targetPc
553
+
554
+ slope = (max - min) / (maxVw - minVw)
555
+ intercept = min - slope * minVw
556
+ → clamp({min/16}rem, {intercept/16}rem + {slope*100}vw, {max/16}rem)
557
+
558
+ Example:
559
+ slope = (72 - 32) / (1920 - 360) = 0.02564
560
+ intercept = 32 - 0.02564 × 360 = 22.77
561
+ → clamp(2rem, 1.423rem + 2.564vw, 4.5rem)
562
+ ```
563
+
564
+ **Tailwind (responsive — if Tailwind detected):**
565
+
566
+ Use Tailwind's responsive prefixes instead of clamp() for layout, clamp() for typography/spacing:
567
+
568
+ ```js
569
+ export const figmaTokens = {
570
+ fontSize: {
571
+ 'figma-h1': ['clamp(1.5rem, 1.076rem + 1.878vw, 3rem)', { lineHeight: '1.2' }],
572
+ 'figma-body': ['clamp(0.875rem, 0.828rem + 0.188vw, 1rem)', { lineHeight: '1.5' }],
573
+ },
574
+ spacing: {
575
+ 'figma-section': 'clamp(1rem, 0.248rem + 3.286vw, 3rem)',
576
+ },
577
+ };
578
+ ```
579
+
580
+ **SCSS (responsive):**
581
+
582
+ ```scss
583
+ // _figma-tokens.scss — figma-fluid() 함수로 자동 계산
584
+ @use 'sass:math';
585
+
586
+ $figma-bp: 1024px;
587
+ $figma-bp-mobile-min: 360px;
588
+ $figma-bp-pc-target: 1920px;
589
+
590
+ @function figma-fluid($mobile, $desktop, $min-vw: $figma-bp-mobile-min, $max-vw: $figma-bp-pc-target) {
591
+ $slope: math.div($desktop - $mobile, $max-vw - $min-vw);
592
+ $intercept: $mobile - $slope * $min-vw;
593
+ @return clamp(#{$mobile}, #{$intercept} + #{$slope * 100}vw, #{$desktop});
594
+ }
595
+
596
+ @mixin figma-pc { @media (min-width: $figma-bp) { @content; } }
597
+
598
+ // Token 사용
599
+ $figma-text-h1: figma-fluid(2rem, 4.5rem);
600
+ $figma-text-body: figma-fluid(0.875rem, 1rem);
601
+ $figma-space-section: figma-fluid(1rem, 3rem);
602
+ ```
603
+
604
+ ```scss
605
+ // Component.module.scss — 사용 예시
606
+ @use 'figma-tokens' as figma;
607
+
608
+ .heroSection {
609
+ padding: figma.$figma-space-section;
610
+ }
611
+
612
+ .heroTitle {
613
+ font-size: figma.$figma-text-h1;
614
+ }
615
+
616
+ .cardGrid {
617
+ display: grid;
618
+ grid-template-columns: 1fr;
619
+
620
+ @include figma.figma-pc {
621
+ grid-template-columns: repeat(3, 1fr);
622
+ }
623
+ }
624
+ ```
625
+
626
+ ### 4-4. Class Naming Rules
627
+
628
+ 클래스 이름은 **역할(role)**을 드러내야 하며, 구조나 스타일 속성을 이름에 넣지 않는다.
629
+
630
+ #### 네이밍 원칙
631
+
632
+ | 원칙 | 좋은 예 | 나쁜 예 |
633
+ |------|--------|--------|
634
+ | **역할 기반** | `.heroSection`, `.productCard`, `.navPrimary` | `.section1`, `.card`, `.nav` |
635
+ | **용도 명시** | `.heroBg`, `.cardThumbnail`, `.avatarImg` | `.bg`, `.img`, `.image1` |
636
+ | **상태 포함** | `.buttonPrimary`, `.inputError` | `.blueButton`, `.redBorder` |
637
+ | **관계 표현** | `.heroTitle`, `.heroDescription` | `.title`, `.text` |
638
+ | **축약 금지** | `.navigationMenu`, `.backgroundImage` | `.navMnu`, `.bgImg` |
639
+
640
+ #### 구체적 규칙
641
+
642
+ ```
643
+ 1. 컴포넌트 루트: 섹션/컴포넌트 이름 그대로
644
+ .loginForm, .heroSection, .productGrid
645
+
646
+ 2. 자식 요소: 부모이름 + 역할
647
+ .heroTitle, .heroDescription, .heroCta
648
+ .loginFormInput, .loginFormSubmit
649
+
650
+ 3. 이미지 클래스: 반드시 용도를 명시
651
+ .heroBg ← 히어로 배경 이미지
652
+ .heroBgOverlay ← 배경 위 오버레이
653
+ .productPhoto ← 상품 사진
654
+ .brandLogo ← 브랜드 로고
655
+
656
+ 4. 상태 변형: variant/state 접미사
657
+ .buttonPrimary, .buttonDisabled
658
+ .cardHighlight, .cardCompact
659
+ ```
660
+
661
+ #### Anti-Patterns
662
+
663
+ ```css
664
+ /* WRONG: 의미 없는 이름 */
665
+ .wrapper { }
666
+ .inner { }
667
+ .box { }
668
+ .item { }
669
+ .text1 { }
670
+
671
+ /* CORRECT: 역할이 드러나는 이름 */
672
+ .eventSection { }
673
+ .eventContent { }
674
+ .rewardCard { }
675
+ .rewardItem { }
676
+ .eventDescription { }
677
+ ```
678
+
679
+ **Component style file MUST reference global tokens:**
680
+
681
+ ```css
682
+ /* LoginForm.module.css */
683
+ .loginForm {
684
+ padding: var(--figma-space-6);
685
+ background: var(--figma-surface);
686
+ border-radius: var(--figma-radius-lg);
687
+ box-shadow: var(--figma-shadow-md);
688
+ }
689
+
690
+ .loginFormTitle {
691
+ font-size: var(--figma-text-xl);
692
+ font-weight: 600;
693
+ color: var(--figma-text-primary);
694
+ line-height: var(--figma-leading-tight);
695
+ }
696
+
697
+ .loginFormSubmit {
698
+ background: var(--figma-primary);
699
+ color: var(--figma-surface);
700
+ border-radius: var(--figma-radius-md);
701
+ padding: var(--figma-space-2) var(--figma-space-4);
702
+ transition: background 150ms ease;
703
+ }
704
+
705
+ .loginFormSubmit:hover {
706
+ background: var(--figma-primary-hover);
707
+ }
708
+ ```
709
+
710
+ ---
711
+
712
+ ## Phase 5: Markup Quality Standards
713
+
714
+ ### 5-1. Semantic HTML (Mandatory)
715
+
716
+ Every element MUST use the most specific semantic tag available. `<div>` is a last resort.
717
+
718
+ | Visual Element | Correct Tag | Wrong |
719
+ |---------------|------------|-------|
720
+ | Page section | `<section>`, `<article>`, `<aside>` | `<div>` |
721
+ | Navigation | `<nav>` | `<div class="nav">` |
722
+ | Page header | `<header>` | `<div class="header">` |
723
+ | Page footer | `<footer>` | `<div class="footer">` |
724
+ | Heading hierarchy | `<h1>`→`<h6>` (sequential, no skips) | `<div class="title">` |
725
+ | Paragraph text | `<p>` | `<div>` or `<span>` |
726
+ | List of items | `<ul>`/`<ol>` + `<li>` | `<div>` repeated |
727
+ | Clickable action | `<button>` | `<div onClick>` |
728
+ | Navigation link | `<a href>` | `<span onClick>` |
729
+ | Form field | `<input>` + `<label>` | `<div contenteditable>` |
730
+ | Image | `<img alt="descriptive">` or `<figure>` + `<figcaption>` | `<div style="background-image">` for content images |
731
+ | Tabular data | `<table>` + `<thead>` + `<tbody>` | `<div>` grid |
732
+ | Time/Date | `<time datetime>` | `<span>` |
733
+ | Emphasized text | `<strong>`, `<em>` | `<span class="bold">` |
734
+ | Grouped fields | `<fieldset>` + `<legend>` | `<div>` |
735
+
736
+ ### 5-2. Accessibility Checklist
737
+
738
+ Every generated component MUST pass:
739
+
740
+ - [ ] All interactive elements keyboard-reachable (tab order)
741
+ - [ ] `<button>` for actions, `<a>` for navigation — never reversed
742
+ - [ ] `<img>` has descriptive `alt` (not "image", not filename)
743
+ - [ ] Form `<input>` linked to `<label>` (via `htmlFor` / `id`)
744
+ - [ ] Color contrast >= 4.5:1 (text), >= 3:1 (large text, UI controls)
745
+ - [ ] Focus indicator visible on all interactive elements
746
+ - [ ] `aria-label` on icon-only buttons
747
+ - [ ] `role` attribute on custom interactive widgets
748
+ - [ ] Heading hierarchy is sequential (no h1 → h3 skip)
749
+ - [ ] `<ul>`/`<ol>` for any visually listed items
750
+
751
+ ### 5-3. Wrapper Elimination (Fragment / template)
752
+
753
+ **불필요한 래핑 태그를 제거**하고 프레임워크가 제공하는 투명 래퍼를 사용:
754
+
755
+ | Stack | 투명 래퍼 | 사용 시점 |
756
+ |-------|----------|----------|
757
+ | React / Next.js | `<>...</>` 또는 `<React.Fragment>` | 형제 요소를 그룹핑할 때 (DOM에 노드 추가 안 함) |
758
+ | Vue / Nuxt | `<template>` (컴포넌트 루트 이외) | `v-if`, `v-for` 로 여러 요소를 조건부 렌더링할 때 |
759
+ | Svelte | `{#if}`, `{#each}` 블록 | 자체적으로 래핑 불필요 |
760
+
761
+ ```tsx
762
+ // WRONG: 불필요한 div 래핑
763
+ <div>
764
+ <Header />
765
+ <Main />
766
+ <Footer />
767
+ </div>
768
+
769
+ // CORRECT: React Fragment
770
+ <>
771
+ <Header />
772
+ <Main />
773
+ <Footer />
774
+ </>
775
+ ```
776
+
777
+ ```vue
778
+ <!-- WRONG: 불필요한 div 래핑 -->
779
+ <div v-if="showGroup">
780
+ <ItemA />
781
+ <ItemB />
782
+ </div>
783
+
784
+ <!-- CORRECT: Vue template (DOM에 렌더링 안 됨) -->
785
+ <template v-if="showGroup">
786
+ <ItemA />
787
+ <ItemB />
788
+ </template>
789
+ ```
790
+
791
+ **규칙**: 래핑 요소에 스타일이나 이벤트가 없으면 → Fragment/template 사용. 스타일이 있으면 → semantic 태그 사용.
792
+
793
+ ### 5-4. Similar UI Consolidation (80% Rule)
794
+
795
+ 디자인에서 **유사도 80% 이상**인 UI 패턴이 발견되면, 별도 컴포넌트로 분리하지 말고 **하나의 컴포넌트 + variant props/slots**으로 통합:
796
+
797
+ ```
798
+ 유사도 판단 기준:
799
+ - 레이아웃 구조 동일
800
+ - 색상/크기/텍스트만 다름
801
+ → 하나의 컴포넌트 + props로 변형
802
+
803
+ - 구조 자체가 다름 (요소 추가/제거, 레이아웃 방향 변경)
804
+ → 별도 컴포넌트
805
+ ```
806
+
807
+ | Stack | 변형 방법 |
808
+ |-------|----------|
809
+ | React | `variant` prop + 조건부 className / style |
810
+ | Vue | `variant` prop + `<slot>` for 커스텀 영역 |
811
+ | Svelte | `variant` prop + `<slot>` |
812
+ | React Native | `variant` prop + StyleSheet 조건 선택 |
813
+
814
+ ```tsx
815
+ // React — 하나의 Card 컴포넌트가 3가지 변형 처리
816
+ interface CardProps {
817
+ variant: 'default' | 'highlight' | 'compact';
818
+ title: string;
819
+ children: React.ReactNode;
820
+ }
821
+
822
+ export function Card({ variant, title, children }: CardProps): JSX.Element {
823
+ return (
824
+ <article className={styles[variant]}>
825
+ <h3 className={styles.title}>{title}</h3>
826
+ {children}
827
+ </article>
828
+ );
829
+ }
830
+ ```
831
+
832
+ ```vue
833
+ <!-- Vue — slot으로 커스텀 영역 제공 -->
834
+ <template>
835
+ <article :class="$style[variant]">
836
+ <h3 :class="$style.title">{{ title }}</h3>
837
+ <slot />
838
+ </article>
839
+ </template>
840
+
841
+ <script setup lang="ts">
842
+ defineProps<{ variant: 'default' | 'highlight' | 'compact'; title: string }>();
843
+ </script>
844
+ ```
845
+
846
+ ### 5-5. Component Structure Rules
847
+
848
+ ```
849
+ Max nesting depth: 3 levels (container > group > element)
850
+ Max component length: 50 lines
851
+ Max props: 5 per component
852
+ ```
853
+
854
+ **Split triggers:**
855
+
856
+ | Signal | Action |
857
+ |--------|--------|
858
+ | Component > 50 lines | Split into sub-components |
859
+ | Repeated visual pattern (2+ times) | Extract shared component |
860
+ | **Similar pattern (80%+ match)** | **Single component + variant prop** |
861
+ | Distinct visual boundary (card, modal, form) | Own component + own style file |
862
+ | 3+ related props | Group into object prop or extract sub-component |
863
+
864
+ ### 5-6. Markup Anti-Patterns (NEVER Generate)
865
+
866
+ ```tsx
867
+ // WRONG: div soup
868
+ <div className="card">
869
+ <div className="card-header">
870
+ <div className="title">Login</div>
871
+ </div>
872
+ <div className="card-body">
873
+ <div className="input-group">
874
+ <div className="label">Email</div>
875
+ <div className="input"><input /></div>
876
+ </div>
877
+ </div>
878
+ </div>
879
+
880
+ // CORRECT: semantic markup
881
+ <article className={styles.card}>
882
+ <header className={styles.header}>
883
+ <h2 className={styles.title}>Login</h2>
884
+ </header>
885
+ <form className={styles.body}>
886
+ <fieldset className={styles.fieldGroup}>
887
+ <label htmlFor="email" className={styles.label}>Email</label>
888
+ <input id="email" type="email" className={styles.input} />
889
+ </fieldset>
890
+ </form>
891
+ </article>
892
+ ```
893
+
894
+ ```tsx
895
+ // WRONG: 불필요한 래핑
896
+ <div>
897
+ <ComponentA />
898
+ <ComponentB />
899
+ </div>
900
+
901
+ // CORRECT: Fragment
902
+ <>
903
+ <ComponentA />
904
+ <ComponentB />
905
+ </>
906
+ ```
907
+
908
+ ```tsx
909
+ // WRONG: 유사한 UI를 별도 컴포넌트로
910
+ <DefaultCard /> // 구조 동일, 색상만 다름
911
+ <HighlightCard /> // 구조 동일, 색상만 다름
912
+
913
+ // CORRECT: 단일 컴포넌트 + variant
914
+ <Card variant="default" />
915
+ <Card variant="highlight" />
916
+ ```
917
+
918
+ ---
919
+
920
+ ## Phase 6: Code Generation
921
+
922
+ ### 6-0. Apply Design Context (from Phase 3-2)
923
+
924
+ If `design-context.json` was loaded, apply these rules to ALL generated code:
925
+
926
+ | Context Field | Effect on Code Generation |
927
+ |---------------|--------------------------|
928
+ | `constraints.accessibility = "AAA"` | Use `aria-describedby` on all form fields, `prefers-reduced-motion` media query, `prefers-contrast` support |
929
+ | `constraints.devices = ["mobile"]` | Mobile-only layout, no desktop breakpoints, touch target ≥ 44px |
930
+ | `constraints.devices = ["mobile","desktop"]` | Mobile-first with `md:` breakpoint |
931
+ | `aesthetic.style = "minimal"` | Reduce visual weight — fewer shadows, thinner borders, more whitespace |
932
+ | `aesthetic.style = "bold"` | Stronger shadows, thicker borders, larger typography scale |
933
+ | `brand.personality` | Preserve brand-unique visual patterns (do NOT distill these away) |
934
+ | `detectedStack.fonts` | Use project's existing font stack instead of Figma's font family |
935
+
936
+ ### 6-1. Stack-Specific Rules
937
+
938
+ Generate code following these rules per stack:
939
+
940
+ #### React / Next.js + TypeScript
941
+
942
+ ```
943
+ - Functional components with explicit return types
944
+ - Props interface defined above component
945
+ - Named exports (not default)
946
+ - <></> Fragment for sibling grouping (no unnecessary wrapper div)
947
+ - CSS Modules: import styles from './Component.module.css'
948
+ - Tailwind: classes in JSX, extract repeated patterns to @apply
949
+ - Responsive: mobile-first breakpoints
950
+ - Variant pattern: discriminated union props for similar UI
951
+ - useMemo/useCallback only when measurably needed (not by default)
952
+ - Next.js: use 'use client' only when client interactivity needed
953
+ - Next.js: Image component for images, Link for navigation
954
+ ```
955
+
956
+ #### Vue 3 / Nuxt
957
+
958
+ ```
959
+ - <script setup lang="ts"> composition API
960
+ - defineProps with TypeScript interface (with defaults via withDefaults)
961
+ - <template> for invisible grouping (v-if, v-for on multiple elements)
962
+ - <style scoped> (or <style module>) with CSS custom property references
963
+ - v-bind in <style> for dynamic values: color: v-bind(themeColor)
964
+ - <slot> + named slots for composable variant patterns
965
+ - <Teleport> for modals/overlays
966
+ - Or Tailwind classes in template
967
+ - Nuxt: <NuxtLink> for navigation, <NuxtImg> for images
968
+ - Nuxt: definePageMeta for page-level config
969
+ - computed() for derived state (not methods for template expressions)
970
+ ```
971
+
972
+ #### Svelte
973
+
974
+ ```
975
+ - TypeScript in <script lang="ts">
976
+ - Export let for props with types
977
+ - {#if}/{#each}/{#await} blocks — inherently wrapper-free
978
+ - <slot> for composable patterns
979
+ - <style> block with CSS custom property references
980
+ - Or Tailwind classes in markup
981
+ - Reactive declarations ($:) for derived values
982
+ - transition: directive for animations
983
+ ```
984
+
985
+ #### SCSS (any framework with SCSS)
986
+
987
+ ```
988
+ - @use 'figma-tokens' as figma — namespaced import (not @import)
989
+ - $변수 for tokens: figma.$figma-primary, figma.$figma-space-4
990
+ - @include figma.figma-pc { } for breakpoint — @media 직접 사용 금지
991
+ - figma-fluid($mobile, $desktop) for responsive values — 수동 clamp() 금지
992
+ - Nesting max 3 levels: .section { .title { .icon { } } } ← 한계
993
+ - & for BEM-like modifiers: &--active, &__title
994
+ - @each for variant generation from map
995
+ - Partials: _figma-tokens.scss, _figma-mixins.scss
996
+ - Vue: <style lang="scss" scoped> with @use
997
+ ```
998
+
999
+ ```scss
1000
+ // Component usage example
1001
+ @use 'figma-tokens' as figma;
1002
+
1003
+ .heroSection {
1004
+ position: relative;
1005
+ padding: figma.figma-fluid(1rem, 3rem);
1006
+
1007
+ &Title {
1008
+ font-size: figma.figma-fluid(1.5rem, 3rem);
1009
+ color: figma.$figma-text-primary;
1010
+ }
1011
+
1012
+ &Cta {
1013
+ background: figma.$figma-primary;
1014
+ border-radius: figma.$figma-radius-md;
1015
+
1016
+ &:hover { background: figma.$figma-primary-hover; }
1017
+ }
1018
+ }
1019
+
1020
+ .cardGrid {
1021
+ display: grid;
1022
+ grid-template-columns: 1fr;
1023
+
1024
+ @include figma.figma-pc {
1025
+ grid-template-columns: repeat(3, 1fr);
1026
+ }
1027
+ }
1028
+ ```
1029
+
1030
+ #### React Native
1031
+
1032
+ ```
1033
+ - StyleSheet.create at bottom of file
1034
+ - <></> Fragment for sibling grouping
1035
+ - Dimensions-aware responsive layout
1036
+ - Platform.select / Platform.OS for platform-specific styles
1037
+ - Extract shared style constants to styles/tokens.ts
1038
+ ```
1039
+
1040
+ #### Flutter (Dart)
1041
+
1042
+ ```
1043
+ - StatelessWidget or StatefulWidget as appropriate
1044
+ - Theme.of(context) for design tokens
1045
+ - Extract shared values to lib/theme/figma_tokens.dart
1046
+ - Proper widget composition
1047
+ ```
1048
+
1049
+ ### 6-2. Image Code Patterns (from Phase 2-A classification)
1050
+
1051
+ Phase 2-A에서 분류된 이미지 유형별 코드 생성 규칙:
1052
+
1053
+ #### Background Image Class Separation (핵심 원칙)
1054
+
1055
+ 배경 이미지 관련 요소는 **용도별로 별도 클래스**로 분리한다. 레이아웃과 이미지를 합치지 않는다.
1056
+
1057
+ ##### 별도 클래스로 분리해야 하는 유형
1058
+
1059
+ | 유형 | 클래스 예시 | 분리 이유 |
1060
+ |------|-----------|----------|
1061
+ | **섹션 전면 배경** | `.heroBg`, `.eventBg`, `.rewardsBg` | 이벤트 기간/시즌별 이미지 교체 |
1062
+ | **오버레이** | `.heroBgOverlay`, `.eventBgOverlay` | 투명도/그라데이션 독립 조절 |
1063
+ | **패턴/텍스처** | `.sectionPattern`, `.headerTexture` | `background-repeat`/`size` 별도 제어 |
1064
+ | **파티클/장식 효과** | `.heroParticle`, `.sparkleEffect` | 애니메이션 on/off, 성능 이슈 시 제거 |
1065
+ | **캐릭터/일러스트** | `.heroCharacter`, `.eventIllust` | 콜라보/캐릭터별 교체, position 조절 |
1066
+ | **그라데이션** | `.sectionGradient`, `.fadeBottom` | 테마별 색상 변경 |
1067
+ | **비디오 포스터** | `.videoPoster` | 비디오 로드 전 폴백, JS에서 교체 |
1068
+
1069
+ ##### Multi-Layer Pattern (섹션 배경 기본 구조)
1070
+
1071
+ Figma에서 배경 이미지가 있는 섹션은 다음 레이어 구조로 생성:
1072
+
1073
+ ```
1074
+ .{section}Section ← 레이아웃 (position, size, padding, overflow)
1075
+ .{section}Bg ← 배경 이미지 (URL, size, position) — z-index: 0
1076
+ .{section}BgOverlay ← 오버레이 (gradient, opacity) — z-index: 1
1077
+ .{section}Character ← 캐릭터/일러스트 (선택) — z-index: 2
1078
+ .{section}Pattern ← 패턴/텍스처 (선택) — z-index: 1
1079
+ .{section}Content ← 텍스트/버튼/UI — z-index: 최상위
1080
+ ```
1081
+
1082
+ ```tsx
1083
+ // 실제 마크업 예시 — 게임 이벤트 히어로 섹션
1084
+ <section className={styles.heroSection}>
1085
+ <div className={styles.heroBg} />
1086
+ <div className={styles.heroBgOverlay} />
1087
+ <div className={styles.heroCharacter} />
1088
+ <div className={styles.heroContent}>
1089
+ <h1 className={styles.heroTitle}>Stellar Blade × PUBG</h1>
1090
+ <p className={styles.heroDescription}>기간한정 콜라보 이벤트</p>
1091
+ <a href="#rewards" className={styles.heroCta}>보상 확인하기</a>
1092
+ </div>
1093
+ </section>
1094
+ ```
1095
+
1096
+ ```css
1097
+ /* heroSection.module.css */
1098
+ .heroSection { position: relative; overflow: hidden; min-height: 100vh; }
1099
+
1100
+ .heroBg {
1101
+ position: absolute; inset: 0; z-index: 0;
1102
+ background-image: url('/assets/hero-bg.webp');
1103
+ background-size: cover;
1104
+ background-position: center;
1105
+ }
1106
+
1107
+ .heroBgOverlay {
1108
+ position: absolute; inset: 0; z-index: 1;
1109
+ background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.7));
1110
+ }
1111
+
1112
+ .heroCharacter {
1113
+ position: absolute; bottom: 0; right: 5%; z-index: 2;
1114
+ width: 40%; height: 80%;
1115
+ background-image: url('/assets/hero-character.webp');
1116
+ background-size: contain;
1117
+ background-position: bottom center;
1118
+ background-repeat: no-repeat;
1119
+ }
1120
+
1121
+ .heroContent { position: relative; z-index: 3; }
1122
+ ```
1123
+
1124
+ **모든 배경 관련 이미지가 독립 클래스**이므로:
1125
+ - JS에서 `.heroBg`만 교체하면 배경만 바뀜
1126
+ - `.heroCharacter`만 교체하면 캐릭터만 바뀜
1127
+ - `.heroBgOverlay`의 opacity를 조절하면 텍스트 가독성만 조절 가능
1128
+ - 성능 이슈 시 `.heroParticle`만 `display: none`
1129
+
1130
+ #### Responsive Background Image (반응형 배경 — PC/Mobile 분기)
1131
+
1132
+ ```css
1133
+ /* 배경 이미지 클래스 안에서만 반응형 처리 */
1134
+ .heroBg {
1135
+ background-image: url('/assets/hero-mobile.webp');
1136
+ background-size: cover;
1137
+ background-position: center;
1138
+ position: absolute;
1139
+ inset: 0;
1140
+ z-index: 0;
1141
+ }
1142
+
1143
+ @media (min-width: 1024px) { /* {breakpoint}px */
1144
+ .heroBg {
1145
+ background-image: url('/assets/hero-pc.webp');
1146
+ }
1147
+ }
1148
+ ```
1149
+
1150
+ **섹션별 배경 이미지가 여러 개인 경우** — 각각 고유 클래스:
1151
+
1152
+ ```css
1153
+ .heroBg { background-image: url('/assets/hero-bg.webp'); /* ... */ }
1154
+ .eventBg { background-image: url('/assets/event-bg.webp'); /* ... */ }
1155
+ .rewardsBg { background-image: url('/assets/rewards-bg.webp'); /* ... */ }
1156
+ ```
1157
+
1158
+ **Figma 오버레이 감지**: 레이어에 `opacity < 1` 또는 `fills`에 반투명 색상이 IMAGE 위에 있으면 → `{section}BgOverlay` 클래스로 처리.
1159
+
1160
+ #### Content Image (독립적인 이미지 콘텐츠)
1161
+
1162
+ ```tsx
1163
+ // React / Next.js
1164
+ <img
1165
+ src="/assets/product.webp"
1166
+ alt="상품 설명" // Figma 레이어 이름 기반
1167
+ width={600} // Figma 레이어 width (scaled)
1168
+ height={400} // Figma 레이어 height (scaled)
1169
+ loading="lazy" // 뷰포트 밖이면 lazy
1170
+ />
1171
+
1172
+ // Next.js — Image 컴포넌트 우선
1173
+ import Image from 'next/image';
1174
+ <Image
1175
+ src="/assets/product.webp"
1176
+ alt="상품 설명"
1177
+ width={600}
1178
+ height={400}
1179
+ priority={false} // hero 이미지만 priority={true}
1180
+ />
1181
+ ```
1182
+
1183
+ ```vue
1184
+ <!-- Nuxt — NuxtImg 우선 -->
1185
+ <NuxtImg
1186
+ src="/assets/product.webp"
1187
+ alt="상품 설명"
1188
+ width="600"
1189
+ height="400"
1190
+ loading="lazy"
1191
+ />
1192
+ ```
1193
+
1194
+ #### Responsive Content Image (뷰포트별 다른 이미지)
1195
+
1196
+ ```html
1197
+ <picture>
1198
+ <source media="(min-width: 1024px)" srcset="/assets/hero-pc.webp" />
1199
+ <img src="/assets/hero-mobile.webp" alt="히어로 이미지" loading="eager" />
1200
+ </picture>
1201
+ ```
1202
+
1203
+ #### 이미지 공통 규칙
1204
+
1205
+ | 규칙 | 설명 |
1206
+ |------|------|
1207
+ | **format** | `.webp` 우선 (fallback: `.png`/`.jpg`) |
1208
+ | **alt** | Figma 레이어 이름에서 파생, 장식 이미지는 `alt=""` + `aria-hidden="true"` |
1209
+ | **width/height** | 항상 명시 (CLS 방지), Figma 레이어 크기를 스케일 팩터 적용 후 사용 |
1210
+ | **loading** | 뷰포트 상단(hero, header) → `eager`, 나머지 → `lazy` |
1211
+ | **object-fit** | `cover` (배경/히어로), `contain` (로고/아이콘), `fill` 사용 금지 |
1212
+ | **반응형 크기** | Content image는 `max-width: 100%; height: auto;` 기본 |
1213
+ | **배경 이미지 + 텍스트** | 반드시 오버레이(`::before` 또는 gradient)로 텍스트 가독성 확보 |
1214
+ | **장식 패턴** | `background-repeat: repeat` + `background-size` 조절 |
1215
+
1216
+ #### Anti-Patterns
1217
+
1218
+ ```tsx
1219
+ // WRONG: 레이아웃 클래스에 background-image를 합침
1220
+ <section className={styles.hero} /> /* .hero에 bg-image + padding + flex 등 다 섞임 */
1221
+
1222
+ // CORRECT: 배경 이미지는 별도 클래스
1223
+ <section className={styles.heroSection}>
1224
+ <div className={styles.heroBg} />
1225
+ <div className={styles.heroContent}>...</div>
1226
+ </section>
1227
+ ```
1228
+
1229
+ ```tsx
1230
+ // WRONG: inline style로 배경 이미지
1231
+ <div style={{ backgroundImage: `url(${bg})` }} />
1232
+
1233
+ // CORRECT: 전용 클래스에 이미지 URL
1234
+ <div className={styles.heroBg} />
1235
+ /* JS 동적 교체 시: element.style.backgroundImage = url(...) on .heroBg만 */
1236
+ ```
1237
+
1238
+ ```css
1239
+ /* WRONG: 배경 + 텍스트, 오버레이 없음 */
1240
+ .hero { background-image: url(...); color: white; }
1241
+
1242
+ /* CORRECT: 3-layer 분리 (bg / overlay / content) */
1243
+ .heroSection { position: relative; }
1244
+ .heroBg { background-image: url(...); position: absolute; inset: 0; z-index: 0; }
1245
+ .heroBgOverlay { position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 1; }
1246
+ .heroContent { position: relative; z-index: 2; color: white; }
1247
+ ```
1248
+
1249
+ ```css
1250
+ /* WRONG: 의미 없는 이름 */
1251
+ .bg { }
1252
+ .bgImg { }
1253
+ .overlay { }
1254
+
1255
+ /* CORRECT: 섹션별 명시적 이름 */
1256
+ .heroBg { }
1257
+ .heroBgOverlay { }
1258
+ .eventBg { }
1259
+ .rewardsBg { }
1260
+ ```
1261
+
1262
+ ### 6-3. Responsive Code Generation (responsive mode only)
1263
+
1264
+ When `responsive.json` exists, apply these rules on top of stack-specific rules:
1265
+
1266
+ #### Principle: Fluid First, Breakpoint Second
1267
+
1268
+ ```
1269
+ 1. Typography & Spacing → clamp() fluid tokens (no breakpoints needed)
1270
+ 2. Layout direction changes → @media at project breakpoint (flex-direction, grid-template)
1271
+ 3. Visibility toggles → @media at project breakpoint (display: none/block)
1272
+ 4. Component swaps → conditional render with useMediaQuery or CSS
1273
+ ```
1274
+
1275
+ **Breakpoint selection**: Use `{breakpoints}` from Phase 3-3. Pick the breakpoint closest to where the layout structurally changes between Figma viewports. For example:
1276
+ - Figma mobile=375px, desktop=1440px → use project's `md` (typically 768px) as primary breakpoint
1277
+ - If project has `tablet: 1024px` → use that instead
1278
+ - If 3 viewports (mobile/tablet/desktop) → use 2 breakpoints (e.g., `sm` and `lg`)
1279
+
1280
+ #### CSS Modules (responsive example)
1281
+
1282
+ ```css
1283
+ /* Component.module.css */
1284
+ /* Breakpoints from project: {breakpoints} */
1285
+
1286
+ .container {
1287
+ display: flex;
1288
+ flex-direction: column; /* mobile-first */
1289
+ gap: var(--figma-space-content); /* fluid: clamp() */
1290
+ padding: var(--figma-space-section); /* fluid: clamp() */
1291
+ }
1292
+
1293
+ .title {
1294
+ font-size: var(--figma-text-h1); /* fluid: clamp() */
1295
+ line-height: 1.2;
1296
+ }
1297
+
1298
+ .cardGrid {
1299
+ display: grid;
1300
+ grid-template-columns: 1fr; /* mobile: single column */
1301
+ gap: var(--figma-space-content);
1302
+ }
1303
+
1304
+ /* Layout breakpoint — use project's breakpoint, NOT hardcoded */
1305
+ @media (min-width: 1024px) { /* {breakpoint}px from Phase 3-3 */ /* or project's md value */
1306
+ .container {
1307
+ flex-direction: row;
1308
+ }
1309
+ .cardGrid {
1310
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1311
+ }
1312
+ }
1313
+
1314
+ /* Visibility toggle — same breakpoint */
1315
+ .mobileOnly { display: block; }
1316
+ .desktopOnly { display: none; }
1317
+
1318
+ @media (min-width: 1024px) { /* {breakpoint}px from Phase 3-3 */
1319
+ .mobileOnly { display: none; }
1320
+ .desktopOnly { display: block; }
1321
+ }
1322
+ ```
1323
+
1324
+ #### Tailwind (responsive example)
1325
+
1326
+ Use project's Tailwind screen prefixes (e.g., `sm:`, `md:`, `lg:`) — NOT hardcoded pixel values.
1327
+
1328
+ ```tsx
1329
+ {/* Uses project's Tailwind breakpoints — md: maps to project's screens.md */}
1330
+ <div className="flex flex-col md:flex-row gap-[--figma-space-content] p-[--figma-space-section]">
1331
+ <h1 className="text-[length:--figma-text-h1] leading-tight">Title</h1>
1332
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-[--figma-space-content]">
1333
+ {/* cards */}
1334
+ </div>
1335
+ <nav className="md:hidden">Mobile Nav</nav>
1336
+ <nav className="hidden md:flex">Desktop Nav</nav>
1337
+ </div>
1338
+ ```
1339
+
1340
+ #### Anti-Patterns (NEVER do in responsive mode)
1341
+
1342
+ | Wrong | Right |
1343
+ |-------|-------|
1344
+ | Separate mobile/desktop component files | Single component with responsive CSS |
1345
+ | `@media` for font-size/spacing | `clamp()` via fluid tokens |
1346
+ | Hardcoded `@media (min-width: 768px)` | Use project breakpoint from Phase 3-3 or Tailwind prefix |
1347
+ | Pixel values in responsive styles | Token variables everywhere |
1348
+ | Duplicated markup for each viewport | Visibility toggles or conditional sections |
1349
+ | `window.innerWidth` checks in JS | CSS-only responsive (`@media`, `clamp()`, `auto-fit`) |
1350
+ | Inventing new breakpoints | Use project's existing breakpoint system |
1351
+
1352
+ ---
1353
+
1354
+ ## Phase 7: Token Mapping (default mode)
1355
+
1356
+ **Only in default (project integration) mode.** Map extracted Figma tokens to the project's existing token system.
1357
+
1358
+ ### Token Source Priority
1359
+
1360
+ ```
1361
+ 1. MASTER.md (.claude/vibe/design-system/{project}/MASTER.md) ← 최우선
1362
+ 2. design-context.json (.claude/vibe/design-context.json) ← 보조
1363
+ 3. Project theme files (tailwind.config, CSS variables, etc.) ← 폴백
1364
+ 4. Generate new figma-tokens ← 마지막 수단
1365
+ ```
1366
+
1367
+ ### Mapping Rules
1368
+
1369
+ 1. **MASTER.md exists** → authoritative token source
1370
+ - Figma color ≈ MASTER.md color (ΔE < 5) → use MASTER.md token name
1371
+ - Figma spacing ≈ MASTER.md spacing (±2px) → use MASTER.md token name
1372
+ - Figma font ≈ MASTER.md font → use MASTER.md token name
1373
+ - Unmatched Figma values → add to `figma-tokens.css` as supplementary tokens
1374
+
1375
+ 2. **No MASTER.md, but design-context.json exists** →
1376
+ - Use `detectedStack` info for naming convention
1377
+ - Use `aesthetic.colorMood` to validate token naming (e.g., warm palette → warm- prefix)
1378
+ - Generate `figma-tokens.css` grouped by category
1379
+
1380
+ 3. **No design system at all** →
1381
+ - Generate `figma-tokens.css` (or Tailwind extend)
1382
+ - Group tokens by category (color, typography, spacing, shadow)
1383
+
1384
+ ### Output Mapping Comment
1385
+
1386
+ Always output token mapping as a comment block at the top of the token file:
1387
+
1388
+ ```
1389
+ /* Figma Token Mapping:
1390
+ * Figma "Primary/Default" → var(--figma-primary) = #3B82F6
1391
+ * ✅ Matched: var(--color-blue-500) from MASTER.md
1392
+ * Figma "Text/Body" → var(--figma-text-base) = 1rem / 1.5
1393
+ * ✅ Matched: var(--text-base) from MASTER.md
1394
+ * Figma "Accent/Glow" → var(--figma-accent-glow) = #7C3AED
1395
+ * ⚠️ New token: no existing match
1396
+ */
1397
+ ```
1398
+
1399
+ ---
1400
+
1401
+ ## Phase 8: Correction Notes
1402
+
1403
+ After generating code, output a brief correction report:
1404
+
1405
+ ```markdown
1406
+ ## Correction Notes
1407
+
1408
+ ### Generation Mode
1409
+ - Mode: default / --standalone / responsive
1410
+ - Output directory: {path}
1411
+ - Viewports: {list of viewport labels + widths, if responsive}
1412
+
1413
+ ### Files Generated
1414
+ | File | Type | Description |
1415
+ |------|------|-------------|
1416
+ | styles/tokens.css | Global tokens | {N} colors, {N} spacing, {N} typography |
1417
+ | styles/global.css | Base styles | Reset + typography + layout |
1418
+ | ComponentName/ComponentName.tsx | Component | Root component |
1419
+ | ComponentName/ComponentName.module.css | Styles | Component-specific styles |
1420
+
1421
+ ### Responsive Summary (responsive mode only)
1422
+ | Token | Mobile | Desktop | Strategy |
1423
+ |-------|--------|---------|----------|
1424
+ | --figma-text-h1 | 24px | 48px | clamp() |
1425
+ | --figma-space-section | 16px | 48px | clamp() |
1426
+ | Card grid | 1col | 3col | @media grid |
1427
+ | Sidebar | hidden | visible | @media display |
1428
+
1429
+ - Fluid tokens generated: {N}
1430
+ - Layout breakpoints used: {list}
1431
+ - Component swaps: {list or "none"}
1432
+
1433
+ ### Layer Issues Found
1434
+ - [Layer name] was ambiguous → interpreted as [component] based on image
1435
+ - [Layer structure] didn't match visual → used image-based layout
1436
+
1437
+ ### Markup Quality
1438
+ - Semantic tags used: {list}
1439
+ - Accessibility: {pass/fail items}
1440
+
1441
+ ### Recommendations for Figma File
1442
+ - Use Auto Layout for [specific frame] to improve extraction accuracy
1443
+ - Name layers semantically (e.g., "login-form" not "Frame 47")
1444
+ - Use consistent spacing tokens
1445
+ - (responsive) Keep same component names across mobile/desktop frames for easier mapping
1446
+ ```
1447
+
1448
+ ---
1449
+
1450
+ ## Tool Usage Rules
1451
+
1452
+ | Tool | When |
1453
+ |------|------|
1454
+ | **MCP: get_design_context** | Figma 추출 — 코드 + 스크린샷 + 메타데이터 (토큰 불필요) |
1455
+ | **MCP: get_metadata** | 레이어 구조 XML (노드 ID, 크기, 위치) |
1456
+ | **MCP: get_screenshot** | 프레임 이미지 렌더링 |
1457
+ | Read | Project config, existing components, design-context.json |
1458
+ | Glob | Find existing components, theme files, design tokens |
1459
+ | Grep | Search for existing color/spacing/typography definitions |
1460
+ | Write | Create new component files and style files |
1461
+ | Edit | Update existing theme/token files to add new tokens (default mode) |
1462
+
1463
+ ## Important
1464
+
1465
+ - **Never guess colors** — extract from layers.json or image analysis
1466
+ - **Never invent spacing** — use extracted values mapped to token scale
1467
+ - **Never hardcode values** — all visual properties reference token variables
1468
+ - **Preserve existing patterns** — match the project's existing component style (default mode)
1469
+ - **Image is truth** — when layer structure is confusing, trust what the image shows
1470
+ - **Ask before overwriting** — if a component file already exists, ask the user first
1471
+ - **No console.log** — never include debug logging in generated code
1472
+ - **No div soup** — every element uses the correct semantic tag
1473
+ - **Component size limit** — split components exceeding 50 lines
1474
+ - **Style separation** — global tokens file + per-component style files, always
1475
+
1476
+ ## Next Steps: Design Quality Pipeline
1477
+
1478
+ After generating code, present the following pipeline to the user:
1479
+
1480
+ ### Quick (default recommendation)
1481
+ ```
1482
+ /design-normalize → /design-audit
1483
+ ```
1484
+ - Normalize: 하드코딩 값 → MASTER.md 토큰으로 치환
1485
+ - Audit: A11Y + 성능 + 반응형 + AI Slop 검출
1486
+
1487
+ ### Thorough (recommended for production)
1488
+ ```
1489
+ /design-normalize → /design-audit → /design-critique → /design-polish
1490
+ ```
1491
+ - + Critique: Nielsen 10 휴리스틱 + 5 페르소나 분석
1492
+ - + Polish: 인터랙션 상태 보완 (hover/focus/loading/error)
1493
+
1494
+ ### Pre-requisite check
1495
+ If `.claude/vibe/design-context.json` does NOT exist:
1496
+ ```
1497
+ ⚠️ 디자인 컨텍스트가 없습니다. /design-teach 를 먼저 실행하면
1498
+ 브랜드, 접근성, 타겟 디바이스에 맞춘 더 정확한 코드를 생성할 수 있습니다.
1499
+ ```
1500
+
1501
+ ### Output format
1502
+ ```
1503
+ ## 🎨 Design Quality Pipeline
1504
+
1505
+ 생성된 파일: {file list}
1506
+
1507
+ 추천 다음 단계:
1508
+ 1. /design-normalize — 토큰 정렬 (하드코딩 제거)
1509
+ 2. /design-audit — 기술 품질 검사
1510
+ 3. /design-critique — UX 휴리스틱 리뷰
1511
+ 4. /design-polish — 최종 인터랙션 보완
1512
+
1513
+ 💡 /design-teach 가 아직 설정되지 않았다면 먼저 실행하세요.
1514
+ ```