14 KiB
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
- Hardcoding Colors
- Ignoring Referencing System
- Modifying Source Files Directly
- Direct Value Usage
- Missing Width for Centering
- Font Weight Variables Type Error
- Empty Variables Block
- Token References in Numeric Fields
- Using Emoji Text for Icons
- Build Script Not Scanning Subdirectories
- Components at Root Level
- Wrap Property Not Working Reliably
- 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: verticalcho 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 usetype: "string" variablesblock contains all used tokens- No
$tokensin numeric fields (fontSize, cornerRadius, etc.) - Icons use
icon_fontnot 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.pento validate JSON syntax - Run
python3 scripts/build.py --librarybefore 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:
- Fixed height too small: Dialog has
height: 480but content requires 600px - Footer outside parent frame: Footer is a sibling at root level instead of child of dialog
- 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 usetype: "string" variablesblock contains all used tokens- No
$tokensin numeric fields (fontSize, cornerRadius, etc.) - Icons use
icon_fontnot 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.pento validate JSON syntax - Run
python3 scripts/build.py --librarybefore testing