refactor: Reorganize project structure by moving Pencil files to src/ and introduce build tooling.

This commit is contained in:
Ho Ngoc Hai
2026-01-31 19:46:21 +07:00
parent 1e1a8c249d
commit 2099d0469c
9 changed files with 586 additions and 2 deletions

View File

@@ -0,0 +1,160 @@
---
description: Build Pencil Design files - Standard (component linking) hoặc Monolithic (merged)
---
# Pencil Design Build Workflow
## Overview
Build system cho Pencil Design files với 2 modes:
- **Standard**: Build individual pages với component linking (injecting refs)
- **Monolithic**: Merge tất cả pages + component library thành 1 file
## Prerequisites
- Python 3.11+
- `jq` (optional, for validation)
---
## Build Modes
### 1. Monolithic Build (Recommended)
Merge tất cả pages và component library thành 1 file `tPOS.pen`.
```bash
# Build monolithic file
// turbo
python3 scripts/build.py --monolithic
# Custom output filename
python3 scripts/build.py --monolithic --output myDesign.pen
# Alternative syntax
python3 scripts/build.py --mode monolithic
python3 scripts/build.py -m # short form
```
**Output:**
- File: `tPOS.pen` (project root)
- Chứa: Desktop page + Mobile page + Tablet page + Component Library page
- Size: ~326KB
- Structure: 4 top-level frames
**Use case:**
- ✅ Mở toàn bộ design trong Pencil
- ✅ Share với designer khác
- ✅ Archive/backup complete design
- ✅ Export sang tool khác
---
### 2. Standard Build (Component Linking)
Build individual pages với component refs được inject từ library.
```bash
# Build separate pages
// turbo
python3 scripts/build.py
# Alternative
python3 scripts/build.py --mode standard
```
**Output:**
- Directory: `build/`
- Files: `tPOS-desktop-home.pen`, `tPOS-mobile-home.pen`, `tPOS-tablet-home.pen`
- Mỗi file chứa injected components từ `tPOS-ui-kit.pen`
**Use case:**
- ✅ Development workflow với component refs
- ✅ Testing component linking system
- ✅ Smaller individual files
---
## Validation
```bash
# Verify output structure
jq '{version, childrenCount: (.children | length), childrenNames: [.children[].name]}' tPOS.pen
# Check file size
ls -lh tPOS.pen
# Validate JSON syntax
jq empty tPOS.pen && echo "✅ Valid JSON"
```
---
## Configuration
Edit `pencil.config.json`:
```json
{
"version": "1.0",
"sourceDir": "src",
"buildDir": "build",
"componentLibrary": "src/components/tPOS-ui-kit.pen",
"buildOptions": {
"minify": false,
"validateAfterBuild": true,
"preserveComments": true
}
}
```
---
## Troubleshooting
### Error: Component library not found
```bash
# Check path in config
jq '.componentLibrary' pencil.config.json
# Verify file exists
ls -la src/components/*.pen
```
### Error: No .pen files found
```bash
# Check source directory
ls -la src/pages/*.pen
```
### Python syntax error
```bash
# Use Python 3.11+
python3 --version
# Ensure using python3, not python
which python3
```
---
## Examples
### Quick rebuild
```bash
# Rebuild monolithic after editing pages
// turbo
python3 scripts/build.py -m
```
### Build both modes
```bash
# Standard build
python3 scripts/build.py
# Monolithic build
python3 scripts/build.py -m
```
### Compare outputs
```bash
# Check differences
jq '.children | length' tPOS.pen
jq '.children | length' build/tPOS-desktop-home.pen
```

View File

@@ -158,3 +158,53 @@ All files include these color tokens:
---
**Questions?** Reference the original `implementation_plan.md` in the brain directory.
---
## Build System
### Quick Start
```bash
# Build monolithic file (merge all pages)
python3 scripts/build.py --monolithic
# Output: tPOS.pen (326KB, 4 frames)
```
### Build Modes
**1. Monolithic Build** ⭐ (Recommended)
```bash
python3 scripts/build.py --monolithic
# or
python3 scripts/build.py -m
```
**Output:**
- File: `tPOS.pen` (project root)
- Contains: Desktop + Mobile + Tablet + Component Library
- Use: Open complete design in Pencil, share, archive
**2. Standard Build** (Component Linking)
```bash
python3 scripts/build.py
```
**Output:**
- Directory: `build/`
- Files: Individual pages with injected components
- Use: Development workflow, testing component refs
### Configuration
Edit `pencil.config.json`:
- `componentLibrary`: Path to ui-kit.pen
- `sourceDir`: Source pages directory
- `buildDir`: Output for standard build
### Help
```bash
python3 scripts/build.py --help
```

View File

@@ -0,0 +1,11 @@
{
"version": "1.0",
"sourceDir": "src",
"buildDir": "build",
"componentLibrary": "src/components/tPOS-ui-kit.pen",
"buildOptions": {
"minify": false,
"validateAfterBuild": true,
"preserveComments": true
}
}

363
pencil-design/scripts/build.py Executable file
View File

@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
Pencil Design Build System
Merges component library into page files
"""
import json
import sys
from pathlib import Path
from typing import Dict, Any, List
class PencilBuilder:
def __init__(self, config_path: str = "pencil.config.json"):
"""Initialize builder with config"""
# Get script directory and project root
self.script_dir = Path(__file__).parent.absolute()
self.project_root = self.script_dir.parent
# Load config from project root
config_full_path = self.project_root / config_path
self.config = self.load_config(config_full_path)
self.components = {}
def load_config(self, path: Path) -> Dict[str, Any]:
"""Load build configuration"""
if path.exists():
with open(path, 'r') as f:
return json.load(f)
# Default config
return {
"sourceDir": "src",
"buildDir": "build",
"componentLibrary": "src/components/ui-kit.pen",
"buildOptions": {
"validateAfterBuild": True
}
}
def load_component_library(self) -> Dict[str, Any]:
"""Load all components from ui-kit.pen"""
lib_path = self.project_root / self.config['componentLibrary']
if not lib_path.exists():
print(f"❌ Component library not found: {lib_path}")
sys.exit(1)
with open(lib_path, 'r') as f:
data = json.load(f)
# Extract components
components = {}
def extract_components(children: List[Dict], prefix: str = ""):
"""Recursively extract reusable components"""
for child in children:
name = child.get('name', '')
# Store component by name
if name:
full_name = f"{prefix}/{name}" if prefix else name
components[full_name] = child
# Recurse into children
if 'children' in child:
extract_components(child['children'], full_name)
extract_components(data.get('children', []))
# Also store design variables
self.variables = data.get('variables', {})
print(f"✓ Loaded {len(components)} components from library")
return components
def process_component_refs(self, children: List[Dict], injected_components: List[Dict]) -> None:
"""Process component_ref and replace with proper Pencil refs"""
for i, child in enumerate(children):
if child.get('type') == 'component_ref':
# Get component name from ref
component_path = child.get('component', '')
# Extract component name (after #)
if '#' in component_path:
component_name = component_path.split('#')[1]
else:
component_name = component_path
# Find matching component
component = None
for comp_path, comp_obj in self.components.items():
if comp_path.endswith(component_name):
component = comp_obj
break
if component:
# Add component to injected list (if not already added)
comp_id = component.get('id')
if not any(c.get('id') == comp_id for c in injected_components):
injected_components.append(component.copy())
# Replace component_ref with proper ref
overrides = child.get('overrides', {})
children[i] = {
'type': 'ref',
'ref': comp_id,
'name': child.get('name', f'ref_{comp_id}'),
'descendants': overrides
}
# Copy position/size if specified
for key in ['x', 'y', 'width', 'height']:
if key in child:
children[i][key] = child[key]
else:
print(f"⚠️ Component not found: {component_name}")
# Recurse into children
if 'children' in child:
self.process_component_refs(child['children'], injected_components)
def build_page(self, page_path: Path, output_path: Path) -> None:
"""Build a single page file"""
print(f"Building {page_path.name}...")
# Load page
with open(page_path, 'r') as f:
page = json.load(f)
# Collect injected components
injected_components = []
# Process all component_ref in the page
self.process_component_refs(page.get('children', []), injected_components)
# Inject components at the beginning of children array
# (Pencil expects reusable components to be defined before usage)
page['children'] = injected_components + page['children']
# Ensure design variables are included
if not page.get('variables'):
page['variables'] = self.variables
# Write output
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(page, f, indent=2)
print(f"{len(injected_components)} components injected")
print(f" ✓ Saved to {output_path}")
def validate_output(self, file_path: Path) -> bool:
"""Validate built file"""
try:
with open(file_path, 'r') as f:
data = json.load(f)
# Check structure
assert 'version' in data, "Missing version field"
assert 'children' in data, "Missing children array"
assert 'variables' in data, "Missing variables object"
return True
except Exception as e:
print(f"❌ Validation failed: {e}")
return False
def build_monolithic(self, output_filename: str = "tPOS.pen") -> None:
"""Build monolithic file merging all pages and component library"""
print("🔨 Building Monolithic File")
print("=" * 50)
print(f"📁 Project root: {self.project_root}")
print()
# Load component library
lib_path = self.project_root / self.config['componentLibrary']
if not lib_path.exists():
print(f"❌ Component library not found: {lib_path}")
sys.exit(1)
with open(lib_path, 'r') as f:
lib_data = json.load(f)
# Get variables from component library
variables = lib_data.get('variables', {})
# Find all pages in src/pages/
src_pages = self.project_root / self.config['sourceDir'] / 'pages'
if not src_pages.exists():
print(f"❌ Source pages directory not found: {src_pages}")
sys.exit(1)
pages = sorted(src_pages.glob('*.pen'))
if not pages:
print(f"⚠️ No .pen files found in {src_pages}")
return
print(f"Found {len(pages)} page(s) to merge:")
for page in pages:
print(f" - {page.name}")
print()
# Create monolithic structure
monolithic = {
"version": "2.6",
"children": [],
"variables": variables
}
# Load and add all pages
current_x = 0 # Start position
page_spacing = 470 # Spacing between pages
for page_path in pages:
with open(page_path, 'r') as f:
page_data = json.load(f)
# Extract the main page frame (usually first child or children array)
page_children = page_data.get('children', [])
# Add each top-level frame to monolithic children
for child in page_children:
# Set position for this page
child['x'] = current_x
child['y'] = 0
# Calculate next position based on this page's width
page_width = child.get('width', 1440)
if isinstance(page_width, str): # Handle "fill_container"
page_width = 1440 # Default width
monolithic['children'].append(child)
# Update position for next page
current_x += page_width + page_spacing
print(f" ✓ Merged {page_path.name} ({len(page_children)} frame(s)) at x={child.get('x', 0)}")
# Add component library page at the end
lib_children = lib_data.get('children', [])
for child in lib_children:
child['x'] = current_x
child['y'] = 0
monolithic['children'].append(child)
print(f" ✓ Merged component library ({len(lib_children)} frame(s)) at x={current_x}")
print()
# Write output to project root
output_path = self.project_root / output_filename
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(monolithic, f, indent=2, ensure_ascii=False)
# Validate
if self.validate_output(output_path):
print(f" ✓ Validation passed")
print()
print("=" * 50)
print(f"✅ Monolithic build complete!")
print(f"📄 Output: {output_path.absolute()}")
print(f"📊 Total frames: {len(monolithic['children'])}")
print(f"🎨 Design tokens: {len(variables)}")
def build_all(self) -> None:
"""Build all pages"""
print("🔨 Pencil Design Build System")
print("=" * 50)
print(f"📁 Project root: {self.project_root}")
print()
# Load component library
self.components = self.load_component_library()
# Find all pages in src/pages/ (relative to project root)
src_pages = self.project_root / self.config['sourceDir'] / 'pages'
build_dir = self.project_root / self.config['buildDir']
if not src_pages.exists():
print(f"❌ Source pages directory not found: {src_pages}")
sys.exit(1)
pages = list(src_pages.glob('*.pen'))
if not pages:
print(f"⚠️ No .pen files found in {src_pages}")
return
print(f"\nBuilding {len(pages)} page(s)...\n")
# Build each page
built_count = 0
for page_path in pages:
output_path = build_dir / page_path.name
self.build_page(page_path, output_path)
# Validate if enabled
if self.config['buildOptions'].get('validateAfterBuild'):
if self.validate_output(output_path):
print(f" ✓ Validation passed")
built_count += 1
else:
built_count += 1
print()
print("=" * 50)
print(f"✅ Build complete! ({built_count}/{len(pages)} pages)")
print(f"📂 Output: {build_dir.absolute()}")
def main():
"""Main entry point"""
import argparse
parser = argparse.ArgumentParser(
description='Pencil Design Build System',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Build individual pages with component linking
python build.py
# Build monolithic file (merge all pages)
python build.py --monolithic
python build.py --mode monolithic
# Build monolithic with custom output name
python build.py --monolithic --output myDesign.pen
"""
)
parser.add_argument(
'--monolithic', '--mono', '-m',
action='store_true',
help='Build monolithic file (merge all pages and component library)'
)
parser.add_argument(
'--mode',
choices=['standard', 'monolithic'],
default='standard',
help='Build mode: standard (separate pages) or monolithic (merged file)'
)
parser.add_argument(
'--output', '-o',
default='tPOS.pen',
help='Output filename for monolithic build (default: tPOS.pen)'
)
args = parser.parse_args()
builder = PencilBuilder()
# Determine build mode
if args.monolithic or args.mode == 'monolithic':
builder.build_monolithic(output_filename=args.output)
else:
builder.build_all()
if __name__ == '__main__':
main()

View File

@@ -4566,7 +4566,7 @@
{
"type": "frame",
"id": "TabPg",
"x": 2410,
"x": 2770,
"y": 0,
"name": "aPOS Tablet Page",
"width": 768,
@@ -6377,7 +6377,7 @@
{
"type": "frame",
"id": "CompLib",
"x": 3710,
"x": 4008,
"y": 0,
"name": "aPOS Component Library",
"width": 1440,