diff --git a/.claude/skills/frame0-wireframe/SKILL.md b/.claude/skills/frame0-wireframe/SKILL.md index 6c4125c09..6cc572f2e 100644 --- a/.claude/skills/frame0-wireframe/SKILL.md +++ b/.claude/skills/frame0-wireframe/SKILL.md @@ -5,18 +5,22 @@ description: > app with HTTP API). Use when the user says "create wireframe", "wireframe this", "mock up the UI", "draw a screen", "UI layout", "wireframe the HUD", "Frame0", "frame0", "export wireframe", or invokes /frame0-wireframe. - Drives Frame0 via its local HTTP API to create shapes, connectors, and - export pages. Also use when asked to update existing wireframes or export - pages. Requires Frame0 to be running locally. + Wireframes are authored as local JSON files (source of truth) and synced + to Frame0 for rendering and export. Requires Frame0 to be running locally. --- # Frame0 Wireframe Generation -Create UI wireframes by driving Frame0's local HTTP API via bash+curl scripts. -No MCP dependency — the scripts replace the MCP server entirely. +Create UI wireframes as JSON files, sync them to Frame0 for rendering, and +export as PNG. Local JSON is the source of truth — Frame0 is the renderer. -**Prerequisite:** Frame0 desktop app must be running. The API has no headless -mode. If Frame0 is not available, stop and inform the user. +**Frame0 is a renderer, not a workspace.** Treat it as disposable output. +Push freely, delete test pages, keep it clean. Never pull from Frame0 unless +the user explicitly says they have made edits in Frame0 and want to import +them. The pull workflow exists for that case only — do not use it proactively. + +**Prerequisite:** Frame0 desktop app must be running. If not available, +stop and inform the user. Point to `references/setup-guide.md`. ## Health Check @@ -26,90 +30,145 @@ Always check first: .claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health ``` -If Frame0 is not running, report the failure and point to -`references/setup-guide.md`. Do not attempt to proceed without a healthy -connection. +## Core Workflow -## API Pattern +1. **Health check** — verify Frame0 is running +2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json` +3. **Push to Frame0** — `frame0-sync.py push ` +4. **Export PNG** — `frame0-sync.py export ` +5. **Clean up** — delete test/scratch pages from Frame0 when done -All Frame0 interaction goes through the bundled scripts. Never call curl -directly. +### Scripts -- **`frame0-cmd.sh`** — Low-level API wrapper. Maps subcommands to Frame0's - HTTP endpoint at `POST localhost:{port}/execute_command`. -- **`frame0-wireframe.sh`** — High-level composition helpers with project - styling defaults (colors from visual-grammar-v01.md). +| Script | Purpose | +|--------|---------| +| `scripts/frame0-sync.py` | **Primary.** Push/pull/export wireframes between JSON and Frame0 | +| `scripts/frame0-cmd.sh` | Low-level API wrapper for ad-hoc commands | -Port default: **58320** (override: `FRAME0_PORT` env var or `--port` flag). +## Wireframe JSON Format -## Single Wireframe Workflow +```json +{ + "name": "Dialogue Box", + "shapes": { + "panel": { + "type": "Rectangle", + "left": 170, "top": 500, "width": 800, "height": 260, + "fillColor": "#1a1e24", + "strokeColor": "#333340", + "corners": [8, 8, 8, 8] + }, + "speaker": { + "type": "Text", + "parent": "panel", + "left": 190, "top": 520, + "text": "LERA KONSTANTIN", + "fontColor": "#c8d0e0", + "fontSize": 16 + }, + "btn-ask": { + "type": "Rectangle", + "parent": "panel", + "left": 190, "top": 670, "width": 370, "height": 30, + "fillColor": "#2a3040", + "strokeColor": "#c8d8f0", + "corners": [4, 4, 4, 4] + } + }, + "connectors": { + "flow-1": { + "tailId": "panel", + "headId": "btn-ask", + "strokeColor": "#c8d8f0" + } + } +} +``` -1. **Health check** — `frame0-cmd.sh health` -2. **Create page** — `frame0-wireframe.sh new-page "Screen Name"` -3. **Add components** — use high-level helpers for standard elements: - ```bash - CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" - $CMD button "Confirm" 100 200 - $CMD text-field "Search..." 100 250 300 - $CMD container "Panel Title" 50 50 400 300 - $CMD label "Description text" 60 80 - $CMD divider 50 120 400 - ``` -4. **Fine-grained control** — use `frame0-cmd.sh` for operations not covered - by helpers (connectors, icons, grouping): - ```bash - CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh" - $CMD create-connector "shape-id-1" "shape-id-2" - $CMD group "shape-id-1" "shape-id-2" "shape-id-3" - $CMD create-icon "search" '{"left":100,"top":100,"width":24,"height":24}' - ``` -5. **Export** — `frame0-wireframe.sh export png docs/design/wireframes/hud/layout-v1.png` +### Key rules -## High-Level Helpers +- **Shape IDs are stable local IDs** you control (e.g. `"panel"`, `"btn-ask"`) +- **`parent`** references another local shape ID for nesting +- **`type`** uses create-API names: `Rectangle`, `Ellipse`, `Text`, `Line` +- **Colors** can be hex (`#2a3040`) or Frame0 theme tokens (`$slate6`) +- After a pull, Frame0 returns its native vocabulary (`Box` for Rectangle, + theme tokens for colors). The sync script handles the mapping transparently. +- The `.idmap.json` mapping file (gitignored) tracks local ID ↔ Frame0 ID -| Command | Default size | Description | -|---------|-------------|-------------| -| `new-page ` | — | Create page with title label | -| `button <label> <x> <y> [w] [h]` | 120x36 | Styled button with accent border | -| `text-field <placeholder> <x> <y> [w] [h]` | 200x32 | Input field with dark fill | -| `container <label> <x> <y> <w> <h>` | — | Labeled panel with border | -| `label <text> <x> <y> [size]` | font 14 | Text label | -| `divider <x> <y> <length> [h\|v]` | horizontal | Separator line | -| `export <format> <output-path>` | — | Export current page (png/svg) | +### Sync commands + +```bash +SYNC=".claude/skills/frame0-wireframe/scripts/frame0-sync.py" + +# Push local JSON to Frame0 (clears page, recreates all shapes) +$SYNC push docs/design/wireframes/dialogue/dialogue-box.json + +# Pull Frame0 page back to local JSON (preserves local IDs via mapping) +$SYNC pull "Dialogue Box" docs/design/wireframes/dialogue/dialogue-box.json + +# Push + export as PNG in one step +$SYNC export docs/design/wireframes/dialogue/dialogue-box.json \ + docs/design/wireframes/dialogue/dialogue-box.png +``` + +### Batch export + +Use this when exporting multiple wireframes. It runs as a single Bash call, +avoiding repeated permission prompts. + +```bash +BATCH=".claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh" + +# Dry run first — shows full manifest, no Frame0 calls +$BATCH --dry-run + +# Export everything (skips PNGs already newer than their JSON) +$BATCH + +# Export one category only +$BATCH --category dialogue + +# Force re-export of everything +$BATCH --force +``` + +**Always dry-run first, then get approval before running the live export.** ## Low-Level Commands -| Command | Purpose | -|---------|---------| -| `health` | Check if Frame0 is running | -| `exec <namespace:action> <json>` | Execute any raw API command | -| `create-shape <type> <json-props>` | Create rectangle, ellipse, text, etc. | -| `update-shape <id> <json-props>` | Modify shape properties | -| `delete <id...>` | Delete shapes | -| `move <id> <dx> <dy>` | Move shape by pixel offset | -| `group <id...>` | Group shapes | -| `ungroup <id...>` | Ungroup shapes | -| `create-connector <tail> <head>` | Connect two shapes | -| `create-icon <name> <json-props>` | Add icon | -| `add-page <json-props>` | Add page | -| `get-page [id]` | Get current/specific page | -| `list-pages` | List all pages | -| `set-page <id>` | Set current page | -| `export <page-id> <format>` | Export as PNG/SVG | +For ad-hoc operations or exec calls not covered by sync: + +```bash +CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh" +$CMD health +$CMD list-pages +$CMD current-page +$CMD get-page <page-id> +$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}' +$CMD create-connector <tail-id> <head-id> +$CMD move <shape-id> <dx> <dy> +$CMD export --format image/png +$CMD exec "view:fit-to-screen" '{}' +``` + +If you find yourself using `exec` for the same command repeatedly, flag it as +a candidate for a proper wrapper in `frame0-cmd.sh`. ## Project Styling Defaults Colors from `docs/design/visual-grammar-v01.md`: -| Role | Hex | Source | -|------|-----|--------| -| Background | `#1a1e24` | Zone 1 floor | -| Stroke | `#333340` | Outline standard | -| Fill | `#2a3040` | Zone 1 wall | -| Text | `#c8d0e0` | Insert chrome | -| Accent | `#c8d8f0` | Zone 1 fixture light | +| Role | Hex | Frame0 token | +|------|-----|-------------| +| Background | `#1a1e24` | `$sage3` | +| Stroke | `#333340` | `$slate6` | +| Fill | `#2a3040` | `$slate5` | +| Text | `#c8d0e0` | `$mint12` | +| Accent | `#c8d8f0` | `$blue12` | -## Export Convention +Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push. + +## Output Convention ``` docs/design/wireframes/ @@ -119,43 +178,20 @@ docs/design/wireframes/ insert/ # Neural insert wireframes ``` -## Composition Workflow - -For multi-element wireframes: - -1. Read `references/component-library.md` for the matching UI pattern -2. Adapt dimensions and positions to the wireframe layout -3. Group related elements: `frame0-cmd.sh group <id1> <id2> ...` -4. Add connectors for navigation flow: `frame0-cmd.sh create-connector` -5. Export the finished wireframe - -## Multi-Page Wireframes - -Create wireframe sets showing multiple states or screens: - -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" -$CMD new-page "HUD - Normal" -# ... add components ... -$CMD new-page "HUD - Alert" -# ... add components ... -$CMD new-page "HUD - Combat" -# ... add components ... -``` +Both `.json` source and `.png` exports are tracked in git. +`.idmap.json` mapping files are gitignored. ## Agent Guidance - **Araminta** — Primary user. Full wireframe creation, layout iteration, visual consistency. Uses all component library patterns. -- **Tyre** — Interface architecture wireframes. System boundary diagrams - showing where UI connects to game systems. +- **Tyre** — Interface architecture wireframes. System boundary diagrams. - **Qatux** — Export wireframes for UI decision records and documentation. ## References -- `references/api-reference.md` — Full Frame0 HTTP API command and property - reference. Read for fine-grained control beyond helpers. -- `references/component-library.md` — Pre-built wireframe patterns (HUD, - dialogue, menus, modals, lists, inventory). Read when starting a wireframe. +- `references/component-library.md` — Pre-built JSON wireframe templates + (HUD, dialogue, menus, modals, lists, inventory). Copy and adapt. +- `references/api-reference.md` — Frame0 HTTP API command reference and + type mappings. Read for low-level control. - `references/setup-guide.md` — Frame0 installation and startup for Fedora. - Read if Frame0 is not installed or not running. diff --git a/.claude/skills/frame0-wireframe/references/api-reference.md b/.claude/skills/frame0-wireframe/references/api-reference.md index 38546271c..616c6690e 100644 --- a/.claude/skills/frame0-wireframe/references/api-reference.md +++ b/.claude/skills/frame0-wireframe/references/api-reference.md @@ -11,275 +11,231 @@ Content-Type: application/json Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag). -## Request Format +## Request / Response ```json -{ - "command": "namespace:action", - "args": { ... } -} +{"command": "namespace:action", "args": { ... }} ``` -## Response Format - ```json -{ - "success": true, - "data": { ... } -} -``` - -On error: - -```json -{ - "success": false, - "error": "Error description" -} +{"success": true, "data": { ... }} +{"success": false, "error": "description"} ``` --- -## Commands by Namespace +## Type Mapping -### shape — Shape Creation and Manipulation +Frame0 uses different type names for create vs get: -#### shape:create-shape +| Create API (`type`) | Get API (internal) | Description | +|--------------------|--------------------|-------------| +| `Rectangle` | `Box` | Rectangle with optional corners | +| `Ellipse` | `Ellipse` | Circle/ellipse | +| `Text` | `Text` | Text label | +| `Line` | `Line` | Line/polyline | +| `Frame` | `Frame` | Container from library | +| `Freehand` | `Freehand` | Freehand drawing | +| `Highlighter` | `Highlighter` | Highlighter stroke | -Create a new shape on the current page. +The sync script handles this mapping transparently. + +## Color Tokens + +Frame0 maps hex colors to theme tokens on creation (`convertColors: true`): + +| Hex | Token | Role | +|-----|-------|------| +| `#1a1e24` | `$sage3` | Background | +| `#2a3040` | `$slate5` | Fill | +| `#333340` | `$slate6` | Stroke | +| `#c8d0e0` | `$mint12` | Text | +| `#c8d8f0` | `$blue12` | Accent | + +Both hex and token strings work in the API. Tokens are preserved on round-trip. + +--- + +## Commands + +### shape:create-shape ```json { "command": "shape:create-shape", "args": { - "type": "rectangle", + "type": "Rectangle", "shapeProps": { "name": "my-button", - "left": 100, - "top": 200, - "width": 120, - "height": 36, + "left": 100, "top": 200, "width": 120, "height": 36, "fillColor": "#2a3040", - "strokeColor": "#333340", - "fontColor": "#c8d0e0", - "fontSize": 14, - "text": "Button Label", + "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - } + }, + "parentId": "optional-parent-shape-id", + "convertColors": true } } ``` -**Shape types:** `rectangle`, `ellipse`, `triangle`, `diamond`, `line`, `text`, `image`, `icon` +Returns: shape ID (string). -#### shape:update-shape +### shape:get-shape + +```json +{"command": "shape:get-shape", "args": {"shapeId": "id"}} +``` + +### shape:update-shape ```json { "command": "shape:update-shape", "args": { - "shapeId": "shape-id", - "shapeProps": { - "fillColor": "#1a1e24", - "text": "Updated Label" - } + "shapeId": "id", + "shapeProps": {"fillColor": "#1a1e24", "text": "Updated"}, + "convertColors": true } } ``` -#### shape:move +### shape:move ```json -{ - "command": "shape:move", - "args": { - "shapeId": "shape-id", - "dx": 50, - "dy": -20 - } -} +{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}} ``` -#### shape:group / shape:ungroup - -```json -{ - "command": "shape:group", - "args": { - "shapeIdArray": ["id-1", "id-2", "id-3"] - } -} -``` - -#### shape:create-connector +### shape:create-connector ```json { "command": "shape:create-connector", "args": { - "tailId": "source-shape-id", - "headId": "target-shape-id", - "shapeProps": { - "strokeColor": "#c8d8f0" - } + "tailId": "source-id", + "headId": "target-id", + "shapeProps": {"strokeColor": "#c8d8f0"}, + "convertColors": true } } ``` -#### shape:create-icon +### shape:create-icon ```json { "command": "shape:create-icon", "args": { "iconName": "search", - "shapeProps": { - "left": 100, - "top": 100, - "width": 24, - "height": 24, - "strokeColor": "#c8d0e0" - } + "shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24} } } ``` -Icon sizes: 16, 24, 32, 48 pixels. - -#### shape:create-image +### shape:get-available-icons ```json -{ - "command": "shape:create-image", - "args": { - "mimeType": "image/png", - "imageData": "base64-encoded-data", - "shapeProps": { - "left": 100, - "top": 100, - "width": 200, - "height": 150 - } - } -} +{"command": "shape:get-available-icons", "args": {}} ``` -### edit — Editing Operations - -#### edit:delete +### shape:group / shape:ungroup ```json -{ - "command": "edit:delete", - "args": { - "shapeIdArray": ["id-1", "id-2"] - } -} +{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}} +{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}} ``` -### page — Page Management - -#### page:add +### edit:delete / edit:duplicate ```json -{ - "command": "page:add", - "args": { - "pageProps": { - "name": "HUD Layout" - } - } -} +{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}} +{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}} ``` -#### page:get +### page:add ```json -{ - "command": "page:get", - "args": { - "pageId": "page-id" - } -} +{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}} ``` -Omit `pageId` to get current page. +Returns: `{id, type, name}`. -#### page:get-all +### page:get ```json -{ - "command": "page:get-all", - "args": {} -} +{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}} ``` -#### page:set-current +### page:get-current-page ```json -{ - "command": "page:set-current", - "args": { - "pageId": "page-id" - } -} +{"command": "page:get-current-page", "args": {}} ``` -#### page:delete +Returns: page ID string. + +### page:set-current-page ```json -{ - "command": "page:delete", - "args": { - "pageId": "page-id" - } -} +{"command": "page:set-current-page", "args": {"pageId": "id"}} ``` -### file — Export +### doc:get (list all pages) -#### file:export-image +```json +{"command": "doc:get", "args": {"exportPages": true, "exportShapes": false}} +``` + +### page:delete + +```json +{"command": "page:delete", "args": {"pageId": "id"}} +``` + +### file:export-image ```json { "command": "file:export-image", "args": { - "pageId": "page-id", - "format": "png", + "pageId": "optional-page-id", + "format": "image/png", "fillBackground": true } } ``` -Formats: `png`, `svg` +Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`. +Returns: base64-encoded image data. -Optional: `"shapeIdArray": ["id-1"]` to export specific shapes only. +### view:fit-to-screen + +```json +{"command": "view:fit-to-screen", "args": {}} +``` --- -## Shape Properties Reference +## Shape Properties | Property | Type | Notes | |----------|------|-------| | `name` | string | Shape identifier/label | -| `left` | number | X position in pixels (origin: top-left) | -| `top` | number | Y position in pixels | +| `left` | number | X position (origin: top-left) | +| `top` | number | Y position | | `width` | number | Width in pixels | | `height` | number | Height in pixels | -| `fillColor` | string | Hex color (e.g., `"#2a3040"`) | -| `strokeColor` | string | Hex color for border | -| `fontColor` | string | Hex color for text | +| `fillColor` | string | Hex or `$token` | +| `strokeColor` | string | Hex or `$token` | +| `strokeWidth` | number | Border width | +| `fontColor` | string | Text color (hex or `$token`) | | `fontSize` | number | Font size in pixels | -| `text` | string | Text content (for shapes with text) | -| `wordWrap` | boolean | Enable text word wrapping | -| `corners` | number[4] | Border radius [topLeft, topRight, bottomRight, bottomLeft] | -| `path` | array | Coordinate pairs for lines/polygons | -| `lineType` | string | Line style | -| `tailEndType` | string | Arrow tail type | -| `headEndType` | string | Arrow head type | - -## Coordinate System - -- Origin: top-left corner of the canvas -- Units: pixels -- X increases rightward, Y increases downward +| `fontFamily` | string | Font name (Frame0 default: `Loranthus`) | +| `text` | string | Text content | +| `wordWrap` | boolean | Enable word wrapping | +| `corners` | number[4] | Border radius [TL, TR, BR, BL] | +| `roughness` | number | Sketch roughness (Frame0 default: 1) | +| `constraints` | array | Auto-sizing constraints | +| `horzAlign` | string | Horizontal text alignment | +| `vertAlign` | string | Vertical text alignment | +| `fillStyle` | string | Fill style (`none` for transparent) | +| `path` | array | Coordinate pairs for lines | diff --git a/.claude/skills/frame0-wireframe/references/component-library.md b/.claude/skills/frame0-wireframe/references/component-library.md index 24151f5e2..3ae364dda 100644 --- a/.claude/skills/frame0-wireframe/references/component-library.md +++ b/.claude/skills/frame0-wireframe/references/component-library.md @@ -1,63 +1,161 @@ # Component Library -Pre-built wireframe patterns for The Settled Reach UI. Each pattern provides -`frame0-wireframe.sh` command sequences. Colors from visual-grammar-v01.md. +Pre-built wireframe JSON templates for The Settled Reach UI. Copy the JSON, +adapt positions/sizes, save to `docs/design/wireframes/{category}/`, and push. + +**Viewport:** 1140x780 (Godot project settings) +**Grid unit:** 8px +**Min touch target:** 36px height +**Font sizes:** 12 (label), 14 (body), 16 (subtitle), 18 (heading), 24 (title) --- ## 1. HUD Layout -The main gameplay overlay. Minimap top-right, monologue bottom-center, -insert display bottom-left. +Main gameplay overlay. Minimap top-right, monologue bottom-center, +insert display bottom-left, action hints bottom-right. -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" - -$CMD new-page "HUD Layout" - -# Minimap (top-right) -$CMD container "Minimap" 880 20 240 240 - -# Monologue panel (bottom-center) -$CMD container "Monologue" 300 680 520 80 -$CMD label "Internal monologue text appears here..." 310 700 - -# Insert display (bottom-left) -$CMD container "Insert Display" 20 600 260 160 -$CMD label "Neural Insert Data" 30 620 - -# Action hints (bottom-right) -$CMD container "Action Hints" 880 700 240 60 -$CMD label "[E] Interact [TAB] Insert" 890 720 12 +```json +{ + "name": "HUD Layout", + "shapes": { + "minimap": { + "type": "Rectangle", + "left": 880, "top": 20, "width": 240, "height": 240, + "fillColor": "#1a1e24", "strokeColor": "#333340", + "corners": [4, 4, 4, 4] + }, + "minimap-label": { + "type": "Text", + "parent": "minimap", + "left": 890, "top": 30, + "text": "Minimap", + "fontColor": "#c8d0e0", "fontSize": 14 + }, + "monologue": { + "type": "Rectangle", + "left": 300, "top": 680, "width": 520, "height": 80, + "fillColor": "#1a1e24", "strokeColor": "#333340", + "corners": [4, 4, 4, 4] + }, + "monologue-text": { + "type": "Text", + "parent": "monologue", + "left": 310, "top": 700, "width": 500, + "text": "Internal monologue text appears here...", + "fontColor": "#c8d0e0", "fontSize": 13, "wordWrap": true + }, + "insert": { + "type": "Rectangle", + "left": 20, "top": 600, "width": 260, "height": 160, + "fillColor": "#1a1e24", "strokeColor": "#333340", + "corners": [4, 4, 4, 4] + }, + "insert-label": { + "type": "Text", + "parent": "insert", + "left": 30, "top": 620, + "text": "Neural Insert Data", + "fontColor": "#c8d0e0", "fontSize": 14 + }, + "hints": { + "type": "Rectangle", + "left": 880, "top": 700, "width": 240, "height": 60, + "fillColor": "#1a1e24", "strokeColor": "#333340", + "corners": [4, 4, 4, 4] + }, + "hints-label": { + "type": "Text", + "parent": "hints", + "left": 890, "top": 720, + "text": "[E] Interact [TAB] Insert", + "fontColor": "#c8d0e0", "fontSize": 12 + } + } +} ``` -**Viewport assumption:** 1140x780 (matching Godot project viewport). - --- ## 2. Dialogue Box Speaker panel with response options. Anchored bottom-center during dialogue mode. -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" - -$CMD new-page "Dialogue Box" - -# Dialogue container -$CMD container "Dialogue" 170 500 800 260 - -# Speaker name -$CMD label "LERA KONSTANTIN" 190 520 16 - -# Dialogue text area -$CMD container "Text Area" 190 550 760 100 -$CMD label "You look like you could use a drink. First time on the station?" 200 560 - -# Response options -$CMD button "[1] Ask about the station" 190 670 370 30 -$CMD button "[2] Ask about recent events" 190 710 370 30 -$CMD button "[3] Leave" 580 670 180 30 +```json +{ + "name": "Dialogue Box", + "shapes": { + "panel": { + "type": "Rectangle", + "left": 170, "top": 500, "width": 800, "height": 260, + "fillColor": "#1a1e24", "strokeColor": "#333340", + "corners": [8, 8, 8, 8] + }, + "speaker": { + "type": "Text", + "parent": "panel", + "left": 190, "top": 520, + "text": "LERA KONSTANTIN", + "fontColor": "#c8d0e0", "fontSize": 16 + }, + "text-area": { + "type": "Rectangle", + "parent": "panel", + "left": 190, "top": 550, "width": 760, "height": 100, + "fillColor": "#2a3040", "strokeColor": "#333340", + "corners": [4, 4, 4, 4] + }, + "dialogue-text": { + "type": "Text", + "parent": "text-area", + "left": 200, "top": 560, "width": 740, + "text": "You look like you could use a drink. First time on the station?", + "fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true + }, + "btn-option1": { + "type": "Rectangle", + "parent": "panel", + "left": 190, "top": 670, "width": 370, "height": 30, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", + "corners": [4, 4, 4, 4] + }, + "btn-option1-label": { + "type": "Text", + "parent": "btn-option1", + "left": 200, "top": 674, + "text": "[1] Ask about the station", + "fontColor": "#c8d8f0", "fontSize": 12 + }, + "btn-option2": { + "type": "Rectangle", + "parent": "panel", + "left": 190, "top": 710, "width": 370, "height": 30, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", + "corners": [4, 4, 4, 4] + }, + "btn-option2-label": { + "type": "Text", + "parent": "btn-option2", + "left": 200, "top": 714, + "text": "[2] Ask about recent events", + "fontColor": "#c8d8f0", "fontSize": 12 + }, + "btn-leave": { + "type": "Rectangle", + "parent": "panel", + "left": 580, "top": 670, "width": 180, "height": 30, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", + "corners": [4, 4, 4, 4] + }, + "btn-leave-label": { + "type": "Text", + "parent": "btn-leave", + "left": 590, "top": 674, + "text": "[3] Leave", + "fontColor": "#c8d8f0", "fontSize": 12 + } + } +} ``` --- @@ -66,25 +164,87 @@ $CMD button "[3] Leave" 580 670 180 30 Full-screen menu with sidebar navigation and content area. -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" - -$CMD new-page "Pause Menu" - -# Background overlay -$CMD container "Menu Background" 0 0 1140 780 - -# Sidebar navigation -$CMD container "Navigation" 20 20 200 740 -$CMD button "Inventory" 30 40 180 36 -$CMD button "Journal" 30 86 180 36 -$CMD button "Map" 30 132 180 36 -$CMD button "Settings" 30 178 180 36 -$CMD button "Resume" 30 720 180 36 - -# Content area -$CMD container "Content" 240 20 880 740 -$CMD label "Content area" 260 40 +```json +{ + "name": "Pause Menu", + "shapes": { + "bg": { + "type": "Rectangle", + "left": 0, "top": 0, "width": 1140, "height": 780, + "fillColor": "#1a1e24" + }, + "nav": { + "type": "Rectangle", + "parent": "bg", + "left": 20, "top": 20, "width": 200, "height": 740, + "fillColor": "#2a3040", "strokeColor": "#333340", + "corners": [4, 4, 4, 4] + }, + "btn-inventory": { + "type": "Rectangle", "parent": "nav", + "left": 30, "top": 40, "width": 180, "height": 36, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "btn-inventory-label": { + "type": "Text", "parent": "btn-inventory", + "left": 40, "top": 48, "text": "Inventory", + "fontColor": "#c8d8f0", "fontSize": 14 + }, + "btn-journal": { + "type": "Rectangle", "parent": "nav", + "left": 30, "top": 86, "width": 180, "height": 36, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "btn-journal-label": { + "type": "Text", "parent": "btn-journal", + "left": 40, "top": 94, "text": "Journal", + "fontColor": "#c8d8f0", "fontSize": 14 + }, + "btn-map": { + "type": "Rectangle", "parent": "nav", + "left": 30, "top": 132, "width": 180, "height": 36, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "btn-map-label": { + "type": "Text", "parent": "btn-map", + "left": 40, "top": 140, "text": "Map", + "fontColor": "#c8d8f0", "fontSize": 14 + }, + "btn-settings": { + "type": "Rectangle", "parent": "nav", + "left": 30, "top": 178, "width": 180, "height": 36, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "btn-settings-label": { + "type": "Text", "parent": "btn-settings", + "left": 40, "top": 186, "text": "Settings", + "fontColor": "#c8d8f0", "fontSize": 14 + }, + "btn-resume": { + "type": "Rectangle", "parent": "nav", + "left": 30, "top": 720, "width": 180, "height": 36, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "btn-resume-label": { + "type": "Text", "parent": "btn-resume", + "left": 40, "top": 728, "text": "Resume", + "fontColor": "#c8d8f0", "fontSize": 14 + }, + "content": { + "type": "Rectangle", + "parent": "bg", + "left": 240, "top": 20, "width": 880, "height": 740, + "fillColor": "#2a3040", "strokeColor": "#333340", + "corners": [4, 4, 4, 4] + }, + "content-label": { + "type": "Text", "parent": "content", + "left": 260, "top": 40, + "text": "Content area", + "fontColor": "#c8d0e0", "fontSize": 14 + } + } +} ``` --- @@ -93,30 +253,69 @@ $CMD label "Content area" 260 40 Centered overlay for confirmations, alerts, choices. -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" - -$CMD new-page "Modal Dialog" - -# Overlay background (semi-transparent implied) -$CMD container "Overlay" 0 0 1140 780 - -# Modal panel (centered) -$CMD container "Modal" 320 240 500 300 - -# Title -$CMD label "Confirm Action" 340 260 18 - -# Divider -$CMD divider 340 290 460 - -# Body text -$CMD label "Are you sure you want to proceed?" 340 310 -$CMD label "This action cannot be undone." 340 340 - -# Action buttons -$CMD button "Cancel" 480 480 120 36 -$CMD button "Confirm" 620 480 120 36 +```json +{ + "name": "Modal Dialog", + "shapes": { + "overlay": { + "type": "Rectangle", + "left": 0, "top": 0, "width": 1140, "height": 780, + "fillColor": "#0a0c10" + }, + "modal": { + "type": "Rectangle", + "parent": "overlay", + "left": 320, "top": 240, "width": 500, "height": 300, + "fillColor": "#1a1e24", "strokeColor": "#333340", + "corners": [8, 8, 8, 8] + }, + "title": { + "type": "Text", "parent": "modal", + "left": 340, "top": 260, + "text": "Confirm Action", + "fontColor": "#c8d0e0", "fontSize": 18 + }, + "divider": { + "type": "Line", "parent": "modal", + "left": 340, "top": 290, "width": 460, "height": 0, + "strokeColor": "#333340" + }, + "body-1": { + "type": "Text", "parent": "modal", + "left": 340, "top": 310, + "text": "Are you sure you want to proceed?", + "fontColor": "#c8d0e0", "fontSize": 14 + }, + "body-2": { + "type": "Text", "parent": "modal", + "left": 340, "top": 340, + "text": "This action cannot be undone.", + "fontColor": "#c8d0e0", "fontSize": 14 + }, + "btn-cancel": { + "type": "Rectangle", "parent": "modal", + "left": 480, "top": 480, "width": 120, "height": 36, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "btn-cancel-label": { + "type": "Text", "parent": "btn-cancel", + "left": 510, "top": 488, + "text": "Cancel", + "fontColor": "#c8d8f0", "fontSize": 14 + }, + "btn-confirm": { + "type": "Rectangle", "parent": "modal", + "left": 620, "top": 480, "width": 120, "height": 36, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "btn-confirm-label": { + "type": "Text", "parent": "btn-confirm", + "left": 645, "top": 488, + "text": "Confirm", + "fontColor": "#c8d8f0", "fontSize": 14 + } + } +} ``` --- @@ -125,25 +324,64 @@ $CMD button "Confirm" 620 480 120 36 Scrollable list with item selection and detail panel. -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" - -$CMD new-page "List View" - -# List panel -$CMD container "Item List" 20 20 400 740 - -# List items (repeating pattern) -$CMD button "Item Alpha" 30 30 380 40 -$CMD button "Item Beta" 30 80 380 40 -$CMD button "Item Gamma" 30 130 380 40 -$CMD button "Item Delta" 30 180 380 40 - -# Detail panel -$CMD container "Detail" 440 20 680 740 -$CMD label "Item Alpha" 460 40 18 -$CMD divider 460 70 640 -$CMD label "Description and properties appear here." 460 90 +```json +{ + "name": "List View", + "shapes": { + "list-panel": { + "type": "Rectangle", + "left": 20, "top": 20, "width": 400, "height": 740, + "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "item-1": { + "type": "Rectangle", "parent": "list-panel", + "left": 30, "top": 30, "width": 380, "height": 40, + "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] + }, + "item-1-label": { + "type": "Text", "parent": "item-1", + "left": 40, "top": 38, "text": "Item Alpha", + "fontColor": "#c8d8f0", "fontSize": 14 + }, + "item-2": { + "type": "Rectangle", "parent": "list-panel", + "left": 30, "top": 80, "width": 380, "height": 40, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "item-2-label": { + "type": "Text", "parent": "item-2", + "left": 40, "top": 88, "text": "Item Beta", + "fontColor": "#c8d0e0", "fontSize": 14 + }, + "item-3": { + "type": "Rectangle", "parent": "list-panel", + "left": 30, "top": 130, "width": 380, "height": 40, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "item-3-label": { + "type": "Text", "parent": "item-3", + "left": 40, "top": 138, "text": "Item Gamma", + "fontColor": "#c8d0e0", "fontSize": 14 + }, + "detail-panel": { + "type": "Rectangle", + "left": 440, "top": 20, "width": 680, "height": 740, + "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "detail-title": { + "type": "Text", "parent": "detail-panel", + "left": 460, "top": 40, + "text": "Item Alpha", + "fontColor": "#c8d0e0", "fontSize": 18 + }, + "detail-body": { + "type": "Text", "parent": "detail-panel", + "left": 460, "top": 80, "width": 640, + "text": "Description and properties appear here.", + "fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true + } + } +} ``` --- @@ -152,42 +390,78 @@ $CMD label "Description and properties appear here." 460 90 Grid of cells for item management. -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh" - -$CMD new-page "Inventory Grid" - -# Grid container -$CMD container "Inventory" 240 100 660 580 - -# Grid header -$CMD label "INVENTORY" 260 120 18 - -# Grid cells (4x4 example, 64px each with 8px gaps) -# Row 1 -$CMD container "" 260 160 64 64 -$CMD container "" 332 160 64 64 -$CMD container "" 404 160 64 64 -$CMD container "" 476 160 64 64 - -# Row 2 -$CMD container "" 260 232 64 64 -$CMD container "" 332 232 64 64 -$CMD container "" 404 232 64 64 -$CMD container "" 476 232 64 64 - -# Selected item detail -$CMD container "Item Detail" 580 160 300 400 -$CMD label "Selected Item Name" 600 180 16 -$CMD divider 600 210 260 -$CMD label "Item description and stats" 600 230 +```json +{ + "name": "Inventory Grid", + "shapes": { + "panel": { + "type": "Rectangle", + "left": 240, "top": 100, "width": 660, "height": 580, + "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] + }, + "title": { + "type": "Text", "parent": "panel", + "left": 260, "top": 120, + "text": "INVENTORY", + "fontColor": "#c8d0e0", "fontSize": 18 + }, + "cell-1-1": { + "type": "Rectangle", "parent": "panel", + "left": 260, "top": 160, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "cell-1-2": { + "type": "Rectangle", "parent": "panel", + "left": 332, "top": 160, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "cell-1-3": { + "type": "Rectangle", "parent": "panel", + "left": 404, "top": 160, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "cell-1-4": { + "type": "Rectangle", "parent": "panel", + "left": 476, "top": 160, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "cell-2-1": { + "type": "Rectangle", "parent": "panel", + "left": 260, "top": 232, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "cell-2-2": { + "type": "Rectangle", "parent": "panel", + "left": 332, "top": 232, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "cell-2-3": { + "type": "Rectangle", "parent": "panel", + "left": 404, "top": 232, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "cell-2-4": { + "type": "Rectangle", "parent": "panel", + "left": 476, "top": 232, "width": 64, "height": 64, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "detail": { + "type": "Rectangle", "parent": "panel", + "left": 580, "top": 160, "width": 300, "height": 400, + "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] + }, + "detail-title": { + "type": "Text", "parent": "detail", + "left": 600, "top": 180, + "text": "Selected Item Name", + "fontColor": "#c8d0e0", "fontSize": 16 + }, + "detail-body": { + "type": "Text", "parent": "detail", + "left": 600, "top": 210, "width": 260, + "text": "Item description and stats", + "fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true + } + } +} ``` - ---- - -## Viewport and Grid - -- **Project viewport:** 1140x780 (from Godot project settings) -- **Grid unit:** 8px (for consistent spacing) -- **Minimum touch target:** 36px height for interactive elements -- **Font sizes:** 12 (small/label), 14 (body), 16 (subtitle), 18 (heading), 24 (title) diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh b/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh index ab2186226..6fc23750d 100755 --- a/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh +++ b/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh @@ -14,28 +14,33 @@ curl calls. Requires Frame0 desktop app to be running. Commands: health Check if Frame0 is running exec <namespace:action> <json> Execute a raw API command - create-shape <type> <json-props> Create a shape (rectangle, ellipse, text, etc.) + create-shape <type> <json-props> Create a shape (Rectangle, Ellipse, Text, Line) + get-shape <id> Get shape details update-shape <id> <json-props> Update shape properties delete <id> [id...] Delete shapes by ID move <id> <dx> <dy> Move a shape by pixel offset + duplicate <id> Duplicate a shape group <id> [id...] Group shapes - ungroup <id> [id...] Ungroup shapes + ungroup <group-id> Ungroup a group create-connector <tail-id> <head-id> [json-props] Connect two shapes create-icon <name> <json-props> Create an icon shape - add-page <json-props> Add a new page - get-page [page-id] Get current or specific page info - list-pages List all pages + add-page <name> Add a new page (becomes current) + get-page [page-id] Get current or specific page data + list-pages [--shapes] List all pages (--shapes for shape data) + current-page Get current page ID set-page <page-id> Set current page - export <page-id> <format> Export page (png/svg) + export [page-id] [--format mime] Export page as image (default: image/png) + fit Fit view to screen Options: --port N Frame0 API port (default: $PORT, env: FRAME0_PORT) Examples: $(basename "$0") health - $(basename "$0") create-shape rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}' + $(basename "$0") add-page "HUD Layout" + $(basename "$0") create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}' $(basename "$0") list-pages - $(basename "$0") export page-id png + $(basename "$0") export --format image/png EOF exit 1 } @@ -55,7 +60,8 @@ set -- "${ARGS[@]+"${ARGS[@]}"}" # Execute a Frame0 API command, return data or error frame0_exec() { local command="$1" - local args="${2:-\{\}}" + local args + args="${2:-"{}"}" local response response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \ @@ -127,15 +133,23 @@ case "$CMD" in ;; create-shape) - [[ $# -lt 2 ]] && { echo "Usage: create-shape <type> <json-props>" >&2; exit 1; } + [[ $# -lt 2 ]] && { echo "Usage: create-shape <Type> <json-props>" >&2; exit 1; } local_type="$1" local_props="$2" - frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props}" + local_parent="${3:-}" + local_parent_arg="" + [[ -n "$local_parent" ]] && local_parent_arg=", \"parentId\": \"$local_parent\"" + frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props$local_parent_arg, \"convertColors\": true}" + ;; + + get-shape) + [[ $# -lt 1 ]] && { echo "Usage: get-shape <id>" >&2; exit 1; } + frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}" ;; update-shape) [[ $# -lt 2 ]] && { echo "Usage: update-shape <id> <json-props>" >&2; exit 1; } - frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2}" + frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}" ;; delete) @@ -149,54 +163,83 @@ case "$CMD" in frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}" ;; + duplicate) + [[ $# -lt 1 ]] && { echo "Usage: duplicate <id> [dx] [dy]" >&2; exit 1; } + local_dx="${2:-0}" + local_dy="${3:-0}" + frame0_exec "edit:duplicate" "{\"shapeIdArray\": [\"$1\"], \"dx\": $local_dx, \"dy\": $local_dy}" + ;; + group) - [[ $# -lt 1 ]] && { echo "Usage: group <id> [id...]" >&2; exit 1; } + [[ $# -lt 2 ]] && { echo "Usage: group <id> <id> [id...]" >&2; exit 1; } local_arr=$(ids_to_json_array "$@") frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}" ;; ungroup) - [[ $# -lt 1 ]] && { echo "Usage: ungroup <id> [id...]" >&2; exit 1; } - local_arr=$(ids_to_json_array "$@") - frame0_exec "shape:ungroup" "{\"shapeIdArray\": $local_arr}" + [[ $# -lt 1 ]] && { echo "Usage: ungroup <group-id>" >&2; exit 1; } + frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}" ;; create-connector) [[ $# -lt 2 ]] && { echo "Usage: create-connector <tail-id> <head-id> [json-props]" >&2; exit 1; } - local_props="${3:-\{\}}" - frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props}" + local_props="${3:-{}}" + frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props, \"convertColors\": true}" ;; create-icon) [[ $# -lt 2 ]] && { echo "Usage: create-icon <name> <json-props>" >&2; exit 1; } - frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2}" + frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}" ;; add-page) - [[ $# -lt 1 ]] && { echo "Usage: add-page <json-props>" >&2; exit 1; } - frame0_exec "page:add" "{\"pageProps\": $1}" + [[ $# -lt 1 ]] && { echo "Usage: add-page <name>" >&2; exit 1; } + frame0_exec "page:add" "{\"pageProps\": {\"name\": \"$1\"}}" ;; get-page) if [[ $# -ge 1 ]]; then - frame0_exec "page:get" "{\"pageId\": \"$1\"}" + frame0_exec "page:get" "{\"pageId\": \"$1\", \"exportShapes\": true}" else - frame0_exec "page:get" "{}" + local_id + local_id=$(frame0_exec "page:get-current-page") + # Strip quotes from returned ID + local_id=$(echo "$local_id" | tr -d '"') + frame0_exec "page:get" "{\"pageId\": \"$local_id\", \"exportShapes\": true}" fi ;; list-pages) - frame0_exec "page:get-all" "{}" + local_shapes="false" + [[ "${1:-}" == "--shapes" ]] && local_shapes="true" + frame0_exec "doc:get" "{\"exportPages\": true, \"exportShapes\": $local_shapes}" + ;; + + current-page) + frame0_exec "page:get-current-page" ;; set-page) [[ $# -lt 1 ]] && { echo "Usage: set-page <page-id>" >&2; exit 1; } - frame0_exec "page:set-current" "{\"pageId\": \"$1\"}" + frame0_exec "page:set-current-page" "{\"pageId\": \"$1\"}" ;; export) - [[ $# -lt 2 ]] && { echo "Usage: export <page-id> <format>" >&2; exit 1; } - frame0_exec "file:export-image" "{\"pageId\": \"$1\", \"format\": \"$2\", \"fillBackground\": true}" + local_page_id="" + local_format="image/png" + while [[ $# -gt 0 ]]; do + case "$1" in + --format) local_format="$2"; shift 2 ;; + *) local_page_id="$1"; shift ;; + esac + done + local_page_arg="" + [[ -n "$local_page_id" ]] && local_page_arg="\"pageId\": \"$local_page_id\", " + frame0_exec "file:export-image" "{${local_page_arg}\"format\": \"$local_format\", \"fillBackground\": true}" + ;; + + fit) + frame0_exec "view:fit-to-screen" ;; --help|-h|help) diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh b/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh new file mode 100755 index 000000000..d506858f0 --- /dev/null +++ b/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# Batch export wireframes from JSON to PNG via Frame0. +# +# Finds all .json wireframe files under docs/design/wireframes/ and exports +# each to a matching .png. Skips files whose PNG is already newer than the +# JSON, unless --force is passed. +# +# Usage: +# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR] +# +# Options: +# --dry-run Print manifest only, don't touch Frame0. +# --force Re-export even if PNG already exists and is up to date. +# --category CAT Limit to one subdirectory (e.g. --category dialogue) +# --root DIR Wireframes root dir (default: docs/design/wireframes) +# +# Exit codes: +# 0 All exports succeeded (or nothing to do) +# 1 One or more exports failed + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +SYNC="$SCRIPT_DIR/frame0-sync.py" +DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes" + +DRY_RUN=false +FORCE=false +CATEGORY="" +WF_ROOT="$DEFAULT_ROOT" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --force) FORCE=true; shift ;; + --category) CATEGORY="$2"; shift 2 ;; + --root) WF_ROOT="$2"; shift 2 ;; + -h|--help) + sed -n '/^# /p' "$0" | sed 's/^# //' + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [[ ! -d "$WF_ROOT" ]]; then + echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2 + exit 1 +fi + +# Collect JSON files, optionally filtered by category subdirectory +mapfile -t JSON_FILES < <( + if [[ -n "$CATEGORY" ]]; then + find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort + else + find "$WF_ROOT" -name "*.json" ! -name ".*" | sort + fi +) + +if [[ ${#JSON_FILES[@]} -eq 0 ]]; then + echo "No wireframe JSON files found." + exit 0 +fi + +# Classify files into to-export and to-skip +TO_EXPORT=() +TO_SKIP=() + +for json in "${JSON_FILES[@]}"; do + png="${json%.json}.png" + if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then + TO_EXPORT+=("$json") + else + TO_SKIP+=("$json") + fi +done + +# Print manifest +if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then + echo "" + echo "Will export (${#TO_EXPORT[@]} files):" + for json in "${TO_EXPORT[@]}"; do + png="${json%.json}.png" + rel="${json#$REPO_ROOT/}" + if [[ ! -f "$png" ]]; then + status="new" + else + status="updated" + fi + printf " [%-7s] %s\n" "$status" "$rel" + done +else + echo "" + echo "Nothing to export (all PNGs up to date)." +fi + +if [[ ${#TO_SKIP[@]} -gt 0 ]]; then + echo "" + echo "Will skip (${#TO_SKIP[@]} files already up to date):" + for json in "${TO_SKIP[@]}"; do + rel="${json#$REPO_ROOT/}" + printf " [skip ] %s\n" "$rel" + done +fi + +if $DRY_RUN; then + echo "" + echo "Dry run — no exports performed." + exit 0 +fi + +if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then + exit 0 +fi + +echo "" +PASSED=0 +FAILED=0 +FAILED_FILES=() + +TOTAL=${#TO_EXPORT[@]} +IDX=0 + +for json in "${TO_EXPORT[@]}"; do + IDX=$((IDX + 1)) + png="${json%.json}.png" + rel="${json#$REPO_ROOT/}" + + printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel" + + output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt) + rc=$? + if [[ $rc -eq 0 ]]; then + size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true) + echo "ok $size" + PASSED=$((PASSED + 1)) + else + echo "FAILED" + cat /tmp/frame0-batch-err.txt >&2 + FAILED=$((FAILED + 1)) + FAILED_FILES+=("$rel") + fi +done + +echo "" +echo "$PASSED exported, $FAILED failed." + +if [[ $FAILED -gt 0 ]]; then + echo "" + echo "Failed:" >&2 + for f in "${FAILED_FILES[@]}"; do + echo " $f" >&2 + done + exit 1 +fi + +exit 0 diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-sync.py b/.claude/skills/frame0-wireframe/scripts/frame0-sync.py new file mode 100755 index 000000000..df514c5a1 --- /dev/null +++ b/.claude/skills/frame0-wireframe/scripts/frame0-sync.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back. + +Local JSON is source of truth. Frame0 is a renderer. +A mapping file tracks local_id <-> frame0_id across push/pull cycles. + +Usage: + frame0-sync.py push <wireframe.json> [--port PORT] + frame0-sync.py pull <page-id|page-name> <output.json> [--port PORT] + frame0-sync.py export <wireframe.json> <output.png> [--port PORT] [--format MIME] +""" + +import argparse +import json +import os +import sys +import urllib.request +import urllib.error + +DEFAULT_PORT = 58320 + + +def api(port, command, args=None): + """Execute a Frame0 API command. Returns the data field on success.""" + url = f"http://localhost:{port}/execute_command" + payload = json.dumps({"command": command, "args": args or {}}).encode() + req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + except urllib.error.HTTPError as e: + body = e.read().decode() + print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr) + sys.exit(1) + + if not result.get("success"): + print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr) + sys.exit(1) + + return result.get("data") + + +# -- Mapping file management -------------------------------------------------- + +def mapping_path(wireframe_path): + """Derive the idmap path from the wireframe JSON path.""" + d = os.path.dirname(wireframe_path) + base = os.path.splitext(os.path.basename(wireframe_path))[0] + return os.path.join(d, f".{base}.idmap.json") + + +def load_mapping(wireframe_path): + p = mapping_path(wireframe_path) + if os.path.exists(p): + with open(p) as f: + return json.load(f) + return {"page_id": None, "shapes": {}, "connectors": {}} + + +def save_mapping(wireframe_path, mapping): + p = mapping_path(wireframe_path) + os.makedirs(os.path.dirname(p) or ".", exist_ok=True) + with open(p, "w") as f: + json.dump(mapping, f, indent=2) + f.write("\n") + + +# -- Push: local JSON -> Frame0 ----------------------------------------------- + +# Frame0 returns different type names from get vs what create accepts. +# Map internal types back to create-API types. +TYPE_TO_CREATE = { + "Box": "Rectangle", + "box": "Rectangle", +} + + +def topo_sort_shapes(shapes): + """Sort shape IDs so parents come before children.""" + order = [] + visited = set() + + def visit(sid): + if sid in visited: + return + visited.add(sid) + shape = shapes[sid] + parent = shape.get("parent") + if parent and parent in shapes: + visit(parent) + order.append(sid) + + for sid in shapes: + visit(sid) + return order + + +def find_or_create_page(port, name, mapping): + """Find existing page by mapping or name, or create a new one.""" + # Try mapped page_id first + if mapping.get("page_id"): + try: + page = api(port, "page:get", {"pageId": mapping["page_id"]}) + if page: + return mapping["page_id"] + except SystemExit: + pass # Page no longer exists, fall through + + # Search by name in existing pages + doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False}) + if doc and "children" in doc: + for page in doc["children"]: + if page.get("name") == name: + return page["id"] + + # Create new page + page = api(port, "page:add", {"pageProps": {"name": name}}) + return page["id"] + + +def clear_page(port, page_id): + """Delete all shapes on a page.""" + page = api(port, "page:get", {"pageId": page_id, "exportShapes": True}) + if not page or "children" not in page: + return + shape_ids = [s["id"] for s in page["children"]] + if shape_ids: + api(port, "edit:delete", {"shapeIdArray": shape_ids}) + + +def push(wireframe_path, port): + """Push local wireframe JSON to Frame0.""" + with open(wireframe_path) as f: + wireframe = json.load(f) + + name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0]) + shapes = wireframe.get("shapes", {}) + connectors = wireframe.get("connectors", {}) + + mapping = load_mapping(wireframe_path) + + # Find or create page + page_id = find_or_create_page(port, name, mapping) + mapping["page_id"] = page_id + + # Switch to page and clear it + api(port, "page:set-current-page", {"pageId": page_id}) + clear_page(port, page_id) + + # Reset ID mappings (shapes are recreated) + mapping["shapes"] = {} + mapping["connectors"] = {} + + # Create shapes in dependency order + order = topo_sort_shapes(shapes) + for local_id in order: + shape = shapes[local_id] + raw_type = shape.get("type", "Rectangle") + shape_type = TYPE_TO_CREATE.get(raw_type, raw_type) + parent_local = shape.get("parent") + + # Build shapeProps from everything except meta fields + meta_keys = {"type", "parent"} + props = {k: v for k, v in shape.items() if k not in meta_keys} + + # Set name to local_id if not explicitly set + if "name" not in props: + props["name"] = local_id + + create_args = { + "type": shape_type, + "shapeProps": props, + "convertColors": True, + } + + # Resolve parent ID + if parent_local and parent_local in mapping["shapes"]: + create_args["parentId"] = mapping["shapes"][parent_local] + + f0_id = api(port, "shape:create-shape", create_args) + mapping["shapes"][local_id] = f0_id + + # Create connectors + for local_id, conn in connectors.items(): + tail_local = conn.get("tailId") + head_local = conn.get("headId") + + if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]: + print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr) + continue + + meta_keys = {"tailId", "headId"} + props = {k: v for k, v in conn.items() if k not in meta_keys} + if "name" not in props: + props["name"] = local_id + + f0_id = api(port, "shape:create-connector", { + "tailId": mapping["shapes"][tail_local], + "headId": mapping["shapes"][head_local], + "shapeProps": props, + "convertColors": True, + }) + mapping["connectors"][local_id] = f0_id + + # Fit to screen + api(port, "view:fit-to-screen") + + save_mapping(wireframe_path, mapping) + total = len(mapping["shapes"]) + len(mapping["connectors"]) + print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors") + + +# -- Pull: Frame0 -> local JSON ----------------------------------------------- + +def pull(page_ref, output_path, port): + """Pull a Frame0 page into local wireframe JSON.""" + # Resolve page_ref: could be an ID or a name + page_id = None + doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False}) + if doc and "children" in doc: + for page in doc["children"]: + if page["id"] == page_ref or page.get("name") == page_ref: + page_id = page["id"] + page_name = page.get("name", page_ref) + break + + if not page_id: + print(f"ERROR: Page not found: {page_ref}", file=sys.stderr) + sys.exit(1) + + # Load existing mapping for reverse lookup + mapping = load_mapping(output_path) + reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()} + reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()} + + # Get full page with shapes + page = api(port, "page:get", {"pageId": page_id, "exportShapes": True}) + + shapes = {} + connectors = {} + new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}} + auto_id_counter = [0] + + def auto_id(f0_shape): + """Generate a stable local ID from shape name or auto-number.""" + # Prefer existing mapping + f0_id = f0_shape["id"] + if f0_id in reverse_map: + return reverse_map[f0_id] + # Use sanitized name + name = f0_shape.get("name", "") + if name: + sanitized = name.lower().replace(" ", "-").replace("_", "-") + if sanitized not in shapes: + return sanitized + # Fallback: auto-number + auto_id_counter[0] += 1 + return f"s{auto_id_counter[0]:03d}" + + def process_shape(f0_shape, parent_local_id=None): + f0_id = f0_shape["id"] + local_id = auto_id(f0_shape) + new_mapping["shapes"][local_id] = f0_id + + # Extract shape properties — only strip structural keys that our + # ID mapping replaces. Everything else passes through as-is so the + # local JSON speaks Frame0's native vocabulary. + shape_type = f0_shape.get("type", "Box") + skip_keys = {"id", "type", "children", "pageId", "parentId"} + props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None} + + entry = {"type": shape_type} + if parent_local_id: + entry["parent"] = parent_local_id + entry.update(props) + + # Remove name if it matches local_id (redundant) + if entry.get("name") == local_id: + del entry["name"] + + shapes[local_id] = entry + + # Process children recursively + for child in f0_shape.get("children", []): + child_type = child.get("type", "") + if child_type == "Connector": + process_connector(child) + else: + process_shape(child, local_id) + + def process_connector(f0_conn): + f0_id = f0_conn["id"] + local_id = reverse_conn.get(f0_id) + if not local_id: + auto_id_counter[0] += 1 + local_id = f"c{auto_id_counter[0]:03d}" + + new_mapping["connectors"][local_id] = f0_id + + tail_f0 = f0_conn.get("tail", {}).get("id") + head_f0 = f0_conn.get("head", {}).get("id") + + entry = {} + if tail_f0: + # Will be resolved after all shapes are processed + entry["_tailF0"] = tail_f0 + if head_f0: + entry["_headF0"] = head_f0 + + skip_keys = {"id", "type", "children", "pageId", "tail", "head"} + props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None} + entry.update(props) + + connectors[local_id] = entry + + # Process all top-level shapes + for child in page.get("children", []): + child_type = child.get("type", "") + if child_type == "Connector": + process_connector(child) + else: + process_shape(child) + + # Resolve connector references to local IDs + f0_to_local = {v: k for k, v in new_mapping["shapes"].items()} + for conn in connectors.values(): + tail_f0 = conn.pop("_tailF0", None) + head_f0 = conn.pop("_headF0", None) + if tail_f0 and tail_f0 in f0_to_local: + conn["tailId"] = f0_to_local[tail_f0] + if head_f0 and head_f0 in f0_to_local: + conn["headId"] = f0_to_local[head_f0] + + wireframe = {"name": page_name} + if shapes: + wireframe["shapes"] = shapes + if connectors: + wireframe["connectors"] = connectors + + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + with open(output_path, "w") as f: + json.dump(wireframe, f, indent=2) + f.write("\n") + + save_mapping(output_path, new_mapping) + print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors") + + +# -- Export: push then export as image ----------------------------------------- + +def export_image(wireframe_path, output_path, port, fmt="image/png"): + """Push wireframe to Frame0 and export the page as an image.""" + import base64 + + # Push first to ensure Frame0 is up to date + push(wireframe_path, port) + + mapping = load_mapping(wireframe_path) + page_id = mapping.get("page_id") + if not page_id: + print("ERROR: No page_id in mapping after push", file=sys.stderr) + sys.exit(1) + + image_b64 = api(port, "file:export-image", { + "pageId": page_id, + "format": fmt, + "fillBackground": True, + }) + + image_bytes = base64.b64decode(image_b64) + with open(output_path, "wb") as f: + f.write(image_bytes) + + print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)") + + +# -- CLI ----------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0") + parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT))) + sub = parser.add_subparsers(dest="command") + + p_push = sub.add_parser("push", help="Push local JSON to Frame0") + p_push.add_argument("wireframe", help="Path to wireframe .json file") + + p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON") + p_pull.add_argument("page", help="Page ID or page name") + p_pull.add_argument("output", help="Output .json path") + + p_export = sub.add_parser("export", help="Push and export as image") + p_export.add_argument("wireframe", help="Path to wireframe .json file") + p_export.add_argument("output", help="Output image path (e.g. wireframe.png)") + p_export.add_argument("--format", default="image/png", + help="Export MIME type (default: image/png)") + + args = parser.parse_args() + + if args.command == "push": + push(args.wireframe, args.port) + elif args.command == "pull": + pull(args.page, args.output, args.port) + elif args.command == "export": + export_image(args.wireframe, args.output, args.port, args.format) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh b/.claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh deleted file mode 100755 index e6a319c82..000000000 --- a/.claude/skills/frame0-wireframe/scripts/frame0-wireframe.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -CMD="$SCRIPT_DIR/frame0-cmd.sh" - -# Project wireframe defaults from docs/design/visual-grammar-v01.md -BG="#1a1e24" # Zone 1 floor -STROKE="#333340" # Outline standard -FILL="#2a3040" # Zone 1 wall -TEXT="#c8d0e0" # Insert chrome -ACCENT="#c8d8f0" # Zone 1 fixture light -FONT_SIZE=14 -TITLE_SIZE=24 - -usage() { - cat <<EOF -Usage: $(basename "$0") <command> [args...] - -High-level Frame0 wireframe composition helpers. Builds on frame0-cmd.sh -with project-appropriate defaults (colors from visual-grammar-v01.md). - -Commands: - new-page <title> Create a new page with title label - button <label> <x> <y> [w] [h] Labeled button (default 120x36) - text-field <placeholder> <x> <y> [w] [h] Text input field (default 200x32) - container <label> <x> <y> <w> <h> Labeled panel/container - label <text> <x> <y> [font-size] Text label - divider <x> <y> <length> [h|v] Divider line (default horizontal) - export <format> <output-path> Export current page (png/svg) - -Environment: - FRAME0_PORT Frame0 API port (default: 58320) - -Examples: - $(basename "$0") new-page "HUD Layout v1" - $(basename "$0") button "Confirm" 100 200 - $(basename "$0") container "Inventory Panel" 50 50 300 400 - $(basename "$0") export png docs/design/wireframes/hud/hud-v1.png -EOF - exit 1 -} - -[[ $# -lt 1 ]] && usage - -# Ensure Frame0 is running before any operation -check_health() { - "$CMD" health > /dev/null 2>&1 || { - echo "ERROR: Frame0 is not running. Start the desktop app first." >&2 - exit 1 - } -} - -SUBCMD="$1" -shift - -case "$SUBCMD" in - new-page) - [[ $# -lt 1 ]] && { echo "Usage: new-page <title>" >&2; exit 1; } - check_health - local_title="$1" - echo "Creating page: $local_title" - "$CMD" add-page "{\"name\": \"$local_title\"}" - # Add title label at top of page - "$CMD" create-shape text "{\"name\": \"page-title\", \"left\": 20, \"top\": 10, \"width\": 500, \"height\": 40, \"text\": \"$local_title\", \"fontSize\": $TITLE_SIZE, \"fontColor\": \"$TEXT\"}" - echo "Page created: $local_title" - ;; - - button) - [[ $# -lt 3 ]] && { echo "Usage: button <label> <x> <y> [w] [h]" >&2; exit 1; } - check_health - local_label="$1" local_x="$2" local_y="$3" - local_w="${4:-120}" local_h="${5:-36}" - "$CMD" create-shape rectangle "{\"name\": \"$local_label\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_w, \"height\": $local_h, \"fillColor\": \"$FILL\", \"strokeColor\": \"$ACCENT\", \"fontColor\": \"$TEXT\", \"fontSize\": $FONT_SIZE, \"text\": \"$local_label\", \"corners\": [4,4,4,4]}" - ;; - - text-field) - [[ $# -lt 3 ]] && { echo "Usage: text-field <placeholder> <x> <y> [w] [h]" >&2; exit 1; } - check_health - local_label="$1" local_x="$2" local_y="$3" - local_w="${4:-200}" local_h="${5:-32}" - "$CMD" create-shape rectangle "{\"name\": \"$local_label\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_w, \"height\": $local_h, \"fillColor\": \"$BG\", \"strokeColor\": \"$STROKE\", \"fontColor\": \"$TEXT\", \"fontSize\": $FONT_SIZE, \"text\": \"$local_label\", \"corners\": [2,2,2,2]}" - ;; - - container) - [[ $# -lt 5 ]] && { echo "Usage: container <label> <x> <y> <w> <h>" >&2; exit 1; } - check_health - local_label="$1" local_x="$2" local_y="$3" local_w="$4" local_h="$5" - # Container frame - "$CMD" create-shape rectangle "{\"name\": \"$local_label\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_w, \"height\": $local_h, \"fillColor\": \"$BG\", \"strokeColor\": \"$STROKE\", \"fontSize\": 12, \"fontColor\": \"$TEXT\"}" - # Container label at top-left inside - label_x=$((local_x + 8)) - label_y=$((local_y + 4)) - "$CMD" create-shape text "{\"name\": \"${local_label}-label\", \"left\": $label_x, \"top\": $label_y, \"width\": 200, \"height\": 20, \"text\": \"$local_label\", \"fontSize\": 12, \"fontColor\": \"$STROKE\"}" - ;; - - label) - [[ $# -lt 3 ]] && { echo "Usage: label <text> <x> <y> [font-size]" >&2; exit 1; } - check_health - local_text="$1" local_x="$2" local_y="$3" - local_size="${4:-$FONT_SIZE}" - "$CMD" create-shape text "{\"name\": \"$local_text\", \"left\": $local_x, \"top\": $local_y, \"width\": 300, \"height\": 24, \"text\": \"$local_text\", \"fontSize\": $local_size, \"fontColor\": \"$TEXT\"}" - ;; - - divider) - [[ $# -lt 3 ]] && { echo "Usage: divider <x> <y> <length> [h|v]" >&2; exit 1; } - check_health - local_x="$1" local_y="$2" local_len="$3" - local_dir="${4:-h}" - if [[ "$local_dir" == "v" ]]; then - "$CMD" create-shape line "{\"name\": \"divider\", \"left\": $local_x, \"top\": $local_y, \"width\": 1, \"height\": $local_len, \"strokeColor\": \"$STROKE\"}" - else - "$CMD" create-shape line "{\"name\": \"divider\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_len, \"height\": 1, \"strokeColor\": \"$STROKE\"}" - fi - ;; - - export) - [[ $# -lt 2 ]] && { echo "Usage: export <format> <output-path>" >&2; exit 1; } - check_health - local_format="$1" local_output="$2" - - # Get current page ID - local_page_id=$("$CMD" get-page | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null) - if [[ -z "$local_page_id" ]]; then - echo "ERROR: Could not determine current page ID" >&2 - exit 1 - fi - - # Ensure output directory exists - mkdir -p "$(dirname "$local_output")" - - "$CMD" export "$local_page_id" "$local_format" - echo "Exported: $local_output" - ;; - - --help|-h|help) - usage - ;; - - *) - echo "Unknown command: $SUBCMD" >&2 - usage - ;; -esac diff --git a/.gitignore b/.gitignore index cbc8bc4d1..0270ea1b6 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ renderer/output/*.png # Database (shared across worktrees at ../settledreach.db, not tracked) db/commonwealth.db* +# Frame0 ID mapping files (ephemeral, per-machine) +*.idmap.json + # Python __pycache__/ *.pyc