01 — Figma-to-Code Workflow
Dev Guide — Savoy Signature Hotels
PRD refs:04_Frontend_Architecture.md,05_Design_System_and_Theming.md,07_Modules_and_Templates.md
1. Overview
Section titled “1. Overview”This guide defines the complete workflow for translating a Figma design into a production-ready React component. The workflow ensures that every component is token-based, theme-aware, responsive, accessible, and documented in Storybook.
2. Workflow Steps
Section titled “2. Workflow Steps”3. Step-by-Step
Section titled “3. Step-by-Step”Step 1: Analyze Figma Structure
Section titled “Step 1: Analyze Figma Structure”Before writing any code, analyze the Figma design:
Checklist:
- What is the module ID? (e.g.,
M05 — Hero Slider) - Is it a UI component (
packages/ui) or a CMS module (packages/modules)? - Which breakpoints are designed? (Mobile 375px, Tablet 768px, Desktop 1440px)
- Are there Desktop AND Mobile image variants?
- What interactive behaviors exist? (hover states, animations, transitions)
- Does this need to be a Client Component? (interactivity = yes)
- What accessibility requirements apply? (keyboard nav, ARIA roles, focus management)
Decision Tree — UI Component vs. Module:
Step 2: Extract Design Tokens
Section titled “Step 2: Extract Design Tokens”If the Figma design introduces values NOT already in the token system:
- Check
packages/themes/src/_base.cssfor shared tokens - Check
packages/themes/src/{site}.cssfor theme-specific tokens - If new values exist, add them following the token naming convention (see Guide
02)
Rule: NEVER hardcode color, font, spacing, or shadow values. Always use var(--token-name).
Step 3: Identify Component Hierarchy
Section titled “Step 3: Identify Component Hierarchy”Break the Figma design into its component tree:
Rules:
- Modules compose UI components, never the reverse
- If a sub-element is reusable across modules, extract it to
packages/ui - Interactive sub-components go in a separate
.client.tsxfile
Step 4: Create Storybook Story (skeleton first)
Section titled “Step 4: Create Storybook Story (skeleton first)”Write the story BEFORE implementing the component. This is “Storybook-first development”:
import type { Meta, StoryObj } from '@storybook/react';import { HeroSlider } from './HeroSlider';
const meta: Meta<typeof HeroSlider> = { title: 'Modules/M05 — Hero Slider', component: HeroSlider, tags: ['autodocs'], parameters: { layout: 'fullscreen', design: { type: 'figma', url: 'https://www.figma.com/file/...?node-id=XX', }, },};
export default meta;type Story = StoryObj<typeof HeroSlider>;
export const Default: Story = { args: { // Fill with realistic mock data matching the Figma },};Step 5: Implement React Component + SCSS
Section titled “Step 5: Implement React Component + SCSS”Follow the file structure from PRD 07:
packages/modules/src/m05-hero-slider/ index.ts HeroSlider.tsx HeroSlider.client.tsx (if interactive) HeroSlider.scss HeroSlider.types.ts HeroSlider.mapper.ts HeroSlider.stories.tsx HeroSlider.test.tsxImplementation order within a module:
.types.ts— Define the props interface.tsx— Implement the component (use tokens, BEM classes).scss— Style with BEM + SASS + CSS variables.stories.tsx— Complete all story variants.mapper.ts— Map Umbraco API response to props.test.tsx— Unit tests for mapper + component renderingindex.ts— Named export
Step 6: Write Mapper + Types
Section titled “Step 6: Write Mapper + Types”The mapper is the bridge between CMS data and component props:
export interface HeroSliderProps { title: string; subtitle?: string; slides: HeroSlide[]; autoplay?: boolean; interval?: number; siteKey: string; locale: string;}
export interface HeroSlide { imageDesktop: { url: string; width: number; height: number }; imageMobile: { url: string; width: number; height: number }; altText: string; caption?: string; cta?: { label: string; href: string };}import { UmbracoElement } from '@savoy/cms-client';import { HeroSliderProps } from './HeroSlider.types';
export function mapHeroSlider(element: UmbracoElement): Omit<HeroSliderProps, 'siteKey' | 'locale'> { return { title: element.properties.title, subtitle: element.properties.subtitle, slides: element.properties.slides.map((slide: any) => ({ imageDesktop: { url: slide.imageDesktop?.url, width: slide.imageDesktop?.width, height: slide.imageDesktop?.height, }, imageMobile: { url: slide.imageMobile?.url, width: slide.imageMobile?.width, height: slide.imageMobile?.height, }, altText: slide.altText || '', caption: slide.caption, cta: slide.cta ? { label: slide.cta.name, href: slide.cta.url } : undefined, })), autoplay: element.properties.autoplay ?? true, interval: element.properties.interval ?? 5000, };}Step 7: Register in Module Registry
Section titled “Step 7: Register in Module Registry”import { HeroSlider } from './m05-hero-slider';import { mapHeroSlider } from './m05-hero-slider/HeroSlider.mapper';
export const moduleRegistry = { // ... existing modules heroSlider: { component: HeroSlider, mapper: mapHeroSlider, moduleId: "M05" },};Step 8: Validate in Storybook
Section titled “Step 8: Validate in Storybook”Validation checklist:
- Renders correctly in all 8 themes (use Storybook theme switcher)
- Responsive at all breakpoints: 375px, 768px, 1024px, 1280px, 1440px
- All variants/states visible (default, with CTA, multiple slides, etc.)
- Interactive behaviors work (if Client Component)
- No hardcoded colors, fonts, or spacing (only CSS variables)
- Figma link present in Storybook story parameters
Step 8.5: Pixel Perfect Visual Testing (MANDATORY)
Section titled “Step 8.5: Pixel Perfect Visual Testing (MANDATORY)”No module is complete without passing visual tests. This step validates your implementation against the Figma design using automated screenshot comparison.
- Fetch Figma baselines — Use Figma MCP
get_screenshotto capture the design at desktop (1440x900) and mobile (375x812). Save toapps/storybook/__figma_baselines__/{storyId}/ - Register mapping — Add story → Figma node in
.storybook/visual-testing/figma-mapping.ts - Run visual tests —
pnpm --filter storybook visual:test(Storybook must be running) - Review diffs — Open the Pixel Perfect panel in Storybook to see diff % and diff overlay images
- Iterate — Fix CSS until diff is within threshold (default: 0.5%)
- Promote baselines — Click “Baseline” in the panel to capture the current state as regression baseline
- Commit baselines —
__figma_baselines__/and__visual_snapshots__/are version-controlled
Visual test checklist:
- Figma baselines fetched for desktop and mobile viewports
- Figma mapping registered in
figma-mapping.ts -
pnpm --filter storybook visual:testpasses (diff within threshold) - Regression baselines promoted and committed
- Pixel Perfect panel shows all tests passing
Step 9: Write Tests
Section titled “Step 9: Write Tests”import { describe, it, expect } from 'vitest';import { mapHeroSlider } from './HeroSlider.mapper';
describe('mapHeroSlider', () => { it('maps API response to component props', () => { const apiResponse = { /* mock Umbraco element */ }; const props = mapHeroSlider(apiResponse); expect(props.title).toBe('Expected Title'); expect(props.slides).toHaveLength(2); });
it('defaults autoplay to true when not specified', () => { const apiResponse = { properties: { slides: [] } }; const props = mapHeroSlider(apiResponse); expect(props.autoplay).toBe(true); });});Step 10: PR Review
Section titled “Step 10: PR Review”PR checklist (max 400 lines changed):
- Linked to Zoho Project task
- All files follow naming convention
- No hardcoded values
- Stories render in all themes
- Tests pass (
pnpm test) - Pixel Perfect visual tests pass (
pnpm --filter storybook visual:test) - Figma baselines and regression baselines committed (if new module)
- ESLint + TypeScript clean
- Conventional commit message (
feat: Add M05 Hero Slider module)
4. Common Pitfalls
Section titled “4. Common Pitfalls”| Pitfall | Solution |
|---|---|
Hardcoded #1a365d instead of var(--color-primary) | Always use CSS variable tokens |
| Forgot mobile image variant | Every image module needs imageDesktop + imageMobile |
| Made everything a Client Component | Default to Server Component. Only add 'use client' when interactivity requires it |
| Skipped Storybook stories | Stories are mandatory for every module — they drive visual regression tests |
| Wrote mapper without types | Always define .types.ts first, then implement mapper against those types |
| Nested modules inside modules | Modules compose UI components only. Never import one module inside another |
5. Figma-to-CSS Token Mapping Quick Reference
Section titled “5. Figma-to-CSS Token Mapping Quick Reference”| Figma Property | CSS Variable Pattern | Example |
|---|---|---|
| Fill color (primary) | var(--color-primary) | background-color: var(--color-primary) |
| Text color | var(--color-text) | color: var(--color-text) |
| Font family (heading) | var(--font-heading) | font-family: var(--font-heading) |
| Font size | var(--text-{size}) | font-size: var(--text-2xl) |
| Spacing / padding | var(--space-{n}) | padding: var(--space-8) |
| Border radius | var(--radius-{size}) | border-radius: var(--radius-md) |
| Box shadow | var(--shadow-{size}) | box-shadow: var(--shadow-md) |
| Transition | var(--transition-{speed}) | transition: background-color var(--transition-normal) |