refactor: Reorganize project structure by moving Pencil files to src/ and introduce build tooling.
This commit is contained in:
160
.agent/workflows/pencil-build.md
Normal file
160
.agent/workflows/pencil-build.md
Normal 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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
|
||||
11
pencil-design/pencil.config.json
Normal file
11
pencil-design/pencil.config.json
Normal 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
363
pencil-design/scripts/build.py
Executable 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()
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user