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

14 KiB
Raw Blame History

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
  2. Ignoring Referencing System
  3. Modifying Source Files Directly
  4. Direct Value Usage
  5. Missing Width for Centering
  6. Font Weight Variables Type Error
  7. Empty Variables Block
  8. Token References in Numeric Fields
  9. Using Emoji Text for Icons
  10. Build Script Not Scanning Subdirectories
  11. Components at Root Level
  12. Wrap Property Not Working Reliably
  13. Dialog/Frame 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.

/* ❌ 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.

// ❌ 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.

# ❌ 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).

// ❌ 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.

// ❌ 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.

// ❌ 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:

"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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

# ❌ 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:

# 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.

// ❌ 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.

// ❌ 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

// ❌ 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

// ❌ 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