Files
pos-system/microservices/.agent/skills/pencil-design/references/CODE_CONVERSION.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

13 KiB

Code Conversion Reference

Convert Pencil designs to production code (HTML/CSS/React/Blazor).

Overview

This guide covers converting .pen files to production code with proper structure, responsive design, and design system integration.

Element Mapping

Pencil to HTML/CSS

Pencil Element HTML CSS Layout Notes
frame (layout:vertical) <div> flex-direction: column Flexbox container
frame (layout:horizontal) <div> flex-direction: row Flexbox container
frame (layout:none) <div> position: relative Free positioning
text <p>, <h1>-<h6>, <span> - Based on fontSize
rectangle <div> - With background
ellipse <div> border-radius: 50% Circular shape
icon_font <i class="lucide-{name}"> - Lucide icons

Pencil to React/Blazor

Pencil React Blazor
frame <div className=""> <div class="">
text <p>{content}</p> <p>@content</p>
icon_font <Icon name={iconName} /> <i class="lucide-@iconName"></i>
Component ref <Button> <Button />

Property Mapping

Layout Properties → CSS

// Pencil layout properties
{
  "layout": "vertical",
  "gap": 16,
  "padding": [24, 32],
  "justifyContent": "center",
  "alignItems": "center"
}

// CSS output
.container {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 24px 32px;
  justify-content: center;
  align-items: center;
}

Size Properties → CSS

// Fixed size
{ "width": 400, "height": 200 }
// CSS
.element { width: 400px; height: 200px; }

// Fill container
{ "width": "fill_container", "height": 200 }
// CSS
.element { width: 100%; height: 200px; }

Color Properties → CSS

// Solid color
{ "fill": "#FF5C00" }
// CSS
.element { background: #FF5C00; }

// Design token
{ "fill": "$bg-page" }
// CSS
.element { background: var(--bg-page); }

// Gradient
{
  "fill": {
    "type": "gradient",
    "colors": [
      {"color": "#FF5C00", "position": 0},
      {"color": "#FF8A4C", "position": 1}
    ]
  }
}
// CSS
.element {
  background: linear-gradient(180deg, #FF5C00 0%, #FF8A4C 100%);
}

Border Properties → CSS

// Corner radius
{ "cornerRadius": 10 }
// CSS
.element { border-radius: 10px; }

// Stroke
{ "stroke": "$border-default", "strokeWidth": 2 }
// CSS
.element { border: 2px solid var(--border-default); }

Conversion Workflow

Step 1: Extract Design Tokens

import pencilData from './design.pen';

// Extract variables
const tokens = pencilData.variables;

// Generate CSS Variables
const cssTokens = Object.entries(tokens)
  .map(([key, value]) => {
    if (value.type === 'color') {
      return `  --${key}: ${value.value};`;
    }
    if (value.type === 'number') {
      return `  --${key}: ${value.value}px;`;
    }
    return `  --${key}: ${value.value};`;
  })
  .join('\n');

console.log(`:root {\n${cssTokens}\n}`);

Output:

:root {
  --bg-page: #0A0A0B;
  --text-primary: #FFFFFF;
  --orange-primary: #FF5C00;
  --space-4: 16px;
  --radius-md: 10px;
}

Step 2: Convert Layout Structure

function convertElement(element) {
  if (element.type === 'frame') {
    return convertFrame(element);
  }
  if (element.type === 'text') {
    return convertText(element);
  }
  if (element.type === 'icon_font') {
    return convertIcon(element);
  }
  if (element.type === 'ref') {
    return convertRef(element);
  }
}

function convertFrame(frame) {
  const className = frame.name.toLowerCase().replace(/\s+/g, '-');
  const children = frame.children?.map(convertElement).join('\n') || '';
  
  return `<div class="${className}">\n${children}\n</div>`;
}

function convertText(text) {
  const tag = getTextTag(text.fontSize);
  return `<${tag}>${text.content}</${tag}>`;
}

function getTextTag(fontSize) {
  if (fontSize >= 32) return 'h1';
  if (fontSize >= 24) return 'h2';
  if (fontSize >= 18) return 'h3';
  if (fontSize >= 16) return 'h4';
  return 'p';
}

function convertIcon(icon) {
  return `<i class="lucide-${icon.iconFontName}"></i>`;
}

Step 3: Generate CSS Classes

function generateCSS(element, className) {
  const styles = [];
  
  // Layout
  if (element.layout) {
    styles.push('display: flex');
    styles.push(`flex-direction: ${element.layout === 'vertical' ? 'column' : 'row'}`);
  }
  
  // Gap
  if (element.gap) {
    styles.push(`gap: ${element.gap}px`);
  }
  
  // Padding
  if (element.padding) {
    const padding = Array.isArray(element.padding)
      ? `${element.padding[0]}px ${element.padding[1]}px`
      : `${element.padding}px`;
    styles.push(`padding: ${padding}`);
  }
  
  // Size
  if (element.width) {
    const width = element.width === 'fill_container' ? '100%' : `${element.width}px`;
    styles.push(`width: ${width}`);
  }
  if (element.height) {
    styles.push(`height: ${element.height}px`);
  }
  
  // Fill (background)
  if (element.fill) {
    if (element.fill.startsWith('$')) {
      // Design token
      const token = element.fill.substring(1);
      styles.push(`background: var(--${token})`);
    } else if (typeof element.fill === 'object' && element.fill.type === 'gradient') {
      // Gradient
      const gradient = convertGradient(element.fill);
      styles.push(`background: ${gradient}`);
    } else {
      // Solid color
      styles.push(`background: ${element.fill}`);
    }
  }
  
  // Border radius
  if (element.cornerRadius) {
    styles.push(`border-radius: ${element.cornerRadius}px`);
  }
  
  // Alignment
  if (element.justifyContent) {
    const value = element.justifyContent.replace('_', '-');
    styles.push(`justify-content: ${value}`);
  }
  if (element.alignItems) {
    styles.push(`align-items: ${element.alignItems}`);
  }
  
  return `.${className} {\n  ${styles.join(';\n  ')};\n}`;
}

function convertGradient(fill) {
  const colors = fill.colors
    .map(c => `${c.color} ${c.position * 100}%`)
    .join(', ');
  return `linear-gradient(180deg, ${colors})`;
}

Best Practices

1. Semantic HTML

<!-- ❌ BAD: All divs -->
<div class="page">
  <div class="header">...</div>
  <div class="content">...</div>
  <div class="footer">...</div>
</div>

<!-- ✅ GOOD: Semantic elements -->
<main class="page">
  <header class="header">...</header>
  <section class="content">...</section>
  <footer class="footer">...</footer>
</main>

2. CSS Variables for Tokens

/* ❌ BAD: Hardcoded colors */
.button {
  background: #FF5C00;
  color: #FFFFFF;
}

/* ✅ GOOD: CSS variables */
.button {
  background: var(--orange-primary);
  color: var(--text-primary);
}

3. Mobile-First Responsive

/* ✅ GOOD: Mobile-first */
.container {
  padding: 16px;
  flex-direction: column;
}

@media (min-width: 768px) {
  .container {
    padding: 24px;
    flex-direction: row;
  }
}

@media (min-width: 1440px) {
  .container {
    padding: 80px 120px;
  }
}

4. Component Structure

// ✅ GOOD: Match Pencil component hierarchy
// Atom/Button/Primary/Default → Button.jsx

function Button({ children, variant = 'primary', state = 'default' }) {
  return (
    <button className={`button button--${variant} button--${state}`}>
      {children}
    </button>
  );
}

// CSS
.button {
  /* Base styles */
}
.button--primary {
  background: var(--orange-primary);
}
.button--default {
  /* Default state */
}
.button--hover {
  background: var(--orange-light);
}

Framework-Specific Examples

React Component

import React from 'react';
import './HeroSection.css';

export function HeroSection({ title, subtitle, ctaText }) {
  return (
    <section className="hero-section">
      <div className="hero-content">
        <h1 className="hero-title">{title}</h1>
        <p className="hero-subtitle">{subtitle}</p>
        <button className="hero-cta">{ctaText}</button>
      </div>
    </section>
  );
}
/* HeroSection.css */
.hero-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 80px 120px;
  background: var(--bg-page);
}

.hero-content {
  max-width: 800px;
  text-align: center;
}

.hero-title {
  font-size: 48px;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 16px;
}

.hero-subtitle {
  font-size: 18px;
  color: var(--text-secondary);
  margin-bottom: 32px;
}

.hero-cta {
  padding: 16px 32px;
  background: var(--orange-primary);
  color: var(--text-primary);
  border: none;
  border-radius: 10px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
}

.hero-cta:hover {
  background: var(--orange-light);
}

Blazor Component

@* HeroSection.razor *@
<section class="hero-section">
    <div class="hero-content">
        <h1 class="hero-title">@Title</h1>
        <p class="hero-subtitle">@Subtitle</p>
        <button class="hero-cta" @onclick="OnCtaClick">@CtaText</button>
    </div>
</section>

@code {
    [Parameter] public string Title { get; set; } = "";
    [Parameter] public string Subtitle { get; set; } = "";
    [Parameter] public string CtaText { get; set; } = "";
    [Parameter] public EventCallback OnCtaClick { get; set; }
}

Tailwind CSS

From design tokens to Tailwind config:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        'bg-page': '#0A0A0B',
        'bg-surface': '#1A1A1C',
        'text-primary': '#FFFFFF',
        'orange-primary': '#FF5C00',
      },
      spacing: {
        '1': '4px',
        '2': '8px',
        '4': '16px',
        '6': '24px',
      },
      borderRadius: {
        'sm': '6px',
        'md': '10px',
        'lg': '16px',
      }
    }
  }
}
// Component with Tailwind
<section className="flex flex-col items-center px-30 py-20 bg-bg-page">
  <div className="max-w-3xl text-center">
    <h1 className="text-5xl font-bold text-text-primary mb-4">
      {title}
    </h1>
    <p className="text-lg text-text-secondary mb-8">
      {subtitle}
    </p>
    <button className="px-8 py-4 bg-orange-primary text-text-primary rounded-md hover:bg-orange-light">
      {ctaText}
    </button>
  </div>
</section>

Conversion Tools

Automated Conversion Script

import fs from 'fs';

function convertPencilToHTML(pencilFile) {
  const data = JSON.parse(fs.readFileSync(pencilFile, 'utf-8'));
  
  // Extract main page
  const mainPage = data.children[0];
  
  // Generate HTML
  const html = convertElement(mainPage);
  
  // Generate CSS
  const css = generateCSSForAll(mainPage);
  
  // Generate design tokens
  const tokens = generateTokensCSS(data.variables);
  
  return {
    html,
    css: `${tokens}\n\n${css}`,
    tokens: data.variables
  };
}

// Example usage
const result = convertPencilToHTML('design.pen');
fs.writeFileSync('output.html', result.html);
fs.writeFileSync('output.css', result.css);
console.log('✅ Conversion complete!');

Common Patterns

Navigation Bar

<nav class="navbar">
  <div class="navbar-logo">
    <img src="logo.svg" alt="Logo" />
  </div>
  <ul class="navbar-menu">
    <li><a href="#home">Home</a></li>
    <li><a href="#features">Features</a></li>
    <li><a href="#pricing">Pricing</a></li>
  </ul>
  <button class="navbar-cta">Sign Up</button>
</nav>
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 120px;
  background: var(--bg-surface);
}

.navbar-menu {
  display: flex;
  gap: 32px;
  list-style: none;
}

.navbar-menu a {
  color: var(--text-primary);
  text-decoration: none;
}

Card Component

<div class="card">
  <div class="card-header">
    <i class="lucide-star"></i>
    <h3>Feature Title</h3>
  </div>
  <p class="card-description">
    Feature description text goes here.
  </p>
  <a href="#" class="card-link">Learn More →</a>
</div>
.card {
  display: flex;
  flex-direction: column;
  padding: 24px;
  background: var(--bg-surface);
  border-radius: var(--radius-md);
  gap: 16px;
}

.card-header {
  display: flex;
  align-items: center;
  gap: 12px;
}

.card-description {
  color: var(--text-secondary);
  line-height: 1.6;
}

.card-link {
  color: var(--orange-primary);
  text-decoration: none;
  font-weight: 600;
}

Resources