From 78d5f22104e3813750b7e744af4d7f609514ff61 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 2 Jul 2026 11:55:28 +0200 Subject: [PATCH] docs(skills): prune bundled skills to clide-specific set (T-507) The 2026-07-02 skill review found the repo bundling skills that don't belong to it: d2-diagram and frame0-wireframe are repo-agnostic and carried settled-reach game content verbatim (agent personas, palette, templates) presented as clide conventions; skill-create was a byte-identical copy of a generic skill; clean-house was double-installed (the pql-managed user-scope copy is canonical); the generic commit skill collided with git-commit on its trigger word while contradicting three repo safety rules; penpot-login was fully broken and published personal infrastructure in a public repo. d2-diagram, frame0-wireframe, and skill-create now live de-contaminated at user scope (~/.claude/skills/). The commit/penpot-login/clean-house/ skill-create deletions landed with the previous commit (pre-staged by git rm); this one removes the centralized pair and rewrites the README index to the five clide-specific skills that remain. Co-Authored-By: Claude --- .claude/skills/README.md | 23 +- .claude/skills/d2-diagram/SKILL.md | 151 ------ .../d2-diagram/references/d2-syntax-guide.md | 212 -------- .../references/diagram-templates.md | 251 ---------- .claude/skills/d2-diagram/scripts/d2-batch.sh | 84 ---- .../skills/d2-diagram/scripts/d2-render.sh | 92 ---- .claude/skills/frame0-wireframe/SKILL.md | 222 --------- .../references/api-reference.md | 241 --------- .../references/component-library.md | 467 ------------------ .../references/setup-guide.md | 53 -- .../frame0-wireframe/scripts/frame0-cmd.sh | 253 ---------- .../scripts/frame0-export-batch.sh | 159 ------ .../frame0-wireframe/scripts/frame0-sync.py | 414 ---------------- CHANGELOG.md | 5 + 14 files changed, 18 insertions(+), 2609 deletions(-) delete mode 100644 .claude/skills/d2-diagram/SKILL.md delete mode 100644 .claude/skills/d2-diagram/references/d2-syntax-guide.md delete mode 100644 .claude/skills/d2-diagram/references/diagram-templates.md delete mode 100755 .claude/skills/d2-diagram/scripts/d2-batch.sh delete mode 100755 .claude/skills/d2-diagram/scripts/d2-render.sh delete mode 100644 .claude/skills/frame0-wireframe/SKILL.md delete mode 100644 .claude/skills/frame0-wireframe/references/api-reference.md delete mode 100644 .claude/skills/frame0-wireframe/references/component-library.md delete mode 100644 .claude/skills/frame0-wireframe/references/setup-guide.md delete mode 100755 .claude/skills/frame0-wireframe/scripts/frame0-cmd.sh delete mode 100755 .claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh delete mode 100755 .claude/skills/frame0-wireframe/scripts/frame0-sync.py diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 36ad049c..993aab71 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -5,19 +5,22 @@ lives in its own directory with a `SKILL.md` (frontmatter + body) and any helper scripts. This index is for humans skimming what's available; Claude discovers skills automatically from the directory structure. +Only clide-specific skills live in this repo. Repo-agnostic skills +(`d2-diagram`, `frame0-wireframe`, `skill-create`) live at user scope +(`~/.claude/skills/`), as do the pql-distributed ones (`pql`, `clean-house`, +installed via `pql init`). + | Skill | Purpose | |---|---| -| [`d2-diagram`](d2-diagram/SKILL.md) | Generate technical diagrams from `.d2` source with the d2 CLI; renders to PNG. | -| [`frame0-wireframe`](frame0-wireframe/SKILL.md) | Author UI wireframes as local JSON and sync to the Frame0 desktop app for rendering + export. | -| [`git-commit`](git-commit/SKILL.md) | Commit conventions for this repo — message style, CHANGELOG discipline (40/60 word cap), attribution trailer, safety rules. | -| [`pql`](pql/SKILL.md) | Query and plan against the markdown vault via the `pql` CLI (decisions, tickets, structural queries). | -| [`skill-create`](skill-create/SKILL.md) | Guidance for creating new skills — SKILL.md structure, bundling scripts, packaging. | -| [`testmode`](testmode/SKILL.md) | Run and interpret the `ClideTestApp` platform integration harness; smoke-test after toolchain / IPC / theme / native changes. | -| [`ui-design`](ui-design/SKILL.md) | Visual design guide — surface tokens, control geometry, Phosphor icons. | +| [`clide`](clide/SKILL.md) | Observe/drive the live clide UI through the `clide` CLI; discover the surface with `clide capabilities`. | +| [`git-commit`](git-commit/SKILL.md) | Commit conventions for this repo — message style, CHANGELOG discipline (60-word cap), attribution trailer, safety rules. | +| [`testmode`](testmode/SKILL.md) | Run and interpret the `ClideTestApp` platform integration harness; smoke-test after toolchain / IPC / PTY / theme / native changes. | +| [`ui-design`](ui-design/SKILL.md) | Visual design guide — surface tokens, control geometry, Phosphor icons (`byName` + generated glyph table). | | [`whats-next`](whats-next/SKILL.md) | Dependency-driven batch selection against the pql backlog. Walks the initiative/epic tree, filters unblocked work, refines, optionally activates. | ## Adding a skill -Use the `skill-create` skill (or follow its SKILL.md by hand). Add a row to -the table above so the inventory stays accurate; the index is otherwise just -a directory listing. +Use the `skill-create` skill (user scope; or follow its SKILL.md by hand). +Clide-specific skills go here; repo-agnostic ones go to `~/.claude/skills/`. +Add a row to the table above so the inventory stays accurate; the index is +otherwise just a directory listing. diff --git a/.claude/skills/d2-diagram/SKILL.md b/.claude/skills/d2-diagram/SKILL.md deleted file mode 100644 index c39ef4c9..00000000 --- a/.claude/skills/d2-diagram/SKILL.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -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 deleted file mode 100644 index e1c05de5..00000000 --- a/.claude/skills/d2-diagram/references/d2-syntax-guide.md +++ /dev/null @@ -1,212 +0,0 @@ -# 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 deleted file mode 100644 index d134c973..00000000 --- a/.claude/skills/d2-diagram/references/diagram-templates.md +++ /dev/null @@ -1,251 +0,0 @@ -# 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 deleted file mode 100755 index fe456ad5..00000000 --- a/.claude/skills/d2-diagram/scripts/d2-batch.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/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 deleted file mode 100755 index fc561d92..00000000 --- a/.claude/skills/d2-diagram/scripts/d2-render.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/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 deleted file mode 100644 index dc80e021..00000000 --- a/.claude/skills/frame0-wireframe/SKILL.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -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 - -Wireframing is a **design conversation, not a batch job.** Produce **one screen -at a time**, show it, and **stop for the user's reaction** before authoring the -next. Each shown screen is a checkpoint — never fan out a whole set of -wireframes unprompted, even if the user described several screens up front. - -1. **Health check** — verify Frame0 is running -2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json`. - Author **one** screen. -3. **Push to Frame0** — `frame0-sync.py push ` -4. **Export PNG** — `frame0-sync.py export ` -5. **Show it inline** — surface the exported PNG in the conversation so the - user sees the result without opening Frame0. Always do this after an export, - and again whenever you re-export or the user asks to see it. - - **Inside clide** (the IDE hosting this session — `CLIDE_SOCK` is set): - render it as a native image card in the live Claude pane with - `clide image show [--caption "…"]` (T-249). This is the *proper* - way to put an image in the clide conversation — a bare `Read` only shows - it in the transcript, not in the UI the user is looking at. - - **Otherwise**: `Read` the PNG so it renders inline in the transcript. -6. **STOP and wait for approval** — after showing a screen, pause and ask the - user for feedback. Do **not** author, push, or export the next screen until - they give an explicit go-ahead. Iterate on the current screen until they're - happy, then proceed to the next one — looping back to step 2 for each. -7. **Clean up** — delete test/scratch pages from Frame0 when done - -> **Multi-screen requests are still one-at-a-time.** If the user asks for -> several screens, treat it as a queue: build the first, show it, get approval, -> then move on. The only exception is an explicit, unambiguous instruction to -> generate a batch without stopping (and even then prefer the batch-export -> dry-run + approval flow below). - -### 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 deleted file mode 100644 index 616c6690..00000000 --- a/.claude/skills/frame0-wireframe/references/api-reference.md +++ /dev/null @@ -1,241 +0,0 @@ -# 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 deleted file mode 100644 index 3ae364dd..00000000 --- a/.claude/skills/frame0-wireframe/references/component-library.md +++ /dev/null @@ -1,467 +0,0 @@ -# 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 deleted file mode 100644 index c0f56abd..00000000 --- a/.claude/skills/frame0-wireframe/references/setup-guide.md +++ /dev/null @@ -1,53 +0,0 @@ -# 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 deleted file mode 100755 index 6fc23750..00000000 --- a/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh +++ /dev/null @@ -1,253 +0,0 @@ -#!/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 deleted file mode 100755 index d506858f..00000000 --- a/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/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 deleted file mode 100755 index df514c5a..00000000 --- a/.claude/skills/frame0-wireframe/scripts/frame0-sync.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/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/CHANGELOG.md b/CHANGELOG.md index 4a17eb4e..29a1e338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- Bundled Claude skills pruned to clide-specific ones (T-507): broken/duplicate + skills removed, repo-agnostic ones moved to user scope, and stale guidance in + the remaining five (paths, commands, icon API) brought back in line with the + repo. + ### Fixed ## [2.9.0] — 2026-06-30