add frame0-wireframe and d2-diagram skills

Two general-purpose skills for visual design work:
- frame0-wireframe drives Frame0 (local wireframing app) from
  JSON source files, with push/pull/export and a batch script.
- d2-diagram wraps the d2 text-to-diagram CLI for architecture
  and flow diagrams.

Sourced from settled-reach/main where they were already in use.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-06 11:10:36 +02:00
co-authored by Claude
parent 2d64740007
commit 18d4c5978b
13 changed files with 2575 additions and 1 deletions
+151
View File
@@ -0,0 +1,151 @@
---
name: d2-diagram
description: >
Generate technical diagrams using d2 (text-to-diagram CLI). Use when the
user says "create a diagram", "draw architecture", "make a flowchart",
"diagram this", "render d2", "d2", "data flow diagram", "entity relationship",
"state machine", "sequence diagram", "UI flow", or invokes /d2-diagram.
Produces .d2 source files and renders them to PNG. Also use when asked
to update, re-render, or batch render existing diagrams.
---
# d2 Diagram Generation
Generate technical diagrams from text using d2 (v0.7.1). Pure CLI, no
external dependencies beyond the d2 binary.
**Binary:** `/home/linuxbrew/.linuxbrew/bin/d2`
## Project Defaults
| Setting | Value | Override |
|---------|-------|----------|
| Theme | 200 (Dark Mauve) | `--theme N` |
| Layout | dagre | `--layout elk` |
| Padding | 100px | — |
| Format | PNG | `--svg` |
## Output Convention
```
docs/diagrams/
architecture/ # System architecture, IPC, component layout
data-flow/ # Sequence diagrams, data pipelines
entity/ # ER diagrams, ECS component schemas
state/ # State machines, behavior trees
ui/ # UI navigation flow, screen transitions
```
Both `.d2` source and `.png` output are tracked in git.
## Single Diagram Workflow
1. **Determine category** — architecture, data-flow, entity, state, or ui
2. **Read template**`references/diagram-templates.md` for the matching category
3. **Read syntax**`references/d2-syntax-guide.md` if unfamiliar with d2 syntax
4. **Write .d2 source** — to `docs/diagrams/{category}/{name}.d2`
5. **Validate**`.claude/skills/d2-diagram/scripts/d2-render.sh validate {file}`
6. **Render**`.claude/skills/d2-diagram/scripts/d2-render.sh {file}`
7. **Read SVG** — verify the output, present to user
### Script Usage
```bash
# Render with project defaults
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/ipc-bridge.d2
# Validate syntax only
.claude/skills/d2-diagram/scripts/d2-render.sh validate docs/diagrams/architecture/ipc-bridge.d2
# Auto-format source
.claude/skills/d2-diagram/scripts/d2-render.sh fmt docs/diagrams/architecture/ipc-bridge.d2
# Sketch mode (hand-drawn look for drafts)
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/ui/flow.d2 --sketch
# Light theme (for printable docs)
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/entity/schema.d2 --theme 0
# SVG output (if specifically needed)
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/overview.d2 --svg
```
## Batch Render
Re-render all diagrams after theme or style changes:
```bash
# All diagrams
.claude/skills/d2-diagram/scripts/d2-batch.sh
# One category
.claude/skills/d2-diagram/scripts/d2-batch.sh docs/diagrams/architecture/
# Preview what would render
.claude/skills/d2-diagram/scripts/d2-batch.sh --dry-run
# Force re-render everything
.claude/skills/d2-diagram/scripts/d2-batch.sh --force
```
Batch skips files whose PNG is newer than the `.d2` source unless `--force`.
## Advanced Patterns
### Variables for consistent styling
```d2
vars: {
color-bg: "#2a3040"
color-stroke: "#333340"
color-text: "#c8d0e0"
color-accent: "#c8d8f0"
}
```
### Multi-board (layers)
```d2
# Base diagram here
layers: {
detailed: {
# More detailed view
}
}
```
### Sequence diagrams
```d2
shape: sequence_diagram
client: Godot Client
server: Rust Server
client -> server: TickRequest(delta)
server -> client: WorldState(entities)
```
### Imports
Split shared definitions into a separate file and import:
```d2
...@shared-defs.d2
```
## Agent Guidance
- **Qatux** — Architecture decision records, system overview diagrams, data
schemas. Prefer architecture and entity templates.
- **Tyre** — IPC bridge, ECS system flow, chunk loading pipeline, perception
system data flow. Prefer architecture and data-flow templates.
- **Araminta** — UI navigation flow, screen transitions, component hierarchy.
Prefer UI flow template.
## References
- `references/d2-syntax-guide.md` — Language quick reference (shapes, edges,
containers, styling, variables). Read when unfamiliar with d2 syntax.
- `references/diagram-templates.md` — Five category templates with complete
d2 source examples. Read when starting a new diagram.
@@ -0,0 +1,212 @@
# D2 Language Quick Reference
## Nodes
```d2
server # Implicit label from key
server: Simulation Server # Explicit label
server: Simulation Server { # With properties
shape: hexagon
style.fill: "#2d3436"
}
```
## Edges
```d2
a -> b # Directed
a <- b # Reverse directed
a <-> b # Bidirectional
a -- b # Undirected
a -> b: "label" # Labeled edge
a -> b -> c # Chained
```
## Containers (nesting)
```d2
infrastructure: {
server: Simulation Server
database: State Store {
shape: cylinder
}
}
```
## Shapes
| Shape | Use for |
|-------|---------|
| `rectangle` | Default. Components, modules, generic. |
| `hexagon` | Systems, services, major components. |
| `cylinder` | Databases, storage, persistent state. |
| `diamond` | Decisions, conditions, branch points. |
| `oval` / `circle` | Start/end states, events. |
| `cloud` | External systems, networks. |
| `person` | Actors, users, NPCs. |
| `queue` | Message queues, buffers. |
| `page` | Documents, files. |
| `package` | Packages, modules, crates. |
| `sql_table` | Database tables, ECS component schemas. |
| `class` | Class diagrams, ECS system definitions. |
| `code` | Code blocks (set `language` property). |
| `markdown` | Rich text blocks. |
## SQL Tables
```d2
entity: {
shape: sql_table
id: u64 {constraint: primary_key}
name: String
position: Vec2
faction_id: u64 {constraint: foreign_key}
}
```
## Class Diagrams
```d2
perception_system: {
shape: class
+run(world: &mut World)
-calculate_los(entity: Entity): HashSet<Entity>
#update_knowledge(entity: Entity, seen: HashSet<Entity>)
}
```
## Sequence Diagrams
```d2
shape: sequence_diagram
client: Godot Client
server: Rust Server
client -> server: TickRequest(delta)
server -> server: run ECS systems
server -> client: WorldState(entities)
```
## Styling
```d2
node: Label {
style: {
fill: "#2d3436"
stroke: "#333340"
stroke-width: 2
stroke-dash: 5 # Dashed line
opacity: 0.8
font-size: 14
font-color: "#c8d0e0"
bold: true
italic: false
border-radius: 4
shadow: true
3d: true # Rectangles only
multiple: true # Stacked appearance
double-border: true # Rectangles/ovals only
}
}
```
### Edge styling
```d2
a -> b: {
style: {
stroke: "#c8d8f0"
stroke-width: 2
stroke-dash: 5
opacity: 0.8
animated: true # Animated flow
}
}
```
## Variables
```d2
vars: {
color-bg: "#1a1e24"
color-stroke: "#333340"
color-text: "#c8d0e0"
color-accent: "#c8d8f0"
}
node: {
style.fill: ${color-bg}
style.stroke: ${color-stroke}
style.font-color: ${color-text}
}
```
## Direction
```d2
direction: right # left-to-right (default for dagre)
direction: down # top-to-bottom
direction: up
direction: left
```
## Imports
```d2
...@shared-defs.d2 # Spread import (inline all definitions)
```
## Icons
```d2
node: Label {
icon: https://icons.terrastruct.com/essentials/time.svg
}
```
## Layers (multi-board)
```d2
# Base diagram content here
layers: {
detailed: {
# More detailed view
}
simplified: {
# Simplified overview
}
}
```
## Scenarios (animated transitions)
```d2
# Base state
scenarios: {
alert: {
# Changes from base for alert state
}
combat: {
# Changes from base for combat state
}
}
```
## Comments
```d2
# This is a comment
node: Label # Inline comment
```
## Project Colors (from visual-grammar-v01.md)
| Constant | Hex | Usage |
|----------|-----|-------|
| Zone 1 floor | `#1a1e24` | Dark backgrounds |
| Zone 1 wall | `#2a3040` | Component fill |
| Outline standard | `#333340` | Borders, strokes |
| Insert chrome | `#c8d0e0` | Text, labels |
| Zone 1 fixture | `#c8d8f0` | Accents, highlights |
@@ -0,0 +1,251 @@
# Diagram Templates
Copy, adapt, and render. Each template uses project colors from visual-grammar-v01.md.
---
## 1. Architecture Diagram
System components, relationships, communication channels.
**When to use:** IPC bridge, perception pipeline, chunk loading, ECS system layout, client-server architecture.
**Agents:** Tyre (system architecture), Qatux (architecture decision records).
```d2
vars: {
color-bg: "#2a3040"
color-stroke: "#333340"
color-text: "#c8d0e0"
color-accent: "#c8d8f0"
}
direction: right
client: Godot Client {
shape: hexagon
style.fill: ${color-bg}
style.font-color: ${color-text}
rendering: Rendering {
style.fill: ${color-bg}
style.font-color: ${color-text}
}
ui: UI Layer {
style.fill: ${color-bg}
style.font-color: ${color-text}
}
bridge: IPC Bridge {
style.fill: ${color-bg}
style.font-color: ${color-text}
style.stroke: ${color-accent}
}
}
server: Rust Server {
shape: hexagon
style.fill: ${color-bg}
style.font-color: ${color-text}
ecs: bevy_ecs {
style.fill: ${color-bg}
style.font-color: ${color-text}
}
perception: Perception {
style.fill: ${color-bg}
style.font-color: ${color-text}
}
bridge: IPC Bridge {
style.fill: ${color-bg}
style.font-color: ${color-text}
style.stroke: ${color-accent}
}
}
client.bridge -> server.bridge: "stdin/stdout" {
style.stroke: ${color-accent}
style.stroke-dash: 5
}
```
---
## 2. Entity Relationship
Data schemas, ECS components, knowledge graph structure.
**When to use:** Database tables, component definitions, entity relationships, knowledge store schema.
**Agents:** Tyre (ECS component design), Qatux (schema documentation).
```d2
entity: Entity {
shape: sql_table
id: u64 {constraint: primary_key}
name: String
faction_id: u64 {constraint: foreign_key}
}
position: Position {
shape: sql_table
entity_id: u64 {constraint: foreign_key}
x: f32
y: f32
chunk_id: u32
}
knowledge: KnowledgeEntry {
shape: sql_table
observer_id: u64 {constraint: foreign_key}
subject_id: u64 {constraint: foreign_key}
fact_type: FactType
confidence: f32
last_seen_tick: u64
}
entity.id -> position.entity_id
entity.id -> knowledge.observer_id
entity.id -> knowledge.subject_id
```
---
## 3. Sequence / Data Flow
Ordered interactions between systems over time.
**When to use:** IPC message flow, tick processing, perception update cycle, dialogue system exchanges.
**Agents:** Tyre (system interaction design), Qatux (protocol documentation).
```d2
shape: sequence_diagram
client: Godot Client
bridge: IPC Bridge
server: Rust Server
ecs: ECS Systems
client -> bridge: TickRequest(delta, input)
bridge -> server: deserialize + dispatch
server -> ecs: run_systems(delta)
ecs -> ecs: perception, AI, physics
ecs -> server: collect WorldState
server -> bridge: serialize WorldState
bridge -> client: WorldState(entities, events)
client -> client: update rendering
```
---
## 4. State Machine
Entity states, transitions, conditions.
**When to use:** NPC behavior states, game mode transitions, dialogue state, investigation phases.
**Agents:** Tyre (behavior system design), Qatux (state documentation).
```d2
vars: {
color-state: "#2a3040"
color-text: "#c8d0e0"
color-edge: "#c8d8f0"
color-decision: "#333340"
}
idle: Idle {
style.fill: ${color-state}
style.font-color: ${color-text}
}
alert: Alert {
style.fill: ${color-state}
style.font-color: ${color-text}
}
investigate: Investigate {
style.fill: ${color-state}
style.font-color: ${color-text}
}
combat: Combat {
style.fill: ${color-state}
style.font-color: ${color-text}
style.stroke: "#f0b840"
}
flee: Flee {
style.fill: ${color-state}
style.font-color: ${color-text}
}
idle -> alert: "stimulus detected" { style.stroke: ${color-edge} }
alert -> investigate: "stimulus confirmed" { style.stroke: ${color-edge} }
alert -> idle: "timeout / stimulus lost" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
investigate -> combat: "threat confirmed" { style.stroke: "#f0b840" }
investigate -> idle: "nothing found" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
combat -> flee: "health < threshold" { style.stroke: "#f0b840" }
combat -> idle: "threat eliminated" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
flee -> idle: "safe distance reached" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
```
---
## 5. UI Flow
Screen navigation, component hierarchy, interaction paths.
**When to use:** HUD layout relationships, menu navigation, dialogue flow, insert mode transitions.
**Agents:** Araminta (UI/visual design), Tyre (interface architecture), Qatux (UI documentation).
```d2
vars: {
color-screen: "#1a1e24"
color-panel: "#2a3040"
color-text: "#c8d0e0"
color-nav: "#c8d8f0"
}
gameplay: Gameplay {
style.fill: ${color-screen}
style.font-color: ${color-text}
hud: HUD {
style.fill: ${color-panel}
style.font-color: ${color-text}
minimap: Minimap
monologue: Monologue Panel
insert_display: Insert Display
}
world: World View {
style.fill: ${color-panel}
style.font-color: ${color-text}
}
}
pause: Pause Menu {
style.fill: ${color-screen}
style.font-color: ${color-text}
inventory: Inventory
journal: Journal
settings: Settings
}
dialogue: Dialogue Mode {
style.fill: ${color-screen}
style.font-color: ${color-text}
speaker: Speaker Panel
responses: Response List
}
gameplay -> pause: "ESC" { style.stroke: ${color-nav} }
pause -> gameplay: "ESC / Resume" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
gameplay -> dialogue: "interact with NPC" { style.stroke: ${color-nav} }
dialogue -> gameplay: "end conversation" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
```
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
RENDER="$SCRIPT_DIR/d2-render.sh"
usage() {
cat <<EOF
Usage: $(basename "$0") [directory] [options]
Batch render all .d2 files in a directory. Skips files whose PNG is
newer than the source unless --force is used.
Options:
--dry-run List files that would be rendered
--force Re-render even if SVG is up to date
--theme N Override theme for all files
Examples:
$(basename "$0") # All in docs/diagrams/
$(basename "$0") docs/diagrams/architecture/ # One category
$(basename "$0") --dry-run # Preview
$(basename "$0") --force # Re-render everything
EOF
exit 1
}
DIR="$REPO_ROOT/docs/diagrams"
DRY_RUN=false
FORCE=false
EXTRA_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--force) FORCE=true; shift ;;
--theme) EXTRA_ARGS+=(--theme "$2"); shift 2 ;;
--help|-h) usage ;;
*)
if [[ -d "$1" ]] || [[ -d "$REPO_ROOT/$1" ]]; then
DIR="$1"
[[ "$DIR" != /* ]] && DIR="$REPO_ROOT/$DIR"
else
echo "Unknown option or directory: $1" >&2; exit 1
fi
shift
;;
esac
done
[[ ! -d "$DIR" ]] && { echo "ERROR: Directory not found: $DIR" >&2; exit 1; }
RENDERED=0
SKIPPED=0
FAILED=0
while IFS= read -r -d '' d2_file; do
png_file="${d2_file%.d2}.png"
# Skip if PNG is newer than source (unless --force)
if [[ "$FORCE" != true ]] && [[ -f "$png_file" ]] && [[ "$png_file" -nt "$d2_file" ]]; then
SKIPPED=$((SKIPPED + 1))
continue
fi
rel_path="${d2_file#"$REPO_ROOT/"}"
if [[ "$DRY_RUN" == true ]]; then
echo "Would render: $rel_path"
RENDERED=$((RENDERED + 1))
continue
fi
if "$RENDER" "$d2_file" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; then
RENDERED=$((RENDERED + 1))
else
echo "FAILED: $rel_path" >&2
FAILED=$((FAILED + 1))
fi
done < <(find "$DIR" -name '*.d2' -print0 | sort -z)
echo ""
echo "Batch complete: $RENDERED rendered, $SKIPPED skipped (up to date), $FAILED failed"
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
set -euo pipefail
D2="/home/linuxbrew/.linuxbrew/bin/d2"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
DEFAULT_THEME=200
DEFAULT_LAYOUT="dagre"
DEFAULT_PAD=100
usage() {
cat <<EOF
Usage: $(basename "$0") [validate|fmt] <file.d2> [options]
Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout).
Commands:
validate <file> Check syntax without rendering
fmt <file> Auto-format in place
Options:
--theme N Override theme (default: $DEFAULT_THEME)
--layout NAME Override layout engine (default: $DEFAULT_LAYOUT)
--sketch Enable hand-drawn sketch mode
--output PATH Override output path (default: input with .png extension)
--svg Render to SVG instead of PNG
Examples:
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2
$(basename "$0") validate docs/diagrams/architecture/ipc-bridge.d2
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 --sketch --theme 0
EOF
exit 1
}
[[ $# -lt 1 ]] && usage
# Parse subcommand
SUBCMD=""
case "$1" in
validate|fmt)
SUBCMD="$1"
shift
;;
esac
[[ $# -lt 1 ]] && usage
INPUT="$1"
shift
# Resolve to absolute path
[[ "$INPUT" != /* ]] && INPUT="$REPO_ROOT/$INPUT"
[[ ! -f "$INPUT" ]] && { echo "ERROR: File not found: $INPUT" >&2; exit 1; }
# Handle subcommands
if [[ -n "$SUBCMD" ]]; then
"$D2" "$SUBCMD" "$INPUT"
echo "OK: $SUBCMD $INPUT"
exit 0
fi
# Parse render options
THEME="$DEFAULT_THEME"
LAYOUT="$DEFAULT_LAYOUT"
SKETCH=""
OUTPUT=""
FORMAT="png"
while [[ $# -gt 0 ]]; do
case "$1" in
--theme) THEME="$2"; shift 2 ;;
--layout) LAYOUT="$2"; shift 2 ;;
--sketch) SKETCH="-s"; shift ;;
--output) OUTPUT="$2"; shift 2 ;;
--svg) FORMAT="svg"; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# Derive output path
if [[ -z "$OUTPUT" ]]; then
OUTPUT="${INPUT%.d2}.$FORMAT"
fi
# Render
"$D2" -t "$THEME" -l "$LAYOUT" --pad "$DEFAULT_PAD" $SKETCH "$INPUT" "$OUTPUT"
SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null)
echo "Rendered: $OUTPUT ($(( SIZE / 1024 ))KB)"
+197
View File
@@ -0,0 +1,197 @@
---
name: frame0-wireframe
description: >
Create and export UI wireframes using Frame0 (local desktop wireframing
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.
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 as JSON files, sync them to Frame0 for rendering, and
export as PNG. Local JSON is the source of truth — Frame0 is the renderer.
**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
Always check first:
```bash
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
```
## Core Workflow
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
### Scripts
| 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 |
## Wireframe JSON Format
```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"
}
}
}
```
### Key rules
- **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
### 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
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 | Frame0 token |
|------|-----|-------------|
| Background | `#1a1e24` | `$sage3` |
| Stroke | `#333340` | `$slate6` |
| Fill | `#2a3040` | `$slate5` |
| Text | `#c8d0e0` | `$mint12` |
| Accent | `#c8d8f0` | `$blue12` |
Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push.
## Output Convention
```
docs/design/wireframes/
hud/ # HUD layout wireframes
menus/ # Menu screen wireframes
dialogue/ # Dialogue box wireframes
insert/ # Neural insert wireframes
```
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.
- **Qatux** — Export wireframes for UI decision records and documentation.
## References
- `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.
@@ -0,0 +1,241 @@
# Frame0 HTTP API Reference
Frame0 exposes a local HTTP API when the desktop app is running.
## Endpoint
```
POST http://localhost:{port}/execute_command
Content-Type: application/json
```
Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag).
## Request / Response
```json
{"command": "namespace:action", "args": { ... }}
```
```json
{"success": true, "data": { ... }}
{"success": false, "error": "description"}
```
---
## Type Mapping
Frame0 uses different type names for create vs get:
| 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 |
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",
"shapeProps": {
"name": "my-button",
"left": 100, "top": 200, "width": 120, "height": 36,
"fillColor": "#2a3040",
"strokeColor": "#c8d8f0",
"corners": [4, 4, 4, 4]
},
"parentId": "optional-parent-shape-id",
"convertColors": true
}
}
```
Returns: shape ID (string).
### shape:get-shape
```json
{"command": "shape:get-shape", "args": {"shapeId": "id"}}
```
### shape:update-shape
```json
{
"command": "shape:update-shape",
"args": {
"shapeId": "id",
"shapeProps": {"fillColor": "#1a1e24", "text": "Updated"},
"convertColors": true
}
}
```
### shape:move
```json
{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}}
```
### shape:create-connector
```json
{
"command": "shape:create-connector",
"args": {
"tailId": "source-id",
"headId": "target-id",
"shapeProps": {"strokeColor": "#c8d8f0"},
"convertColors": true
}
}
```
### shape:create-icon
```json
{
"command": "shape:create-icon",
"args": {
"iconName": "search",
"shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24}
}
}
```
### shape:get-available-icons
```json
{"command": "shape:get-available-icons", "args": {}}
```
### shape:group / shape:ungroup
```json
{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}}
{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}}
```
### edit:delete / edit:duplicate
```json
{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}}
{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}}
```
### page:add
```json
{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}}
```
Returns: `{id, type, name}`.
### page:get
```json
{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}}
```
### page:get-current-page
```json
{"command": "page:get-current-page", "args": {}}
```
Returns: page ID string.
### page:set-current-page
```json
{"command": "page:set-current-page", "args": {"pageId": "id"}}
```
### doc:get (list all pages)
```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": "optional-page-id",
"format": "image/png",
"fillBackground": true
}
}
```
Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`.
Returns: base64-encoded image data.
### view:fit-to-screen
```json
{"command": "view:fit-to-screen", "args": {}}
```
---
## Shape Properties
| Property | Type | Notes |
|----------|------|-------|
| `name` | string | Shape identifier/label |
| `left` | number | X position (origin: top-left) |
| `top` | number | Y position |
| `width` | number | Width in pixels |
| `height` | number | Height in pixels |
| `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 |
| `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 |
@@ -0,0 +1,467 @@
# Component Library
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
Main gameplay overlay. Minimap top-right, monologue bottom-center,
insert display bottom-left, action hints bottom-right.
```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
}
}
}
```
---
## 2. Dialogue Box
Speaker panel with response options. Anchored bottom-center during dialogue mode.
```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
}
}
}
```
---
## 3. Menu Screen
Full-screen menu with sidebar navigation and content area.
```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
}
}
}
```
---
## 4. Modal Dialog
Centered overlay for confirmations, alerts, choices.
```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
}
}
}
```
---
## 5. List View
Scrollable list with item selection and detail panel.
```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
}
}
}
```
---
## 6. Inventory Grid
Grid of cells for item management.
```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
}
}
}
```
@@ -0,0 +1,53 @@
# Frame0 Setup Guide
## Installation (Fedora)
Download from https://frame0.app/download and install the RPM:
```bash
sudo dnf install ./frame0-*.x86_64.rpm
```
Requires: Fedora 40 or later (x86_64).
## Starting Frame0
Launch the desktop application:
```bash
frame0 &
```
Frame0 exposes an HTTP API at `localhost:58320` when running.
## Verify API Access
```bash
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
```
Expected output: `Frame0 is running on port 58320`
## Port Configuration
Default port: **58320**
To use a different port, set the environment variable:
```bash
export FRAME0_PORT=58321
```
Or pass `--port` to any script:
```bash
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh --port 58321 health
```
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| "Connection refused" | Frame0 not running | Start the desktop app |
| "Port in use" | Another instance running | Close duplicate or use different port |
| Script hangs | API unresponsive | Restart Frame0 |
+253
View File
@@ -0,0 +1,253 @@
#!/bin/bash
set -euo pipefail
PORT="${FRAME0_PORT:-58320}"
ENDPOINT="http://localhost:${PORT}/execute_command"
usage() {
cat <<EOF
Usage: $(basename "$0") <command> [args...] [--port N]
Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct
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, 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 <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 <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 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") 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 --format image/png
EOF
exit 1
}
# Parse --port from anywhere in args
ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--port) PORT="$2"; ENDPOINT="http://localhost:${PORT}/execute_command"; shift 2 ;;
*) ARGS+=("$1"); shift ;;
esac
done
set -- "${ARGS[@]+"${ARGS[@]}"}"
[[ $# -lt 1 ]] && usage
# Execute a Frame0 API command, return data or error
frame0_exec() {
local command="$1"
local args
args="${2:-"{}"}"
local response
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || {
echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2
echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
return 1
}
local http_code body
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')
if [[ "$http_code" != 2* ]]; then
echo "ERROR: HTTP $http_code from Frame0" >&2
echo "$body" >&2
return 1
fi
# Parse success/error from response
python3 -c "
import sys, json
try:
r = json.loads(sys.stdin.read())
if r.get('success'):
d = r.get('data')
if d is not None:
print(json.dumps(d, indent=2))
else:
print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr)
sys.exit(1)
" <<< "$body"
}
# Build JSON array from remaining args
ids_to_json_array() {
local arr="["
local first=true
for id in "$@"; do
[[ "$first" == true ]] && first=false || arr+=","
arr+="\"$id\""
done
arr+="]"
echo "$arr"
}
CMD="${1:-}"
shift || true
case "$CMD" in
health)
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then
echo "Frame0 is running on port $PORT"
else
echo "Frame0 is NOT running on port $PORT" >&2
echo "Start Frame0 desktop app, then retry." >&2
echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
exit 1
fi
;;
exec)
[[ $# -lt 2 ]] && { echo "Usage: exec <command> <json-args>" >&2; exit 1; }
frame0_exec "$1" "$2"
;;
create-shape)
[[ $# -lt 2 ]] && { echo "Usage: create-shape <Type> <json-props>" >&2; exit 1; }
local_type="$1"
local_props="$2"
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, \"convertColors\": true}"
;;
delete)
[[ $# -lt 1 ]] && { echo "Usage: delete <id> [id...]" >&2; exit 1; }
local_arr=$(ids_to_json_array "$@")
frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}"
;;
move)
[[ $# -lt 3 ]] && { echo "Usage: move <id> <dx> <dy>" >&2; exit 1; }
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 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 <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, \"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, \"convertColors\": true}"
;;
add-page)
[[ $# -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\", \"exportShapes\": true}"
else
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)
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-page" "{\"pageId\": \"$1\"}"
;;
export)
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)
usage
;;
*)
echo "Unknown command: $CMD" >&2
usage
;;
esac
@@ -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
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
{
"exported_at": "2026-05-06T09:10:17Z",
"exported_at": "2026-05-06T09:10:36Z",
"decisions": [
{
"id": "D-1",