# Pencil Design Pitfalls / Lỗi Thường Gặp

Comprehensive list of common mistakes when working with Pencil `.pen` files and their solutions.

## Table of Contents

1. [Hardcoding Colors](#1-hardcoding-colors)
2. [Ignoring Referencing System](#2-ignoring-referencing-system)
3. [Modifying Source Files Directly](#3-modifying-source-files-directly)
4. [Direct Value Usage](#4-direct-value-usage)
5. [Missing Width for Centering](#5-missing-width-for-centering)
6. [Font Weight Variables Type Error](#6-font-weight-variables-type-error)
7. [Empty Variables Block](#7-empty-variables-block)
8. [Token References in Numeric Fields](#8-token-references-in-numeric-fields)
9. [Using Emoji Text for Icons](#9-using-emoji-text-for-icons)
10. [Build Script Not Scanning Subdirectories](#10-build-script-not-scanning-subdirectories)
11. [Components at Root Level](#11-components-at-root-level)
12. [Wrap Property Not Working Reliably](#12-wrap-property-not-working-reliably)
13. [Dialog/Frame Height Causing Content Overlap](#13-dialogframe-height-causing-content-overlap)

---

## 1. Hardcoding Colors

**Problem**: Using hex codes directly in components (e.g. `#000000`), making theming impossible.

**Solution**: Always use design token variables.

```css
/* ❌ BAD: Hardcoded colors */
background: #0A0A0B;

/* ✅ GOOD: Use CSS variables */
background: var(--bg-page);
```

---

## 2. Ignoring Referencing System

**Problem**: Treating `ref` elements as normal frames, losing link to main component.

**Solution**: Resolve `ref` by looking up the component ID.

```javascript
// ❌ BAD: Skip ref elements
if (element.type === 'ref') return;

// ✅ GOOD: Resolve ref properly
if (element.type === 'ref') {
  const component = findById(element.ref);
  return mergeWithDescendants(component, element.descendants);
}
```

---

## 3. Modifying Source Files Directly

**Problem**: Opening and editing partial files in `src/` without building, causing "Missing Component" errors in Pencil.

**Solution**: Always run build script before opening in Pencil.

```bash
# ❌ BAD: Open source files directly
open src/pages/desktop-home.pen

# ✅ GOOD: Build first
python3 scripts/build.py -m
open tPOS.pen
```

---

## 4. Direct Value Usage

**Problem**: Using numeric values for spacing/fontsize, breaking design system consistency.

**Solution**: Use semantic variables (for colors only - see #8).

```json
// ❌ BAD: Direct color values
{
  "fill": "#FF5C00"
}

// ✅ GOOD: Use color variables
{
  "fill": "$orange-primary"
}
```

---

## 5. Missing Width for Centering

**Problem**: Setting `alignItems: "center"` without explicit width. The frame only takes content width, making horizontal centering ineffective.

**Solution**: Add `width: "fill_container"` to parent frame.

```json
// ❌ BAD: Centering won't work
{
  "type": "frame",
  "layout": "vertical",
  "alignItems": "center",
  "children": [...]
}

// ✅ GOOD: Explicit width enables centering
{
  "type": "frame",
  "width": "fill_container",
  "layout": "vertical",
  "alignItems": "center",
  "children": [...]
}
```

---

## 6. Font Weight Variables Type Error

**Problem**: Defining font-weight variables with `type: "number"` causes Pencil to fail with:
```
Variable 'font-medium' has type 'number' (expected 'string')
```

**Solution**: Font weights MUST use `type: "string"` with quoted values.

```json
// ❌ BAD: Number type
"font-medium": { "type": "number", "value": 500 }

// ✅ GOOD: String type with quoted value
"font-medium": { "type": "string", "value": "500" }
```

**All font weight variables to fix:**
```json
"font-normal": { "type": "string", "value": "400" },
"font-medium": { "type": "string", "value": "500" },
"font-semibold": { "type": "string", "value": "600" },
"font-bold": { "type": "string", "value": "700" }
```

---

## 7. Empty Variables Block

**Problem**: Using token references like `$orange-primary` but leaving `variables: {}` empty. Tokens won't resolve and colors appear wrong.

**Solution**: Always define all used tokens in the file's `variables` block.

```json
// ❌ BAD: Empty variables but using $tokens
{
  "children": [{ "fill": "$orange-primary" }],
  "variables": {}
}

// ✅ GOOD: Define all used tokens
{
  "children": [{ "fill": "$orange-primary" }],
  "variables": {
    "orange-primary": { "type": "color", "value": "#FF5C00" }
  }
}
```

---

## 8. Token References in Numeric Fields

**Problem**: Using token syntax (`$text-sm`) for properties that MUST be numbers: `fontSize`, `fontWeight`, `cornerRadius`, `width`, `height`.

**Solution**: Use actual numbers for numeric properties. Only colors support token refs.

```json
// ❌ BAD: Token in fontSize (won't work)
{ 
  "fontSize": "$text-sm", 
  "fontWeight": "$font-medium",
  "cornerRadius": "$radius-md"
}

// ✅ GOOD: Direct numeric values
{ 
  "fontSize": 14, 
  "fontWeight": "500",
  "cornerRadius": 8
}

// ✅ ALSO OK: Tokens for colors only
{ "fill": "$text-primary" }
```

**Rule of thumb:**
- Color properties (`fill`, `stroke`) → Use `$tokens`
- Numeric properties → Use numbers directly

---

## 9. Using Emoji Text for Icons

**Problem**: Emoji characters in `type: "text"` don't render in Pencil.

**Solution**: Use `type: "icon_font"` with Lucide icons.

```json
// ❌ BAD: Emoji text (won't render)
{ "type": "text", "content": "🍽️", "fontSize": 16 }

// ✅ GOOD: Lucide icon_font
{
  "type": "icon_font",
  "id": "RestaurantIcon",
  "iconFontName": "utensils",
  "iconFontFamily": "lucide",
  "width": 16,
  "height": 16,
  "fill": "#FFFFFF"
}
```

**Common Lucide icon mappings:**

| Emoji | Lucide Name | Use Case |
|-------|-------------|----------|
| 🍽️ | `utensils` | Restaurant |
| 🍸 | `wine` | Bar |
| 🎤 | `mic` | Karaoke |
| 💆 | `sparkles` | Spa |
| ✓ | `check` | Success |
| ✗ | `x` | Close/Error |
| + | `plus` | Add |
| - | `minus` | Remove |
| 🔍 | `search` | Search |
| ⚙️ | `settings` | Settings |
| 👤 | `user` | Profile |
| 🏠 | `home` | Home |
| 💳 | `credit-card` | Payment |
| 🛒 | `shopping-cart` | Cart |

---

## 10. Build Script Not Scanning Subdirectories

**Problem**: Using `glob('*.pen')` only scans top-level files, missing subdirectories like `organisms/vertical-specific/`.

**Solution**: Use `rglob('*.pen')` to scan recursively.

```python
# ❌ BAD: Only top-level files
for pen_file in dir_path.glob('*.pen'):

# ✅ GOOD: Include subdirectories
for pen_file in dir_path.rglob('*.pen'):
```

**In `scripts/build.py`:**
```python
# Line ~367
for pen_file in sorted(dir_path.rglob('*.pen')):
```

---

## 11. Components at Root Level

**Problem**: Placing multiple reusable components at root `children[]` causes them to stack/overlap incorrectly in Pencil.

**Solution**: Wrap in a Showcase frame with layout.

```json
// ❌ BAD: Components at root level (will overlap)
{
  "children": [
    { "name": "Component/A", "reusable": true },
    { "name": "Component/B", "reusable": true }
  ]
}

// ✅ GOOD: Wrapped in Showcase frame
{
  "children": [
    {
      "type": "frame",
      "name": "Component Showcase",
      "width": 1200,
      "fill": "$bg-page",
      "layout": "vertical",
      "gap": 40,
      "padding": 40,
      "children": [
        { "name": "Component/A", "reusable": true },
        { "name": "Component/B", "reusable": true }
      ]
    }
  ]
}
```

---

## 12. Wrap Property Not Working Reliably

**Problem**: Setting `wrap: true` on a frame expects children to wrap to next row when exceeding container width. However, Pencil's wrap behavior is unreliable and often causes overflow instead of wrapping.

**Symptom**: Elements overflow beyond container boundary instead of wrapping.

**Solution**: Use manual rows with explicit horizontal layout instead of relying on wrap.

```json
// ❌ BAD: wrap không hoạt động như mong đợi
{
  "type": "frame",
  "id": "TableGrid",
  "width": "fill_container",
  "layout": "horizontal",
  "wrap": true,
  "gap": 8,
  "children": [
    {"type": "frame", "id": "Table1", "width": 72, "height": 72},
    {"type": "frame", "id": "Table2", "width": 72, "height": 72},
    {"type": "frame", "id": "Table3", "width": 72, "height": 72},
    {"type": "frame", "id": "Table4", "width": 72, "height": 72},
    {"type": "frame", "id": "Table5", "width": 72, "height": 72},
    {"type": "frame", "id": "Table6", "width": 72, "height": 72}
  ]
}

// ✅ GOOD: Manual rows - chắc chắn layout đúng
{
  "type": "frame",
  "id": "TableGrid",
  "width": "fill_container",
  "layout": "vertical",
  "gap": 8,
  "clip": true,
  "children": [
    {
      "type": "frame", 
      "id": "Row1", 
      "width": "fill_container", 
      "gap": 8, 
      "children": [
        {"type": "frame", "id": "Table1", "width": 72, "height": 72},
        {"type": "frame", "id": "Table2", "width": 72, "height": 72},
        {"type": "frame", "id": "Table3", "width": 72, "height": 72}
      ]
    },
    {
      "type": "frame", 
      "id": "Row2", 
      "width": "fill_container", 
      "gap": 8, 
      "children": [
        {"type": "frame", "id": "Table4", "width": 72, "height": 72},
        {"type": "frame", "id": "Table5", "width": 72, "height": 72},
        {"type": "frame", "id": "Table6", "width": 72, "height": 72}
      ]
    }
  ]
}
```

**Tip**: Khi cần grid layout trong container có chiều rộng cố định:
- Dùng `layout: vertical` cho container chính
- Tạo các row con với `layout: horizontal` (mặc định)
- Thêm `clip: true` để tránh overflow

---

## Quick Checklist / Checklist Nhanh

Before committing `.pen` files:

- [ ] All `font-*` variables use `type: "string"`
- [ ] `variables` block contains all used tokens
- [ ] No `$tokens` in numeric fields (fontSize, cornerRadius, etc.)
- [ ] Icons use `icon_font` not emoji text
- [ ] Build script uses `rglob()` for subdirectories
- [ ] Components wrapped in layout frames, not at root level
- [ ] **Avoid `wrap: true` - use manual rows instead**
- [ ] **Check dialog height vs content height (avoid footer overlap)**
- [ ] Run `jq empty file.pen` to validate JSON syntax
- [ ] Run `python3 scripts/build.py --library` before testing

---

## 13. Dialog/Frame Height Causing Content Overlap

**Problem**: Footer buttons or bottom sections overlap with content above because the dialog/frame `height` is too small to contain all children. This is especially common in dialogs with many form fields.

**Symptoms**:
- Footer buttons covering input fields or cards
- Bottom section text cut off or hidden
- Content appears "squeezed" at the bottom

**Root causes**:
1. **Fixed height too small**: Dialog has `height: 480` but content requires 600px
2. **Footer outside parent frame**: Footer is a sibling at root level instead of child of dialog
3. **Content expands but container doesn't**: Using `height: "fill_container"` on content but parent has fixed small height

**Solution 1: Calculate correct height**

```json
// ❌ BAD: Height không đủ cho content
{
  "type": "frame",
  "id": "CustomerEditDialog",
  "height": 480,  // Quá nhỏ!
  "layout": "vertical",
  "children": [
    { "id": "Header", "height": 80 },      // 80px
    { "id": "Content", "height": "fill" },  // 6 fields × 70px = 420px
    { "id": "Footer", "height": 84 }        // 84px
    // Total: 80 + 420 + 84 = 584px (vượt 480px!)
  ]
}

// ✅ GOOD: Height đủ chứa tất cả content
{
  "type": "frame",
  "id": "CustomerEditDialog",
  "height": 660,  // Đủ không gian
  "layout": "vertical",
  "children": [...]
}
```

**Height calculation formula:**
```
Total Height = Header + Content + Footer + Padding

Where:
- Header: ~80-100px (icon + title)
- Content: (field count × field height) + ((field count - 1) × gap)
- Footer: ~84-100px (buttons + padding)
- Padding: header/content/footer padding combined

Example (6 fields):
- Header: 80px
- Content: 6 × 70 + 5 × 18 = 420 + 90 = 510px  
- Footer: 84px
- Padding: ~40px
- Total: 80 + 510 + 84 + 40 = 714px → Use 720-760px
```

**Solution 2: Ensure footer is INSIDE dialog frame**

```json
// ❌ BAD: Footer ở cùng cấp root với Dialog (sẽ float ngoài)
{
  "children": [
    {
      "type": "frame",
      "id": "CustomerAddDialog",
      "height": 600,
      "children": [
        { "id": "Header" },
        { "id": "Content" }
      ]  // Footer thiếu!
    },
    {
      "type": "frame",
      "id": "AddFooter",  // ← NẰM NGOÀI dialog!
      "y": 600,           // ← Absolute positioning = BAD
      "children": [...]
    }
  ]
}

// ✅ GOOD: Footer là child của Dialog
{
  "children": [
    {
      "type": "frame",
      "id": "CustomerAddDialog",
      "height": 680,
      "layout": "vertical",
      "children": [
        { "id": "Header" },
        { "id": "Content", "height": "fill_container" },
        { "id": "AddFooter" }  // ← BÊN TRONG dialog!
      ]
    }
  ]
}
```

**Quick checks khi tạo dialog:**

| Check | How to verify |
|-------|---------------|
| Footer inside dialog? | Footer phải là child cuối cùng của dialog frame |
| No absolute y position? | Footer không có `y: xxx` |
| Height đủ? | Height ≥ Header + Content + Footer + Gaps |
| Using vertical layout? | Dialog có `layout: "vertical"` |

**Common dialog sizes reference:**

| Dialog type | Recommended height |
|-------------|-------------------|
| Simple confirm (title + 2 buttons) | 200-250px |
| Form 3 fields | 400-450px |
| Form 5 fields | 550-620px |
| Form 6+ fields | 680-760px |
| Complex with cards/lists | 700-800px |

---

## Quick Checklist / Checklist Nhanh

Before committing `.pen` files:

- [ ] All `font-*` variables use `type: "string"`
- [ ] `variables` block contains all used tokens
- [ ] No `$tokens` in numeric fields (fontSize, cornerRadius, etc.)
- [ ] Icons use `icon_font` not emoji text
- [ ] Build script uses `rglob()` for subdirectories
- [ ] Components wrapped in layout frames, not at root level
- [ ] **Avoid `wrap: true` - use manual rows instead**
- [ ] **Dialog height accommodates all children (header + content + footer)**
- [ ] **Footer is INSIDE dialog frame, not at root level**
- [ ] Run `jq empty file.pen` to validate JSON syntax
- [ ] Run `python3 scripts/build.py --library` before testing
