diff --git a/README.md b/README.md index a6eb09c..428c260 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ The bridge server exposes the following tools to your AI IDE: ## Limits -- **500 node maximum** per extraction — the plugin counts all nodes (including deeply nested children) and will show an error if the selection exceeds this limit. This prevents performance issues with very large component trees. +- **Configurable node limit (500 default)** — the plugin counts all nodes (including deeply nested children) and validates selection against the configured limit before extraction. Users can adjust the **Max Nodes** setting directly in the plugin UI (persisted across sessions). --- diff --git a/bridge-server/src/server.ts b/bridge-server/src/server.ts index f28db3b..17b8ae9 100644 --- a/bridge-server/src/server.ts +++ b/bridge-server/src/server.ts @@ -55,8 +55,14 @@ function startWebSocketServer(): WebSocketServer { ws.send(JSON.stringify({ type: "connected", message: "Bridge server ready" })); }); - wss.on("error", (err) => { + wss.on("error", (err: any) => { logToStderr(`[bridge] WebSocket server error: ${err.message}`); + if (err.code === "EADDRINUSE") { + logToStderr(`[bridge] Fatal error: Port ${WS_PORT} is already in use. Exiting process.`); + } else { + logToStderr(`[bridge] Fatal server error. Exiting process.`); + } + process.exit(1); }); return wss; diff --git a/code.ts b/code.ts index 4f1edab..4206a1b 100644 --- a/code.ts +++ b/code.ts @@ -1,8 +1,19 @@ -figma.showUI(__html__, { width: 320, height: 180 }); +figma.showUI(__html__, { width: 320, height: 215 }); -const NODE_LIMIT = 500; +async function loadSavedSettings() { + try { + const savedLimit = await figma.clientStorage.getAsync('nodeLimit'); + if (savedLimit !== undefined && savedLimit !== null) { + figma.ui.postMessage({ type: 'load-settings', nodeLimit: Number(savedLimit) }); + } + } catch (e) { + console.error('Failed to load settings from storage', e); + } +} -async function runExport() { +loadSavedSettings(); + +async function runExport(nodeLimit: number = 500) { const selection = figma.currentPage.selection; if (selection.length === 0) { @@ -20,8 +31,8 @@ async function runExport() { for (const node of selection) countNodes(node); - if (nodeCount > NODE_LIMIT) { - figma.ui.postMessage({ type: 'error', message: `Selection exceeds limit. Selected: ${nodeCount}. Limit: ${NODE_LIMIT}.` }); + if (nodeCount > nodeLimit) { + figma.ui.postMessage({ type: 'error', message: `Selection exceeds limit. Selected: ${nodeCount}. Limit: ${nodeLimit}.` }); return; } @@ -65,6 +76,47 @@ async function runExport() { return val; } + function toHex(c: { r: number; g: number; b: number; a?: number }): string { + const r = Math.round(c.r * 255).toString(16).padStart(2, '0'); + const g = Math.round(c.g * 255).toString(16).padStart(2, '0'); + const b = Math.round(c.b * 255).toString(16).padStart(2, '0'); + if (c.a !== undefined && c.a < 1) { + const a = Math.round(c.a * 255).toString(16).padStart(2, '0'); + return `#${r}${g}${b}${a}`; + } + return `#${r}${g}${b}`; + } + + function convertColors(obj: any): any { + if (obj === null || obj === undefined) return obj; + // Detect a Figma color object: has r, g, b all numbers in [0,1] + if ( + typeof obj === 'object' && + !Array.isArray(obj) && + typeof obj.r === 'number' && + typeof obj.g === 'number' && + typeof obj.b === 'number' + ) { + const hex = toHex(obj); + // Keep alpha as separate field only if meaningful + if (typeof obj.a === 'number' && obj.a < 1) { + return { hex, a: Math.round(obj.a * 100) / 100 }; + } + return hex; + } + if (Array.isArray(obj)) { + return obj.map(convertColors); + } + if (typeof obj === 'object') { + const out: any = {}; + for (const key of Object.keys(obj)) { + out[key] = convertColors(obj[key]); + } + return out; + } + return obj; + } + function cleanRawProps(props: any): any { const cleaned: any = {}; for (const key of Object.keys(props)) { @@ -127,9 +179,9 @@ async function runExport() { height: node.height, x: localX, y: localY, - fills: "fills" in node ? node.fills : null, - strokes: "strokes" in node ? node.strokes : null, - effects: "effects" in node ? node.effects : null, + fills: "fills" in node ? convertColors(node.fills) : null, + strokes: "strokes" in node ? convertColors(node.strokes) : null, + effects: "effects" in node ? convertColors(node.effects) : null, opacity: "opacity" in node ? node.opacity : null, rotation: "rotation" in node ? node.rotation : null }; @@ -209,8 +261,22 @@ async function runExport() { }); } -figma.ui.onmessage = (msg) => { +figma.ui.onmessage = async (msg) => { if (msg.type === 'run-extraction') { - runExport(); + const limit = typeof msg.nodeLimit === 'number' && msg.nodeLimit > 0 ? msg.nodeLimit : 500; + try { + await figma.clientStorage.setAsync('nodeLimit', limit); + } catch (e) { + console.error('Failed to save settings to storage', e); + } + runExport(limit); + } else if (msg.type === 'save-settings') { + if (typeof msg.nodeLimit === 'number' && msg.nodeLimit > 0) { + try { + await figma.clientStorage.setAsync('nodeLimit', msg.nodeLimit); + } catch (e) { + console.error('Failed to save settings to storage', e); + } + } } }; \ No newline at end of file diff --git a/ui.html b/ui.html index 8b33810..6105145 100644 --- a/ui.html +++ b/ui.html @@ -28,7 +28,7 @@ display: flex; align-items: center; justify-content: center; - height: 180px; + height: 215px; width: 320px; overflow: hidden; margin: 0; @@ -256,6 +256,39 @@ background-color: #10b981; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5); } + + .settings-row { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 10px; + font-size: 11px; + color: var(--text-muted); + } + + .settings-row label { + font-weight: 500; + } + + .settings-row input { + width: 70px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-color); + font-size: 11px; + font-weight: 600; + padding: 4px 6px; + text-align: center; + outline: none; + transition: all 0.2s ease; + } + + .settings-row input:focus { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); + }
@@ -269,6 +302,10 @@