# 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