13 KiB
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;
}