chore(skills): add frame0-wireframe skill for UI wireframing

Replaces Frame0 MCP server with portable bash+curl scripts that call
Frame0's local HTTP API directly. Includes low-level API wrapper,
high-level composition helpers with project styling, component library
patterns, and setup guide. Requires Frame0 desktop app running locally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 11:57:00 +01:00
co-authored by Claude Opus 4.6
parent 1d4b9cf299
commit 08dfadb863
6 changed files with 1046 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
#!/bin/bash
set -euo pipefail
PORT="${FRAME0_PORT:-58320}"
ENDPOINT="http://localhost:${PORT}/execute_command"
usage() {
cat <<EOF
Usage: $(basename "$0") <command> [args...] [--port N]
Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct
curl calls. Requires Frame0 desktop app to be running.
Commands:
health Check if Frame0 is running
exec <namespace:action> <json> Execute a raw API command
create-shape <type> <json-props> Create a shape (rectangle, ellipse, text, etc.)
update-shape <id> <json-props> Update shape properties
delete <id> [id...] Delete shapes by ID
move <id> <dx> <dy> Move a shape by pixel offset
group <id> [id...] Group shapes
ungroup <id> [id...] Ungroup shapes
create-connector <tail-id> <head-id> [json-props] Connect two shapes
create-icon <name> <json-props> Create an icon shape
add-page <json-props> Add a new page
get-page [page-id] Get current or specific page info
list-pages List all pages
set-page <page-id> Set current page
export <page-id> <format> Export page (png/svg)
Options:
--port N Frame0 API port (default: $PORT, env: FRAME0_PORT)
Examples:
$(basename "$0") health
$(basename "$0") create-shape rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
$(basename "$0") list-pages
$(basename "$0") export page-id 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="${2:-\{\}}"
local response
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || {
echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2
echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
return 1
}
local http_code body
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')
if [[ "$http_code" != 2* ]]; then
echo "ERROR: HTTP $http_code from Frame0" >&2
echo "$body" >&2
return 1
fi
# Parse success/error from response
python3 -c "
import sys, json
try:
r = json.loads(sys.stdin.read())
if r.get('success'):
d = r.get('data')
if d is not None:
print(json.dumps(d, indent=2))
else:
print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr)
sys.exit(1)
" <<< "$body"
}
# Build JSON array from remaining args
ids_to_json_array() {
local arr="["
local first=true
for id in "$@"; do
[[ "$first" == true ]] && first=false || arr+=","
arr+="\"$id\""
done
arr+="]"
echo "$arr"
}
CMD="${1:-}"
shift || true
case "$CMD" in
health)
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then
echo "Frame0 is running on port $PORT"
else
echo "Frame0 is NOT running on port $PORT" >&2
echo "Start Frame0 desktop app, then retry." >&2
echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
exit 1
fi
;;
exec)
[[ $# -lt 2 ]] && { echo "Usage: exec <command> <json-args>" >&2; exit 1; }
frame0_exec "$1" "$2"
;;
create-shape)
[[ $# -lt 2 ]] && { echo "Usage: create-shape <type> <json-props>" >&2; exit 1; }
local_type="$1"
local_props="$2"
frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props}"
;;
update-shape)
[[ $# -lt 2 ]] && { echo "Usage: update-shape <id> <json-props>" >&2; exit 1; }
frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2}"
;;
delete)
[[ $# -lt 1 ]] && { echo "Usage: delete <id> [id...]" >&2; exit 1; }
local_arr=$(ids_to_json_array "$@")
frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}"
;;
move)
[[ $# -lt 3 ]] && { echo "Usage: move <id> <dx> <dy>" >&2; exit 1; }
frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}"
;;
group)
[[ $# -lt 1 ]] && { echo "Usage: group <id> [id...]" >&2; exit 1; }
local_arr=$(ids_to_json_array "$@")
frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}"
;;
ungroup)
[[ $# -lt 1 ]] && { echo "Usage: ungroup <id> [id...]" >&2; exit 1; }
local_arr=$(ids_to_json_array "$@")
frame0_exec "shape:ungroup" "{\"shapeIdArray\": $local_arr}"
;;
create-connector)
[[ $# -lt 2 ]] && { echo "Usage: create-connector <tail-id> <head-id> [json-props]" >&2; exit 1; }
local_props="${3:-\{\}}"
frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props}"
;;
create-icon)
[[ $# -lt 2 ]] && { echo "Usage: create-icon <name> <json-props>" >&2; exit 1; }
frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2}"
;;
add-page)
[[ $# -lt 1 ]] && { echo "Usage: add-page <json-props>" >&2; exit 1; }
frame0_exec "page:add" "{\"pageProps\": $1}"
;;
get-page)
if [[ $# -ge 1 ]]; then
frame0_exec "page:get" "{\"pageId\": \"$1\"}"
else
frame0_exec "page:get" "{}"
fi
;;
list-pages)
frame0_exec "page:get-all" "{}"
;;
set-page)
[[ $# -lt 1 ]] && { echo "Usage: set-page <page-id>" >&2; exit 1; }
frame0_exec "page:set-current" "{\"pageId\": \"$1\"}"
;;
export)
[[ $# -lt 2 ]] && { echo "Usage: export <page-id> <format>" >&2; exit 1; }
frame0_exec "file:export-image" "{\"pageId\": \"$1\", \"format\": \"$2\", \"fillBackground\": true}"
;;
--help|-h|help)
usage
;;
*)
echo "Unknown command: $CMD" >&2
usage
;;
esac
+144
View File
@@ -0,0 +1,144 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CMD="$SCRIPT_DIR/frame0-cmd.sh"
# Project wireframe defaults from docs/design/visual-grammar-v01.md
BG="#1a1e24" # Zone 1 floor
STROKE="#333340" # Outline standard
FILL="#2a3040" # Zone 1 wall
TEXT="#c8d0e0" # Insert chrome
ACCENT="#c8d8f0" # Zone 1 fixture light
FONT_SIZE=14
TITLE_SIZE=24
usage() {
cat <<EOF
Usage: $(basename "$0") <command> [args...]
High-level Frame0 wireframe composition helpers. Builds on frame0-cmd.sh
with project-appropriate defaults (colors from visual-grammar-v01.md).
Commands:
new-page <title> Create a new page with title label
button <label> <x> <y> [w] [h] Labeled button (default 120x36)
text-field <placeholder> <x> <y> [w] [h] Text input field (default 200x32)
container <label> <x> <y> <w> <h> Labeled panel/container
label <text> <x> <y> [font-size] Text label
divider <x> <y> <length> [h|v] Divider line (default horizontal)
export <format> <output-path> Export current page (png/svg)
Environment:
FRAME0_PORT Frame0 API port (default: 58320)
Examples:
$(basename "$0") new-page "HUD Layout v1"
$(basename "$0") button "Confirm" 100 200
$(basename "$0") container "Inventory Panel" 50 50 300 400
$(basename "$0") export png docs/design/wireframes/hud/hud-v1.png
EOF
exit 1
}
[[ $# -lt 1 ]] && usage
# Ensure Frame0 is running before any operation
check_health() {
"$CMD" health > /dev/null 2>&1 || {
echo "ERROR: Frame0 is not running. Start the desktop app first." >&2
exit 1
}
}
SUBCMD="$1"
shift
case "$SUBCMD" in
new-page)
[[ $# -lt 1 ]] && { echo "Usage: new-page <title>" >&2; exit 1; }
check_health
local_title="$1"
echo "Creating page: $local_title"
"$CMD" add-page "{\"name\": \"$local_title\"}"
# Add title label at top of page
"$CMD" create-shape text "{\"name\": \"page-title\", \"left\": 20, \"top\": 10, \"width\": 500, \"height\": 40, \"text\": \"$local_title\", \"fontSize\": $TITLE_SIZE, \"fontColor\": \"$TEXT\"}"
echo "Page created: $local_title"
;;
button)
[[ $# -lt 3 ]] && { echo "Usage: button <label> <x> <y> [w] [h]" >&2; exit 1; }
check_health
local_label="$1" local_x="$2" local_y="$3"
local_w="${4:-120}" local_h="${5:-36}"
"$CMD" create-shape rectangle "{\"name\": \"$local_label\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_w, \"height\": $local_h, \"fillColor\": \"$FILL\", \"strokeColor\": \"$ACCENT\", \"fontColor\": \"$TEXT\", \"fontSize\": $FONT_SIZE, \"text\": \"$local_label\", \"corners\": [4,4,4,4]}"
;;
text-field)
[[ $# -lt 3 ]] && { echo "Usage: text-field <placeholder> <x> <y> [w] [h]" >&2; exit 1; }
check_health
local_label="$1" local_x="$2" local_y="$3"
local_w="${4:-200}" local_h="${5:-32}"
"$CMD" create-shape rectangle "{\"name\": \"$local_label\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_w, \"height\": $local_h, \"fillColor\": \"$BG\", \"strokeColor\": \"$STROKE\", \"fontColor\": \"$TEXT\", \"fontSize\": $FONT_SIZE, \"text\": \"$local_label\", \"corners\": [2,2,2,2]}"
;;
container)
[[ $# -lt 5 ]] && { echo "Usage: container <label> <x> <y> <w> <h>" >&2; exit 1; }
check_health
local_label="$1" local_x="$2" local_y="$3" local_w="$4" local_h="$5"
# Container frame
"$CMD" create-shape rectangle "{\"name\": \"$local_label\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_w, \"height\": $local_h, \"fillColor\": \"$BG\", \"strokeColor\": \"$STROKE\", \"fontSize\": 12, \"fontColor\": \"$TEXT\"}"
# Container label at top-left inside
label_x=$((local_x + 8))
label_y=$((local_y + 4))
"$CMD" create-shape text "{\"name\": \"${local_label}-label\", \"left\": $label_x, \"top\": $label_y, \"width\": 200, \"height\": 20, \"text\": \"$local_label\", \"fontSize\": 12, \"fontColor\": \"$STROKE\"}"
;;
label)
[[ $# -lt 3 ]] && { echo "Usage: label <text> <x> <y> [font-size]" >&2; exit 1; }
check_health
local_text="$1" local_x="$2" local_y="$3"
local_size="${4:-$FONT_SIZE}"
"$CMD" create-shape text "{\"name\": \"$local_text\", \"left\": $local_x, \"top\": $local_y, \"width\": 300, \"height\": 24, \"text\": \"$local_text\", \"fontSize\": $local_size, \"fontColor\": \"$TEXT\"}"
;;
divider)
[[ $# -lt 3 ]] && { echo "Usage: divider <x> <y> <length> [h|v]" >&2; exit 1; }
check_health
local_x="$1" local_y="$2" local_len="$3"
local_dir="${4:-h}"
if [[ "$local_dir" == "v" ]]; then
"$CMD" create-shape line "{\"name\": \"divider\", \"left\": $local_x, \"top\": $local_y, \"width\": 1, \"height\": $local_len, \"strokeColor\": \"$STROKE\"}"
else
"$CMD" create-shape line "{\"name\": \"divider\", \"left\": $local_x, \"top\": $local_y, \"width\": $local_len, \"height\": 1, \"strokeColor\": \"$STROKE\"}"
fi
;;
export)
[[ $# -lt 2 ]] && { echo "Usage: export <format> <output-path>" >&2; exit 1; }
check_health
local_format="$1" local_output="$2"
# Get current page ID
local_page_id=$("$CMD" get-page | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
if [[ -z "$local_page_id" ]]; then
echo "ERROR: Could not determine current page ID" >&2
exit 1
fi
# Ensure output directory exists
mkdir -p "$(dirname "$local_output")"
"$CMD" export "$local_page_id" "$local_format"
echo "Exported: $local_output"
;;
--help|-h|help)
usage
;;
*)
echo "Unknown command: $SUBCMD" >&2
usage
;;
esac