diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..830ca8d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,124 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "figma-bridge-wasm" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "switchboard-guest-sdk", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "switchboard-guest-sdk" +version = "0.1.0" +source = "git+https://github.com/daltoniam/switchboard.git#62423750d8f845cca1e14e840a81d0775e094c54" +dependencies = [ + "base64", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/README.md b/README.md index dda39b4..71dafc6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A home for integrations that make more sense as standalone WASM modules than as | Plugin | Tools | Description | |--------|-------|-------------| -| _none yet_ | | | +| `figma_bridge` | 15 | Figma Desktop Bridge — write to canvas via the Plugin API (create frames, components, auto layout, execute arbitrary plugin code) | Prebuilt binaries live in [`dist/`](dist/) and are referenced by [`manifest.json`](manifest.json). diff --git a/dist/figma_bridge.wasm b/dist/figma_bridge.wasm new file mode 100755 index 0000000..494fc0b Binary files /dev/null and b/dist/figma_bridge.wasm differ diff --git a/manifest.json b/manifest.json index db60b1b..947a75e 100644 --- a/manifest.json +++ b/manifest.json @@ -2,5 +2,19 @@ "schema_version": 1, "name": "daltoniam-plugins", "description": "Third-party Switchboard WASM plugins by @daltoniam", - "plugins": [] + "plugins": [ + { + "name": "figma_bridge", + "description": "Figma Desktop Bridge — access the Figma Plugin API for write operations (create frames, components, auto layout, etc.) via a companion plugin running in Figma Desktop.", + "versions": [ + { + "version": "0.1.0", + "sha256": "d9ef1dc78172583a8e254dbb984c099320e87c3ae2c789df89846d21358d0c4d", + "size": 172703, + "released_at": "2025-05-15T00:00:00Z", + "url": "https://raw.githubusercontent.com/daltoniam/switchboard_plugins/main/dist/figma_bridge.wasm" + } + ] + } + ] } diff --git a/plugins/figma-bridge/Cargo.toml b/plugins/figma-bridge/Cargo.toml new file mode 100644 index 0000000..fb4af01 --- /dev/null +++ b/plugins/figma-bridge/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "figma-bridge-wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +switchboard-guest-sdk = { git = "https://github.com/daltoniam/switchboard.git" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/plugins/figma-bridge/src/lib.rs b/plugins/figma-bridge/src/lib.rs new file mode 100644 index 0000000..26413cf --- /dev/null +++ b/plugins/figma-bridge/src/lib.rs @@ -0,0 +1,384 @@ +mod tools; + +use std::collections::HashMap; +use std::sync::Mutex; +use switchboard_guest_sdk as sdk; + +static CONFIG: Mutex> = Mutex::new(None); + +struct Config { + bridge_url: String, +} + +fn with_config(f: F) -> R +where + F: FnOnce(&Config) -> R, +{ + let guard = CONFIG.lock().unwrap(); + f(guard.as_ref().expect("not configured")) +} + +fn bridge_url() -> String { + with_config(|c| c.bridge_url.clone()) +} + +#[no_mangle] +pub extern "C" fn name() -> u64 { + sdk::leaked_string("figma_bridge") +} + +#[no_mangle] +pub extern "C" fn tools() -> u64 { + let defs = tools::tool_definitions(); + let data = serde_json::to_vec(&defs).unwrap_or_default(); + sdk::leaked_result(&data) +} + +#[no_mangle] +pub extern "C" fn configure(ptr_size: u64) -> u64 { + let input = sdk::read_input(ptr_size); + let creds: HashMap = match serde_json::from_slice(&input) { + Ok(c) => c, + Err(e) => return sdk::leaked_string(&format!("invalid credentials JSON: {e}")), + }; + + let url = creds + .get("bridge_url") + .map(|s| s.trim_end_matches('/').to_string()) + .unwrap_or_else(|| "http://127.0.0.1:9223".into()); + + *CONFIG.lock().unwrap() = Some(Config { bridge_url: url }); + 0 +} + +#[no_mangle] +pub extern "C" fn execute(ptr_size: u64) -> u64 { + let input = sdk::read_input(ptr_size); + let req: sdk::ExecuteRequest = match serde_json::from_slice(&input) { + Ok(r) => r, + Err(e) => { + let r = sdk::err_result(&format!("invalid request: {e}")); + let data = serde_json::to_vec(&r).unwrap_or_default(); + return sdk::leaked_result(&data); + } + }; + + let result = dispatch(&req.tool_name, req.args); + let data = serde_json::to_vec(&result).unwrap_or_default(); + sdk::leaked_result(&data) +} + +#[no_mangle] +pub extern "C" fn healthy() -> i32 { + match bridge_request("status", &serde_json::Value::Null) { + Ok(_) => 1, + Err(_) => 0, + } +} + +#[no_mangle] +pub extern "C" fn metadata() -> u64 { + sdk::leaked_metadata(&sdk::PluginMetadata { + name: "figma_bridge".into(), + version: "0.1.0".into(), + abi_version: 1, + description: "Figma Desktop Bridge — access the Figma Plugin API for write operations (create frames, components, auto layout, etc.) via a companion plugin running in Figma Desktop.".into(), + author: "daltoniam".into(), + homepage: "https://github.com/daltoniam/switchboard_plugins".into(), + license: "MIT".into(), + capabilities: vec!["http".into()], + credential_keys: vec!["bridge_url".into()], + plain_text_keys: vec!["bridge_url".into()], + optional_keys: vec!["bridge_url".into()], + placeholders: HashMap::from([( + "bridge_url".into(), + "http://127.0.0.1:9223 (default — Figma Desktop Bridge port)".into(), + )]), + }) +} + +// ── Dispatch ──────────────────────────────────────────────────────────────── + +type HandlerFn = fn(HashMap) -> sdk::ToolResult; + +fn dispatch(tool_name: &str, args: HashMap) -> sdk::ToolResult { + let handler: Option = match tool_name { + "figma_bridge_status" => Some(handle_status), + "figma_bridge_get_selection" => Some(handle_get_selection), + "figma_bridge_get_page_nodes" => Some(handle_get_page_nodes), + "figma_bridge_create_frame" => Some(handle_create_frame), + "figma_bridge_create_text" => Some(handle_create_text), + "figma_bridge_create_rectangle" => Some(handle_create_rectangle), + "figma_bridge_set_node_property" => Some(handle_set_node_property), + "figma_bridge_delete_node" => Some(handle_delete_node), + "figma_bridge_clone_node" => Some(handle_clone_node), + "figma_bridge_create_component" => Some(handle_create_component), + "figma_bridge_create_instance" => Some(handle_create_instance), + "figma_bridge_set_auto_layout" => Some(handle_set_auto_layout), + "figma_bridge_set_fills" => Some(handle_set_fills), + "figma_bridge_set_strokes" => Some(handle_set_strokes), + "figma_bridge_execute" => Some(handle_execute), + _ => None, + }; + + match handler { + Some(f) => f(args), + None => sdk::err_result(&format!("unknown tool: {tool_name}")), + } +} + +// ── Bridge Communication ──────────────────────────────────────────────────── + +fn bridge_request(command: &str, params: &serde_json::Value) -> Result { + let body = serde_json::json!({ + "command": command, + "params": params + }); + + let req = sdk::HttpRequest { + method: "POST".into(), + url: format!("{}/api/command", bridge_url()), + headers: { + let mut h = HashMap::new(); + h.insert("Content-Type".into(), "application/json".into()); + h + }, + body: body.to_string(), + body_base64: String::new(), + }; + + let resp = sdk::host_http_request(&req)?; + if resp.status >= 400 { + return Err(format!( + "Figma Bridge error ({}): {}", + resp.status, resp.body + )); + } + Ok(resp.body) +} + +// ── Handlers ──────────────────────────────────────────────────────────────── + +fn handle_status(_args: HashMap) -> sdk::ToolResult { + match bridge_request("status", &serde_json::Value::Null) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&format!( + "Bridge not connected: {e}. Ensure the Figma Desktop Bridge plugin is running." + )), + } +} + +fn handle_get_selection(_args: HashMap) -> sdk::ToolResult { + match bridge_request("getSelection", &serde_json::Value::Null) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_get_page_nodes(_args: HashMap) -> sdk::ToolResult { + match bridge_request("getPageNodes", &serde_json::Value::Null) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_create_frame(args: HashMap) -> sdk::ToolResult { + let name = sdk::arg_str(&args, "name"); + if name.is_empty() { + return sdk::err_result("name is required"); + } + let params = serde_json::json!({ + "name": name, + "x": sdk::arg_int(&args, "x").unwrap_or(0), + "y": sdk::arg_int(&args, "y").unwrap_or(0), + "width": sdk::arg_int(&args, "width").unwrap_or(400), + "height": sdk::arg_int(&args, "height").unwrap_or(300), + }); + match bridge_request("createFrame", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_create_text(args: HashMap) -> sdk::ToolResult { + let text = sdk::arg_str(&args, "text"); + if text.is_empty() { + return sdk::err_result("text is required"); + } + let params = serde_json::json!({ + "text": text, + "parentId": sdk::arg_str(&args, "parent_id"), + "x": sdk::arg_int(&args, "x").unwrap_or(0), + "y": sdk::arg_int(&args, "y").unwrap_or(0), + "fontSize": sdk::arg_int(&args, "font_size").unwrap_or(16), + }); + match bridge_request("createText", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_create_rectangle(args: HashMap) -> sdk::ToolResult { + let params = serde_json::json!({ + "name": sdk::arg_str(&args, "name"), + "parentId": sdk::arg_str(&args, "parent_id"), + "x": sdk::arg_int(&args, "x").unwrap_or(0), + "y": sdk::arg_int(&args, "y").unwrap_or(0), + "width": sdk::arg_int(&args, "width").unwrap_or(100), + "height": sdk::arg_int(&args, "height").unwrap_or(100), + "fillColor": sdk::arg_str(&args, "fill_color"), + }); + match bridge_request("createRectangle", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_set_node_property(args: HashMap) -> sdk::ToolResult { + let node_id = sdk::arg_str(&args, "node_id"); + let property = sdk::arg_str(&args, "property"); + let value = sdk::arg_str(&args, "value"); + if node_id.is_empty() || property.is_empty() { + return sdk::err_result("node_id and property are required"); + } + let parsed_value: serde_json::Value = + serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value)); + let params = serde_json::json!({ + "nodeId": node_id, + "property": property, + "value": parsed_value, + }); + match bridge_request("setNodeProperty", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_delete_node(args: HashMap) -> sdk::ToolResult { + let node_id = sdk::arg_str(&args, "node_id"); + if node_id.is_empty() { + return sdk::err_result("node_id is required"); + } + let params = serde_json::json!({ "nodeId": node_id }); + match bridge_request("deleteNode", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_clone_node(args: HashMap) -> sdk::ToolResult { + let node_id = sdk::arg_str(&args, "node_id"); + if node_id.is_empty() { + return sdk::err_result("node_id is required"); + } + let params = serde_json::json!({ "nodeId": node_id }); + match bridge_request("cloneNode", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_create_component(args: HashMap) -> sdk::ToolResult { + let name = sdk::arg_str(&args, "name"); + if name.is_empty() { + return sdk::err_result("name is required"); + } + let params = serde_json::json!({ + "nodeId": sdk::arg_str(&args, "node_id"), + "name": name, + "width": sdk::arg_int(&args, "width").unwrap_or(100), + "height": sdk::arg_int(&args, "height").unwrap_or(100), + }); + match bridge_request("createComponent", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_create_instance(args: HashMap) -> sdk::ToolResult { + let component_id = sdk::arg_str(&args, "component_id"); + if component_id.is_empty() { + return sdk::err_result("component_id is required"); + } + let params = serde_json::json!({ + "componentId": component_id, + "x": sdk::arg_int(&args, "x").unwrap_or(0), + "y": sdk::arg_int(&args, "y").unwrap_or(0), + }); + match bridge_request("createInstance", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_set_auto_layout(args: HashMap) -> sdk::ToolResult { + let node_id = sdk::arg_str(&args, "node_id"); + let direction = sdk::arg_str(&args, "direction"); + if node_id.is_empty() || direction.is_empty() { + return sdk::err_result("node_id and direction are required"); + } + let params = serde_json::json!({ + "nodeId": node_id, + "direction": direction, + "spacing": sdk::arg_int(&args, "spacing").unwrap_or(0), + "padding": sdk::arg_str(&args, "padding"), + "align": sdk::arg_str(&args, "align"), + }); + match bridge_request("setAutoLayout", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_set_fills(args: HashMap) -> sdk::ToolResult { + let node_id = sdk::arg_str(&args, "node_id"); + let fills_str = sdk::arg_str(&args, "fills"); + if node_id.is_empty() || fills_str.is_empty() { + return sdk::err_result("node_id and fills are required"); + } + let fills: serde_json::Value = match serde_json::from_str(&fills_str) { + Ok(v) => v, + Err(e) => return sdk::err_result(&format!("invalid JSON for fills: {e}")), + }; + let params = serde_json::json!({ + "nodeId": node_id, + "fills": fills, + }); + match bridge_request("setFills", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_set_strokes(args: HashMap) -> sdk::ToolResult { + let node_id = sdk::arg_str(&args, "node_id"); + let strokes_str = sdk::arg_str(&args, "strokes"); + if node_id.is_empty() || strokes_str.is_empty() { + return sdk::err_result("node_id and strokes are required"); + } + let strokes: serde_json::Value = match serde_json::from_str(&strokes_str) { + Ok(v) => v, + Err(e) => return sdk::err_result(&format!("invalid JSON for strokes: {e}")), + }; + let params = serde_json::json!({ + "nodeId": node_id, + "strokes": strokes, + "strokeWeight": sdk::arg_int(&args, "stroke_weight"), + }); + match bridge_request("setStrokes", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} + +fn handle_execute(args: HashMap) -> sdk::ToolResult { + let code = sdk::arg_str(&args, "code"); + if code.is_empty() { + return sdk::err_result("code is required"); + } + let params = serde_json::json!({ "code": code }); + match bridge_request("execute", ¶ms) { + Ok(data) => sdk::raw_result(data), + Err(e) => sdk::err_result(&e), + } +} diff --git a/plugins/figma-bridge/src/tools.rs b/plugins/figma-bridge/src/tools.rs new file mode 100644 index 0000000..e7bfbef --- /dev/null +++ b/plugins/figma-bridge/src/tools.rs @@ -0,0 +1,180 @@ +use std::collections::HashMap; +use switchboard_guest_sdk::ToolDefinition; + +pub fn tool_definitions() -> Vec { + vec![ + // ── Connection ────────────────────────────────────────────────── + ToolDefinition { + name: "figma_bridge_status".into(), + description: "Check the connection status to the Figma Desktop Bridge plugin. Start here to verify the bridge is running.".into(), + parameters: HashMap::new(), + required: vec![], + }, + // ── File & Selection ──────────────────────────────────────────── + ToolDefinition { + name: "figma_bridge_get_selection".into(), + description: "Get the currently selected nodes in Figma Desktop (IDs, names, types, properties)".into(), + parameters: HashMap::new(), + required: vec![], + }, + ToolDefinition { + name: "figma_bridge_get_page_nodes".into(), + description: "Get all top-level nodes on the current page".into(), + parameters: HashMap::new(), + required: vec![], + }, + // ── Node Manipulation ─────────────────────────────────────────── + ToolDefinition { + name: "figma_bridge_create_frame".into(), + description: "Create a new frame on the current page".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("name".into(), "Frame name".into()); + m.insert("x".into(), "X position (default 0)".into()); + m.insert("y".into(), "Y position (default 0)".into()); + m.insert("width".into(), "Width in pixels (default 400)".into()); + m.insert("height".into(), "Height in pixels (default 300)".into()); + m + }, + required: vec!["name".into()], + }, + ToolDefinition { + name: "figma_bridge_create_text".into(), + description: "Create a text node on the current page or inside a specified parent".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("text".into(), "Text content".into()); + m.insert("parent_id".into(), "Parent node ID (optional, defaults to current page)".into()); + m.insert("x".into(), "X position (default 0)".into()); + m.insert("y".into(), "Y position (default 0)".into()); + m.insert("font_size".into(), "Font size in pixels (default 16)".into()); + m + }, + required: vec!["text".into()], + }, + ToolDefinition { + name: "figma_bridge_create_rectangle".into(), + description: "Create a rectangle node".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("name".into(), "Node name (default 'Rectangle')".into()); + m.insert("parent_id".into(), "Parent node ID (optional)".into()); + m.insert("x".into(), "X position (default 0)".into()); + m.insert("y".into(), "Y position (default 0)".into()); + m.insert("width".into(), "Width (default 100)".into()); + m.insert("height".into(), "Height (default 100)".into()); + m.insert("fill_color".into(), "Fill color as hex (e.g. #FF5733)".into()); + m + }, + required: vec![], + }, + ToolDefinition { + name: "figma_bridge_set_node_property".into(), + description: "Set a property on an existing node (position, size, name, opacity, fills, etc.)".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("node_id".into(), "Target node ID".into()); + m.insert("property".into(), "Property name (e.g. name, x, y, width, height, opacity, visible)".into()); + m.insert("value".into(), "New value (JSON for complex types, string/number for simple)".into()); + m + }, + required: vec!["node_id".into(), "property".into(), "value".into()], + }, + ToolDefinition { + name: "figma_bridge_delete_node".into(), + description: "Delete a node by ID".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("node_id".into(), "Node ID to delete".into()); + m + }, + required: vec!["node_id".into()], + }, + ToolDefinition { + name: "figma_bridge_clone_node".into(), + description: "Clone/duplicate a node by ID".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("node_id".into(), "Node ID to clone".into()); + m + }, + required: vec!["node_id".into()], + }, + // ── Components & Instances ────────────────────────────────────── + ToolDefinition { + name: "figma_bridge_create_component".into(), + description: "Convert a frame/group into a component, or create a new component".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("node_id".into(), "Existing node ID to convert (optional)".into()); + m.insert("name".into(), "Component name".into()); + m.insert("width".into(), "Width if creating new (default 100)".into()); + m.insert("height".into(), "Height if creating new (default 100)".into()); + m + }, + required: vec!["name".into()], + }, + ToolDefinition { + name: "figma_bridge_create_instance".into(), + description: "Create an instance of a local component by its ID".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("component_id".into(), "Component node ID".into()); + m.insert("x".into(), "X position (default 0)".into()); + m.insert("y".into(), "Y position (default 0)".into()); + m + }, + required: vec!["component_id".into()], + }, + // ── Auto Layout ───────────────────────────────────────────────── + ToolDefinition { + name: "figma_bridge_set_auto_layout".into(), + description: "Apply or modify auto layout on a frame".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("node_id".into(), "Frame node ID".into()); + m.insert("direction".into(), "HORIZONTAL or VERTICAL".into()); + m.insert("spacing".into(), "Item spacing in pixels".into()); + m.insert("padding".into(), "Padding (single number for all sides, or JSON object with top/right/bottom/left)".into()); + m.insert("align".into(), "Primary axis alignment: MIN, CENTER, MAX, SPACE_BETWEEN".into()); + m + }, + required: vec!["node_id".into(), "direction".into()], + }, + // ── Styles & Variables ────────────────────────────────────────── + ToolDefinition { + name: "figma_bridge_set_fills".into(), + description: "Set fill colors on a node".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("node_id".into(), "Target node ID".into()); + m.insert("fills".into(), "JSON array of fill paints (e.g. [{\"type\":\"SOLID\",\"color\":{\"r\":1,\"g\":0,\"b\":0}}])".into()); + m + }, + required: vec!["node_id".into(), "fills".into()], + }, + ToolDefinition { + name: "figma_bridge_set_strokes".into(), + description: "Set stroke colors on a node".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("node_id".into(), "Target node ID".into()); + m.insert("strokes".into(), "JSON array of stroke paints".into()); + m.insert("stroke_weight".into(), "Stroke weight in pixels (optional)".into()); + m + }, + required: vec!["node_id".into(), "strokes".into()], + }, + // ── Execute Raw Plugin Code ───────────────────────────────────── + ToolDefinition { + name: "figma_bridge_execute".into(), + description: "Execute arbitrary Figma Plugin API JavaScript code in the desktop app context. Use for advanced operations not covered by other tools.".into(), + parameters: { + let mut m = HashMap::new(); + m.insert("code".into(), "JavaScript code to execute (has access to the `figma` global object)".into()); + m + }, + required: vec!["code".into()], + }, + ] +}