From 2099d0469c504f29a8920291252db5c64132c5e7 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 31 Jan 2026 19:46:21 +0700 Subject: [PATCH] refactor: Reorganize project structure by moving Pencil files to `src/` and introduce build tooling. --- .agent/workflows/pencil-build.md | 160 ++++++++ pencil-design/USAGE_GUIDE.md | 50 +++ pencil-design/pencil.config.json | 11 + pencil-design/scripts/build.py | 363 ++++++++++++++++++ .../{ => src}/components/tPOS-ui-kit.pen | 0 .../{ => src}/pages/tPOS-desktop-home.pen | 0 .../{ => src}/pages/tPOS-mobile-home.pen | 0 .../{ => src}/pages/tPOS-tablet-home.pen | 0 pencil-design/tPOS.pen | 4 +- 9 files changed, 586 insertions(+), 2 deletions(-) create mode 100644 .agent/workflows/pencil-build.md create mode 100644 pencil-design/pencil.config.json create mode 100755 pencil-design/scripts/build.py rename pencil-design/{ => src}/components/tPOS-ui-kit.pen (100%) rename pencil-design/{ => src}/pages/tPOS-desktop-home.pen (100%) rename pencil-design/{ => src}/pages/tPOS-mobile-home.pen (100%) rename pencil-design/{ => src}/pages/tPOS-tablet-home.pen (100%) diff --git a/.agent/workflows/pencil-build.md b/.agent/workflows/pencil-build.md new file mode 100644 index 00000000..240795c5 --- /dev/null +++ b/.agent/workflows/pencil-build.md @@ -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 +``` diff --git a/pencil-design/USAGE_GUIDE.md b/pencil-design/USAGE_GUIDE.md index cf11bcf3..4e2e1102 100644 --- a/pencil-design/USAGE_GUIDE.md +++ b/pencil-design/USAGE_GUIDE.md @@ -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 +``` diff --git a/pencil-design/pencil.config.json b/pencil-design/pencil.config.json new file mode 100644 index 00000000..32adf932 --- /dev/null +++ b/pencil-design/pencil.config.json @@ -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 + } +} \ No newline at end of file diff --git a/pencil-design/scripts/build.py b/pencil-design/scripts/build.py new file mode 100755 index 00000000..6b2521a3 --- /dev/null +++ b/pencil-design/scripts/build.py @@ -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() diff --git a/pencil-design/components/tPOS-ui-kit.pen b/pencil-design/src/components/tPOS-ui-kit.pen similarity index 100% rename from pencil-design/components/tPOS-ui-kit.pen rename to pencil-design/src/components/tPOS-ui-kit.pen diff --git a/pencil-design/pages/tPOS-desktop-home.pen b/pencil-design/src/pages/tPOS-desktop-home.pen similarity index 100% rename from pencil-design/pages/tPOS-desktop-home.pen rename to pencil-design/src/pages/tPOS-desktop-home.pen diff --git a/pencil-design/pages/tPOS-mobile-home.pen b/pencil-design/src/pages/tPOS-mobile-home.pen similarity index 100% rename from pencil-design/pages/tPOS-mobile-home.pen rename to pencil-design/src/pages/tPOS-mobile-home.pen diff --git a/pencil-design/pages/tPOS-tablet-home.pen b/pencil-design/src/pages/tPOS-tablet-home.pen similarity index 100% rename from pencil-design/pages/tPOS-tablet-home.pen rename to pencil-design/src/pages/tPOS-tablet-home.pen diff --git a/pencil-design/tPOS.pen b/pencil-design/tPOS.pen index 5633450a..66985912 100644 --- a/pencil-design/tPOS.pen +++ b/pencil-design/tPOS.pen @@ -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,