Migrate
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
# 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
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:**
|
||||
```css
|
||||
:root {
|
||||
--bg-page: #0A0A0B;
|
||||
--text-primary: #FFFFFF;
|
||||
--orange-primary: #FF5C00;
|
||||
--space-4: 16px;
|
||||
--radius-md: 10px;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Convert Layout Structure
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```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
|
||||
|
||||
```css
|
||||
/* ❌ BAD: Hardcoded colors */
|
||||
.button {
|
||||
background: #FF5C00;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* ✅ GOOD: CSS variables */
|
||||
.button {
|
||||
background: var(--orange-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Mobile-First Responsive
|
||||
|
||||
```css
|
||||
/* ✅ 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
|
||||
|
||||
```jsx
|
||||
// ✅ 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
|
||||
|
||||
```jsx
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* 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
|
||||
|
||||
```razor
|
||||
@* 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:
|
||||
|
||||
```javascript
|
||||
// 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',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```jsx
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```html
|
||||
<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>
|
||||
```
|
||||
|
||||
```css
|
||||
.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
|
||||
|
||||
```html
|
||||
<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>
|
||||
```
|
||||
|
||||
```css
|
||||
.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
|
||||
|
||||
- Back to [Pencil Design Skill](../SKILL.md)
|
||||
- [File Format Reference](./FILE_FORMAT.md)
|
||||
- [Build System Reference](./BUILD_SYSTEM.md)
|
||||
- [Atomic Design Reference](./ATOMIC_DESIGN.md)
|
||||
- [Tailwind Design System](../tailwind-design-system/SKILL.md)
|
||||
- [React UI Components](../react-ui-components/SKILL.md)
|
||||
- [Blazor Theme Patterns](../blazor-theme-patterns/SKILL.md)
|
||||
- [MAUI Branding Expert](../maui-branding-expert/SKILL.md)
|
||||
- [Swift UI Components](../swift-ui-components/SKILL.md)
|
||||
Reference in New Issue
Block a user