diff --git a/.claude/skills/d2-diagram/SKILL.md b/.claude/skills/d2-diagram/SKILL.md new file mode 100644 index 0000000..c39ef4c --- /dev/null +++ b/.claude/skills/d2-diagram/SKILL.md @@ -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. diff --git a/.claude/skills/d2-diagram/references/d2-syntax-guide.md b/.claude/skills/d2-diagram/references/d2-syntax-guide.md new file mode 100644 index 0000000..e1c05de --- /dev/null +++ b/.claude/skills/d2-diagram/references/d2-syntax-guide.md @@ -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 + #update_knowledge(entity: Entity, seen: HashSet) +} +``` + +## 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 | diff --git a/.claude/skills/d2-diagram/references/diagram-templates.md b/.claude/skills/d2-diagram/references/diagram-templates.md new file mode 100644 index 0000000..d134c97 --- /dev/null +++ b/.claude/skills/d2-diagram/references/diagram-templates.md @@ -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 } +``` diff --git a/.claude/skills/d2-diagram/scripts/d2-batch.sh b/.claude/skills/d2-diagram/scripts/d2-batch.sh new file mode 100755 index 0000000..fe456ad --- /dev/null +++ b/.claude/skills/d2-diagram/scripts/d2-batch.sh @@ -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 <&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" diff --git a/.claude/skills/d2-diagram/scripts/d2-render.sh b/.claude/skills/d2-diagram/scripts/d2-render.sh new file mode 100755 index 0000000..fc561d9 --- /dev/null +++ b/.claude/skills/d2-diagram/scripts/d2-render.sh @@ -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 < [options] + +Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout). + +Commands: + validate Check syntax without rendering + fmt 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)" diff --git a/.claude/skills/frame0-wireframe/SKILL.md b/.claude/skills/frame0-wireframe/SKILL.md new file mode 100644 index 0000000..6cc572f --- /dev/null +++ b/.claude/skills/frame0-wireframe/SKILL.md @@ -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 ` +4. **Export PNG** — `frame0-sync.py export ` +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 +$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}' +$CMD create-connector +$CMD move +$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. diff --git a/.claude/skills/frame0-wireframe/references/api-reference.md b/.claude/skills/frame0-wireframe/references/api-reference.md new file mode 100644 index 0000000..616c669 --- /dev/null +++ b/.claude/skills/frame0-wireframe/references/api-reference.md @@ -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 | diff --git a/.claude/skills/frame0-wireframe/references/component-library.md b/.claude/skills/frame0-wireframe/references/component-library.md new file mode 100644 index 0000000..3ae364d --- /dev/null +++ b/.claude/skills/frame0-wireframe/references/component-library.md @@ -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 + } + } +} +``` diff --git a/.claude/skills/frame0-wireframe/references/setup-guide.md b/.claude/skills/frame0-wireframe/references/setup-guide.md new file mode 100644 index 0000000..c0f56ab --- /dev/null +++ b/.claude/skills/frame0-wireframe/references/setup-guide.md @@ -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 | diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh b/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh new file mode 100755 index 0000000..6fc2375 --- /dev/null +++ b/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh @@ -0,0 +1,253 @@ +#!/bin/bash +set -euo pipefail + +PORT="${FRAME0_PORT:-58320}" +ENDPOINT="http://localhost:${PORT}/execute_command" + +usage() { + cat < [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 Execute a raw API command + create-shape Create a shape (Rectangle, Ellipse, Text, Line) + get-shape Get shape details + update-shape Update shape properties + delete [id...] Delete shapes by ID + move Move a shape by pixel offset + duplicate Duplicate a shape + group [id...] Group shapes + ungroup Ungroup a group + create-connector [json-props] Connect two shapes + create-icon Create an icon shape + add-page 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 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 " >&2; exit 1; } + frame0_exec "$1" "$2" + ;; + + create-shape) + [[ $# -lt 2 ]] && { echo "Usage: create-shape " >&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 " >&2; exit 1; } + frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}" + ;; + + update-shape) + [[ $# -lt 2 ]] && { echo "Usage: update-shape " >&2; exit 1; } + frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}" + ;; + + delete) + [[ $# -lt 1 ]] && { echo "Usage: delete [id...]" >&2; exit 1; } + local_arr=$(ids_to_json_array "$@") + frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}" + ;; + + move) + [[ $# -lt 3 ]] && { echo "Usage: move " >&2; exit 1; } + frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}" + ;; + + duplicate) + [[ $# -lt 1 ]] && { echo "Usage: duplicate [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...]" >&2; exit 1; } + local_arr=$(ids_to_json_array "$@") + frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}" + ;; + + ungroup) + [[ $# -lt 1 ]] && { echo "Usage: ungroup " >&2; exit 1; } + frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}" + ;; + + create-connector) + [[ $# -lt 2 ]] && { echo "Usage: create-connector [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 " >&2; exit 1; } + frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}" + ;; + + add-page) + [[ $# -lt 1 ]] && { echo "Usage: add-page " >&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 " >&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 diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh b/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh new file mode 100755 index 0000000..d506858 --- /dev/null +++ b/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# Batch export wireframes from JSON to PNG via Frame0. +# +# Finds all .json wireframe files under docs/design/wireframes/ and exports +# each to a matching .png. Skips files whose PNG is already newer than the +# JSON, unless --force is passed. +# +# Usage: +# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR] +# +# Options: +# --dry-run Print manifest only, don't touch Frame0. +# --force Re-export even if PNG already exists and is up to date. +# --category CAT Limit to one subdirectory (e.g. --category dialogue) +# --root DIR Wireframes root dir (default: docs/design/wireframes) +# +# Exit codes: +# 0 All exports succeeded (or nothing to do) +# 1 One or more exports failed + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +SYNC="$SCRIPT_DIR/frame0-sync.py" +DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes" + +DRY_RUN=false +FORCE=false +CATEGORY="" +WF_ROOT="$DEFAULT_ROOT" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --force) FORCE=true; shift ;; + --category) CATEGORY="$2"; shift 2 ;; + --root) WF_ROOT="$2"; shift 2 ;; + -h|--help) + sed -n '/^# /p' "$0" | sed 's/^# //' + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [[ ! -d "$WF_ROOT" ]]; then + echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2 + exit 1 +fi + +# Collect JSON files, optionally filtered by category subdirectory +mapfile -t JSON_FILES < <( + if [[ -n "$CATEGORY" ]]; then + find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort + else + find "$WF_ROOT" -name "*.json" ! -name ".*" | sort + fi +) + +if [[ ${#JSON_FILES[@]} -eq 0 ]]; then + echo "No wireframe JSON files found." + exit 0 +fi + +# Classify files into to-export and to-skip +TO_EXPORT=() +TO_SKIP=() + +for json in "${JSON_FILES[@]}"; do + png="${json%.json}.png" + if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then + TO_EXPORT+=("$json") + else + TO_SKIP+=("$json") + fi +done + +# Print manifest +if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then + echo "" + echo "Will export (${#TO_EXPORT[@]} files):" + for json in "${TO_EXPORT[@]}"; do + png="${json%.json}.png" + rel="${json#$REPO_ROOT/}" + if [[ ! -f "$png" ]]; then + status="new" + else + status="updated" + fi + printf " [%-7s] %s\n" "$status" "$rel" + done +else + echo "" + echo "Nothing to export (all PNGs up to date)." +fi + +if [[ ${#TO_SKIP[@]} -gt 0 ]]; then + echo "" + echo "Will skip (${#TO_SKIP[@]} files already up to date):" + for json in "${TO_SKIP[@]}"; do + rel="${json#$REPO_ROOT/}" + printf " [skip ] %s\n" "$rel" + done +fi + +if $DRY_RUN; then + echo "" + echo "Dry run — no exports performed." + exit 0 +fi + +if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then + exit 0 +fi + +echo "" +PASSED=0 +FAILED=0 +FAILED_FILES=() + +TOTAL=${#TO_EXPORT[@]} +IDX=0 + +for json in "${TO_EXPORT[@]}"; do + IDX=$((IDX + 1)) + png="${json%.json}.png" + rel="${json#$REPO_ROOT/}" + + printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel" + + output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt) + rc=$? + if [[ $rc -eq 0 ]]; then + size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true) + echo "ok $size" + PASSED=$((PASSED + 1)) + else + echo "FAILED" + cat /tmp/frame0-batch-err.txt >&2 + FAILED=$((FAILED + 1)) + FAILED_FILES+=("$rel") + fi +done + +echo "" +echo "$PASSED exported, $FAILED failed." + +if [[ $FAILED -gt 0 ]]; then + echo "" + echo "Failed:" >&2 + for f in "${FAILED_FILES[@]}"; do + echo " $f" >&2 + done + exit 1 +fi + +exit 0 diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-sync.py b/.claude/skills/frame0-wireframe/scripts/frame0-sync.py new file mode 100755 index 0000000..df514c5 --- /dev/null +++ b/.claude/skills/frame0-wireframe/scripts/frame0-sync.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back. + +Local JSON is source of truth. Frame0 is a renderer. +A mapping file tracks local_id <-> frame0_id across push/pull cycles. + +Usage: + frame0-sync.py push [--port PORT] + frame0-sync.py pull [--port PORT] + frame0-sync.py export [--port PORT] [--format MIME] +""" + +import argparse +import json +import os +import sys +import urllib.request +import urllib.error + +DEFAULT_PORT = 58320 + + +def api(port, command, args=None): + """Execute a Frame0 API command. Returns the data field on success.""" + url = f"http://localhost:{port}/execute_command" + payload = json.dumps({"command": command, "args": args or {}}).encode() + req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + except urllib.error.HTTPError as e: + body = e.read().decode() + print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr) + sys.exit(1) + + if not result.get("success"): + print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr) + sys.exit(1) + + return result.get("data") + + +# -- Mapping file management -------------------------------------------------- + +def mapping_path(wireframe_path): + """Derive the idmap path from the wireframe JSON path.""" + d = os.path.dirname(wireframe_path) + base = os.path.splitext(os.path.basename(wireframe_path))[0] + return os.path.join(d, f".{base}.idmap.json") + + +def load_mapping(wireframe_path): + p = mapping_path(wireframe_path) + if os.path.exists(p): + with open(p) as f: + return json.load(f) + return {"page_id": None, "shapes": {}, "connectors": {}} + + +def save_mapping(wireframe_path, mapping): + p = mapping_path(wireframe_path) + os.makedirs(os.path.dirname(p) or ".", exist_ok=True) + with open(p, "w") as f: + json.dump(mapping, f, indent=2) + f.write("\n") + + +# -- Push: local JSON -> Frame0 ----------------------------------------------- + +# Frame0 returns different type names from get vs what create accepts. +# Map internal types back to create-API types. +TYPE_TO_CREATE = { + "Box": "Rectangle", + "box": "Rectangle", +} + + +def topo_sort_shapes(shapes): + """Sort shape IDs so parents come before children.""" + order = [] + visited = set() + + def visit(sid): + if sid in visited: + return + visited.add(sid) + shape = shapes[sid] + parent = shape.get("parent") + if parent and parent in shapes: + visit(parent) + order.append(sid) + + for sid in shapes: + visit(sid) + return order + + +def find_or_create_page(port, name, mapping): + """Find existing page by mapping or name, or create a new one.""" + # Try mapped page_id first + if mapping.get("page_id"): + try: + page = api(port, "page:get", {"pageId": mapping["page_id"]}) + if page: + return mapping["page_id"] + except SystemExit: + pass # Page no longer exists, fall through + + # Search by name in existing pages + doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False}) + if doc and "children" in doc: + for page in doc["children"]: + if page.get("name") == name: + return page["id"] + + # Create new page + page = api(port, "page:add", {"pageProps": {"name": name}}) + return page["id"] + + +def clear_page(port, page_id): + """Delete all shapes on a page.""" + page = api(port, "page:get", {"pageId": page_id, "exportShapes": True}) + if not page or "children" not in page: + return + shape_ids = [s["id"] for s in page["children"]] + if shape_ids: + api(port, "edit:delete", {"shapeIdArray": shape_ids}) + + +def push(wireframe_path, port): + """Push local wireframe JSON to Frame0.""" + with open(wireframe_path) as f: + wireframe = json.load(f) + + name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0]) + shapes = wireframe.get("shapes", {}) + connectors = wireframe.get("connectors", {}) + + mapping = load_mapping(wireframe_path) + + # Find or create page + page_id = find_or_create_page(port, name, mapping) + mapping["page_id"] = page_id + + # Switch to page and clear it + api(port, "page:set-current-page", {"pageId": page_id}) + clear_page(port, page_id) + + # Reset ID mappings (shapes are recreated) + mapping["shapes"] = {} + mapping["connectors"] = {} + + # Create shapes in dependency order + order = topo_sort_shapes(shapes) + for local_id in order: + shape = shapes[local_id] + raw_type = shape.get("type", "Rectangle") + shape_type = TYPE_TO_CREATE.get(raw_type, raw_type) + parent_local = shape.get("parent") + + # Build shapeProps from everything except meta fields + meta_keys = {"type", "parent"} + props = {k: v for k, v in shape.items() if k not in meta_keys} + + # Set name to local_id if not explicitly set + if "name" not in props: + props["name"] = local_id + + create_args = { + "type": shape_type, + "shapeProps": props, + "convertColors": True, + } + + # Resolve parent ID + if parent_local and parent_local in mapping["shapes"]: + create_args["parentId"] = mapping["shapes"][parent_local] + + f0_id = api(port, "shape:create-shape", create_args) + mapping["shapes"][local_id] = f0_id + + # Create connectors + for local_id, conn in connectors.items(): + tail_local = conn.get("tailId") + head_local = conn.get("headId") + + if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]: + print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr) + continue + + meta_keys = {"tailId", "headId"} + props = {k: v for k, v in conn.items() if k not in meta_keys} + if "name" not in props: + props["name"] = local_id + + f0_id = api(port, "shape:create-connector", { + "tailId": mapping["shapes"][tail_local], + "headId": mapping["shapes"][head_local], + "shapeProps": props, + "convertColors": True, + }) + mapping["connectors"][local_id] = f0_id + + # Fit to screen + api(port, "view:fit-to-screen") + + save_mapping(wireframe_path, mapping) + total = len(mapping["shapes"]) + len(mapping["connectors"]) + print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors") + + +# -- Pull: Frame0 -> local JSON ----------------------------------------------- + +def pull(page_ref, output_path, port): + """Pull a Frame0 page into local wireframe JSON.""" + # Resolve page_ref: could be an ID or a name + page_id = None + doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False}) + if doc and "children" in doc: + for page in doc["children"]: + if page["id"] == page_ref or page.get("name") == page_ref: + page_id = page["id"] + page_name = page.get("name", page_ref) + break + + if not page_id: + print(f"ERROR: Page not found: {page_ref}", file=sys.stderr) + sys.exit(1) + + # Load existing mapping for reverse lookup + mapping = load_mapping(output_path) + reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()} + reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()} + + # Get full page with shapes + page = api(port, "page:get", {"pageId": page_id, "exportShapes": True}) + + shapes = {} + connectors = {} + new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}} + auto_id_counter = [0] + + def auto_id(f0_shape): + """Generate a stable local ID from shape name or auto-number.""" + # Prefer existing mapping + f0_id = f0_shape["id"] + if f0_id in reverse_map: + return reverse_map[f0_id] + # Use sanitized name + name = f0_shape.get("name", "") + if name: + sanitized = name.lower().replace(" ", "-").replace("_", "-") + if sanitized not in shapes: + return sanitized + # Fallback: auto-number + auto_id_counter[0] += 1 + return f"s{auto_id_counter[0]:03d}" + + def process_shape(f0_shape, parent_local_id=None): + f0_id = f0_shape["id"] + local_id = auto_id(f0_shape) + new_mapping["shapes"][local_id] = f0_id + + # Extract shape properties — only strip structural keys that our + # ID mapping replaces. Everything else passes through as-is so the + # local JSON speaks Frame0's native vocabulary. + shape_type = f0_shape.get("type", "Box") + skip_keys = {"id", "type", "children", "pageId", "parentId"} + props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None} + + entry = {"type": shape_type} + if parent_local_id: + entry["parent"] = parent_local_id + entry.update(props) + + # Remove name if it matches local_id (redundant) + if entry.get("name") == local_id: + del entry["name"] + + shapes[local_id] = entry + + # Process children recursively + for child in f0_shape.get("children", []): + child_type = child.get("type", "") + if child_type == "Connector": + process_connector(child) + else: + process_shape(child, local_id) + + def process_connector(f0_conn): + f0_id = f0_conn["id"] + local_id = reverse_conn.get(f0_id) + if not local_id: + auto_id_counter[0] += 1 + local_id = f"c{auto_id_counter[0]:03d}" + + new_mapping["connectors"][local_id] = f0_id + + tail_f0 = f0_conn.get("tail", {}).get("id") + head_f0 = f0_conn.get("head", {}).get("id") + + entry = {} + if tail_f0: + # Will be resolved after all shapes are processed + entry["_tailF0"] = tail_f0 + if head_f0: + entry["_headF0"] = head_f0 + + skip_keys = {"id", "type", "children", "pageId", "tail", "head"} + props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None} + entry.update(props) + + connectors[local_id] = entry + + # Process all top-level shapes + for child in page.get("children", []): + child_type = child.get("type", "") + if child_type == "Connector": + process_connector(child) + else: + process_shape(child) + + # Resolve connector references to local IDs + f0_to_local = {v: k for k, v in new_mapping["shapes"].items()} + for conn in connectors.values(): + tail_f0 = conn.pop("_tailF0", None) + head_f0 = conn.pop("_headF0", None) + if tail_f0 and tail_f0 in f0_to_local: + conn["tailId"] = f0_to_local[tail_f0] + if head_f0 and head_f0 in f0_to_local: + conn["headId"] = f0_to_local[head_f0] + + wireframe = {"name": page_name} + if shapes: + wireframe["shapes"] = shapes + if connectors: + wireframe["connectors"] = connectors + + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + with open(output_path, "w") as f: + json.dump(wireframe, f, indent=2) + f.write("\n") + + save_mapping(output_path, new_mapping) + print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors") + + +# -- Export: push then export as image ----------------------------------------- + +def export_image(wireframe_path, output_path, port, fmt="image/png"): + """Push wireframe to Frame0 and export the page as an image.""" + import base64 + + # Push first to ensure Frame0 is up to date + push(wireframe_path, port) + + mapping = load_mapping(wireframe_path) + page_id = mapping.get("page_id") + if not page_id: + print("ERROR: No page_id in mapping after push", file=sys.stderr) + sys.exit(1) + + image_b64 = api(port, "file:export-image", { + "pageId": page_id, + "format": fmt, + "fillBackground": True, + }) + + image_bytes = base64.b64decode(image_b64) + with open(output_path, "wb") as f: + f.write(image_bytes) + + print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)") + + +# -- CLI ----------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0") + parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT))) + sub = parser.add_subparsers(dest="command") + + p_push = sub.add_parser("push", help="Push local JSON to Frame0") + p_push.add_argument("wireframe", help="Path to wireframe .json file") + + p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON") + p_pull.add_argument("page", help="Page ID or page name") + p_pull.add_argument("output", help="Output .json path") + + p_export = sub.add_parser("export", help="Push and export as image") + p_export.add_argument("wireframe", help="Path to wireframe .json file") + p_export.add_argument("output", help="Output image path (e.g. wireframe.png)") + p_export.add_argument("--format", default="image/png", + help="Export MIME type (default: image/png)") + + args = parser.parse_args() + + if args.command == "push": + push(args.wireframe, args.port) + elif args.command == "pull": + pull(args.page, args.output, args.port) + elif args.command == "export": + export_image(args.wireframe, args.output, args.port, args.format) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/skill-create/SKILL.md b/.claude/skills/skill-create/SKILL.md new file mode 100644 index 0000000..623dd0c --- /dev/null +++ b/.claude/skills/skill-create/SKILL.md @@ -0,0 +1,203 @@ +--- +name: skill-create +description: > + Guidance for creating effective Claude Code skills (.skill packages). + Use when the user wants to create, build, design, or iterate on a skill — + including writing SKILL.md files, bundling scripts/references/assets, + initializing new skills, packaging skills, or improving existing ones. + Triggers on requests like "create a skill", "make a new skill", + "build a skill for X", "package this skill", or "improve my skill". +--- + + +# Skill Creator + +## About Skills + +Skills are modular, self-contained packages that extend Claude's capabilities +by providing specialized knowledge, workflows, and tools. They transform Claude +from a general-purpose agent into a specialized agent equipped with procedural +knowledge that no model can fully possess. + +### What Skills Provide + +- **Specialized workflows** — Multi-step procedures for specific domains +- **Tool integrations** — Instructions for working with specific file formats or APIs +- **Domain expertise** — Company-specific knowledge, schemas, business logic +- **Bundled resources** — Scripts, references, and assets for complex and repetitive tasks + +## Core Principles + +### Concise is Key + +The context window is a public good. Skills share it with everything else Claude +needs: system prompt, conversation history, other skills' metadata, and the +actual user request. + +Default assumption: Claude is already very smart. Only add context Claude doesn't +already have. Challenge each piece of information: "Does Claude really need this +explanation?" and "Does this paragraph justify its token cost?" + +Prefer concise examples over verbose explanations. + +### Set Appropriate Degrees of Freedom + +Match specificity to the task's fragility and variability: + +- **High freedom** (text-based instructions): Multiple approaches valid, decisions + depend on context, heuristics guide the approach. +- **Medium freedom** (pseudocode or scripts with parameters): Preferred pattern + exists, some variation acceptable, configuration affects behavior. +- **Low freedom** (specific scripts, few parameters): Operations are fragile and + error-prone, consistency is critical, specific sequence must be followed. + +Think of Claude as exploring a path: a narrow bridge with cliffs needs specific +guardrails (low freedom), while an open field allows many routes (high freedom). + +## Anatomy of a Skill + +``` +skill-name/ +├── SKILL.md (required) +│ ├── YAML frontmatter metadata (required) +│ │ ├── name: (required) +│ │ ├── description: (required) +│ │ └── compatibility: (optional, rarely needed) +│ └── Markdown instructions (required) +└── Bundled Resources (optional) + ├── scripts/ - Executable code (Python/Bash/etc.) + ├── references/ - Documentation loaded into context as needed + └── assets/ - Files used in output (templates, icons, fonts, etc.) +``` + +### SKILL.md (required) + +- **Frontmatter (YAML)**: `name` and `description` fields (required). Only these + are read by Claude to determine when the skill triggers — be clear and + comprehensive. The `compatibility` field is for environment requirements but + most skills don't need it. +- **Body (Markdown)**: Instructions and guidance. Only loaded AFTER the skill + triggers. + +### Bundled Resources (optional) + +**Scripts (`scripts/`)** — Executable code for tasks requiring deterministic +reliability or that are repeatedly rewritten. + +**References (`references/`)** — Documentation loaded as needed into context. +Keep SKILL.md lean; move detailed reference material, schemas, and examples here. +If files are large (>10k words), include grep search patterns in SKILL.md. + +**Assets (`assets/`)** — Files used in output, not loaded into context (templates, +images, icons, boilerplate code, fonts). + +### What to NOT Include + +Do NOT create extraneous files like README.md, INSTALLATION_GUIDE.md, +QUICK_REFERENCE.md, CHANGELOG.md, etc. The skill should only contain information +needed for an AI agent to do the job. + +## Progressive Disclosure + +Skills use a three-level loading system: + +1. **Metadata** (name + description) — Always in context (~100 words) +2. **SKILL.md body** — When skill triggers (<5k words) +3. **Bundled resources** — As needed (unlimited; scripts can run without reading) + +Keep SKILL.md body under 500 lines. Split content into separate files when +approaching this limit. Reference split files from SKILL.md with clear +descriptions of when to read them. + +### Disclosure Patterns + +**Pattern 1: High-level guide with references** — Keep overview in SKILL.md, +link to detail files loaded only when needed. + +**Pattern 2: Domain-specific organization** — Organize content by domain +(e.g., `references/finance.md`, `references/sales.md`) so only relevant content +is loaded. + +**Pattern 3: Conditional details** — Show basic content, link to advanced +content loaded only when the user needs those features. + +Guidelines: +- Avoid deeply nested references — keep one level deep from SKILL.md +- Structure longer reference files with a table of contents at the top + +## Skill Creation Process + +Follow these steps in order, skipping only with clear reason: + +### Step 1: Understand the Skill with Concrete Examples + +Skip only when usage patterns are already clearly understood. + +Ask the user for concrete examples of how the skill will be used: +- "What functionality should the skill support?" +- "Can you give some examples of how this skill would be used?" +- "What would a user say that should trigger this skill?" + +Avoid overwhelming users — start with the most important questions. + +### Step 2: Plan the Reusable Skill Contents + +Analyze each example by considering how to execute from scratch and identifying +what scripts, references, and assets would help with repeated execution. + +Establish a list of reusable resources: scripts, references, and assets. + +### Step 3: Initialize the Skill + +Create the skill directory manually: + +``` +mkdir -p / +``` + +Then create `SKILL.md` with frontmatter and body. Add `scripts/`, `references/`, +and `assets/` subdirectories only as needed. + +Skip if iterating on an existing skill. + +### Step 4: Edit the Skill + +Remember the skill is for another Claude instance to use. Include beneficial, +non-obvious information. + +For design patterns, consult: +- `references/workflows.md` — Sequential workflows and conditional logic +- `references/output-patterns.md` — Template and example patterns + +**Implementation order:** +1. Start with reusable resources (`scripts/`, `references/`, `assets/`) +2. Test added scripts by running them +3. Delete unused example files from initialization +4. Update SKILL.md + +**Writing guidelines:** Always use imperative/infinitive form. + +**Frontmatter:** +- `name`: The skill name — use **domain-action** naming: `{domain}-{action}`. + The domain is the system/area the skill operates on, the action is what it does. + Examples: `pr-review`, `sprint-plan`, `docs-search`, `git-commit`, `debt-scan`. + Multi-action wrappers (like `ticket`) can use the domain name alone. + The directory name must match the `name` field. +- `description`: Primary triggering mechanism. Include what the skill does AND + specific triggers/contexts. All "when to use" info goes here (not in body). + +**Body:** Instructions for using the skill and its bundled resources. + +### Step 5: Validate the Skill + +Check the skill manually: +- Frontmatter has `name` and `description` +- SKILL.md body is under 500 lines +- No extraneous files (README.md, CHANGELOG.md, etc.) +- Scripts are executable and tested +- References are referenced from SKILL.md + +### Step 6: Iterate + +After real usage, notice struggles or inefficiencies, identify needed updates, +implement changes, and test again.