chore(skills): rewrite frame0-wireframe skill with JSON sync workflow
Replace individual API call workflow with JSON-as-truth architecture. Local wireframe JSON files are the source of truth; Frame0 is treated purely as a renderer. New frame0-sync.py handles push/pull/export with stable local ID <-> ephemeral Frame0 ID mapping via .idmap.json files. Key changes: - frame0-sync.py: push/pull/export with topo-sorted shape creation - frame0-export-batch.sh: batch export with staleness check - frame0-cmd.sh: fixed command names, list-pages JSON bug, added commands - Removed frame0-wireframe.sh (superseded by JSON workflow) - SKILL.md: renderer-only guidance, simplified workflow - api-reference.md: corrected commands, type/color token mapping - component-library.md: all 6 patterns rewritten as JSON templates - .gitignore: added *.idmap.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <file.json>`
|
||||
4. **Export PNG** — `frame0-sync.py export <file.json> <output.png>`
|
||||
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 <title>` | — | 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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
+414
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user