529 lines
14 KiB
Markdown
529 lines
14 KiB
Markdown
# 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
|