@keyfunc/keybricks-ds 2.4.11 → 2.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"icons/Lucide.jsx":"import React from \"react\";\nimport { lucideBody, lucideReady, onLucideReady } from \"./lucide-loader.js\";\nexport { LUCIDE_VERSION, setLucideSource, loadLucide } from \"./lucide-loader.js\";\n\n/** Full Lucide icon library (lucide.dev, ISC) — 1,700+ stroke glyphs, same 24-grid / stroke 2 / round-cap style as the core Icon set. Loaded once from a pinned CDN build (lucide-static 1.61.0); renders an empty box of the right size until it arrives. name = Lucide id, e.g. \"calendar-clock\". Prefer Icon when the glyph is in the core set. */\nexport function Lucide({ name, size = 16, strokeWidth = 2, color, style, className }) {\n const [, tick] = React.useState(0);\n React.useEffect(() => { if (!lucideReady()) onLucideReady(() => tick(t => t + 1)); }, []);\n return (\n <svg\n width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\"\n aria-hidden=\"true\" className={className}\n style={{ display: \"block\", flexShrink: 0, color, ...style }}\n dangerouslySetInnerHTML={{ __html: lucideBody(name) }}\n />\n );\n}\n","icons/Lucide.vue":"<script setup>\n// Full Lucide icon library (lucide.dev, ISC) — loaded once from a pinned CDN build via lucide-loader.js. name = Lucide id.\nimport { ref, onMounted } from 'vue'\nimport { lucideBody, lucideReady, onLucideReady } from './lucide-loader.js'\nconst props = defineProps({ name: String, size: { type: Number, default: 16 }, strokeWidth: { type: Number, default: 2 }, color: String, style: Object })\nconst ready = ref(lucideReady())\nonMounted(() => { if (!ready.value) onLucideReady(() => { ready.value = true }) })\n</script>\n<template>\n <svg :width=\"size\" :height=\"size\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" :stroke-width=\"strokeWidth\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"\n :style=\"{ display: 'block', flexShrink: 0, color, ...style }\" v-html=\"ready ? lucideBody(name) : ''\" />\n</template>\n","icons/lucide-loader.js":"// Lucide icon library loader — framework-neutral, shared by the React and Vue <Lucide> components.\n// The full Lucide set (ISC licence, lucide.dev) is fetched once from a pinned CDN build and cached in memory;\n// nothing is bundled. Point it at a self-hosted copy of lucide-static's icon-nodes.json with setLucideSource().\nexport const LUCIDE_VERSION = \"1.61.0\";\nlet src = \"https://cdn.jsdelivr.net/npm/lucide-static@\" + LUCIDE_VERSION + \"/icon-nodes.json\";\nlet bodies = null, pending = null;\nconst subs = new Set();\nconst esc = (v) => String(v).replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\nconst toMarkup = (nodes) => nodes.map(([tag, attrs]) => \"<\" + tag + Object.keys(attrs).map(k => \" \" + k + '=\"' + esc(attrs[k]) + '\"').join(\"\") + \"/>\").join(\"\");\n/** Use a self-hosted icon-nodes.json (e.g. for offline / intranet deploys). Call before the first icon renders. */\nexport function setLucideSource(url) { src = url; bodies = null; pending = null; }\n/** Fetch the set (once). Resolves to { name: innerSvgMarkup }. */\nexport function loadLucide() {\n if (bodies) return Promise.resolve(bodies);\n if (!pending) pending = fetch(src).then(r => r.json()).then(json => {\n const out = {};\n for (const n in json) out[n] = toMarkup(json[n]);\n bodies = out; subs.forEach(f => f()); subs.clear(); return out;\n }).catch(e => { pending = null; throw e; });\n return pending;\n}\n/** Inner SVG markup for a name, or \"\" until the set has loaded / for unknown names. */\nexport function lucideBody(name) { return (bodies && bodies[name]) || \"\"; }\n/** True once the set is in memory. */\nexport function lucideReady() { return !!bodies; }\n/** Run fn once when the set arrives (immediately if already loaded). */\nexport function onLucideReady(fn) { if (bodies) fn(); else { subs.add(fn); loadLucide().catch(() => {}); } }\n","icons/Icon.jsx":"import React from \"react\";\n// Keybricks icon set — the source file uses Lucide stroke glyphs (24-grid, stroke 2,\n// round caps/joins, currentColor) rendered at 14/16px. Path data below is from the\n// public Lucide set, matching the glyphs used across Keyfunc UI v8, plus the Keybricks\n// glyphs (event … ai-sparkles, and the business set: dimension-*, rules-*, task-* …) drawn to the same rules.\nexport const ICON_PATHS = {\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n bell: '<path d=\"M10.268 21a2 2 0 0 0 3.464 0\"/><path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\"/>',\n \"panel-left\": '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M9 3v18\"/>',\n house: '<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/>',\n file: '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/>',\n \"file-text\": '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"M10 9H8\"/><path d=\"M16 13H8\"/><path d=\"M16 17H8\"/>',\n \"file-check\": '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"m9 15 2 2 4-4\"/>',\n \"layout-grid\": '<rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\"/>',\n database: '<ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"/><path d=\"M3 5V19A9 3 0 0 0 21 19V5\"/><path d=\"M3 12A9 3 0 0 0 21 12\"/>',\n scale: '<path d=\"m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z\"/><path d=\"m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z\"/><path d=\"M7 21h10\"/><path d=\"M12 3v18\"/><path d=\"M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2\"/>',\n network: '<rect x=\"16\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"2\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"9\" y=\"2\" width=\"6\" height=\"6\" rx=\"1\"/><path d=\"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3\"/><path d=\"M12 12V8\"/>',\n folder: '<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/>',\n \"folder-open\": '<path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/>',\n \"folder-cog\": '<path d=\"M10.5 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v3\"/><circle cx=\"18\" cy=\"18\" r=\"3\"/><path d=\"M18 14v1\"/><path d=\"M18 21v1\"/><path d=\"M22 18h-1\"/><path d=\"M15 18h-1\"/><path d=\"m21.2 16.2-.9.5\"/><path d=\"m15.7 19.3-.9.5\"/><path d=\"m21.2 19.8-.9-.5\"/><path d=\"m15.7 16.7-.9-.5\"/>',\n settings: '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n \"settings-2\": '<path d=\"M20 7h-9\"/><path d=\"M14 17H5\"/><circle cx=\"17\" cy=\"17\" r=\"3\"/><circle cx=\"7\" cy=\"7\" r=\"3\"/>',\n moon: '<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/>',\n \"circle-help\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>',\n model: '<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>',\n box: '<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>', // legacy alias of model\n workflow: '<rect width=\"8\" height=\"8\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M7 11v4a2 2 0 0 0 2 2h4\"/><rect width=\"8\" height=\"8\" x=\"13\" y=\"13\" rx=\"2\"/>',\n shield: '<path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/>',\n \"refresh-cw\": '<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/>',\n \"chart-line\": '<path d=\"M3 3v16a2 2 0 0 0 2 2h16\"/><path d=\"m19 9-5 5-4-4-3 3\"/>',\n \"chart-column\": '<path d=\"M3 3v16a2 2 0 0 0 2 2h16\"/><path d=\"M18 17V9\"/><path d=\"M13 17V5\"/><path d=\"M8 17v-3\"/>',\n \"chart-no-axes-column\": '<line x1=\"18\" x2=\"18\" y1=\"20\" y2=\"10\"/><line x1=\"12\" x2=\"12\" y1=\"20\" y2=\"4\"/><line x1=\"6\" x2=\"6\" y1=\"20\" y2=\"14\"/>',\n \"list-todo\": '<rect x=\"3\" y=\"5\" width=\"6\" height=\"6\" rx=\"1\"/><path d=\"m3 17 2 2 4-4\"/><path d=\"M13 6h8\"/><path d=\"M13 12h8\"/><path d=\"M13 18h8\"/>',\n \"circle-check\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/>',\n target: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>',\n \"book-open\": '<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/>',\n \"chevron-down\": '<path d=\"m6 9 6 6 6-6\"/>',\n \"chevron-right\": '<path d=\"m9 18 6-6-6-6\"/>',\n \"chevron-left\": '<path d=\"m15 18-6-6 6-6\"/>',\n \"chevron-up\": '<path d=\"m18 15-6-6-6 6\"/>',\n \"arrow-left\": '<path d=\"m12 19-7-7 7-7\"/><path d=\"M19 12H5\"/>',\n \"arrow-right\": '<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>',\n \"arrow-right-left\": '<path d=\"m16 3 4 4-4 4\"/><path d=\"M20 7H4\"/><path d=\"m8 21-4-4 4-4\"/><path d=\"M4 17h16\"/>',\n table: '<path d=\"M12 3v18\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/>',\n layers: '<path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\"/><path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65\"/><path d=\"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"/>',\n \"grid-3x3\": '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/><path d=\"M9 3v18\"/><path d=\"M15 3v18\"/>',\n \"text-search\": '<path d=\"M21 6H3\"/><path d=\"M10 12H3\"/><path d=\"M10 18H3\"/><circle cx=\"17\" cy=\"15\" r=\"3\"/><path d=\"m21 19-1.9-1.9\"/>',\n shuffle: '<path d=\"M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22\"/><path d=\"m18 2 4 4-4 4\"/><path d=\"M2 6h1.9c1.5 0 2.9.9 3.6 2.2\"/><path d=\"M22 18h-5.9c-1.3 0-2.6-.7-3.3-1.8l-.5-.8\"/><path d=\"m18 14 4 4-4 4\"/>',\n split: '<path d=\"M16 3h5v5\"/><path d=\"M8 3H3v5\"/><path d=\"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3\"/><path d=\"m15 9 6-6\"/>',\n menu: '<line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\"/><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\"/><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\"/>',\n list: '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/><line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>',\n funnel: '<polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"/>',\n \"sliders-horizontal\": '<line x1=\"21\" x2=\"14\" y1=\"4\" y2=\"4\"/><line x1=\"10\" x2=\"3\" y1=\"4\" y2=\"4\"/><line x1=\"21\" x2=\"12\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"16\" y1=\"20\" y2=\"20\"/><line x1=\"12\" x2=\"3\" y1=\"20\" y2=\"20\"/><line x1=\"14\" x2=\"14\" y1=\"2\" y2=\"6\"/><line x1=\"8\" x2=\"8\" y1=\"10\" y2=\"14\"/><line x1=\"16\" x2=\"16\" y1=\"18\" y2=\"22\"/>',\n \"trending-up\": '<polyline points=\"22 7 13.5 15.5 8.5 10.5 2 17\"/><polyline points=\"16 7 22 7 22 13\"/>',\n code: '<path d=\"m16 18 6-6-6-6\"/><path d=\"m8 6-6 6 6 6\"/>',\n merge: '<path d=\"m8 6 4-4 4 4\"/><path d=\"M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22\"/><path d=\"m20 22-5-5\"/>',\n upload: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" x2=\"12\" y1=\"3\" y2=\"15\"/>',\n download: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" x2=\"12\" y1=\"15\" y2=\"3\"/>',\n check: '<path d=\"M20 6 9 17l-5-5\"/>',\n x: '<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>',\n plus: '<path d=\"M5 12h14\"/><path d=\"M12 5v14\"/>',\n minus: '<path d=\"M5 12h14\"/>',\n ellipsis: '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/>',\n play: '<polygon points=\"6 3 20 12 6 21 6 3\"/>',\n send: '<path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/>',\n eye: '<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n copy: '<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/>',\n \"trash-2\": '<path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/><line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"/><line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/>',\n calendar: '<path d=\"M8 2v4\"/><path d=\"M16 2v4\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M3 10h18\"/>',\n \"undo-2\": '<path d=\"M9 14 4 9l5-5\"/><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\"/>',\n \"redo-2\": '<path d=\"m15 14 5-5-5-5\"/><path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\"/>',\n history: '<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/><path d=\"M3 3v5h5\"/><path d=\"M12 7v5l4 2\"/>',\n bug: '<path d=\"m8 2 1.88 1.88\"/><path d=\"M14.12 3.88 16 2\"/><path d=\"M9 7.13v-1a3.003 3.003 0 1 1 6 0v1\"/><path d=\"M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6\"/><path d=\"M12 20v-9\"/><path d=\"M6.53 9C4.6 8.8 3 7.1 3 5\"/><path d=\"M6 13H2\"/><path d=\"M3 21c0-2.1 1.7-3.9 3.8-4\"/><path d=\"M20.97 5c0 2.1-1.6 3.8-3.5 4\"/><path d=\"M22 13h-4\"/><path d=\"M17.2 17c2.1.1 3.8 1.9 3.8 4\"/>',\n info: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>',\n \"triangle-alert\": '<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 20h16a2 2 0 0 0 1.73-2Z\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>',\n calendar: '<path d=\"M8 2v4\"/><path d=\"M16 2v4\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M3 10h18\"/>',\n // Keybricks glyphs — drawn to the core-set rules (see readme.md › Iconography)\n event: '<path d=\"M17 2 22 12 17 22 12 12Z\"/><path d=\"M7 2 2 12l5 10\"/><path d=\"M12 2 7 12l5 10\"/>',\n consolidation: '<rect width=\"4\" height=\"4\" x=\"2\" y=\"17\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"10\" y=\"17\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"18\" y=\"17\" rx=\"1\"/><path d=\"M4 17v-5a2 2 0 0 1 2-2h2\"/><path d=\"M20 17v-5a2 2 0 0 0-2-2h-2\"/><path d=\"M12 17v-4\"/><path d=\"M12 3v6\"/><path d=\"M9 6h6\"/>',\n journal: '<rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"M2 8h20\"/><path d=\"M12 8v12\"/><path d=\"M6 12h2\"/><path d=\"M16 16h2\"/>',\n reconciliation: '<path d=\"M3 6h8\"/><path d=\"m15 4 2 2 4-4\"/><path d=\"M3 10h18\"/><path d=\"M3 18h8\"/><path d=\"m15 16 2 2 4-4\"/><path d=\"M3 22h18\"/>',\n \"view-tree\": '<rect width=\"4\" height=\"4\" x=\"4\" y=\"2\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"16\" y=\"10\" rx=\"1\"/><rect width=\"4\" height=\"4\" x=\"16\" y=\"18\" rx=\"1\"/><path d=\"M6 6v4a2 2 0 0 0 2 2h8\"/><path d=\"M6 10v8a2 2 0 0 0 2 2h8\"/>',\n \"drilldown\": '<path d=\"M12 2v11\"/><path d=\"m8 9 4 4 4-4\"/><path d=\"M19 17H5\"/><path d=\"M16 21H8\"/>',\n \"add-below\": '<path d=\"M11 2v10\"/><path d=\"M6 7h10\"/><path d=\"M22 9v5a4 4 0 0 1-4 4H2\"/><path d=\"m6 14-4 4 4 4\"/>',\n \"copy-data\": '<path d=\"M14 22h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5\"/><path d=\"m11 16 4-4-4-4\"/><path d=\"M3 22v-6a4 4 0 0 1 4-4h8\"/>',\n \"task-execution\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><path d=\"M8 14h8\"/><path d=\"m13 11 3 3-3 3\"/>',\n \"time-future\": '<path d=\"M20 6A10 10 0 1 0 22 12\"/><path d=\"M12 7v5h5\"/><path d=\"M21 2v5h-5\"/>',\n \"time-restatement\": '<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/><path d=\"M3 3v5h5\"/><path d=\"M12 7v5l4 2\"/>',\n // AI glyphs (2.3.0) — traced from the two reference glyphs, redrawn to core rules (stroke 2, no fill, integer endpoints, >=4 between separate elements)\n ai: '<path d=\"M9 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-3\"/><path d=\"M16 2c1.2 3 3 4.8 6 6c-3 1.2-4.8 3-6 6c-1.2-3-3-4.8-6-6c3-1.2 4.8-3 6-6Z\"/><path d=\"M9 12c.6 1.5 1.5 2.4 3 3c-1.5.6-2.4 1.5-3 3c-.6-1.5-1.5-2.4-3-3c1.5-.6 2.4-1.5 3-3Z\"/>',\n \"ai-sparkles\": '<path d=\"M9 2c1.4 3.5 3.5 5.6 7 7c-3.5 1.4-5.6 3.5-7 7c-1.4-3.5-3.5-5.6-7-7c3.5-1.4 5.6-3.5 7-7Z\"/><path d=\"M17 13c.8 2 2 3.2 4 4c-2 .8-3.2 2-4 4c-.8-2-2-3.2-4-4c2-.8 3.2-2 4-4Z\"/>',\n // Keybricks business glyphs (2.4.6) — dimensions, modules, rules-*, task-*; drawn to core rules or taken from Lucide\n \"dimension-account\": '<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/><path d=\"M5 8h3\"/><path d=\"M5 11h3\"/><path d=\"M5 14h3\"/><path d=\"M17 6.5v1\"/><path d=\"M17 12.5v1\"/><path d=\"M18.5 7.5h-2a1.25 1.25 0 0 0 0 2.5h1a1.25 1.25 0 0 1 0 2.5h-2\"/>',\n \"dimension-audit\": '<path d=\"M11.1 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.589 3.588A2.4 2.4 0 0 1 20 8v3.25\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5\"/><path d=\"m21 22-2.88-2.88\"/><circle cx=\"16\" cy=\"17\" r=\"3\"/>',\n \"dimension-currency\": '<path d=\"M7 2v1.5\"/><path d=\"M7 10.5V12\"/><path d=\"M10 3.5H5.25a1.75 1.75 0 0 0 0 3.5h3.5a1.75 1.75 0 0 1 0 3.5H4\"/><path d=\"M13.5 12.5 17 17l3.5-4.5\"/><path d=\"M17 17v4.5\"/><path d=\"M14 18.5h6\"/><path d=\"M14 3.5h3.5a2 2 0 0 1 2 2V9\"/><path d=\"m18 7.5 1.5 1.5 1.5-1.5\"/><path d=\"M10 20.5H6.5a2 2 0 0 1-2-2V15\"/><path d=\"m3 16.5 1.5-1.5 1.5 1.5\"/>',\n \"dimension-custom_default\": '<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>',\n \"dimension-entity\": '<path d=\"M2 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18\"/><path d=\"M14 10h6a2 2 0 0 1 2 2v10\"/><path d=\"M1 22h22\"/><path d=\"M6 6h.01\"/><path d=\"M10 6h.01\"/><path d=\"M6 10h.01\"/><path d=\"M10 10h.01\"/><path d=\"M6 14h.01\"/><path d=\"M10 14h.01\"/><path d=\"M18 14h.01\"/><path d=\"M18 18h.01\"/><path d=\"M6 22v-3h4v3\"/>',\n \"dimension-group\": '<rect x=\"16\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"2\" y=\"16\" width=\"6\" height=\"6\" rx=\"1\"/><rect x=\"9\" y=\"2\" width=\"6\" height=\"6\" rx=\"1\"/><path d=\"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3\"/><path d=\"M12 12V8\"/>',\n \"dimension-intercompany\": '<path d=\"M2 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18\"/><path d=\"M1 22h22\"/><path d=\"M6 6h.01\"/><path d=\"M10 6h.01\"/><path d=\"M6 10h.01\"/><path d=\"M6 14h.01\"/><path d=\"M10 10h.01\"/><path d=\"M10 14h.01\"/><path d=\"M6 22v-3h4v3\"/><path d=\"M14 15h6a2 2 0 0 1 2 2v5\"/><path d=\"M18 18.5h.01\"/><path d=\"M16 4h6\"/><path d=\"m20 2 2 2-2 2\"/><path d=\"M22 9h-6\"/><path d=\"m18 7-2 2 2 2\"/>',\n \"dimension-subtable\": '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/><path d=\"M9 9v12\"/><path d=\"M15 9v12\"/>',\n \"dimension-time\": '<path d=\"M16 14v2.2l1.6 1\"/><path d=\"M16 2v4\"/><path d=\"M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5\"/><path d=\"M3 10h5\"/><path d=\"M8 2v4\"/><circle cx=\"16\" cy=\"16\" r=\"6\"/>',\n \"dimension-version\": '<line x1=\"6\" y1=\"3\" x2=\"6\" y2=\"15\"/><circle cx=\"18\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><path d=\"M18 9a9 9 0 0 1-9 9\"/>',\n \"version-exempt\": '<g transform=\"translate(.35 .35) scale(.8)\" stroke-width=\"2.5\"><line x1=\"6\" y1=\"3\" x2=\"6\" y2=\"15\"></line><circle cx=\"18\" cy=\"6\" r=\"3\"></circle><circle cx=\"6\" cy=\"18\" r=\"3\"></circle><path d=\"M18 9a9 9 0 0 1-9 9\"></path></g><path d=\"m16.25 16.25 5 5\"/><path d=\"m21.25 16.25-5 5\"/>',\n \"model-exempt\": '<path d=\"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/><path d=\"m17 13 5 5\"/><path d=\"m22 13-5 5\"/>',\n modeling: '<rect width=\"20\" height=\"15\" x=\"2\" y=\"2\" rx=\"2\"/><path d=\"M8 21h8\"/><path d=\"M12 17v4\"/><path d=\"M12 5 16 7.25v4.5L12 14l-4-2.25v-4.5Z\"/><path d=\"m8 7.25 4 2.25 4-2.25\"/><path d=\"M12 9.5v4.5\"/>',\n restatement: '<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/><path d=\"M3 3v5h5\"/><path d=\"M12 7v5l4 2\"/>',\n review: '<path d=\"M11.1 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.589 3.588A2.4 2.4 0 0 1 20 8v3.25\"/><path d=\"M14 2v5a1 1 0 0 0 1 1h5\"/><path d=\"M12 17.5a5.6 5.6 0 0 1 10 0a5.6 5.6 0 0 1-10 0\"/><circle cx=\"17\" cy=\"17.5\" r=\".75\"/>',\n admin: '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n analytics: '<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><path d=\"M8 21h8\"/><path d=\"M12 17v4\"/><path d=\"M6 7h4\"/><path d=\"M6 10h4\"/><path d=\"M6 13h2\"/><path d=\"m13 13 2-3 2 1 2-4\"/>',\n automation: '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><polygon points=\"10.5 9 15 12 10.5 15 10.5 9\"/>',\n \"data_integration\": '<ellipse cx=\"17\" cy=\"7\" rx=\"5\" ry=\"2\"/><path d=\"M12 7v10a5 2 0 0 0 10 0V7\"/><path d=\"M12 12a5 2 0 0 0 10 0\"/><circle cx=\"3\" cy=\"5\" r=\"2\"/><circle cx=\"3\" cy=\"12\" r=\"2\"/><circle cx=\"3\" cy=\"19\" r=\"2\"/><path d=\"M5 5h1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5\"/><path d=\"M5 12h7\"/>',\n \"reconciliation-entity\": '<path d=\"M2 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18\"/><path d=\"M14 10h6a2 2 0 0 1 2 2v10\"/><path d=\"M1 22h22\"/><path d=\"M6 6h.01\"/><path d=\"M10 6h.01\"/><path d=\"M6 10h.01\"/><path d=\"M10 10h.01\"/><path d=\"M6 14h.01\"/><path d=\"M10 14h.01\"/><path d=\"M18 14h.01\"/><path d=\"M18 18h.01\"/><path d=\"M6 22v-3h4v3\"/>',\n \"reconciliation-balance\": '<path d=\"M13.744 17.736a6 6 0 1 1-7.48-7.48\"/><path d=\"M15 6h1v4\"/><path d=\"m6.134 14.768.866-.5 2 3.464\"/><circle cx=\"16\" cy=\"8\" r=\"6\"/>',\n scenario: '<circle cx=\"18\" cy=\"18\" r=\"3\"/><circle cx=\"6\" cy=\"6\" r=\"3\"/><path d=\"M13 6h3a2 2 0 0 1 2 2v7\"/><path d=\"M11 18H8a2 2 0 0 1-2-2V9\"/>',\n report: '<path d=\"M3 3v16a2 2 0 0 0 2 2h16\"/><path d=\"m19 9-5 5-4-4-3 3\"/>',\n spreadsheet: '<path d=\"M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18\"/>',\n sheet: '<path d=\"M12 3v18\"/><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M3 15h18\"/>',\n script: '<path d=\"m16 18 6-6-6-6\"/><path d=\"m8 6-6 6 6 6\"/>',\n \"workflow_group\": '<path d=\"M3 9.5v-5A1.5 1.5 0 0 1 4.5 3h1.7a1 1 0 0 1 .8.4L8.2 5h1.3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-5A1.5 1.5 0 0 1 3 9.5Z\"/><path d=\"M7 11v4a2 2 0 0 0 2 2h4\"/><rect width=\"8\" height=\"8\" x=\"13\" y=\"13\" rx=\"2\"/>',\n \"model_interaction\": '<path d=\"M7 3 11 5v4L7 11l-4-2v-4Z\"/><path d=\"M17 13 21 15v4L17 21l-4-2v-4Z\"/><path d=\"M14 3h4a2 2 0 0 1 2 2v4\"/><path d=\"m18 7 2 2 2-2\"/><path d=\"M10 21H6a2 2 0 0 1-2-2v-4\"/><path d=\"m2 17 2-2 2 2\"/>',\n announcement: '<path d=\"M4 20V4a2 2 0 0 1 2-2h8l6 6v1\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"M4 20a2 2 0 0 0 2 2h3\"/><g transform=\"translate(10.6 9.3) scale(.62)\" stroke-width=\"3.23\"><path d=\"M4.5 6H10l7-3a1 1 0 0 1 1.4.92v11.16a1 1 0 0 1-1.4.92L10 13v5a2.5 2.5 0 0 1-5 0v-5h-.5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2Z\"/></g>',\n approval: '<path d=\"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z\"/><path d=\"m9 12 2 2 4-4\"/>',\n \"data-import\": '<ellipse cx=\"12\" cy=\"13\" rx=\"7\" ry=\"2.5\"/><path d=\"M5 13v6a7 2.5 0 0 0 14 0v-6\"/><path d=\"M12 2v6\"/><path d=\"m9 5 3 3 3-3\"/>',\n \"data-export\": '<ellipse cx=\"12\" cy=\"13\" rx=\"7\" ry=\"2.5\"/><path d=\"M5 13v6a7 2.5 0 0 0 14 0v-6\"/><path d=\"M12 8V2\"/><path d=\"m9 5 3-3 3 3\"/>',\n execution: '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><path d=\"m9.5 12 1.75 1.75L14.5 10.5\"/>',\n submission: '<path d=\"M2 7h11V3l9 9-9 9v-4H2Z\"/><path d=\"m5.5 12 2 2 4-4\"/>',\n \"expense_control\": '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><path d=\"M12 7.5v1\"/><path d=\"M12 15.5v1\"/><path d=\"M14 9h-2.75a1.5 1.5 0 0 0 0 3h1.5a1.5 1.5 0 0 1 0 3H10\"/>',\n \"expense_control-aggregate\": '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><path d=\"m10.5 8.5 1.5 1.5 1.5-1.5\"/><path d=\"m10.5 15.5 1.5-1.5 1.5 1.5\"/><path d=\"m8.5 10.5 1.5 1.5-1.5 1.5\"/><path d=\"m15.5 10.5-1.5 1.5 1.5 1.5\"/>',\n \"expense_control-limit\": '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><path d=\"M10 8.5h4\"/><path d=\"M12 15.5V11\"/><path d=\"m10 13 2-2 2 2\"/>',\n \"expense_control-enumerated\": '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><path d=\"M9 10h.01\"/><path d=\"M12 10h3\"/><path d=\"M9 14h.01\"/><path d=\"M12 14h3\"/>',\n \"expense_control-quota\": '<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><path d=\"m14.5 9.5-5 5\"/><path d=\"M9.5 9.5h.01\"/><path d=\"M14.5 14.5h.01\"/>',\n \"profile-access\": '<path d=\"M20 11v6\"/><path d=\"M20 13h2\"/><path d=\"M3 21v-2a4 4 0 0 1 4-4h6a4 4 0 0 1 2.072.578\"/><circle cx=\"10\" cy=\"7\" r=\"4\"/><circle cx=\"20\" cy=\"19\" r=\"2\"/>',\n usergroup: '<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><path d=\"M16 3.128a4 4 0 0 1 0 7.744\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/>',\n user: '<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>',\n \"profile-system\": '<path d=\"M10 15H6a4 4 0 0 0-4 4v2\"/><path d=\"m14.305 16.53.923-.382\"/><path d=\"m15.228 13.852-.923-.383\"/><path d=\"m16.852 12.228-.383-.923\"/><path d=\"m16.852 17.772-.383.924\"/><path d=\"m19.148 12.228.383-.923\"/><path d=\"m19.53 18.696-.382-.924\"/><path d=\"m20.772 13.852.924-.383\"/><path d=\"m20.772 16.148.924.383\"/><circle cx=\"18\" cy=\"15\" r=\"3\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/>',\n \"rules-initialization\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M12.5 5.5v4\"/><path d=\"M10.25 7.25a3.75 3.75 0 1 0 4.5 0\"/>',\n \"rules-subsidy\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M10.5 5.5v.75\"/><path d=\"M10.5 12.75v.75\"/><path d=\"M12 7h-2a1.25 1.25 0 0 0 0 2.5h1a1.25 1.25 0 0 1 0 2.5H9\"/><path d=\"M15.5 8v3\"/><path d=\"M14 9.5h3\"/>',\n \"rules-transfer\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><g transform=\"translate(7.46 4.46) scale(0.42)\" stroke-width=\"4.76\"><path d=\"m16 3 4 4-4 4\"/><path d=\"M20 7H4\"/><path d=\"m8 21-4-4 4-4\"/><path d=\"M4 17h16\"/></g>',\n \"rules-import\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><g transform=\"translate(7.46 4.46) scale(0.42)\" stroke-width=\"4.76\"><path d=\"M12 17V3\"/><path d=\"m6 11 6 6 6-6\"/><path d=\"M19 21H5\"/></g>',\n \"rules-reclassification\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><g transform=\"translate(7.46 4.46) scale(0.42)\" stroke-width=\"4.76\"><path d=\"M16 3h5v5\"/><path d=\"M8 3H3v5\"/><path d=\"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3\"/><path d=\"m15 9 6-6\"/></g>',\n \"rules-allocation\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M12.5 5.5v3\"/><path d=\"M9 13.5v-3a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3\"/><path d=\"M12.5 8.5v5\"/>',\n \"rules-account\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M9 7.5h.01\"/><path d=\"M11.5 7.5H16\"/><path d=\"M9 11.5h.01\"/><path d=\"M11.5 11.5H16\"/>',\n \"rules-carryforward\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><g transform=\"translate(7.46 4.46) scale(0.42)\" stroke-width=\"4.76\"><path d=\"m15 17 5-5-5-5\"/><path d=\"M4 18v-2a4 4 0 0 1 4-4h12\"/></g>',\n \"rules-compound\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M12.5 5.5 16 7.5v4l-3.5 2-3.5-2v-4Z\"/>',\n \"rules-currency\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M12.5 5v1\"/><path d=\"M12.5 13v1\"/><path d=\"M14.5 6.5h-2.75a1.5 1.5 0 0 0 0 3h1.5a1.5 1.5 0 0 1 0 3H10.5\"/>',\n \"rules-deduction\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M8.5 9.5h2.5\"/><path d=\"M15 5.5v.75\"/><path d=\"M15 12.75v.75\"/><path d=\"M16.25 6.75H14.5a1.25 1.25 0 0 0 0 2.5h1a1.25 1.25 0 0 1 0 2.5h-1.75\"/>',\n \"rules-depreciation\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"m8 7.5 3 3 2.25-2.25L17 12\"/>',\n \"rules-eliminations_n_adjustments\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M12.5 5.25v4.5\"/><path d=\"M10.25 7.5h4.5\"/><path d=\"M10.25 13.25h4.5\"/>',\n \"rules-general\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M9 6.5h7\"/><path d=\"M9 9.5h7\"/><path d=\"M9 12.5h4\"/>',\n \"rules-intercompany\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M8.5 14V6.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V14\"/><path d=\"M13.5 9.5H16a1 1 0 0 1 1 1V14\"/><path d=\"M7.5 14h10\"/><path d=\"M11 8.5h.01\"/><path d=\"M11 11h.01\"/>',\n \"rules-journal_matching\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M8.5 6h8\"/><path d=\"M8.5 8.75h4\"/><path d=\"m11 12 2 2 3.5-3.5\"/>',\n \"rules-method\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><circle cx=\"9.5\" cy=\"6\" r=\"1\"/><path d=\"M11 6h2a1.75 1.75 0 0 1 0 3.5h-1a1.75 1.75 0 0 0 0 3.5h4\"/><path d=\"m14.5 11.5 1.5 1.5-1.5 1.5\"/>',\n \"rules-method_coefficient\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"M10 5.5H8.5v8H10\"/><path d=\"M15 5.5h1.5v8H15\"/><path d=\"m11.25 8.25 2.5 2.5\"/><path d=\"m13.75 8.25-2.5 2.5\"/>',\n \"rules-validation\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><path d=\"m9.5 9.5 2 2 4-4\"/>',\n \"rules-shared\": '<path d=\"M5 17V4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v13\"/><path d=\"M3 20a2 2 0 0 1 2-2v0h15v4H5a2 2 0 0 1-2-2Z\"/><circle cx=\"15.3\" cy=\"6.8\" r=\".75\"/><circle cx=\"9.7\" cy=\"10\" r=\".75\"/><circle cx=\"15.3\" cy=\"13.2\" r=\".75\"/><path d=\"m11.35 9.06 2.3-1.32\"/><path d=\"m11.35 10.94 2.3 1.32\"/>',\n \"task-manual\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><path d=\"M14.5 9.5 16.5 11.5 11 17H9v-2Z\"/>',\n \"task-reclassification\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><g transform=\"translate(7.46 4.46) scale(0.42)\" stroke-width=\"4.76\"><path d=\"M16 3h5v5\"/><path d=\"M8 3H3v5\"/><path d=\"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3\"/><path d=\"m15 9 6-6\"/></g></g>',\n \"task-account\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><path d=\"M9 7.5h.01\"/><path d=\"M11.5 7.5H16\"/><path d=\"M9 11.5h.01\"/><path d=\"M11.5 11.5H16\"/></g>',\n \"task-allocation\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><path d=\"M12.5 5.5v3\"/><path d=\"M9 13.5v-3a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3\"/><path d=\"M12.5 8.5v5\"/></g>',\n \"task-carryforward\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><g transform=\"translate(7.46 4.46) scale(0.42)\" stroke-width=\"4.76\"><path d=\"m15 17 5-5-5-5\"/><path d=\"M4 18v-2a4 4 0 0 1 4-4h12\"/></g></g>',\n \"task-currency\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><path d=\"M12.5 5v1\"/><path d=\"M12.5 13v1\"/><path d=\"M14.5 6.5h-2.75a1.5 1.5 0 0 0 0 3h1.5a1.5 1.5 0 0 1 0 3H10.5\"/></g>',\n \"task-deduction\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><path d=\"M8.5 9.5h2.5\"/><path d=\"M15 5.5v.75\"/><path d=\"M15 12.75v.75\"/><path d=\"M16.25 6.75H14.5a1.25 1.25 0 0 0 0 2.5h1a1.25 1.25 0 0 1 0 2.5h-1.75\"/></g>',\n \"task-depreciation\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><path d=\"m8 7.5 3 3 2.25-2.25L17 12\"/></g>',\n \"task-eliminations_n_adjustments\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><path d=\"M12.5 5.25v4.5\"/><path d=\"M10.25 7.5h4.5\"/><path d=\"M10.25 13.25h4.5\"/></g>',\n \"task-etl\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><ellipse cx=\"12\" cy=\"10.5\" rx=\"3.5\" ry=\"1.5\"/><path d=\"M8.5 10.5v6c0 .83 1.57 1.5 3.5 1.5s3.5-.67 3.5-1.5v-6\"/><path d=\"M8.5 13.5c0 .83 1.57 1.5 3.5 1.5s3.5-.67 3.5-1.5\"/>',\n \"task-shared\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><circle cx=\"15.3\" cy=\"6.8\" r=\".75\"/><circle cx=\"9.7\" cy=\"10\" r=\".75\"/><circle cx=\"15.3\" cy=\"13.2\" r=\".75\"/><path d=\"m11.35 9.06 2.3-1.32\"/><path d=\"m11.35 10.94 2.3 1.32\"/></g>',\n \"task-intercompany\": '<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><g transform=\"translate(-.5 4)\"><path d=\"M8.5 14V6.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V14\"/><path d=\"M13.5 9.5H16a1 1 0 0 1 1 1V14\"/><path d=\"M7.5 14h10\"/><path d=\"M11 8.5h.01\"/><path d=\"M11 11h.01\"/></g>',\n ownership: '<path d=\"M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17\"/><path d=\"m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9\"/><path d=\"m2 16 6 6\"/><circle cx=\"9\" cy=\"5\" r=\"3\"/><path d=\"M12 5h9\"/><path d=\"M18 5v2.5\"/><path d=\"M21 5v2\"/>',\n};\n\nimport { FEATHER_PATHS } from \"./feather-paths.js\";\n\n/** Lucide-style stroke icon. name: key of ICON_PATHS (core set) or any Feather icon name (feather-paths.js fallback); size in px; strokeWidth defaults to 2 (24-grid). Browse both sets with keywords in the Icon Gallery card. */\nexport function Icon({ name, size = 16, strokeWidth = 2, color, style, className }) {\n const body = ICON_PATHS[name] || FEATHER_PATHS[name] || \"\";\n return (\n <svg\n width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className={className}\n style={{ display: \"block\", flexShrink: 0, color, ...style }}\n dangerouslySetInnerHTML={{ __html: body }}\n />\n );\n}\n","icons/Icon.vue":"<script setup>\n// Keybricks icon set — Lucide stroke glyphs (24-grid, stroke 2, round caps/joins, currentColor), rendered at 14/16px.\n// Full path dictionary mirrors icons/Icon.jsx's ICON_PATHS (81 entries) — see that file for the complete list.\nimport { VUE_ICON_PATHS } from './icon-paths.js'\nimport { FEATHER_PATHS } from './feather-paths.js'\nconst props = defineProps({ name: String, size: { type: Number, default: 16 }, strokeWidth: { type: Number, default: 2 }, color: String, style: Object })\nconst body = (n) => VUE_ICON_PATHS[n] || FEATHER_PATHS[n] || ''\n</script>\n<template>\n <svg :width=\"size\" :height=\"size\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" :stroke-width=\"strokeWidth\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n :style=\"{ display: 'block', flexShrink: 0, color, ...style }\" v-html=\"body(name)\" />\n</template>\n","icons/Unicon.jsx":"import React from \"react\";\n\nlet uniconsCss = false;\nfunction ensureUniconsCss() {\n if (uniconsCss || document.getElementById(\"kb-unicons-css\")) { uniconsCss = true; return; }\n const l = document.createElement(\"link\");\n l.id = \"kb-unicons-css\"; l.rel = \"stylesheet\";\n l.href = \"https://unicons.iconscout.com/release/v4.0.8/css/line.css\";\n document.head.appendChild(l); uniconsCss = true;\n}\n\n/** Unicons line icon (iconscout.com/unicons, Apache 2.0) — 1,206 glyphs loaded from the official Unicons CDN webfont. name is the Unicons id without the \"uil-\" prefix (e.g. \"chart-pie-alt\"). Browse names + keywords in the Unicons gallery card; keyword index lives in assets/icons/unicons-index.json. */\nexport function Unicon({ name, size = 16, color, style, className }) {\n React.useEffect(ensureUniconsCss, []);\n return <i aria-hidden=\"true\" className={\"uil uil-\" + name + (className ? \" \" + className : \"\")} style={{ fontSize: size, lineHeight: 1, display: \"inline-block\", flexShrink: 0, fontStyle: \"normal\", color, ...style }}></i>;\n}\n","icons/Unicon.vue":"<script setup>\nimport { onMounted } from 'vue'\n// Unicons line icon — 1,206 glyphs from the official Unicons CDN webfont. name = Unicons id without \"uil-\".\nconst props = defineProps({ name: String, size: { type: Number, default: 16 }, color: String, style: Object })\nonMounted(() => {\n if (document.getElementById('kb-unicons-css')) return\n const l = document.createElement('link')\n l.id = 'kb-unicons-css'; l.rel = 'stylesheet'\n l.href = 'https://unicons.iconscout.com/release/v4.0.8/css/line.css'\n document.head.appendChild(l)\n})\n</script>\n<template>\n <i aria-hidden=\"true\" :class=\"'uil uil-' + name\" :style=\"{ fontSize: size + 'px', lineHeight: 1, display: 'inline-block', flexShrink: 0, fontStyle: 'normal', color, ...style }\" />\n</template>\n"});
@@ -1 +0,0 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"navigation/Breadcrumb.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { applyStoredTheme } from \"./theme-store.js\";\n\nfunction Crumb({ label, last, hovered, onSelect }) {\n const [hover, setHover] = React.useState(false);\n const on = (hover || hovered) && !last;\n return (\n <span role={last ? undefined : \"button\"} tabIndex={last ? undefined : 0}\n onClick={() => !last && onSelect && onSelect(label)}\n onKeyDown={(e) => { if (!last && (e.key === \"Enter\" || e.key === \" \")) { e.preventDefault(); onSelect && onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: last || on ? \"var(--text-heading)\" : \"var(--text-secondary)\", fontWeight: last ? \"var(--weight-medium)\" : \"var(--weight-regular)\", cursor: last ? \"default\" : \"pointer\", borderRadius: \"var(--radius-sm)\", transition: \"var(--t-hover)\" }}>{label}</span>\n );\n}\n\nfunction Sep() {\n return <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"chevron-right\" size={13} /></span>;\n}\n\n/** Overflow crumb: an ellipsis button that drops down the levels hidden by collapsing. */\nfunction Overflow({ items, onSelect }) {\n const [open, setOpen] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const root = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); };\n document.addEventListener(\"mousedown\", h);\n return () => document.removeEventListener(\"mousedown\", h);\n }, [open]);\n return (\n <span ref={root} style={{ position: \"relative\", display: \"inline-flex\" }}>\n <span role=\"button\" tabIndex={0} aria-label={`Show ${items.length} hidden levels`} aria-expanded={open}\n onClick={() => setOpen(!open)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); setOpen(!open); } if (e.key === \"Escape\") setOpen(false); }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", width: 24, height: 24, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", color: open || hover ? \"var(--text-heading)\" : \"var(--text-muted)\", background: open ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <Icon name=\"ellipsis\" size={14} />\n </span>\n {open ? (\n <span style={{ position: \"absolute\", top: \"calc(100% + 5px)\", left: 0, zIndex: \"var(--z-menu)\", minWidth: 168, maxWidth: 260, background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, display: \"flex\", flexDirection: \"column\" }}>\n {items.map((it, i) => <OverflowItem key={i} label={it} depth={i} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />)}\n </span>\n ) : null}\n </span>\n );\n}\n\nfunction OverflowItem({ label, depth, onSelect }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span role=\"menuitem\" tabIndex={0} onClick={() => onSelect(label)}\n onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onSelect(label); } }}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ display: \"flex\", alignItems: \"center\", gap: 6, padding: \"6px 9px\", paddingLeft: 9 + depth * 12, borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: hover ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\", transition: \"var(--t-hover)\" }}>\n {depth > 0 ? <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", flexShrink: 0 }}><Icon name=\"chevron-right\" size={11} /></span> : null}\n {label}\n </span>\n );\n}\n\n/** Breadcrumb trail — 12px, chevron separators, last crumb in heading color. Clickable crumbs darken on hover and carry press + keyboard focus from the global interaction layer. Trails longer than `maxItems` collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis. */\nexport function Breadcrumb({ items = [], onSelect, maxItems = 4, hoveredIndex, style }) {\n const collapse = items.length > maxItems && maxItems >= 3;\n const tailCount = maxItems - 2;\n const head = collapse ? items.slice(0, 1) : items;\n const hidden = collapse ? items.slice(1, items.length - tailCount) : [];\n const tail = collapse ? items.slice(items.length - tailCount) : [];\n /* Crumbs are identified by position, never by label: a trail may repeat a name (\"Workflow › Workflow\", two \"2026\" folders). Only the last index is the current page. */\n const lastIndex = items.length - 1;\n const tailStart = items.length - tailCount;\n return (\n <nav style={{ display: \"flex\", alignItems: \"center\", gap: 4, ...style }}>\n {head.map((it, i) => (\n <React.Fragment key={i}>\n {i > 0 ? <Sep /> : null}\n <Crumb label={it} last={i === lastIndex} hovered={hoveredIndex === i} onSelect={onSelect} />\n </React.Fragment>\n ))}\n {collapse ? (\n <React.Fragment>\n <Sep />\n <Overflow items={hidden} onSelect={onSelect} />\n {tail.map((it, k) => (\n <React.Fragment key={tailStart + k}>\n <Sep />\n <Crumb label={it} last={tailStart + k === lastIndex} onSelect={onSelect} />\n </React.Fragment>\n ))}\n </React.Fragment>\n ) : null}\n </nav>\n );\n}\n\n/** Theme toggle — flips <html data-theme> and persists to localStorage(\"kb-theme\"). Pass `dark`+`onToggle` to control it externally. */\nexport function ThemeToggle({ size = 34, dark: darkProp, onToggle, style }) {\n const controlled = darkProp != null;\n const [darkState, setDark] = React.useState(() => (typeof document !== \"undefined\" && document.documentElement.dataset.theme === \"dark\"));\n const dark = controlled ? darkProp : darkState;\n React.useEffect(() => {\n if (controlled || typeof document === \"undefined\") return;\n document.documentElement.dataset.theme = dark ? \"dark\" : \"light\";\n try { localStorage.setItem(\"kb-theme\", dark ? \"dark\" : \"light\"); } catch (e) {}\n }, [dark, controlled]);\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" aria-label={dark ? \"Switch to light mode\" : \"Switch to dark mode\"} onClick={() => (controlled ? onToggle && onToggle(!dark) : setDark(!dark))}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ width: size, height: size, borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: hover ? \"var(--bg-hover)\" : \"transparent\", color: \"var(--text-body)\", padding: 0, transition: \"var(--t-hover)\", ...style }}>\n <Icon name={dark ? \"sun\" : \"moon\"} size={18} />\n </button>\n );\n}\n\n/** Reads the persisted theme and applies it before first paint. Call once at app start. Defined in theme-store.js, shared with the Vue entry. */\nexport { applyStoredTheme };\n","navigation/Breadcrumb.vue":"<script setup>\nimport { ref, computed, watch, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Breadcrumb trail — 12px, chevron separators, last crumb in heading color.\n// Clickable crumbs darken on hover (no underline) and carry press + keyboard focus from the global interaction layer.\n// Trails longer than maxItems collapse to first · … · last two, with the hidden levels in a dropdown on the ellipsis.\nconst props = defineProps({ items: { type: Array, default: () => [] }, maxItems: { type: Number, default: 4 }, hoveredIndex: { type: Number, default: null }, style: Object })\nconst emit = defineEmits(['select'])\nconst hover = ref(-1)\nconst ovHover = ref(false)\nconst menuHover = ref(-1)\nconst open = ref(false)\nconst root = ref(null)\nconst collapse = computed(() => props.items.length > props.maxItems && props.maxItems >= 3)\nconst tailCount = computed(() => props.maxItems - 2)\nconst head = computed(() => collapse.value ? props.items.slice(0, 1) : props.items)\nconst hidden = computed(() => collapse.value ? props.items.slice(1, props.items.length - tailCount.value) : [])\nconst tail = computed(() => collapse.value ? props.items.slice(props.items.length - tailCount.value) : [])\n// Crumbs are identified by position, never by label: a trail may repeat a name (\"Workflow › Workflow\", two \"2026\" folders). Only the last index is the current page.\nconst tailStart = computed(() => props.items.length - tailCount.value)\nconst isLast = (i) => i === props.items.length - 1\n// i = the crumb's index in items; hoveredIndex reaches the leading crumbs only (as in React)\nconst on = (i, lead) => (hover.value === i || (lead && props.hoveredIndex === i)) && !isLast(i)\nfunction pick(it, i) { if (!isLast(i)) emit('select', it) }\nfunction pickHidden(it) { open.value = false; emit('select', it) }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nwatch(open, (v) => { if (v) document.addEventListener('mousedown', onDoc); else document.removeEventListener('mousedown', onDoc) })\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\nconst crumbStyle = (i, lead) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: isLast(i) || on(i, lead) ? 'var(--text-heading)' : 'var(--text-secondary)', fontWeight: isLast(i) ? 'var(--weight-medium)' : 'var(--weight-regular)', cursor: isLast(i) ? 'default' : 'pointer', borderRadius: 'var(--radius-sm)', transition: 'var(--t-hover)' })\n</script>\n<template>\n <nav :style=\"{ display: 'flex', alignItems: 'center', gap: '4px', ...style }\">\n <template v-for=\"(it, i) in head\" :key=\"i\">\n <span v-if=\"i > 0\" style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(i) ? undefined : 'button'\" :tabindex=\"isLast(i) ? undefined : 0\"\n @click=\"pick(it, i)\" @keydown.enter.prevent=\"pick(it, i)\" @keydown.space.prevent=\"pick(it, i)\"\n @mouseenter=\"hover = i\" @mouseleave=\"hover = -1\" :style=\"crumbStyle(i, true)\">{{ it }}</span>\n </template>\n <template v-if=\"collapse\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span ref=\"root\" style=\"position:relative;display:inline-flex\">\n <span role=\"button\" tabindex=\"0\" :aria-label=\"`Show ${hidden.length} hidden levels`\" :aria-expanded=\"open\"\n @click=\"open = !open\" @keydown.enter.prevent=\"open = !open\" @keydown.space.prevent=\"open = !open\" @keydown.esc=\"open = false\"\n @mouseenter=\"ovHover = true\" @mouseleave=\"ovHover = false\"\n :style=\"{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '24px', height: '24px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', color: open || ovHover ? 'var(--text-heading)' : 'var(--text-muted)', background: open ? 'var(--bg-active)' : ovHover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }\">\n <Icon name=\"ellipsis\" :size=\"14\" />\n </span>\n <span v-if=\"open\" style=\"position:absolute;top:calc(100% + 5px);left:0;z-index:var(--z-menu);min-width:168px;max-width:260px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;display:flex;flex-direction:column\">\n <span v-for=\"(h, hi) in hidden\" :key=\"hi\" role=\"menuitem\" tabindex=\"0\"\n @click=\"pickHidden(h)\" @keydown.enter.prevent=\"pickHidden(h)\" @keydown.space.prevent=\"pickHidden(h)\"\n @mouseenter=\"menuHover = hi\" @mouseleave=\"menuHover = -1\"\n :style=\"{ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 9px', paddingLeft: (9 + hi * 12) + 'px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: menuHover === hi ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', transition: 'var(--t-hover)' }\">\n <span v-if=\"hi > 0\" style=\"color:var(--text-muted);display:inline-flex;flex-shrink:0\"><Icon name=\"chevron-right\" :size=\"11\" /></span>{{ h }}\n </span>\n </span>\n </span>\n <template v-for=\"(it, k) in tail\" :key=\"tailStart + k\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"chevron-right\" :size=\"13\" /></span>\n <span :role=\"isLast(tailStart + k) ? undefined : 'button'\" :tabindex=\"isLast(tailStart + k) ? undefined : 0\"\n @click=\"pick(it, tailStart + k)\" @keydown.enter.prevent=\"pick(it, tailStart + k)\" @keydown.space.prevent=\"pick(it, tailStart + k)\"\n @mouseenter=\"hover = tailStart + k\" @mouseleave=\"hover = -1\" :style=\"crumbStyle(tailStart + k, false)\">{{ it }}</span>\n </template>\n </template>\n </nav>\n</template>\n\n<!-- applyStoredTheme() lives in ./theme-store.js (shared with React) and is exported from the Vue entry.\n Call it once in main.js before mounting. -->\n","navigation/DocTree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\n\n/** Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}]. */\nexport function DocTree({ title = \"Documents\", items = [], active, onSelect, style }) {\n return (\n <div style={{ width: \"var(--sidebar-w)\", background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ height: \"var(--topbar-h)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 12px 0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style={{ padding: \"12px 16px 8px\", borderBottom: \"1px solid var(--border-subtle)\" }}>\n <SearchInput bare placeholder=\"Search...\" style={{ gap: 8 }} />\n </div>\n <div style={{ padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2, overflowY: \"auto\" }}>\n {items.map((it) => {\n const is = active === it.label;\n return (\n <div key={it.label} role=\"treeitem\" tabIndex={0} data-wash={is ? undefined : \"\"} onClick={() => onSelect && onSelect(it.label)}\n style={{ height: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\", background: is ? \"var(--bg-active)\" : \"var(--wash, transparent)\", transition: \"var(--t-hover)\" }}\n onMouseEnter={(e) => { if (!is) e.currentTarget.style.background = \"var(--bg-hover)\"; }}\n onMouseLeave={(e) => { if (!is) e.currentTarget.style.background = \"transparent\"; }}>\n <span style={{ color: it.kind === \"folder\" ? \"var(--text-muted)\" : docHues[it.hue || \"gray\"], display: \"inline-flex\" }}>\n <Icon name={it.icon || (it.kind === \"folder\" ? \"folder\" : \"file-text\")} size={16} />\n </span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{it.label}</span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","navigation/DocTree.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport SearchInput from '../forms/SearchInput.vue'\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\n// Secondary documents panel — 240px: title header, search row, 30px tree items. items: [{label, kind:\"folder\"|\"doc\", hue, icon}].\ndefineProps({ title: { type: String, default: 'Documents' }, items: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['select'])\n</script>\n<template>\n <div :style=\"{ width: 'var(--sidebar-w)', background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"height:var(--topbar-h);border-bottom:1px solid var(--border-card);display:flex;align-items:center;justify-content:space-between;padding:0 12px 0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <IconButton icon=\"chevron-right\" size=\"md\" muted />\n </div>\n <div style=\"padding:12px 16px 8px;border-bottom:1px solid var(--border-subtle)\"><SearchInput bare placeholder=\"Search...\" style=\"gap:8px\" /></div>\n <div style=\"padding:8px;display:flex;flex-direction:column;gap:2px;overflow-y:auto\">\n <div v-for=\"it in items\" :key=\"it.label\" role=\"treeitem\" tabindex=\"0\" @click=\"emit('select', it.label)\" @keydown.enter=\"emit('select', it.label)\"\n :style=\"{ height: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', boxSizing: 'border-box', cursor: 'pointer', background: active === it.label ? 'var(--bg-active)' : 'var(--wash, transparent)', transition: 'var(--t-hover)' }\" :data-wash=\"active === it.label ? undefined : ''\">\n <span :style=\"{ color: it.kind === 'folder' ? 'var(--text-muted)' : docHues[it.hue || 'gray'], display: 'inline-flex' }\"><Icon :name=\"it.icon || (it.kind === 'folder' ? 'folder' : 'file-text')\" :size=\"16\" /></span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ it.label }}</span>\n </div>\n </div>\n </div>\n</template>\n","navigation/Sidebar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT } from \"./nav-model.js\";\nimport { chromeTone, SIDEBAR_COLORS } from \"./chrome-tones.js\";\n\nconst LABEL = { flex: 1, minWidth: 0, overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"18px\", letterSpacing: \"var(--tracking-base)\", fontWeight: \"var(--weight-medium)\" };\n\n/* One rail row: 36px min, radius-md, 16px icon, medium label, optional trailing chevron (the item opens a side panel). */\nfunction NavItem({ icon, label, active, indent, collapsed, trailing, onClick }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div role=\"button\" tabIndex={0} title={collapsed ? label : undefined} aria-current={active ? \"page\" : undefined} onClick={onClick} onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); onClick && onClick(); } }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ minHeight: indent ? 32 : 36, borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", justifyContent: collapsed ? \"center\" : \"flex-start\", gap: 10, padding: indent ? \"6px 10px\" : \"8px 10px\", boxSizing: \"border-box\", cursor: \"pointer\", userSelect: \"none\", background: active ? \"var(--sb-active)\" : hover ? \"var(--sb-hover)\" : \"transparent\", color: active ? \"var(--sb-active-text)\" : \"var(--sb-text)\", boxShadow: active ? \"var(--sb-shadow)\" : \"none\", transition: \"var(--t-hover)\" }}>\n {icon ? <span style={{ display: \"inline-flex\", flexShrink: 0 }}><Icon name={icon} size={indent ? 14 : 16} /></span> : null}\n {!collapsed ? <span style={LABEL}>{label}</span> : null}\n {!collapsed && trailing ? <span style={{ display: \"inline-flex\", flexShrink: 0, opacity: 0.6 }}><Icon name=\"chevron-right\" size={14} /></span> : null}\n </div>\n );\n}\n\n/**\n * Keybricks app sidebar — 240px, 64px icon rail when collapsed. A 56px brand head (no rule), a 2px-gap list, pinned footer items and a version line.\n * tone: \"recessed\" (grey, default) | \"flush\" (white) | \"contrast\" (dark) | \"custom\" (+ color: a SIDEBAR_COLORS key or hex).\n * items / footerItems: [{ icon, label, trailing?, children? }] — trailing draws a chevron (the item opens a SidePanel); children make a collapsible group.\n */\nexport function Sidebar({ items = KEYBRICKS_NAV, footerItems = KEYBRICKS_NAV_FOOTER, active, onSelect, collapsed = false, onToggleCollapse, logo = \"assets/keybricks-logo.png\", brand = \"Keybricks\", version, tone = \"recessed\", color, labels, height = \"100%\", style }) {\n const L = { collapse: \"Collapse\", expand: \"Expand\", ...labels };\n const t = chromeTone(tone, color);\n const [open, setOpen] = React.useState(() => items.filter((i) => i.children && i.children.some((c) => (c.label || c) === active)).map((i) => i.label));\n const toggle = (l) => setOpen(open.includes(l) ? open.filter((x) => x !== l) : [...open, l]);\n const pick = (l) => onSelect && onSelect(l);\n const list = (arr) => arr.map((it) => it.children ? (\n <div key={it.label} style={{ display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n <NavItem icon={it.icon} label={it.label} collapsed={collapsed} trailing={false} active={collapsed && it.children.some((c) => (c.label || c) === active)} onClick={() => (collapsed ? pick(it.label) : toggle(it.label))} />\n {!collapsed && open.includes(it.label) ? <div style={{ marginLeft: 24, display: \"flex\", flexDirection: \"column\", gap: 2 }}>{it.children.map((c) => { const lb = c.label || c; return <NavItem key={lb} icon={c.icon} label={lb} indent active={active === lb} onClick={() => pick(lb)} />; })}</div> : null}\n </div>\n ) : <NavItem key={it.label} icon={it.icon} label={it.label} collapsed={collapsed} trailing={it.trailing} active={active === it.label} onClick={() => pick(it.label)} />);\n return (\n <aside data-theme={t.theme} style={{ ...t.vars, width: collapsed ? \"var(--rail-w)\" : \"var(--sidebar-w)\", height, background: \"var(--sb-bg)\", borderRight: \"1px solid var(--sb-border)\", color: \"var(--sb-text)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, transition: \"width .15s ease\", ...style }}>\n <div style={{ height: \"var(--topbar-h)\", display: \"flex\", alignItems: \"center\", gap: 10, padding: \"0 16px\", boxSizing: \"border-box\", justifyContent: collapsed ? \"center\" : \"flex-start\", flexShrink: 0 }}>\n <img src={logo} alt=\"\" style={{ width: 24, height: 24, objectFit: \"contain\", flexShrink: 0 }} />\n {!collapsed ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", fontWeight: \"var(--weight-semibold)\", color: \"var(--sb-brand)\", whiteSpace: \"nowrap\" }}>{brand}</span> : null}\n </div>\n <nav style={{ flex: 1, overflowY: \"auto\", overflowX: \"hidden\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n {onToggleCollapse ? <NavItem icon={collapsed ? \"panel-left\" : \"chevron-left\"} label={collapsed ? L.expand : L.collapse} collapsed={collapsed} onClick={onToggleCollapse} /> : null}\n {list(items)}\n </nav>\n <div style={{ padding: 8, display: \"flex\", flexDirection: \"column\", gap: 2, flexShrink: 0 }}>\n {list(footerItems)}\n {version ? <span title={version} style={{ padding: collapsed ? \"8px 0 4px\" : \"8px 10px 4px\", textAlign: collapsed ? \"center\" : \"left\", fontFamily: \"var(--font-sans)\", fontSize: collapsed ? \"var(--text-glyph-min)\" : \"var(--text-xs)\", lineHeight: collapsed ? \"var(--leading-2xs)\" : \"var(--leading-xs)\", color: \"var(--sb-muted)\", whiteSpace: \"nowrap\", overflow: \"hidden\" }}>{collapsed ? \"v\" + String(version).split(\" \").pop().replace(/^v/i, \"\") : version}</span> : null}\n </div>\n </aside>\n );\n}\n\n/** Nav models and tones — defined in framework-neutral modules, shared with the Vue entry. */\nexport { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER, KEYBRICKS_MGMT, SIDEBAR_COLORS };\n","navigation/Sidebar.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { KEYBRICKS_NAV, KEYBRICKS_NAV_FOOTER } from './nav-model.js'\nimport { chromeTone } from './chrome-tones.js'\n// Keybricks app sidebar — 240px, 64px icon rail when collapsed. 56px brand head (no rule), 2px-gap list, pinned footer, version line.\n// tone: \"recessed\" (grey, default) | \"flush\" (white) | \"contrast\" (dark) | \"custom\" (+ color: a SIDEBAR_COLORS key or hex).\n// items / footerItems: [{ icon, label, trailing?, children? }] — trailing draws a chevron (the item opens a SidePanel); children make a collapsible group.\nconst props = defineProps({ items: { type: Array, default: () => KEYBRICKS_NAV }, footerItems: { type: Array, default: () => KEYBRICKS_NAV_FOOTER }, active: String, collapsed: Boolean, logo: { type: String, default: 'assets/keybricks-logo.png' }, brand: { type: String, default: 'Keybricks' }, version: String, tone: { type: String, default: 'recessed' }, color: String, labels: Object, height: { type: String, default: '100%' }, showToggle: { type: Boolean, default: true }, style: Object })\nconst emit = defineEmits(['select', 'toggleCollapse'])\nconst L = computed(() => ({ collapse: 'Collapse', expand: 'Expand', ...(props.labels || {}) }))\nconst t = computed(() => chromeTone(props.tone, props.color))\nconst hover = ref(null)\nconst open = ref(props.items.filter(i => i.children && i.children.some(c => (c.label || c) === props.active)).map(i => i.label))\nconst toggle = (l) => { open.value = open.value.includes(l) ? open.value.filter(x => x !== l) : [...open.value, l] }\nconst lb = (c) => c.label || c\nconst LABEL = { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: '18px', letterSpacing: 'var(--tracking-base)', fontWeight: 'var(--weight-medium)' }\nfunction row(key, active, indent) { return { minHeight: indent ? '32px' : '36px', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', justifyContent: props.collapsed ? 'center' : 'flex-start', gap: '10px', padding: indent ? '6px 10px' : '8px 10px', boxSizing: 'border-box', cursor: 'pointer', userSelect: 'none', background: active ? 'var(--sb-active)' : hover.value === key ? 'var(--sb-hover)' : 'transparent', color: active ? 'var(--sb-active-text)' : 'var(--sb-text)', boxShadow: active ? 'var(--sb-shadow)' : 'none', transition: 'var(--t-hover)' } }\nfunction key(e, fn) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fn() } }\nconst groups = computed(() => [{ id: 'main', list: props.items }, { id: 'foot', list: props.footerItems }])\nconst versionText = computed(() => props.collapsed && props.version ? 'v' + String(props.version).split(' ').pop().replace(/^v/i, '') : props.version)\n</script>\n<template>\n <aside :data-theme=\"t.theme\" :style=\"{ ...t.vars, width: collapsed ? 'var(--rail-w)' : 'var(--sidebar-w)', height, background: 'var(--sb-bg)', borderRight: '1px solid var(--sb-border)', color: 'var(--sb-text)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, transition: 'width .15s ease', ...style }\">\n <div :style=\"{ height: 'var(--topbar-h)', display: 'flex', alignItems: 'center', gap: '10px', padding: '0 16px', boxSizing: 'border-box', justifyContent: collapsed ? 'center' : 'flex-start', flexShrink: 0 }\">\n <img :src=\"logo\" alt=\"\" style=\"width:24px;height:24px;object-fit:contain;flex-shrink:0\" />\n <span v-if=\"!collapsed\" style=\"font-family:var(--font-sans);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);font-weight:var(--weight-semibold);color:var(--sb-brand);white-space:nowrap\">{{ brand }}</span>\n </div>\n <template v-for=\"g in groups\" :key=\"g.id\">\n <component :is=\"g.id === 'main' ? 'nav' : 'div'\" :style=\"g.id === 'main' ? { flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '8px', display: 'flex', flexDirection: 'column', gap: '2px' } : { padding: '8px', display: 'flex', flexDirection: 'column', gap: '2px', flexShrink: 0 }\">\n <div v-if=\"g.id === 'main' && showToggle\" role=\"button\" tabindex=\"0\" :title=\"collapsed ? L.expand : undefined\" :style=\"row('__toggle', false)\" @click=\"emit('toggleCollapse')\" @keydown=\"e => key(e, () => emit('toggleCollapse'))\" @mouseenter=\"hover = '__toggle'\" @mouseleave=\"hover = null\">\n <span style=\"display:inline-flex;flex-shrink:0\"><Icon :name=\"collapsed ? 'panel-left' : 'chevron-left'\" :size=\"16\" /></span><span v-if=\"!collapsed\" :style=\"LABEL\">{{ collapsed ? L.expand : L.collapse }}</span>\n </div>\n <template v-for=\"it in g.list\" :key=\"it.label\">\n <div role=\"button\" tabindex=\"0\" :title=\"collapsed ? it.label : undefined\" :aria-current=\"active === it.label ? 'page' : undefined\" :style=\"row(it.label, active === it.label || (collapsed && it.children && it.children.some(c => lb(c) === active)))\" @click=\"it.children && !collapsed ? toggle(it.label) : emit('select', it.label)\" @keydown=\"e => key(e, () => it.children && !collapsed ? toggle(it.label) : emit('select', it.label))\" @mouseenter=\"hover = it.label\" @mouseleave=\"hover = null\">\n <span v-if=\"it.icon\" style=\"display:inline-flex;flex-shrink:0\"><Icon :name=\"it.icon\" :size=\"16\" /></span>\n <span v-if=\"!collapsed\" :style=\"LABEL\">{{ it.label }}</span>\n <span v-if=\"!collapsed && it.trailing\" style=\"display:inline-flex;flex-shrink:0;opacity:.6\"><Icon name=\"chevron-right\" :size=\"14\" /></span>\n </div>\n <div v-if=\"it.children && !collapsed && open.includes(it.label)\" style=\"margin-left:24px;display:flex;flex-direction:column;gap:2px\">\n <div v-for=\"c in it.children\" :key=\"lb(c)\" role=\"button\" tabindex=\"0\" :aria-current=\"active === lb(c) ? 'page' : undefined\" :style=\"row('c:' + lb(c), active === lb(c), true)\" @click=\"emit('select', lb(c))\" @keydown=\"e => key(e, () => emit('select', lb(c)))\" @mouseenter=\"hover = 'c:' + lb(c)\" @mouseleave=\"hover = null\">\n <span v-if=\"c.icon\" style=\"display:inline-flex;flex-shrink:0\"><Icon :name=\"c.icon\" :size=\"14\" /></span><span :style=\"LABEL\">{{ lb(c) }}</span>\n </div>\n </div>\n </template>\n <span v-if=\"g.id === 'foot' && version\" :title=\"version\" :style=\"{ padding: collapsed ? '8px 0 4px' : '8px 10px 4px', textAlign: collapsed ? 'center' : 'left', fontFamily: 'var(--font-sans)', fontSize: collapsed ? 'var(--text-glyph-min)' : 'var(--text-xs)', lineHeight: collapsed ? 'var(--leading-2xs)' : 'var(--leading-xs)', color: 'var(--sb-muted)', whiteSpace: 'nowrap', overflow: 'hidden' }\">{{ versionText }}</span>\n </component>\n </template>\n </aside>\n</template>\n","navigation/TopBar.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Button } from \"../buttons/Button.jsx\";\nimport { SearchInput } from \"../forms/SearchInput.jsx\";\nimport { Avatar } from \"../display/Avatar.jsx\";\nimport { SegmentedControl } from \"../tabs/SegmentedControl.jsx\";\nimport { ContextMenu } from \"../feedback/ContextMenu.jsx\";\n\nconst TXT = { fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\" };\nconst ONE = { whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" };\nexport const THEME_OPTIONS = [{ label: \"Light\", icon: \"sun\" }, { label: \"Dark\", icon: \"moon\" }, { label: \"System\", icon: \"monitor\" }];\n\n/** Account menu (opened from the TopBar's user trigger): avatar + name + email, an Appearance row with a light / dark / system switch, then rows.\n * theme: \"Light\" | \"Dark\" | \"System\" (labels of themeOptions). items: [{ icon, label, danger?, divider? }] → onSelect(label). */\nexport function UserMenu({ name, email, theme, onThemeChange, themeOptions = THEME_OPTIONS, appearanceLabel = \"Appearance\", items = [], onSelect, style }) {\n return (\n <div role=\"menu\" style={{ width: 288, background: \"var(--bg-raised)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", boxShadow: \"var(--shadow-menu)\", padding: 4, boxSizing: \"border-box\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, padding: \"8px 12px\" }}>\n <Avatar name={name} size={32} />\n <div style={{ display: \"flex\", flexDirection: \"column\", minWidth: 0 }}>\n <span style={{ ...TXT, ...ONE, fontWeight: \"var(--weight-medium)\", color: \"var(--text-heading)\" }}>{name}</span>\n {email ? <span style={{ ...ONE, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-secondary)\" }}>{email}</span> : null}\n </div>\n </div>\n <div style={{ height: 1, background: \"var(--border-subtle)\", margin: \"4px 0\" }} />\n {onThemeChange || theme ? <div style={{ height: 40, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 12, padding: \"0 12px\", boxSizing: \"border-box\" }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, minWidth: 0 }}><span style={{ display: \"inline-flex\", color: \"var(--text-body)\" }}><Icon name=\"sliders\" size={16} /></span><span style={{ ...TXT, ...ONE, color: \"var(--text-heading)\" }}>{appearanceLabel}</span></div>\n <SegmentedControl iconOnly options={themeOptions} value={theme} onChange={onThemeChange} style={{ flexShrink: 0 }} />\n </div> : null}\n {items.length ? <ContextMenu items={items} onSelect={onSelect} style={{ display: \"flex\", border: \"none\", boxShadow: \"none\", padding: 0, minWidth: 0, background: \"transparent\" }} /> : null}\n </div>\n );\n}\n\n/** 56px top bar: bare search on the left; AI Assistant (dark), notifications, help and the user trigger on the right.\n * The user trigger opens a UserMenu below it (closes on outside click). actions replaces the right-hand group entirely. */\nexport function TopBar({ placeholder = \"Search documents, workflows, data...\", onSearch, actions, aiLabel = \"AI Assistant\", onAi, onNotifications, onHelp, notificationsLabel = \"Notifications\", helpLabel = \"Help\", user, userMenu, style }) {\n const [open, setOpen] = React.useState(false);\n const ref = React.useRef(null);\n React.useEffect(() => {\n if (!open) return;\n const off = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };\n const esc = (e) => { if (e.key === \"Escape\") setOpen(false); };\n document.addEventListener(\"mousedown\", off); document.addEventListener(\"keydown\", esc);\n return () => { document.removeEventListener(\"mousedown\", off); document.removeEventListener(\"keydown\", esc); };\n }, [open]);\n const [hover, setHover] = React.useState(false);\n return (\n <header style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 16, padding: \"0 24px\", boxSizing: \"border-box\", flexShrink: 0, minWidth: 0, position: \"relative\", ...style }}>\n <SearchInput bare placeholder={placeholder} onChange={onSearch} style={{ flex: \"0 1 576px\", minWidth: 0 }} />\n <div ref={ref} style={{ display: \"flex\", alignItems: \"center\", gap: 8, flexShrink: 0 }}>\n {actions || (<>\n <Button variant=\"dark\" icon=\"ai\" onClick={onAi}>{aiLabel}</Button>\n <IconButton icon=\"bell\" label={notificationsLabel} title={notificationsLabel} onClick={onNotifications} />\n <IconButton icon=\"circle-help\" label={helpLabel} title={helpLabel} onClick={onHelp} />\n {user ? <div role=\"button\" tabIndex={0} aria-haspopup=\"menu\" aria-expanded={open} onClick={() => setOpen(!open)} onKeyDown={(e) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); setOpen(!open); } }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: 32, display: \"flex\", alignItems: \"center\", gap: 8, padding: \"0 8px 0 10px\", borderRadius: \"var(--radius-md)\", cursor: \"pointer\", userSelect: \"none\", background: open ? \"var(--bg-active)\" : hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"var(--t-hover)\" }}>\n <span style={{ ...TXT, fontWeight: \"var(--weight-medium)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\" }}>{user.name}</span>\n <span style={{ display: \"inline-flex\", color: \"var(--text-muted)\" }}><Icon name=\"chevron-down\" size={16} /></span>\n </div> : null}\n </>)}\n {open && user ? <div style={{ position: \"absolute\", right: 24, top: 52, zIndex: \"var(--z-menu)\" }}>{userMenu || <UserMenu name={user.name} email={user.email} />}</div> : null}\n </div>\n </header>\n );\n}\n\n/** Editor toolbar: back chevron, 14px/600 title, \"• Editing\" status, right action slot. */\nexport function PageToolbar({ title, status = \"• Editing\", actions, onBack, size, style }) {\n return (\n <div data-size={size} style={{ height: \"var(--topbar-h)\", background: \"var(--bg-surface)\", borderBottom: \"1px solid var(--border-card)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"0 var(--edge-pad)\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\" }}>\n {onBack ? <IconButton icon=\"chevron-left\" onClick={onBack} label=\"Back\" /> : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n {status ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-muted)\", marginLeft: 6 }}>{status}</span> : null}\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\" }}>{actions}</div>\n </div>\n );\n}\n\n/** Page heading block: 15px/500 title + 13px muted subtitle (System Migration pages). */\nexport function PageHeading({ title, subtitle, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 4, ...style }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-lg)\", lineHeight: \"var(--leading-lg)\", letterSpacing: \"var(--tracking-lg)\", color: \"var(--text-primary)\" }}>{title}</span>\n {subtitle ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-secondary)\" }}>{subtitle}</span> : null}\n </div>\n );\n}\n","navigation/TopBar.vue":"<script setup>\nimport { ref, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport Button from '../buttons/Button.vue'\nimport SearchInput from '../forms/SearchInput.vue'\nimport UserMenu from './UserMenu.vue'\n// 56px top bar: bare search on the left; AI Assistant (dark), notifications, help and the user trigger on the right.\n// The user trigger opens a UserMenu below it (closes on outside click / Escape); #user-menu slot replaces its content.\n// #actions slot replaces the whole right-hand group.\nconst props = defineProps({ placeholder: { type: String, default: 'Search documents, workflows, data...' }, aiLabel: { type: String, default: 'AI Assistant' }, notificationsLabel: { type: String, default: 'Notifications' }, helpLabel: { type: String, default: 'Help' }, user: Object, style: Object })\nconst emit = defineEmits(['search', 'ai', 'notifications', 'help'])\nconst open = ref(false), hover = ref(false), root = ref(null)\nconst off = (e) => { if (open.value && root.value && !root.value.contains(e.target)) open.value = false }\nconst esc = (e) => { if (e.key === 'Escape') open.value = false }\nonMounted(() => { document.addEventListener('mousedown', off); document.addEventListener('keydown', esc) })\nonUnmounted(() => { document.removeEventListener('mousedown', off); document.removeEventListener('keydown', esc) })\nfunction key(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open.value = !open.value } }\n</script>\n<template>\n <header :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, minWidth: 0, position: 'relative', ...style }\">\n <SearchInput bare :placeholder=\"placeholder\" @update:modelValue=\"v => emit('search', v)\" style=\"flex:0 1 576px;min-width:0\" />\n <div ref=\"root\" style=\"display:flex;align-items:center;gap:8px;flex-shrink:0\">\n <slot name=\"actions\">\n <Button variant=\"dark\" icon=\"ai\" @click=\"emit('ai')\">{{ aiLabel }}</Button>\n <IconButton icon=\"bell\" :label=\"notificationsLabel\" :title=\"notificationsLabel\" @click=\"emit('notifications')\" />\n <IconButton icon=\"circle-help\" :label=\"helpLabel\" :title=\"helpLabel\" @click=\"emit('help')\" />\n <div v-if=\"user\" role=\"button\" tabindex=\"0\" aria-haspopup=\"menu\" :aria-expanded=\"open\" @click=\"open = !open\" @keydown=\"key\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ height: '32px', display: 'flex', alignItems: 'center', gap: '8px', padding: '0 8px 0 10px', borderRadius: 'var(--radius-md)', cursor: 'pointer', userSelect: 'none', background: open ? 'var(--bg-active)' : hover ? 'var(--bg-hover)' : 'transparent', transition: 'var(--t-hover)' }\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading);white-space:nowrap\">{{ user.name }}</span>\n <span style=\"display:inline-flex;color:var(--text-muted)\"><Icon name=\"chevron-down\" :size=\"16\" /></span>\n </div>\n </slot>\n <div v-if=\"open && user\" style=\"position:absolute;right:24px;top:52px;z-index:var(--z-menu)\"><slot name=\"user-menu\"><UserMenu :name=\"user.name\" :email=\"user.email\" :show-appearance=\"false\" /></slot></div>\n </div>\n </header>\n</template>\n","navigation/Tree.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\n\nconst docHues = { orange: \"var(--cat-transform)\", green: \"var(--cat-source)\", purple: \"var(--cat-merge)\", blue: \"var(--info-fg)\", teal: \"var(--cat-output)\", gray: \"var(--text-muted)\" };\nconst INDENT = 18;\n/* Insert marker (reorder before / after): a 2px --action-primary line with a 6px dot at its left end, drawn between rows.\n It starts at the target row's content indent (8 + depth * INDENT — the chevron column, which is also the elbow column\n under showLine) and ends at the row's 8px right padding, so the line says which level the item lands on. Its centre\n sits on the gap between rows (gap 1px, or 0 under showLine). */\nfunction insertMarker(pos, depth, gap) {\n const centre = -(gap / 2);\n return {\n position: \"absolute\", left: 8 + depth * INDENT - 3, right: 8, height: 6, pointerEvents: \"none\", zIndex: 1,\n ...(pos === \"before\" ? { top: centre - 3 } : { bottom: centre - 3 }),\n };\n}\nconst markerDot = { position: \"absolute\", left: 0, top: 0, width: 6, height: 6, borderRadius: \"50%\", background: \"var(--action-primary)\" };\nconst markerLine = { position: \"absolute\", left: 3, right: 0, top: 2, height: 2, borderRadius: 1, background: \"var(--action-primary)\" };\n\nconst keyOf = (n) => (n.key != null ? n.key : n.label);\nconst titleOf = (n) => (n.title != null ? n.title : n.label);\nconst kidsOf = (n) => n.children || [];\nconst isBranch = (n) => kidsOf(n).length > 0 || n.isLeaf === false;\n\nfunction walk(nodes, fn, parentKey = null, depth = 0) {\n nodes.forEach((n) => { fn(n, parentKey, depth); walk(kidsOf(n), fn, keyOf(n), depth + 1); });\n}\nfunction indexNodes(items) {\n const byKey = {}, parent = {}, all = [];\n walk(items, (n, p, d) => { const k = keyOf(n); byKey[k] = n; parent[k] = p; all.push({ node: n, key: k, depth: d }); });\n return { byKey, parent, all };\n}\nfunction descendants(node, out = []) {\n kidsOf(node).forEach((c) => { out.push(c); descendants(c, out); });\n return out;\n}\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(items, rootKey, key) {\n const { byKey } = indexNodes(items);\n const root = byKey[rootKey];\n if (!root) return false;\n return rootKey === key || descendants(root).some((d) => keyOf(d) === key);\n}\nfunction keysMatching(items, test) {\n const hit = [];\n const visit = (n, trail) => {\n const t = [...trail, keyOf(n)];\n if (test(n)) hit.push(...trail);\n kidsOf(n).forEach((c) => visit(c, t));\n };\n items.forEach((n) => visit(n, []));\n return [...new Set(hit)];\n}\n\n/* Cascading check: a parent is checked when every enabled descendant is checked,\n indeterminate when only some are. Disabled nodes never change state. */\nfunction cascade(items, key, next) {\n const { byKey, parent } = indexNodes(items);\n const set = new Set(next);\n const node = byKey[key];\n if (!node) return next;\n const checked = set.has(key);\n descendants(node).forEach((d) => {\n if (d.disabled || d.disableCheckbox) return;\n checked ? set.add(keyOf(d)) : set.delete(keyOf(d));\n });\n let p = parent[key];\n while (p != null) {\n const pn = byKey[p];\n const kids = kidsOf(pn).filter((c) => !c.disabled && !c.disableCheckbox);\n const on = kids.filter((c) => set.has(keyOf(c)));\n on.length && on.length === kids.length ? set.add(p) : set.delete(p);\n p = parent[p];\n }\n return [...set];\n}\n/* Display state for every branch: full when all enabled descendants are checked,\n half when only some. Keeps parents honest whatever the caller passed in. */\nfunction rollUp(items, checkedKeys) {\n const full = new Set(checkedKeys), half = new Set();\n const visit = (n) => {\n kidsOf(n).forEach(visit);\n if (!kidsOf(n).length) return;\n const ds = descendants(n).filter((d) => !d.disabled && !d.disableCheckbox);\n if (!ds.length) return;\n const on = ds.filter((d) => full.has(keyOf(d))).length;\n if (on === ds.length) full.add(keyOf(n));\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)); }\n };\n items.forEach(visit);\n return { full, half };\n}\nfunction halfKeys(items, checkedKeys) {\n return [...rollUp(items, checkedKeys).half];\n}\n\nfunction Highlight({ text, query }) {\n if (!query) return text;\n const i = String(text).toLowerCase().indexOf(query.toLowerCase());\n if (i < 0) return text;\n return <>{String(text).slice(0, i)}<mark style={{ background: \"var(--soft-bg)\", color: \"var(--info-fg)\", fontWeight: \"var(--weight-medium)\", padding: 0, borderRadius: 2 }}>{String(text).slice(i, i + query.length)}</mark>{String(text).slice(i + query.length)}</>;\n}\n\n/** Guide lines for showLine: one 18px column per ancestor level, then the elbow. */\nfunction Guides({ trail, isLast, hasKids }) {\n return (\n <>\n {trail.map((ancestorHasNext, i) => (\n <span key={i} style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, borderLeft: ancestorHasNext ? \"1px solid var(--border-subtle)\" : \"1px solid transparent\", marginLeft: i === 0 ? 6 : 0 }} />\n ))}\n <span style={{ width: INDENT, alignSelf: \"stretch\", flexShrink: 0, position: \"relative\", marginLeft: trail.length === 0 ? 6 : 0 }}>\n <span style={{ position: \"absolute\", left: 0, top: 0, height: isLast ? \"50%\" : \"100%\", borderLeft: \"1px solid var(--border-subtle)\" }} />\n <span style={{ position: \"absolute\", left: 0, top: \"50%\", width: hasKids ? 6 : INDENT - 4, borderTop: \"1px solid var(--border-subtle)\" }} />\n </span>\n </>\n );\n}\n\nfunction Row({ item, state, cfg, api }) {\n const { node, key, depth, trail, isLast } = item;\n const kids = kidsOf(node);\n const branch = isBranch(node);\n const open = state.expanded.includes(key);\n const selected = state.selected.includes(key);\n const checked = state.checked.has(key);\n const half = !checked && state.half.has(key);\n const loading = state.loading.includes(key);\n const focused = state.focus === key;\n const ringed = focused && state.focusMoved;\n const [hover, setHover] = React.useState(false);\n const disabled = !!node.disabled;\n const dropAt = state.drop && state.drop.key === key ? state.drop.pos : null;\n const selectable = cfg.selectable && node.selectable !== false;\n\n const chevron = branch ? (\n <span onClick={(e) => { e.stopPropagation(); api.toggle(key); }} style={{ width: 14, height: 14, display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--text-muted)\", flexShrink: 0, cursor: \"pointer\", transform: open ? \"rotate(90deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}>\n {loading ? <Icon name=\"loader\" size={12} style={{ animation: \"kb-tree-spin 900ms linear infinite\" }} /> : <Icon name=\"chevron-right\" size={13} />}\n </span>\n ) : <span style={{ width: cfg.showLine ? 0 : 14, flexShrink: 0 }} />;\n\n return (\n <>\n <div role=\"treeitem\" tabIndex={focused ? 0 : -1} aria-expanded={branch ? open : undefined} aria-selected={selected} aria-disabled={disabled || undefined}\n ref={(el) => { if (el && focused && state.focusMoved) el.focus({ preventScroll: true }); }}\n draggable={cfg.draggable && !disabled}\n onDragStart={(e) => { e.dataTransfer.effectAllowed = \"move\"; api.dragStart(key); }}\n onDragOver={(e) => { if (!cfg.draggable) return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); api.dragOver(key, (e.clientY - r.top) / r.height); }}\n onDragLeave={() => api.dragOver(null)}\n onDrop={(e) => { e.preventDefault(); api.drop(key); }}\n onDragEnd={() => api.dragEnd()}\n onClick={() => { if (disabled) return; api.focus(key); if (selectable) api.select(key); if (branch) api.toggle(key); }}\n onContextMenu={(e) => { if (cfg.onContextMenu) { e.preventDefault(); cfg.onContextMenu(key, e); } }}\n onKeyDown={(e) => api.keyDown(e, item)}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{\n position: \"relative\", minHeight: \"var(--h-tree-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 6,\n padding: cfg.showLine ? \"0 8px 0 2px\" : \"5px 8px\", paddingLeft: cfg.showLine ? 2 : 8 + depth * INDENT, boxSizing: \"border-box\",\n cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? 0.45 : 1,\n background: dropAt === \"inside\" ? \"var(--soft-bg)\" : selected ? \"var(--bg-active)\" : hover && !disabled ? \"var(--bg-hover)\" : \"transparent\",\n outline: dropAt === \"inside\" ? \"2px solid var(--action-primary)\" : ringed ? \"2px solid var(--focus-border)\" : \"none\", outlineOffset: -2,\n transition: \"var(--t-hover)\",\n }}>\n {dropAt === \"before\" || dropAt === \"after\" ? <span aria-hidden=\"true\" data-drop-marker={dropAt} style={insertMarker(dropAt, depth, cfg.showLine ? 0 : 1)}><span style={markerDot} /><span style={markerLine} /></span> : null}\n {cfg.showLine ? <Guides trail={trail} isLast={isLast} hasKids={branch} /> : null}\n {chevron}\n {cfg.checkable && node.checkable !== false ? (\n <span onClick={(e) => e.stopPropagation()} style={{ display: \"inline-flex\", flexShrink: 0 }}>\n <Checkbox checked={checked} indeterminate={half} disabled={disabled || node.disableCheckbox} onChange={() => api.check(key)} />\n </span>\n ) : null}\n {cfg.showIcon ? (\n <span style={{ color: branch ? \"var(--text-muted)\" : docHues[node.hue || \"gray\"], display: \"inline-flex\", flexShrink: 0 }}>\n <Icon name={node.icon || (branch ? (open ? \"folder-open\" : \"folder\") : \"file-text\")} size={15} />\n </span>\n ) : null}\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", fontWeight: selected ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: disabled ? \"var(--text-muted)\" : \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n <Highlight text={titleOf(node)} query={cfg.searchValue} />\n </span>\n {node.tag ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: 1, padding: \"2px 5px\", borderRadius: \"var(--radius-sm)\", background: \"var(--bg-inset)\", color: \"var(--text-secondary)\", flexShrink: 0 }}>{node.tag}</span> : null}\n {node.count != null ? <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\", paddingLeft: 8 }}>{node.count}</span> : null}\n {cfg.rowActions && !disabled ? <span style={{ marginLeft: node.count != null ? 6 : \"auto\", display: \"flex\", gap: 2, opacity: hover ? 1 : 0, transition: \"var(--t-hover)\" }} onClick={(e) => e.stopPropagation()}>{cfg.rowActions(node)}</span> : null}\n </div>\n {open && loading && !kids.length ? (\n <div style={{ paddingLeft: 8 + (depth + 1) * INDENT + 14, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\", height: \"var(--h-tree-item)\", display: \"flex\", alignItems: \"center\" }}>Loading…</div>\n ) : null}\n </>\n );\n}\n\n/**\n * Nested tree view. Expand/collapse, single or multiple selection, cascading checkboxes\n * (with indeterminate parents and `checkStrictly` opt-out), disabled nodes and disabled\n * checkboxes, connector lines, async child loading, search highlighting, drag to move,\n * and full keyboard navigation. Node shape: { key, title, icon, hue, count, tag, children,\n * disabled, disableCheckbox, selectable, checkable, isLeaf }.\n */\nexport function Tree({\n items = [], treeData,\n defaultExpanded = [], expandedKeys, onExpand,\n defaultActive, defaultSelected = [], selectedKeys, onSelect,\n checkable = false, checkStrictly = false, defaultChecked = [], checkedKeys, onCheck,\n multiple = false, selectable = true, showIcon = true, showLine = false, draggable = false,\n searchValue = \"\", autoExpandOnSearch = true, loadData, onDrop, onContextMenu, rowActions, style, reorder = false,\n}) {\n const raw = treeData || items;\n /* Callers usually pass an inline array literal, so compare by value —\n re-seeding on identity alone would wipe async-loaded children. */\n const rawKey = JSON.stringify(raw);\n const [data, setData] = React.useState(raw);\n const seenKey = React.useRef(rawKey);\n React.useEffect(() => { if (seenKey.current !== rawKey) { seenKey.current = rawKey; setData(raw); } }, [rawKey]);\n\n const seedExpanded = defaultExpanded.length ? defaultExpanded : defaultActive ? [] : [];\n const [expandedS, setExpandedS] = React.useState(seedExpanded);\n const [selectedS, setSelectedS] = React.useState(defaultActive ? [defaultActive] : defaultSelected);\n const [checkedS, setCheckedS] = React.useState(defaultChecked);\n const [loading, setLoading] = React.useState([]);\n const [loaded, setLoaded] = React.useState([]);\n const [focus, setFocus] = React.useState(null);\n const [focusMoved, setFocusMoved] = React.useState(false);\n const [dragKey, setDragKey] = React.useState(null);\n const [drop, setDrop] = React.useState(null);\n\n const expanded = expandedKeys || expandedS;\n const selected = selectedKeys || selectedS;\n const checked = checkedKeys || checkedS;\n\n const searchExpanded = React.useMemo(\n () => (searchValue && autoExpandOnSearch ? keysMatching(data, (n) => String(titleOf(n)).toLowerCase().includes(searchValue.toLowerCase())) : []),\n [data, searchValue, autoExpandOnSearch]\n );\n const openKeys = React.useMemo(() => [...new Set([...expanded, ...searchExpanded])], [expanded, searchExpanded]);\n const view = React.useMemo(\n () => (checkable && !checkStrictly ? rollUp(data, checked) : { full: new Set(checked), half: new Set() }),\n [checkable, checkStrictly, data, checked]\n );\n\n /* Visible rows, with the sibling map each row needs to draw its guide lines. */\n const rows = React.useMemo(() => {\n const out = [];\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1;\n const key = keyOf(n);\n out.push({ node: n, key, depth, trail, isLast });\n if (kidsOf(n).length && openKeys.includes(key)) push(kidsOf(n), depth + 1, [...trail, !isLast]);\n });\n };\n push(data, 0, []);\n return out;\n }, [data, openKeys]);\n\n const setExpanded = (next) => { if (!expandedKeys) setExpandedS(next); onExpand && onExpand(next); };\n\n const api = {\n focus: (k) => { setFocus(k); setFocusMoved(false); },\n toggle: (k) => {\n const open = openKeys.includes(k);\n if (!open && loadData && !loaded.includes(k) && !kidsOf(indexNodes(data).byKey[k]).length) {\n setLoading((l) => [...l, k]);\n setExpanded([...expanded, k]);\n Promise.resolve(loadData(indexNodes(data).byKey[k])).then((kids) => {\n setData((d) => {\n const clone = JSON.parse(JSON.stringify(d));\n const { byKey } = indexNodes(clone);\n if (byKey[k]) byKey[k].children = kids || [];\n return clone;\n });\n setLoaded((l) => [...l, k]);\n setLoading((l) => l.filter((x) => x !== k));\n });\n return;\n }\n setExpanded(open ? expanded.filter((x) => x !== k) : [...expanded, k]);\n },\n select: (k) => {\n const next = multiple ? (selected.includes(k) ? selected.filter((x) => x !== k) : [...selected, k]) : selected.includes(k) && multiple ? [] : [k];\n if (!selectedKeys) setSelectedS(next);\n onSelect && onSelect(multiple ? next : k, next);\n },\n check: (k) => {\n const on = view.full.has(k);\n let next = on ? [...view.full].filter((x) => x !== k) : [...view.full, k];\n if (!checkStrictly) next = cascade(data, k, next);\n if (!checkedKeys) setCheckedS(next);\n onCheck && onCheck(next, { key: k, checked: !on, half: checkStrictly ? [] : halfKeys(data, next) });\n },\n dragStart: (k) => setDragKey(k),\n /* Validity lives here, not in the row: only branches can be dropped *into*, and a node\n can never land inside its own subtree (that would detach the whole branch). */\n dragOver: (k, ratio) => {\n if (k == null) return setDrop(null);\n const node = indexNodes(data).byKey[k];\n if (!node || node.disabled || !dragKey || inSubtree(data, dragKey, k)) return setDrop(null);\n /* Without reorder only folders are drop targets: a file can't contain anything, so hovering\n one shows nothing at all. With reorder, a folder's outer 20% becomes an insert rule, and a\n file is an insert target over its whole height — top half before, bottom half after. */\n if (!isBranch(node)) return reorder ? setDrop({ key: k, pos: ratio < 0.5 ? \"before\" : \"after\" }) : setDrop(null);\n const pos = reorder ? (ratio < 0.2 ? \"before\" : ratio > 0.8 ? \"after\" : \"inside\") : \"inside\";\n setDrop({ key: k, pos });\n },\n dragEnd: () => { setDragKey(null); setDrop(null); },\n drop: (k) => {\n const info = drop && drop.key === k ? drop : null;\n setDrop(null);\n if (!dragKey || !info || inSubtree(data, dragKey, k)) return setDragKey(null);\n if (info.pos === \"inside\" && !isBranch(indexNodes(data).byKey[k])) return setDragKey(null);\n /* Computed outside setData: React re-invokes updaters (StrictMode, concurrent\n rebasing), which would fire the consumer's onDrop twice for one gesture. */\n const clone = JSON.parse(JSON.stringify(data));\n let moved = null;\n const prune = (nodes) => nodes.filter((n) => {\n if (keyOf(n) === dragKey) { moved = n; return false; }\n if (n.children) n.children = prune(n.children);\n return true;\n });\n const rootA = prune(clone);\n if (!moved) return setDragKey(null);\n let inserted = false;\n const insert = (nodes) => {\n const out = [];\n nodes.forEach((n) => {\n if (keyOf(n) === k && info.pos === \"before\") { out.push(moved); inserted = true; }\n if (keyOf(n) === k && info.pos === \"inside\") { n.children = [...(n.children || []), moved]; inserted = true; }\n else if (n.children) n.children = insert(n.children);\n out.push(n);\n if (keyOf(n) === k && info.pos === \"after\") { out.push(moved); inserted = true; }\n });\n return out;\n };\n const next = insert(rootA);\n if (!inserted) return setDragKey(null);\n setData(next);\n /* Dropping into a collapsed folder would otherwise look like the node vanished. */\n if (info.pos === \"inside\" && !expanded.includes(k)) setExpanded([...expanded, k]);\n onDrop && onDrop({ dragKey, dropKey: k, position: info.pos, treeData: next });\n setDragKey(null);\n },\n keyDown: (e, item) => {\n const i = rows.findIndex((r) => r.key === item.key);\n const go = (n) => { const t = rows[n]; if (!t) return; setFocus(t.key); setFocusMoved(true); };\n if (e.key === \"ArrowDown\") { e.preventDefault(); go(i + 1); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); go(i - 1); }\n else if (e.key === \"ArrowRight\") { e.preventDefault(); if (isBranch(item.node) && !openKeys.includes(item.key)) api.toggle(item.key); else go(i + 1); }\n else if (e.key === \"ArrowLeft\") { e.preventDefault(); if (isBranch(item.node) && openKeys.includes(item.key)) api.toggle(item.key); else { const p = rows.slice(0, i).reverse().find((r) => r.depth === item.depth - 1); if (p) { setFocus(p.key); setFocusMoved(true); } } }\n else if (e.key === \"Enter\") { e.preventDefault(); if (!item.node.disabled) { if (selectable && item.node.selectable !== false) api.select(item.key); if (isBranch(item.node)) api.toggle(item.key); } }\n else if (e.key === \" \") { e.preventDefault(); if (checkable && !item.node.disabled && item.node.checkable !== false && !item.node.disableCheckbox) api.check(item.key); }\n },\n };\n\n const state = { expanded: openKeys, selected, checked: view.full, half: view.half, loading, focus: focus || (rows[0] && rows[0].key), focusMoved, drop };\n const cfg = { checkable, checkStrictly, multiple, selectable, showIcon, showLine, draggable, searchValue, onContextMenu, rowActions };\n\n return (\n <div role=\"tree\" aria-multiselectable={multiple || undefined} style={{ display: \"flex\", flexDirection: \"column\", gap: showLine ? 0 : 1, ...style }}>\n <style>{\"@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\"}</style>\n {rows.length === 0 ? (\n <div style={{ padding: \"10px 8px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", color: \"var(--text-muted)\" }}>No matching items</div>\n ) : rows.map((r) => <Row key={r.key} item={r} state={state} cfg={cfg} api={api} />)}\n </div>\n );\n}\n","navigation/Tree.vue":"<script setup>\nimport { ref, computed, watch } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Checkbox from '../forms/Checkbox.vue'\n// Nested tree view: expand/collapse, single or multiple selection, cascading checkboxes with\n// indeterminate parents (checkStrictly opts out), disabled nodes, connector lines, async\n// loadData, search highlight, drag to move, keyboard navigation.\nconst docHues = { orange: 'var(--cat-transform)', green: 'var(--cat-source)', purple: 'var(--cat-merge)', blue: 'var(--info-fg)', teal: 'var(--cat-output)', gray: 'var(--text-muted)' }\nconst INDENT = 18\n// Insert marker (reorder before / after): a 2px --action-primary line with a 6px dot at its left end, drawn between rows.\n// It starts at the target row's content indent (8 + depth * INDENT — the chevron column, which is also the elbow column\n// under showLine) and ends at the row's 8px right padding; its centre sits on the gap between rows (1px, or 0 under showLine).\nfunction insertMarker(pos, depth) {\n const centre = -((props.showLine ? 0 : 1) / 2)\n return { position: 'absolute', left: (8 + depth * INDENT - 3) + 'px', right: '8px', height: '6px', pointerEvents: 'none', zIndex: 1, ...(pos === 'before' ? { top: (centre - 3) + 'px' } : { bottom: (centre - 3) + 'px' }) }\n}\nconst markerDot = { position: 'absolute', left: 0, top: 0, width: '6px', height: '6px', borderRadius: '50%', background: 'var(--action-primary)' }\nconst markerLine = { position: 'absolute', left: '3px', right: 0, top: '2px', height: '2px', borderRadius: '1px', background: 'var(--action-primary)' }\nconst props = defineProps({\n items: { type: Array, default: () => [] },\n treeData: Array, // alias of items (same as Tree.jsx)\n expandedKeys: Array, selectedKeys: Array, checkedKeys: Array, // controlled: the parent's value wins whenever it changes\n defaultExpanded: { type: Array, default: () => [] },\n defaultActive: String,\n defaultSelected: { type: Array, default: () => [] },\n defaultChecked: { type: Array, default: () => [] },\n checkable: Boolean, checkStrictly: Boolean, multiple: Boolean,\n selectable: { type: Boolean, default: true },\n showIcon: { type: Boolean, default: true },\n showLine: Boolean, draggable: Boolean,\n searchValue: { type: String, default: '' },\n autoExpandOnSearch: { type: Boolean, default: true },\n reorder: Boolean,\n loadData: Function, rowActions: Function, style: Object,\n})\nconst emit = defineEmits(['select', 'check', 'expand', 'drop'])\n\nconst data = ref(JSON.parse(JSON.stringify(props.treeData || props.items)))\n// Compare by value: re-seeding on identity alone would wipe async-loaded children.\nwatch(() => JSON.stringify(props.treeData || props.items), (next, prev) => { if (next !== prev) data.value = JSON.parse(next) })\nconst expanded = ref([...props.defaultExpanded])\nconst selected = ref(props.defaultActive ? [props.defaultActive] : [...props.defaultSelected])\nconst checked = ref([...props.defaultChecked])\nwatch(() => props.expandedKeys, (v) => { if (v) expanded.value = [...v] }, { immediate: true })\nwatch(() => props.selectedKeys, (v) => { if (v) selected.value = [...v] }, { immediate: true })\nwatch(() => props.checkedKeys, (v) => { if (v) checked.value = [...v] }, { immediate: true })\nconst loading = ref([]); const loaded = ref([]); const focus = ref(null)\nconst dragKey = ref(null); const drop = ref(null)\n\nconst keyOf = n => (n.key != null ? n.key : n.label)\nconst titleOf = n => (n.title != null ? n.title : n.label)\nconst kidsOf = n => n.children || []\nconst isBranch = n => kidsOf(n).length > 0 || n.isLeaf === false\nfunction descendants(n, out = []) { for (const c of kidsOf(n)) { out.push(c); descendants(c, out) } return out }\n/** Is `key` the dragged node itself, or anywhere inside its subtree? */\nfunction inSubtree(rootKey, key) {\n const root = index(data.value).byKey[rootKey]\n if (!root) return false\n return rootKey === key || descendants(root).some(d => keyOf(d) === key)\n}\nfunction index(nodes, parent = null, acc = { byKey: {}, parent: {} }) {\n for (const n of nodes) { acc.byKey[keyOf(n)] = n; acc.parent[keyOf(n)] = parent; index(kidsOf(n), keyOf(n), acc) }\n return acc\n}\nfunction ancestorsOfMatches(nodes, test, trail = [], hit = []) {\n for (const n of nodes) {\n if (test(n)) hit.push(...trail)\n ancestorsOfMatches(kidsOf(n), test, [...trail, keyOf(n)], hit)\n }\n return [...new Set(hit)]\n}\nconst searchExpanded = computed(() => props.searchValue && props.autoExpandOnSearch\n ? ancestorsOfMatches(data.value, n => String(titleOf(n)).toLowerCase().includes(props.searchValue.toLowerCase()))\n : [])\nconst openKeys = computed(() => [...new Set([...expanded.value, ...searchExpanded.value])])\n\nconst view = computed(() => {\n const full = new Set(checked.value); const half = new Set()\n if (!props.checkable || props.checkStrictly) return { full, half }\n const visit = n => {\n kidsOf(n).forEach(visit)\n if (!kidsOf(n).length) return\n const ds = descendants(n).filter(d => !d.disabled && !d.disableCheckbox)\n if (!ds.length) return\n const on = ds.filter(d => full.has(keyOf(d))).length\n if (on === ds.length) full.add(keyOf(n))\n else { full.delete(keyOf(n)); if (on > 0) half.add(keyOf(n)) }\n }\n data.value.forEach(visit)\n return { full, half }\n})\n\nconst rows = computed(() => {\n const out = []\n const push = (nodes, depth, trail) => {\n nodes.forEach((n, i) => {\n const isLast = i === nodes.length - 1\n out.push({ n, key: keyOf(n), depth, trail, isLast })\n if (kidsOf(n).length && openKeys.value.includes(keyOf(n))) push(kidsOf(n), depth + 1, [...trail, !isLast])\n })\n }\n push(data.value, 0, [])\n return out\n})\n\nfunction toggle(k) {\n const open = openKeys.value.includes(k)\n const { byKey } = index(data.value)\n if (!open && props.loadData && !loaded.value.includes(k) && !kidsOf(byKey[k]).length) {\n loading.value = [...loading.value, k]\n expanded.value = [...expanded.value, k]\n Promise.resolve(props.loadData(byKey[k])).then(kids => {\n const clone = JSON.parse(JSON.stringify(data.value))\n const t = index(clone).byKey[k]\n if (t) t.children = kids || []\n data.value = clone\n loaded.value = [...loaded.value, k]\n loading.value = loading.value.filter(x => x !== k)\n })\n return\n }\n expanded.value = open ? expanded.value.filter(x => x !== k) : [...expanded.value, k]\n emit('expand', expanded.value)\n}\nfunction select(k) {\n selected.value = props.multiple\n ? (selected.value.includes(k) ? selected.value.filter(x => x !== k) : [...selected.value, k])\n : [k]\n emit('select', props.multiple ? selected.value : k, selected.value)\n}\nfunction check(k) {\n const set = new Set(view.value.full)\n const on = set.has(k)\n on ? set.delete(k) : set.add(k)\n if (!props.checkStrictly) {\n const { byKey, parent } = index(data.value)\n descendants(byKey[k]).forEach(d => {\n if (d.disabled || d.disableCheckbox) return\n on ? set.delete(keyOf(d)) : set.add(keyOf(d))\n })\n let p = parent[k]\n while (p != null) {\n const kids = kidsOf(byKey[p]).filter(c => !c.disabled && !c.disableCheckbox)\n const hit = kids.filter(c => set.has(keyOf(c)))\n hit.length && hit.length === kids.length ? set.add(p) : set.delete(p)\n p = parent[p]\n }\n }\n checked.value = [...set]\n emit('check', checked.value, { key: k, checked: !on, half: [...view.value.half] })\n}\nfunction rowClick(n) {\n if (n.disabled) return\n focus.value = keyOf(n)\n if (props.selectable && n.selectable !== false) select(keyOf(n))\n if (isBranch(n)) toggle(keyOf(n))\n}\nfunction onDragOver(e, k) {\n if (!props.draggable) return\n e.preventDefault()\n if (!dragKey.value) return\n const n = index(data.value).byKey[k]\n if (!n || n.disabled || inSubtree(dragKey.value, k)) { drop.value = null; return }\n const r = e.currentTarget.getBoundingClientRect()\n const y = (e.clientY - r.top) / r.height\n // Without reorder only folders are drop targets: hovering a file shows nothing, since it can't contain anything.\n // With reorder, a folder's outer 20% becomes an insert rule, and a file is an insert target over its whole height — top half before, bottom half after.\n if (!isBranch(n)) { drop.value = props.reorder ? { key: k, pos: y < 0.5 ? 'before' : 'after' } : null; return }\n drop.value = { key: k, pos: props.reorder ? (y < 0.2 ? 'before' : y > 0.8 ? 'after' : 'inside') : 'inside' }\n}\nfunction onDrop(k) {\n const info = drop.value && drop.value.key === k ? drop.value : null\n drop.value = null\n if (!dragKey.value || !info || inSubtree(dragKey.value, k)) { dragKey.value = null; return }\n if (info.pos === 'inside' && !isBranch(index(data.value).byKey[k])) { dragKey.value = null; return }\n const clone = JSON.parse(JSON.stringify(data.value))\n let moved = null\n const prune = nodes => nodes.filter(n => {\n if (keyOf(n) === dragKey.value) { moved = n; return false }\n if (n.children) n.children = prune(n.children)\n return true\n })\n const pruned = prune(clone)\n if (!moved) { dragKey.value = null; return }\n const insert = nodes => {\n const out = []\n for (const n of nodes) {\n if (keyOf(n) === k && info.pos === 'before') out.push(moved)\n if (keyOf(n) === k && info.pos === 'inside') n.children = [...(n.children || []), moved]\n else if (n.children) n.children = insert(n.children)\n out.push(n)\n if (keyOf(n) === k && info.pos === 'after') out.push(moved)\n }\n return out\n }\n data.value = insert(pruned)\n if (info.pos === 'inside' && !expanded.value.includes(k)) expanded.value = [...expanded.value, k]\n emit('drop', { dragKey: dragKey.value, dropKey: k, position: info.pos, treeData: data.value })\n dragKey.value = null\n}\nfunction keyDown(e, row) {\n const i = rows.value.findIndex(r => r.key === row.key)\n const go = n => { const t = rows.value[n]; if (t) focus.value = t.key }\n if (e.key === 'ArrowDown') { e.preventDefault(); go(i + 1) }\n else if (e.key === 'ArrowUp') { e.preventDefault(); go(i - 1) }\n else if (e.key === 'ArrowRight') { e.preventDefault(); isBranch(row.n) && !openKeys.value.includes(row.key) ? toggle(row.key) : go(i + 1) }\n else if (e.key === 'ArrowLeft') { e.preventDefault(); if (isBranch(row.n) && openKeys.value.includes(row.key)) toggle(row.key) }\n else if (e.key === 'Enter') { e.preventDefault(); rowClick(row.n) }\n else if (e.key === ' ') { e.preventDefault(); if (props.checkable && !row.n.disabled && row.n.checkable !== false && !row.n.disableCheckbox) check(row.key) }\n}\nfunction parts(text) {\n const q = props.searchValue\n const s = String(text)\n if (!q) return [{ t: s, hit: false }]\n const i = s.toLowerCase().indexOf(q.toLowerCase())\n if (i < 0) return [{ t: s, hit: false }]\n return [{ t: s.slice(0, i), hit: false }, { t: s.slice(i, i + q.length), hit: true }, { t: s.slice(i + q.length), hit: false }]\n}\nconst dropAt = k => (drop.value && drop.value.key === k ? drop.value.pos : null)\n</script>\n<template>\n <div role=\"tree\" :style=\"{ display: 'flex', flexDirection: 'column', gap: showLine ? 0 : '1px', ...style }\">\n <div v-for=\"row in rows\" :key=\"row.key\" role=\"treeitem\" class=\"kb-tree-row\"\n :tabindex=\"focus === row.key || (!focus && row === rows[0]) ? 0 : -1\"\n :aria-expanded=\"isBranch(row.n) ? openKeys.includes(row.key) : undefined\"\n :aria-selected=\"selected.includes(row.key)\" :aria-disabled=\"row.n.disabled || undefined\"\n :draggable=\"draggable && !row.n.disabled\"\n @dragstart=\"dragKey = row.key\" @dragover=\"onDragOver($event, row.key)\" @dragleave=\"drop = null\"\n @drop.prevent=\"onDrop(row.key)\" @dragend=\"dragKey = null; drop = null\"\n @click=\"rowClick(row.n)\" @keydown=\"keyDown($event, row)\"\n :class=\"{ 'is-disabled': row.n.disabled }\"\n :style=\"{\n position: 'relative', minHeight: 'var(--h-tree-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '6px',\n padding: showLine ? '0 8px 0 2px' : '5px 8px', paddingLeft: showLine ? '2px' : (8 + row.depth * INDENT) + 'px', boxSizing: 'border-box',\n cursor: row.n.disabled ? 'not-allowed' : 'pointer', opacity: row.n.disabled ? 0.45 : 1,\n background: dropAt(row.key) === 'inside' ? 'var(--soft-bg)' : selected.includes(row.key) ? 'var(--bg-active)' : 'transparent',\n outline: dropAt(row.key) === 'inside' ? '2px solid var(--action-primary)' : 'none',\n outlineOffset: '-2px',\n transition: 'var(--t-hover)' }\">\n <span v-if=\"dropAt(row.key) === 'before' || dropAt(row.key) === 'after'\" aria-hidden=\"true\" :data-drop-marker=\"dropAt(row.key)\" :style=\"insertMarker(dropAt(row.key), row.depth)\"><span :style=\"markerDot\" /><span :style=\"markerLine\" /></span>\n <template v-if=\"showLine\">\n <span v-for=\"(hasNext, i) in row.trail\" :key=\"i\"\n :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, marginLeft: i === 0 ? '6px' : 0, borderLeft: hasNext ? '1px solid var(--border-subtle)' : '1px solid transparent' }\" />\n <span :style=\"{ width: INDENT + 'px', alignSelf: 'stretch', flexShrink: 0, position: 'relative', marginLeft: row.trail.length ? 0 : '6px' }\">\n <span :style=\"{ position: 'absolute', left: 0, top: 0, height: row.isLast ? '50%' : '100%', borderLeft: '1px solid var(--border-subtle)' }\" />\n <span :style=\"{ position: 'absolute', left: 0, top: '50%', width: (isBranch(row.n) ? 6 : INDENT - 4) + 'px', borderTop: '1px solid var(--border-subtle)' }\" />\n </span>\n </template>\n <span v-if=\"isBranch(row.n)\" @click.stop=\"toggle(row.key)\"\n :style=\"{ width: '14px', height: '14px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', flexShrink: 0, cursor: 'pointer', transform: openKeys.includes(row.key) ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\">\n <Icon :name=\"loading.includes(row.key) ? 'loader' : 'chevron-right'\" :size=\"13\" :class=\"loading.includes(row.key) ? 'kb-tree-spin' : ''\" />\n </span>\n <span v-else :style=\"{ width: showLine ? 0 : '14px', flexShrink: 0 }\" />\n <span v-if=\"checkable && row.n.checkable !== false\" @click.stop style=\"display:inline-flex;flex-shrink:0\">\n <Checkbox :checked=\"view.full.has(row.key)\" :indeterminate=\"view.half.has(row.key)\"\n :disabled=\"row.n.disabled || row.n.disableCheckbox\" @change=\"check(row.key)\" />\n </span>\n <span v-if=\"showIcon\" :style=\"{ color: isBranch(row.n) ? 'var(--text-muted)' : docHues[row.n.hue || 'gray'], display: 'inline-flex', flexShrink: 0 }\">\n <Icon :name=\"row.n.icon || (isBranch(row.n) ? (openKeys.includes(row.key) ? 'folder-open' : 'folder') : 'file-text')\" :size=\"15\" />\n </span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', fontWeight: selected.includes(row.key) ? 'var(--weight-medium)' : 'var(--weight-regular)', color: row.n.disabled ? 'var(--text-muted)' : 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }\">\n <template v-for=\"(p, i) in parts(titleOf(row.n))\" :key=\"i\"><mark v-if=\"p.hit\" style=\"background:var(--soft-bg);color:var(--info-fg);font-weight:var(--weight-medium);padding:0;border-radius:2px\">{{ p.t }}</mark><template v-else>{{ p.t }}</template></template>\n </span>\n <span v-if=\"row.n.tag\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:1;padding:2px 5px;border-radius:var(--radius-sm);background:var(--bg-inset);color:var(--text-secondary);flex-shrink:0\">{{ row.n.tag }}</span>\n <span v-if=\"row.n.count != null\" style=\"margin-left:auto;font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-muted);padding-left:8px\">{{ row.n.count }}</span>\n </div>\n <div v-if=\"!rows.length\" style=\"padding:10px 8px;font-family:var(--font-sans);font-size:var(--text-base);color:var(--text-muted)\">No matching items</div>\n </div>\n</template>\n<style scoped>\n.kb-tree-row:not(.is-disabled):hover{background:var(--bg-hover)}\n.kb-tree-row:focus-visible{outline:2px solid var(--focus-border);outline-offset:-2px}\n.kb-tree-spin{animation:kb-tree-spin 900ms linear infinite}\n@keyframes kb-tree-spin{to{transform:rotate(360deg)}}\n</style>\n","navigation/PageHeading.vue":"<script setup>\ndefineProps({ title: String, subtitle: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)\">{{ title }}</span>\n <span v-if=\"subtitle\" style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-secondary)\">{{ subtitle }}</span>\n </div>\n</template>\n","navigation/PageToolbar.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Page toolbar under the top bar: back · title · status, actions on the right. A size region (default inherit → md).\ndefineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, size: String, style: Object })\nconst emit = defineEmits(['back'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 var(--edge-pad)', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"display:flex;align-items:center;gap:var(--control-gap)\">\n <IconButton v-if=\"showBack\" icon=\"chevron-left\" label=\"Back\" @click=\"emit('back')\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span v-if=\"status\" style=\"font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);margin-left:6px\">{{ status }}</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:var(--control-gap)\"><slot name=\"actions\" /></div>\n </div>\n</template>\n","navigation/ThemeToggle.vue":"<script setup>\nimport { px } from '../px.js'\nimport { ref, computed, watch, onMounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst props = defineProps({ size: { type: Number, default: 34 }, dark: { type: Boolean, default: null }, style: Object })\nconst emit = defineEmits(['update:dark'])\nconst darkState = ref(false)\nconst dark = computed(() => props.dark != null ? props.dark : darkState.value)\nconst hover = ref(false)\nonMounted(() => { darkState.value = document.documentElement.dataset.theme === 'dark' })\nwatch(dark, (v) => { if (props.dark != null) return; document.documentElement.dataset.theme = v ? 'dark' : 'light'; try { localStorage.setItem('kb-theme', v ? 'dark' : 'light') } catch (e) {} })\nfunction toggle() { if (props.dark != null) emit('update:dark', !dark.value); else darkState.value = !darkState.value }\n</script>\n<template>\n <button type=\"button\" :aria-label=\"dark ? 'Switch to light mode' : 'Switch to dark mode'\" @click=\"toggle\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: px(size), height: px(size), borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: hover ? 'var(--bg-hover)' : 'transparent', color: 'var(--text-body)', padding: 0, transition: 'var(--t-hover)', ...style }\">\n <Icon :name=\"dark ? 'sun' : 'moon'\" :size=\"18\" />\n </button>\n</template>\n","navigation/SidePanel.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { chromeTone } from \"./chrome-tones.js\";\n\n/**\n * Side panel docked beside the Sidebar (Documents, Management). 240px, same tone as the rail, opens by growing from 0\n * (--dur-slow, --ease-out) while its content fades in from 12px left. 56px header: title + actions + close.\n * Body scrolls; put a SegmentedControl, SearchInput, Tree or rows inside. size=\"sm\" makes its controls compact.\n */\nexport function SidePanel({ open = true, title, actions, onClose, closeLabel = \"Close\", closeIcon = \"x\", tone = \"recessed\", color, width = 240, size, children, style }) {\n const t = chromeTone(tone, color);\n return (\n <div aria-hidden={open ? undefined : \"true\"} style={{ width: open ? width : 0, flexShrink: 0, overflow: \"hidden\", height: \"100%\", transition: \"width var(--dur-slow) var(--ease-out)\" }}>\n <div data-theme={t.theme} data-size={size} style={{ ...t.vars, width, height: \"100%\", background: \"var(--sb-bg)\", borderRight: \"1px solid var(--sb-border)\", color: \"var(--sb-text)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", opacity: open ? 1 : 0, transform: open ? \"none\" : \"translateX(-12px)\", transition: \"opacity var(--dur) var(--ease-out), transform var(--dur-slow) var(--ease-out)\", pointerEvents: open ? \"auto\" : \"none\", ...style }}>\n <div style={{ height: \"var(--topbar-h)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 8, padding: \"0 8px 0 16px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ minWidth: 0, overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", fontWeight: \"var(--weight-medium)\", color: \"var(--text-heading)\" }}>{title}</span>\n <div data-size=\"md\" style={{ display: \"flex\", alignItems: \"center\", gap: 2, flexShrink: 0 }}>{actions}{onClose ? <IconButton icon={closeIcon} muted label={closeLabel} title={closeLabel} onClick={onClose} /> : null}</div>\n </div>\n <div style={{ flex: 1, minHeight: 0, overflow: \"auto\", display: \"flex\", flexDirection: \"column\" }}>{children}</div>\n </div>\n </div>\n );\n}\n","navigation/SidePanel.vue":"<script setup>\nimport { computed } from 'vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport { chromeTone } from './chrome-tones.js'\n// Side panel docked beside the Sidebar (Documents, Management). 240px, same tone as the rail; opens by growing from 0 while\n// its content fades in from 12px left. 56px header: title + #actions slot + close (@close). Default slot is the scrolling body.\nconst props = defineProps({ open: { type: Boolean, default: true }, title: String, closable: { type: Boolean, default: true }, closeLabel: { type: String, default: 'Close' }, closeIcon: { type: String, default: 'x' }, tone: { type: String, default: 'recessed' }, color: String, width: { type: Number, default: 240 }, size: String, style: Object })\nconst emit = defineEmits(['close'])\nconst t = computed(() => chromeTone(props.tone, props.color))\n</script>\n<template>\n <div :aria-hidden=\"open ? undefined : 'true'\" :style=\"{ width: (open ? width : 0) + 'px', flexShrink: 0, overflow: 'hidden', height: '100%', transition: 'width var(--dur-slow) var(--ease-out)' }\">\n <div :data-theme=\"t.theme\" :data-size=\"size\" :style=\"{ ...t.vars, width: width + 'px', height: '100%', background: 'var(--sb-bg)', borderRight: '1px solid var(--sb-border)', color: 'var(--sb-text)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', opacity: open ? 1 : 0, transform: open ? 'none' : 'translateX(-12px)', transition: 'opacity var(--dur) var(--ease-out), transform var(--dur-slow) var(--ease-out)', pointerEvents: open ? 'auto' : 'none', ...style }\">\n <div style=\"height:var(--topbar-h);display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 8px 0 16px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading)\">{{ title }}</span>\n <div data-size=\"md\" style=\"display:flex;align-items:center;gap:2px;flex-shrink:0\"><slot name=\"actions\" /><IconButton v-if=\"closable\" :icon=\"closeIcon\" muted :label=\"closeLabel\" :title=\"closeLabel\" @click=\"emit('close')\" /></div>\n </div>\n <div style=\"flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column\"><slot /></div>\n </div>\n </div>\n</template>\n","navigation/UserMenu.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\nimport Avatar from '../display/Avatar.vue'\nimport SegmentedControl from '../tabs/SegmentedControl.vue'\nimport ContextMenu from '../feedback/ContextMenu.vue'\n// Account menu (opened from the TopBar's user trigger): avatar + name + email, an Appearance row with a light / dark / system switch\n// (v-model:theme, labels of themeOptions), then rows (items → @select(label)).\ndefineProps({ name: String, email: String, theme: String, themeOptions: { type: Array, default: () => [{ label: 'Light', icon: 'sun' }, { label: 'Dark', icon: 'moon' }, { label: 'System', icon: 'monitor' }] }, appearanceLabel: { type: String, default: 'Appearance' }, showAppearance: { type: Boolean, default: true }, items: { type: Array, default: () => [] }, style: Object })\nconst emit = defineEmits(['update:theme', 'select'])\n</script>\n<template>\n <div role=\"menu\" :style=\"{ width: '288px', background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '4px', boxSizing: 'border-box', ...style }\">\n <div style=\"display:flex;align-items:center;gap:10px;padding:8px 12px\">\n <Avatar :name=\"name\" :size=\"32\" />\n <div style=\"display:flex;flex-direction:column;min-width:0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);font-weight:var(--weight-medium);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ name }}</span>\n <span v-if=\"email\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ email }}</span>\n </div>\n </div>\n <div style=\"height:1px;background:var(--border-subtle);margin:4px 0\"></div>\n <div v-if=\"showAppearance\" style=\"height:40px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 12px;box-sizing:border-box\">\n <div style=\"display:flex;align-items:center;gap:10px;min-width:0\"><span style=\"display:inline-flex;color:var(--text-body)\"><Icon name=\"sliders\" :size=\"16\" /></span><span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ appearanceLabel }}</span></div>\n <SegmentedControl icon-only :options=\"themeOptions\" :model-value=\"theme\" @update:model-value=\"v => emit('update:theme', v)\" style=\"flex-shrink:0\" />\n </div>\n <ContextMenu v-if=\"items.length\" :items=\"items\" @select=\"l => emit('select', l)\" :style=\"{ display: 'flex', border: 'none', boxShadow: 'none', padding: 0, minWidth: 0, background: 'transparent' }\" />\n </div>\n</template>\n","navigation/chrome-tones.js":"// App chrome tones — framework-neutral, shared by Sidebar and SidePanel in React and Vue.\n// recessed (default): grey rail, the active item is a white tile with a hairline ring.\n// flush: white rail, neutral selected fill. contrast: the rail is scoped to the dark theme.\n// custom: dark scope on a solid colour (SIDEBAR_COLORS or any hex); washes are white at 10 / 16 %.\n// chromeTone() returns the data-theme for the rail and the CSS custom properties it sets on itself:\n// --sb-* drive the rail's own surfaces; --bg-hover / --bg-active / … are remapped so components inside\n// (SearchInput, SegmentedControl, Tree) sit correctly on the rail colour.\nexport const SIDEBAR_COLORS = { slate: '#1e293b', navy: '#14305c', indigo: '#312e81', violet: '#4c1d95', plum: '#701a75', wine: '#7f1d1d', forest: '#14532d', teal: '#134e4a', umber: '#78350f', charcoal: '#262626' }\nconst wash = (p) => 'color-mix(in srgb, var(--on-accent) ' + p + '%, transparent)'\nconst BASE = { '--sb-border': 'var(--border-card)', '--sb-text': 'var(--text-body)', '--sb-brand': 'var(--text-primary)', '--sb-active-text': 'var(--text-heading)', '--sb-muted': 'var(--text-muted)' }\nconst GREY = { ...BASE, '--sb-bg': 'var(--bg-table-header)', '--sb-active': 'var(--bg-surface)', '--sb-hover': 'var(--bg-press)', '--sb-shadow': '0 0 0 1px var(--border-card)', '--bg-hover': 'var(--bg-press)', '--bg-active': 'var(--bg-surface)', '--bg-inset': 'var(--overlay-hover)' }\nconst WHITE = { ...BASE, '--sb-bg': 'var(--bg-surface)', '--sb-active': 'var(--bg-active)', '--sb-hover': 'var(--bg-hover)', '--sb-shadow': 'none' }\nexport function chromeTone(tone, color) {\n const t = tone || 'recessed'\n if (t === 'flush') return { theme: undefined, vars: WHITE }\n if (t === 'contrast') return { theme: 'dark', vars: GREY }\n if (t === 'custom') {\n const bg = SIDEBAR_COLORS[color] || color || SIDEBAR_COLORS.slate\n return { theme: 'dark', vars: { ...GREY, '--sb-bg': bg, '--sb-border': wash(12), '--sb-active': wash(16), '--sb-hover': wash(10), '--sb-shadow': 'none', '--bg-hover': wash(10), '--bg-press': wash(16), '--bg-active': wash(16), '--bg-inset': wash(10), '--border-control': wash(16) } }\n }\n return { theme: undefined, vars: GREY }\n}\n","navigation/nav-model.js":"// The product's nav model. Framework-neutral: re-exported by the React entry and exported from the Vue entry.\n// KEYBRICKS_NAV — the main rail; trailing: true marks an item that opens a side panel (chevron on the right).\n// KEYBRICKS_NAV_FOOTER — pinned at the bottom of the rail. KEYBRICKS_MGMT — the Management side panel's groups.\nexport const KEYBRICKS_NAV = [\n { icon: 'house', label: 'Home' },\n { icon: 'file-text', label: 'Documents', trailing: true },\n { icon: 'journal', label: 'Voucher' },\n { icon: 'credit-card', label: 'Fund Payment Plan' },\n { icon: 'database', label: 'Data Integration' },\n { icon: 'git-branch', label: 'Data Flow' },\n { icon: 'reconciliation', label: 'Reconciliation' },\n { icon: 'consolidation', label: 'Consolidation' },\n { icon: 'task-execution', label: 'Task Execution' },\n]\nexport const KEYBRICKS_NAV_FOOTER = [\n { icon: 'settings', label: 'Management', trailing: true },\n { icon: 'book-open', label: 'Knowledge Base' },\n]\nexport const KEYBRICKS_MGMT = [\n { icon: 'model', label: 'Modeling', children: ['Dimension', 'Model', 'Environment', 'Journal Template', 'Detail Template', 'Relationship'] },\n { icon: 'workflow', label: 'Workflow', children: ['Workflow', 'Requests', 'Task Tracker'] },\n { icon: 'shield', label: 'Security Management', children: ['Users', 'Roles', 'Permissions'] },\n { icon: 'refresh-cw', label: 'System Migration' },\n { icon: 'rules-validation', label: 'Validation Rules' },\n { icon: 'settings-2', label: 'Control Center' },\n]\n"});
@@ -1 +0,0 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"pipeline/ConfigPanel.jsx":"import React from \"react\";\nimport { Button } from \"../buttons/Button.jsx\";\nimport { PanelTabs } from \"../tabs/Tabs.jsx\";\nimport { NumberStepper } from \"../forms/NumberStepper.jsx\";\n\n/** Right node-config panel — 400px: title/subtitle + Save Settings, preview-rows row, PanelTabs, body slot. A size region (default inherit → md): every control in the header shares one tier. */\nexport function ConfigPanel({ title, subtitle, tabs = [\"Node Info\", \"Calc Rules\", \"Output Fields\", \"SQL Preview\"], activeTab, onTab, previewRows = 100, onPreviewRows, onSave, onPreview, children, height = \"100%\", size, style }) {\n const [tab, setTab] = React.useState(tabs[0]);\n const cur = activeTab != null ? activeTab : tab;\n return (\n <div data-size={size} style={{ width: \"var(--panel-w)\", height, background: \"var(--bg-surface)\", borderLeft: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ padding: \"12px 16px 0\", borderBottom: \"1px solid var(--border-card)\", flexShrink: 0 }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"flex-start\" }}>\n <div style={{ display: \"flex\", flexDirection: \"column\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-md)\", lineHeight: \"var(--leading-md)\", letterSpacing: \"var(--tracking-md)\", color: \"var(--text-heading)\" }}>{title}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", paddingTop: 2 }}>{subtitle}</span>\n </div>\n <Button onClick={onSave}>Save Settings</Button>\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\", padding: \"12px 0\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-secondary)\" }}>Preview rows</span>\n <NumberStepper value={previewRows} onChange={onPreviewRows} />\n <Button variant=\"primary-soft\" icon=\"eye\" onClick={onPreview}>Data Preview</Button>\n </div>\n <PanelTabs tabs={tabs} active={cur} onChange={(t) => { setTab(t); onTab && onTab(t); }} style={{ margin: \"0 -16px\", padding: \"0 16px\", borderBottom: \"none\" }} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 16, display: \"flex\", flexDirection: \"column\", gap: 16 }}>{children}</div>\n </div>\n );\n}\n","pipeline/ConfigPanel.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Button from '../buttons/Button.vue'\nimport PanelTabs from '../tabs/Tabs.vue'\nimport NumberStepper from '../forms/NumberStepper.vue'\n// Right node-config panel — 400px: title/subtitle + Save Settings, preview-rows row, PanelTabs, body slot. A size region (default inherit → md): every control in the header shares one tier.\nconst props = defineProps({ title: String, subtitle: String, tabs: { type: Array, default: () => ['Node Info', 'Calc Rules', 'Output Fields', 'SQL Preview'] }, activeTab: String, previewRows: { type: Number, default: 100 }, height: { type: [String, Number], default: '100%' }, size: String, style: Object })\nconst emit = defineEmits(['tab', 'update:previewRows', 'save', 'preview'])\nconst tabState = ref(props.tabs[0])\nconst cur = computed(() => props.activeTab != null ? props.activeTab : tabState.value)\nfunction setTab(t) { tabState.value = t; emit('tab', t) }\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ width: 'var(--panel-w)', height, background: 'var(--bg-surface)', borderLeft: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"padding:12px 16px 0;border-bottom:1px solid var(--border-card);flex-shrink:0\">\n <div style=\"display:flex;justify-content:space-between;align-items:flex-start\">\n <div style=\"display:flex;flex-direction:column\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)\">{{ title }}</span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);padding-top:2px\">{{ subtitle }}</span>\n </div>\n <Button @click=\"emit('save')\">Save Settings</Button>\n </div>\n <div style=\"display:flex;align-items:center;gap:var(--control-gap);padding:12px 0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-secondary)\">Preview rows</span>\n <NumberStepper :model-value=\"previewRows\" @update:modelValue=\"v => emit('update:previewRows', v)\" />\n <Button variant=\"primary-soft\" icon=\"eye\" @click=\"emit('preview')\">Data Preview</Button>\n </div>\n <PanelTabs :tabs=\"tabs\" :active=\"cur\" @change=\"setTab\" style=\"margin:0 -16px;padding:0 16px;border-bottom:none\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:16px\"><slot /></div>\n </div>\n</template>\n","pipeline/DataPreview.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { Checkbox } from \"../forms/Checkbox.jsx\";\nimport { Table } from \"../data/Table.jsx\";\n\n/** Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table. An sm size region: the header's controls are 24px and the table has 32px rows with 12px text. */\nexport function DataPreview({ title = \"Data Preview\", columns = [], rows = [], debug = false, onDebug, collapsed, onCollapse, size = \"sm\", style }) {\n return (\n <div data-size={size} style={{ background: \"var(--bg-surface)\", borderTop: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", height: collapsed ? \"var(--bar-card-h)\" : \"var(--drawer-h)\", overflow: \"hidden\", boxSizing: \"border-box\", flexShrink: 0, transition: \"height .15s ease\", ...style }}>\n <div style={{ height: \"var(--bar-card-h)\", display: \"flex\", alignItems: \"center\", gap: \"var(--group-gap)\", padding: \"0 var(--edge-pad)\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\" }}>{title}</span>\n <span style={{ flex: 1 }} />\n <Checkbox label=\"Debug Mode\" checked={debug} onChange={onDebug} />\n <IconButton icon={collapsed ? \"chevron-up\" : \"chevron-down\"} muted onClick={onCollapse} label={collapsed ? \"Expand\" : \"Collapse\"} />\n </div>\n {!collapsed ? (\n <div style={{ overflowY: \"auto\", borderTop: \"1px solid var(--border-subtle)\" }}>\n <Table resizable columns={columns} rows={rows} headerBorder={false} />\n </div>\n ) : null}\n </div>\n );\n}\n","pipeline/DataPreview.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\nimport Checkbox from '../forms/Checkbox.vue'\nimport Table from '../data/Table.vue'\n// Bottom data-preview drawer — 250px: 40px header (title, Debug Mode, collapse) + dense table. An sm size region: the header's controls are 24px and the table has 32px rows with 12px text.\ndefineProps({ title: { type: String, default: 'Data Preview' }, columns: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, debug: Boolean, collapsed: Boolean, size: { type: String, default: 'sm' }, style: Object })\nconst emit = defineEmits(['update:debug', 'toggleCollapse'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ background: 'var(--bg-surface)', borderTop: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', height: collapsed ? 'var(--bar-card-h)' : 'var(--drawer-h)', overflow: 'hidden', boxSizing: 'border-box', flexShrink: 0, transition: 'height .15s ease', ...style }\">\n <div style=\"height:var(--bar-card-h);display:flex;align-items:center;gap:var(--group-gap);padding:0 var(--edge-pad);box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)\">{{ title }}</span>\n <span style=\"flex:1\" />\n <Checkbox label=\"Debug Mode\" :checked=\"debug\" @change=\"v => emit('update:debug', v)\" />\n <IconButton :icon=\"collapsed ? 'chevron-up' : 'chevron-down'\" muted :label=\"collapsed ? 'Expand' : 'Collapse'\" @click=\"emit('toggleCollapse')\" />\n </div>\n <div v-if=\"!collapsed\" style=\"overflow-y:auto;border-top:1px solid var(--border-subtle)\">\n <Table resizable :columns=\"columns\" :rows=\"rows\" :header-border=\"false\" />\n </div>\n </div>\n</template>\n","pipeline/NodeCard.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile, StatusBadge } from \"../display/Badge.jsx\";\n\n/* Ports are the connection handles: they light up on hover, stay lit while a wire is\n being dragged out of them, and show a crosshair when they're a valid drop target. */\nfunction portSkin(tone, hover, interactive) {\n const hot = tone === \"active\" || tone === \"target\" || (interactive && hover);\n return {\n background: tone === \"target\" ? \"var(--soft-bg)\" : \"var(--bg-surface)\",\n border: `2px solid ${hot ? \"var(--action-primary)\" : \"var(--port-border)\"}`,\n boxShadow: tone === \"active\" || tone === \"target\" ? \"0 0 0 3px var(--focus-ring)\" : \"none\",\n color: hot ? \"var(--action-primary)\" : \"var(--text-muted)\",\n cursor: interactive ? (tone === \"active\" ? \"grabbing\" : \"grab\") : \"default\",\n transition: \"var(--t-hover)\",\n };\n}\n\n/** Round output/input port — 22px. tone: \"default\" | \"active\" (dragging from it) | \"target\" (drop here). */\nexport function Port({ tone = \"default\", interactive = false, style, onMouseEnter, onMouseLeave, ...rest }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span {...rest}\n onMouseEnter={(e) => { setHover(true); onMouseEnter && onMouseEnter(e); }}\n onMouseLeave={(e) => { setHover(false); onMouseLeave && onMouseLeave(e); }}\n style={{ width: 22, height: 22, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", ...portSkin(tone, hover, interactive), ...style }}>\n <Icon name={tone === \"target\" ? \"plus\" : \"arrow-right\"} size={12} strokeWidth={2.2} />\n </span>\n );\n}\n\n/** Labeled input port pill (\"A >\", \"B >\") — 28×22, same three tones as Port. */\nexport function PortLabel({ label = \"A\", tone = \"default\", interactive = false, style, onMouseEnter, onMouseLeave, ...rest }) {\n const [hover, setHover] = React.useState(false);\n return (\n <span {...rest}\n onMouseEnter={(e) => { setHover(true); onMouseEnter && onMouseEnter(e); }}\n onMouseLeave={(e) => { setHover(false); onMouseLeave && onMouseLeave(e); }}\n style={{ width: 28, height: 22, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", gap: 2, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", letterSpacing: \"var(--tracking-xs)\", lineHeight: 1, ...portSkin(tone, hover, interactive), ...style }}>\n {tone === \"target\" ? <Icon name=\"plus\" size={12} strokeWidth={2.2} /> : <>{label}<Icon name=\"chevron-right\" size={9} strokeWidth={2.5} /></>}\n </span>\n );\n}\n\n/**\n * Canvas node card — 220×90, r16, soft shadow. Selected = blue border + blue-100 ring.\n * badge: \"error\" | \"warning\" | \"clock\" overlay on the icon tile.\n * Port interaction is driven by the canvas: onOutputDown starts a wire, onInputUp lands it,\n * and outputTone / inputTones light the handles.\n */\nexport function NodeCard({ title, subtitle, icon = \"table\", subtitleIcon, hue = \"source\", selected, badge, inputs = [], output = true, showMenu = true, onClick, onMenu, onOutputDown, onInputUp, onInputEnter, onInputLeave, outputTone = \"default\", inputTones = [], style }) {\n const live = !!(onOutputDown || onInputUp);\n return (\n <div onClick={onClick} style={{ position: \"relative\", width: 220, height: 90, borderRadius: \"var(--radius-node)\", background: \"var(--bg-surface)\", border: `1px solid ${selected ? \"var(--selected-border)\" : \"var(--border-control)\"}`, boxShadow: selected ? \"var(--shadow-selected)\" : \"var(--shadow-node)\", boxSizing: \"border-box\", cursor: \"pointer\", ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: 16, height: \"100%\", boxSizing: \"border-box\" }}>\n <span style={{ position: \"relative\", flexShrink: 0 }}>\n <IconTile icon={icon} hue={hue} kind=\"node\" />\n {badge === \"error\" ? <StatusBadge tone=\"danger\" icon=\"bug\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n {badge === \"warning\" ? <StatusBadge tone=\"warning\" icon=\"triangle-alert\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n {badge === \"clock\" ? <StatusBadge tone=\"pending\" icon=\"clock\" size={20} style={{ position: \"absolute\", top: -6, right: -6 }} /> : null}\n </span>\n <div style={{ display: \"flex\", flexDirection: \"column\", minWidth: 0, flex: 1, paddingRight: 18 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-semibold)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-heading)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{title}</span>\n <span style={{ display: \"flex\", alignItems: \"flex-start\", gap: 4, paddingTop: 4, minWidth: 0 }}>\n {subtitleIcon !== false ? <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", alignItems: \"center\", flexShrink: 0, height: \"var(--leading-xs)\", transform: \"translateY(-0.5px)\" }}><Icon name={subtitleIcon || \"info\"} size={12} /></span> : null}\n {/* Node types run long (\"Field Value Calculation\"), so the subtitle wraps to a\n second line and only then clamps — the card size never changes. */}\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", minWidth: 0, overflow: \"hidden\", display: \"-webkit-box\", WebkitBoxOrient: \"vertical\", WebkitLineClamp: 2, overflowWrap: \"break-word\" }}>{subtitle}</span>\n </span>\n </div>\n </div>\n {showMenu ? (\n <button type=\"button\" onClick={(e) => { e.stopPropagation(); onMenu && onMenu(e); }} style={{ position: \"absolute\", top: 10, right: 12, width: 23, height: 23, borderRadius: \"var(--radius-md)\", border: \"none\", background: \"transparent\", cursor: \"pointer\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--text-muted)\", padding: 0 }}\n onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-hover)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}>\n <Icon name=\"ellipsis\" size={15} />\n </button>\n ) : null}\n {inputs.length === 0 ? null : inputs.length === 1 && !inputs[0] ? (\n <Port tone={inputTones[0] || \"default\"} interactive={live}\n onMouseEnter={onInputEnter ? () => onInputEnter(0) : undefined} onMouseLeave={onInputLeave ? () => onInputLeave(0) : undefined}\n onPointerUp={onInputUp ? (e) => onInputUp(0, e) : undefined}\n style={{ position: \"absolute\", left: -11, top: 33 }} />\n ) : inputs.map((l, i) => (\n <PortLabel key={i} label={l} tone={inputTones[i] || \"default\"} interactive={live}\n onMouseEnter={onInputEnter ? () => onInputEnter(i) : undefined} onMouseLeave={onInputLeave ? () => onInputLeave(i) : undefined}\n onPointerUp={onInputUp ? (e) => onInputUp(i, e) : undefined}\n style={{ position: \"absolute\", left: -14, top: inputs.length === 1 ? 33 : 12 + i * 38 }} />\n ))}\n {output ? (\n <Port tone={outputTone} interactive={live}\n onPointerDown={onOutputDown ? (e) => { e.stopPropagation(); onOutputDown(e); } : undefined}\n onClick={onOutputDown ? (e) => e.stopPropagation() : undefined}\n style={{ position: \"absolute\", right: -11, top: 33 }} />\n ) : null}\n </div>\n );\n}\n","pipeline/NodeCard.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconTile from '../display/IconTile.vue'\nimport StatusBadge from '../display/StatusBadge.vue'\n/**\n * Canvas node card — 220×90, r16, soft shadow. Selected = blue border + blue-100 ring.\n * badge: \"error\" | \"warning\" | \"clock\" overlay on the icon tile.\n * Ports light up on hover, stay lit while a wire is dragged out (outputTone=\"active\"),\n * and show a crosshair when they are a valid drop target (inputTones[i]=\"target\").\n */\nconst props = defineProps({ title: String, subtitle: String, icon: { type: String, default: 'table' }, subtitleIcon: { type: [String, Boolean], default: 'info' }, hue: { type: String, default: 'source' }, selected: Boolean, badge: String, inputs: { type: Array, default: () => [] }, output: { type: Boolean, default: true }, showMenu: { type: Boolean, default: true }, live: Boolean, outputTone: { type: String, default: 'default' }, inputTones: { type: Array, default: () => [] }, style: Object })\nconst emit = defineEmits(['click', 'menu', 'outputDown', 'inputUp', 'inputEnter', 'inputLeave'])\nconst hoverOut = ref(false)\nconst hoverIn = ref(-1)\nfunction skin(tone, hover) {\n const hot = tone === 'active' || tone === 'target' || (props.live && hover)\n return {\n background: tone === 'target' ? 'var(--soft-bg)' : 'var(--bg-surface)',\n border: `2px solid ${hot ? 'var(--action-primary)' : 'var(--port-border)'}`,\n boxShadow: tone === 'active' || tone === 'target' ? '0 0 0 3px var(--focus-ring)' : 'none',\n color: hot ? 'var(--action-primary)' : 'var(--text-muted)',\n cursor: props.live ? (tone === 'active' ? 'grabbing' : 'grab') : 'default',\n transition: 'var(--t-hover)',\n }\n}\nconst portBase = { width: '22px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }\nconst pillBase = { width: '28px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '2px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', letterSpacing: 'var(--tracking-xs)', lineHeight: 1 }\nconst tone = i => props.inputTones[i] || 'default'\n</script>\n<template>\n <div @click=\"emit('click')\" :style=\"{ position: 'relative', width: '220px', height: '90px', borderRadius: 'var(--radius-node)', background: 'var(--bg-surface)', border: `1px solid ${selected ? 'var(--selected-border)' : 'var(--border-control)'}`, boxShadow: selected ? 'var(--shadow-selected)' : 'var(--shadow-node)', boxSizing: 'border-box', cursor: 'pointer', ...style }\">\n <div style=\"display:flex;align-items:center;gap:12px;padding:16px;height:100%;box-sizing:border-box\">\n <span style=\"position:relative;flex-shrink:0\">\n <IconTile :icon=\"icon\" :hue=\"hue\" kind=\"node\" />\n <StatusBadge v-if=\"badge === 'error'\" tone=\"danger\" icon=\"bug\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n <StatusBadge v-if=\"badge === 'warning'\" tone=\"warning\" icon=\"triangle-alert\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n <StatusBadge v-if=\"badge === 'clock'\" tone=\"pending\" icon=\"clock\" :size=\"20\" style=\"position:absolute;top:-6px;right:-6px\" />\n </span>\n <div style=\"display:flex;flex-direction:column;min-width:0;flex:1;padding-right:18px\">\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ title }}</span>\n <span style=\"display:flex;align-items:flex-start;gap:4px;padding-top:4px;min-width:0\">\n <span v-if=\"subtitleIcon !== false\" style=\"color:var(--text-muted);display:inline-flex;align-items:center;flex-shrink:0;height:var(--leading-xs);transform:translateY(-0.5px)\"><Icon :name=\"subtitleIcon || 'info'\" :size=\"12\" /></span>\n <!-- Node types run long (\"Field Value Calculation\"): wrap to a second line, then clamp. -->\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);min-width:0;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:break-word\">{{ subtitle }}</span>\n </span>\n </div>\n </div>\n <button v-if=\"showMenu\" type=\"button\" @click.stop=\"emit('menu', $event)\" class=\"kb-node-menu\" style=\"position:absolute;top:10px;right:12px;width:23px;height:23px;border-radius:var(--radius-md);border:none;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-muted);padding:0\">\n <Icon name=\"ellipsis\" :size=\"15\" />\n </button>\n <template v-if=\"inputs.length\">\n <span v-if=\"inputs.length === 1 && !inputs[0]\"\n @mouseenter=\"hoverIn = 0; emit('inputEnter', 0)\" @mouseleave=\"hoverIn = -1; emit('inputLeave', 0)\"\n @pointerup=\"emit('inputUp', 0, $event)\"\n :style=\"{ ...portBase, ...skin(tone(0), hoverIn === 0), position: 'absolute', left: '-11px', top: '33px' }\">\n <Icon :name=\"tone(0) === 'target' ? 'plus' : 'arrow-right'\" :size=\"12\" :stroke-width=\"2.2\" />\n </span>\n <span v-else v-for=\"(l, i) in inputs\" :key=\"i\"\n @mouseenter=\"hoverIn = i; emit('inputEnter', i)\" @mouseleave=\"hoverIn = -1; emit('inputLeave', i)\"\n @pointerup=\"emit('inputUp', i, $event)\"\n :style=\"{ ...pillBase, ...skin(tone(i), hoverIn === i), position: 'absolute', left: '-14px', top: (inputs.length === 1 ? 33 : 12 + i * 38) + 'px' }\">\n <Icon v-if=\"tone(i) === 'target'\" name=\"plus\" :size=\"12\" :stroke-width=\"2.2\" />\n <template v-else>{{ l }}<Icon name=\"chevron-right\" :size=\"9\" :stroke-width=\"2.5\" /></template>\n </span>\n </template>\n <span v-if=\"output\" @mouseenter=\"hoverOut = true\" @mouseleave=\"hoverOut = false\"\n @pointerdown.stop=\"emit('outputDown', $event)\" @click.stop\n :style=\"{ ...portBase, ...skin(outputTone, hoverOut), position: 'absolute', right: '-11px', top: '33px' }\">\n <Icon name=\"arrow-right\" :size=\"12\" :stroke-width=\"2.2\" />\n </span>\n </div>\n</template>\n<style scoped>\n.kb-node-menu:hover{background:var(--bg-hover)}\n</style>\n","pipeline/PaletteItem.vue":"<script setup>\nimport { ref } from 'vue'\nimport IconTile from '../display/IconTile.vue'\n// Palette grid tile — 40px tinted icon + 10px two-line label.\ndefineProps({ icon: String, hue: String, label: String, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div role=\"button\" tabindex=\"0\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: '67.664px', borderRadius: 'var(--radius-lg)', padding: '8px', display: 'flex', flexDirection: 'column', gap: '6px', alignItems: 'center', cursor: 'grab', boxSizing: 'border-box', background: hover ? 'var(--bg-hover)' : 'transparent', transition: 'background .12s ease', ...style }\">\n <IconTile :icon=\"icon\" :hue=\"hue\" kind=\"palette\" />\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-body);text-align:center\">{{ label }}</span>\n </div>\n</template>\n","pipeline/PaletteListItem.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Palette list row — 14px hued glyph + 13px label, indented under its category.\ndefineProps({ icon: String, hue: String, label: String, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\n</script>\n<template>\n <div role=\"button\" tabindex=\"0\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ height: 'var(--h-subnav-item)', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', boxSizing: 'border-box', cursor: 'grab', background: hover ? 'var(--bg-hover)' : 'transparent', ...style }\">\n <span :style=\"{ color: `var(--cat-${hue})`, display: 'inline-flex' }\"><Icon :name=\"icon\" :size=\"14\" /></span>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-body);white-space:nowrap\">{{ label }}</span>\n </div>\n</template>\n","pipeline/PalettePanel.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconTile } from \"../display/Badge.jsx\";\nimport { ViewToggle } from \"../tabs/ViewToggle.jsx\";\nimport { PIPELINE_CATALOG } from \"./pipeline-catalog.js\";\n\n/** Palette grid tile — 40px tinted icon + 10px two-line label. */\nexport function PaletteItem({ icon, hue, label, onClick, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div role=\"button\" tabIndex={0} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ width: 67.664, borderRadius: \"var(--radius-lg)\", padding: 8, display: \"flex\", flexDirection: \"column\", gap: 6, alignItems: \"center\", cursor: \"grab\", boxSizing: \"border-box\", background: hover ? \"var(--bg-hover)\" : \"transparent\", transition: \"background .12s ease\", ...style }}>\n <IconTile icon={icon} hue={hue} kind=\"palette\" />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-body)\", textAlign: \"center\" }}>{label}</span>\n </div>\n );\n}\n\n/** Palette list row — 14px hued glyph + 13px label, indented under its category. */\nexport function PaletteListItem({ icon, hue, label, onClick, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <div role=\"button\" tabIndex={0} onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: \"var(--h-subnav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"grab\", background: hover ? \"var(--bg-hover)\" : \"transparent\", ...style }}>\n <span style={{ color: `var(--cat-${hue})`, display: \"inline-flex\" }}><Icon name={icon} size={14} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-body)\", whiteSpace: \"nowrap\" }}>{label}</span>\n </div>\n );\n}\n\nexport { PIPELINE_CATALOG };\n\n/** 240px \"Components\" palette panel with list/grid toggle and collapsible hued categories. */\nexport function PalettePanel({ catalog = PIPELINE_CATALOG, height = \"100%\", onPick, style }) {\n const [view, setView] = React.useState(\"grid\");\n const [closed, setClosed] = React.useState([]);\n const toggle = (l) => setClosed(closed.includes(l) ? closed.filter((x) => x !== l) : [...closed, l]);\n return (\n <div style={{ width: \"var(--sidebar-w)\", height, background: \"var(--bg-surface)\", borderRight: \"1px solid var(--border-card)\", display: \"flex\", flexDirection: \"column\", boxSizing: \"border-box\", flexShrink: 0, ...style }}>\n <div style={{ height: 43, borderBottom: \"1px solid var(--border-subtle)\", display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"8px 12px\", boxSizing: \"border-box\", flexShrink: 0 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-body)\" }}>Components</span>\n <ViewToggle value={view} onChange={setView} />\n </div>\n <div style={{ flex: 1, overflowY: \"auto\", padding: 8 }}>\n {catalog.map((cat) => (\n <div key={cat.label}>\n <div onClick={() => toggle(cat.label)} data-wash=\"\" style={{ height: \"var(--h-subnav-item)\", borderRadius: \"var(--radius-md)\", display: \"flex\", alignItems: \"center\", gap: 8, padding: \"6px 8px\", boxSizing: \"border-box\", cursor: \"pointer\", background: \"var(--wash, transparent)\", transition: \"var(--t-hover)\" }}>\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\", transform: closed.includes(cat.label) ? \"rotate(-90deg)\" : \"none\", transition: \"transform .15s ease\" }}><Icon name=\"chevron-down\" size={14} /></span>\n <span style={{ color: `var(--cat-${cat.hue})`, display: \"inline-flex\" }}><Icon name={cat.icon} size={14} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: \"var(--text-strong)\" }}>{cat.label}</span>\n </div>\n {!closed.includes(cat.label) ? (\n view === \"grid\" ? (\n <div style={{ display: \"flex\", flexWrap: \"wrap\", padding: \"8px 0 8px 4px\" }}>\n {cat.items.map((it) => <PaletteItem key={it.label} icon={it.icon} hue={cat.hue} label={it.label} onClick={() => onPick && onPick(cat.label, it.label)} />)}\n </div>\n ) : (\n <div style={{ padding: \"2px 0 6px 20px\", display: \"flex\", flexDirection: \"column\" }}>\n {cat.items.map((it) => <PaletteListItem key={it.label} icon={it.icon} hue={cat.hue} label={it.label} onClick={() => onPick && onPick(cat.label, it.label)} />)}\n </div>\n )\n ) : null}\n </div>\n ))}\n </div>\n </div>\n );\n}\n","pipeline/PalettePanel.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { PIPELINE_CATALOG } from './pipeline-catalog.js'\nimport ViewToggle from '../tabs/ViewToggle.vue'\nimport PaletteItem from './PaletteItem.vue'\nimport PaletteListItem from './PaletteListItem.vue'\n\n// 240px \"Components\" palette panel with list/grid toggle and collapsible hued categories.\nconst props = defineProps({ catalog: { type: Array, default: () => PIPELINE_CATALOG }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['pick'])\nconst view = ref('grid')\nconst closed = ref([])\nfunction toggle(l) { closed.value = closed.value.includes(l) ? closed.value.filter(x => x !== l) : [...closed.value, l] }\n</script>\n<template>\n <div :style=\"{ width: 'var(--sidebar-w)', height, background: 'var(--bg-surface)', borderRight: '1px solid var(--border-card)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', flexShrink: 0, ...style }\">\n <div style=\"height:43px;border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;justify-content:space-between;padding:8px 12px;box-sizing:border-box;flex-shrink:0\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-body)\">Components</span>\n <ViewToggle v-model=\"view\" />\n </div>\n <div style=\"flex:1;overflow-y:auto;padding:8px\">\n <div v-for=\"cat in catalog\" :key=\"cat.label\">\n <div @click=\"toggle(cat.label)\" data-wash=\"\" style=\"height:var(--h-subnav-item);border-radius:var(--radius-md);display:flex;align-items:center;gap:8px;padding:6px 8px;box-sizing:border-box;cursor:pointer;background:var(--wash, transparent);transition:var(--t-hover)\">\n <span :style=\"{ color: 'var(--text-muted)', display: 'inline-flex', transform: closed.includes(cat.label) ? 'rotate(-90deg)' : 'none', transition: 'transform .15s ease' }\"><Icon name=\"chevron-down\" :size=\"14\" /></span>\n <span :style=\"{ color: `var(--cat-${cat.hue})`, display: 'inline-flex' }\"><Icon :name=\"cat.icon\" :size=\"14\" /></span>\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-strong)\">{{ cat.label }}</span>\n </div>\n <div v-if=\"!closed.includes(cat.label) && view === 'grid'\" style=\"display:flex;flex-wrap:wrap;padding:8px 0 8px 4px\">\n <PaletteItem v-for=\"it in cat.items\" :key=\"it.label\" :icon=\"it.icon\" :hue=\"cat.hue\" :label=\"it.label\" @click=\"emit('pick', cat.label, it.label)\" />\n </div>\n <div v-else-if=\"!closed.includes(cat.label)\" style=\"padding:2px 0 6px 20px;display:flex;flex-direction:column\">\n <PaletteListItem v-for=\"it in cat.items\" :key=\"it.label\" :icon=\"it.icon\" :hue=\"cat.hue\" :label=\"it.label\" @click=\"emit('pick', cat.label, it.label)\" />\n </div>\n </div>\n </div>\n </div>\n</template>\n","pipeline/PipelineCanvas.jsx":"import React from \"react\";\nimport { NodeCard } from \"./NodeCard.jsx\";\n\nconst NODE_W = 220;\nconst outPt = (n) => ({ x: n.x + NODE_W, y: n.y + 44 });\nconst inPt = (n, portIdx = 0) => {\n const multi = (n.inputs || []).filter(Boolean).length > 1;\n return { x: n.x, y: multi ? n.y + 23 + portIdx * 38 : n.y + 44 };\n};\n/** Horizontal cubic bezier; the control offset keeps a backward link bowed out of the cards. */\nconst curve = (p1, p2) => {\n const dx = Math.max(40, (p2.x - p1.x) / 2);\n return `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x} ${p2.y}`;\n};\n\n/**\n * Pipeline canvas: #fafafa surface, slate bezier edges, absolutely-placed NodeCards.\n * nodes: [{id,x,y,...NodeCardProps}]; edges: [{from,to,toPort}] (toPort index for A/B inputs).\n * With onConnect, holding an output port drags a dashed wire; input ports light up as drop\n * targets and the drop reports {from,to,toPort}. scale tells the canvas its zoom factor so\n * pointer coordinates stay in canvas space.\n */\nexport function PipelineCanvas({ nodes = [], edges = [], selectedId, onSelect, onMenu, onConnect, snapRadius = 30, scale = 1, width = \"100%\", height = \"100%\", style }) {\n const byId = Object.fromEntries(nodes.map((n) => [n.id, n]));\n const ref = React.useRef(null);\n const [drag, setDrag] = React.useState(null); // {from, x, y, over:{id,port}}\n\n /* The drop target is hit-tested from the pointer position rather than port hover:\n during a drag the cursor is often over the wire layer, not the 22px handle. */\n const hitTest = React.useCallback((x, y, fromId) => {\n let best = null;\n nodes.forEach((n) => {\n if (n.id === fromId) return;\n const ports = n.inputs || [];\n if (!ports.length) return;\n ports.forEach((_, i) => {\n const p = inPt(n, i);\n const d = Math.hypot(p.x - x, p.y - y);\n if (d <= snapRadius && (!best || d < best.d)) best = { id: n.id, port: i, d };\n });\n });\n return best ? { id: best.id, port: best.port } : null;\n }, [nodes, snapRadius]);\n\n React.useEffect(() => {\n if (!drag) return;\n const move = (e) => {\n const r = ref.current.getBoundingClientRect();\n const x = (e.clientX - r.left) / scale, y = (e.clientY - r.top) / scale;\n setDrag((d) => d && { ...d, x, y, over: hitTest(x, y, d.from) });\n };\n const up = () => setDrag((d) => {\n if (d && d.over && onConnect) onConnect({ from: d.from, to: d.over.id, toPort: d.over.port });\n return null;\n });\n window.addEventListener(\"pointermove\", move);\n window.addEventListener(\"pointerup\", up);\n return () => { window.removeEventListener(\"pointermove\", move); window.removeEventListener(\"pointerup\", up); };\n }, [drag, scale, onConnect, hitTest]);\n\n const start = (id, e) => {\n const r = ref.current.getBoundingClientRect();\n setDrag({ from: id, x: (e.clientX - r.left) / scale, y: (e.clientY - r.top) / scale, over: null });\n };\n\n const path = (e) => {\n const a = byId[e.from], b = byId[e.to];\n return a && b ? curve(outPt(a), inPt(b, e.toPort || 0)) : \"\";\n };\n const dragPath = () => {\n const a = byId[drag.from];\n if (!a) return \"\";\n const end = drag.over && byId[drag.over.id] ? inPt(byId[drag.over.id], drag.over.port) : { x: drag.x, y: drag.y };\n return curve(outPt(a), end);\n };\n\n return (\n <div ref={ref} style={{ position: \"relative\", width, height, background: \"var(--bg-app)\", overflow: \"hidden\", cursor: drag ? \"grabbing\" : \"default\", ...style }}>\n <svg style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", pointerEvents: \"none\", overflow: \"visible\" }}>\n {edges.map((e, i) => <path key={i} d={path(e)} fill=\"none\" stroke=\"var(--edge)\" strokeWidth=\"1.5\" />)}\n {drag ? <path d={dragPath()} fill=\"none\" stroke=\"var(--action-primary)\" strokeWidth=\"1.5\" strokeDasharray=\"5 4\" /> : null}\n </svg>\n {nodes.map(({ id, x, y, ...props }) => (\n <NodeCard key={id} {...props} selected={selectedId === id}\n onClick={() => !drag && onSelect && onSelect(id)} onMenu={(e) => onMenu && onMenu(id, e)}\n onOutputDown={onConnect ? (e) => start(id, e) : undefined}\n onInputUp={onConnect ? () => {} : undefined}\n outputTone={drag && drag.from === id ? \"active\" : \"default\"}\n inputTones={drag && drag.over && drag.over.id === id ? Object.assign([], { [drag.over.port]: \"target\" }) : []}\n style={{ position: \"absolute\", left: x, top: y }} />\n ))}\n </div>\n );\n}\n","pipeline/PipelineCanvas.vue":"<script setup>\nimport { computed, ref, onUnmounted } from 'vue'\nimport NodeCard from './NodeCard.vue'\n/**\n * Pipeline canvas: #fafafa surface, slate bezier edges, absolutely-placed NodeCards.\n * nodes: [{id,x,y,...NodeCardProps}]; edges: [{from,to,toPort}] (toPort index for A/B inputs).\n * With connectable, holding an output port drags a dashed wire; input ports light up as drop\n * targets and the drop emits connect {from,to,toPort}. scale tells the canvas its zoom factor.\n */\nconst props = defineProps({ nodes: { type: Array, default: () => [] }, edges: { type: Array, default: () => [] }, selectedId: [String, Number], connectable: Boolean, snapRadius: { type: Number, default: 30 }, scale: { type: Number, default: 1 }, width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: '100%' }, style: Object })\nconst emit = defineEmits(['select', 'menu', 'connect'])\nconst NODE_W = 220\nconst root = ref(null)\nconst drag = ref(null)\nconst byId = computed(() => Object.fromEntries(props.nodes.map(n => [n.id, n])))\nfunction outPt(n) { return { x: n.x + NODE_W, y: n.y + 44 } }\nfunction inPt(n, portIdx = 0) { const multi = (n.inputs || []).filter(Boolean).length > 1; return { x: n.x, y: multi ? n.y + 23 + portIdx * 38 : n.y + 44 } }\nfunction curve(p1, p2) { const dx = Math.max(40, (p2.x - p1.x) / 2); return `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x} ${p2.y}` }\nfunction path(e) { const a = byId.value[e.from], b = byId.value[e.to]; return a && b ? curve(outPt(a), inPt(b, e.toPort || 0)) : '' }\nconst dragPath = computed(() => {\n const d = drag.value; if (!d) return ''\n const a = byId.value[d.from]; if (!a) return ''\n const end = d.over && byId.value[d.over.id] ? inPt(byId.value[d.over.id], d.over.port) : { x: d.x, y: d.y }\n return curve(outPt(a), end)\n})\nfunction at(e) { const r = root.value.getBoundingClientRect(); return { x: (e.clientX - r.left) / props.scale, y: (e.clientY - r.top) / props.scale } }\n/* The drop target is hit-tested from the pointer position rather than port hover:\n during a drag the cursor is often over the wire layer, not the 22px handle. */\nfunction hitTest(x, y, fromId) {\n let best = null\n for (const n of props.nodes) {\n if (n.id === fromId) continue\n const ports = n.inputs || []\n for (let i = 0; i < ports.length; i++) {\n const p = inPt(n, i)\n const d = Math.hypot(p.x - x, p.y - y)\n if (d <= props.snapRadius && (!best || d < best.d)) best = { id: n.id, port: i, d }\n }\n }\n return best ? { id: best.id, port: best.port } : null\n}\nfunction move(e) {\n if (!drag.value) return\n const p = at(e)\n drag.value = { ...drag.value, ...p, over: hitTest(p.x, p.y, drag.value.from) }\n}\nfunction up() {\n const d = drag.value\n if (d && d.over) emit('connect', { from: d.from, to: d.over.id, toPort: d.over.port })\n drag.value = null\n window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up)\n}\nfunction start(id, e) {\n if (!props.connectable) return\n drag.value = { from: id, ...at(e), over: null }\n window.addEventListener('pointermove', move); window.addEventListener('pointerup', up)\n}\nconst inputTones = id => (drag.value && drag.value.over && drag.value.over.id === id ? Object.assign([], { [drag.value.over.port]: 'target' }) : [])\nonUnmounted(() => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) })\n</script>\n<template>\n <div ref=\"root\" :style=\"{ position: 'relative', width, height, background: 'var(--bg-app)', overflow: 'hidden', cursor: drag ? 'grabbing' : 'default', ...style }\">\n <svg style=\"position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible\">\n <path v-for=\"(e, i) in edges\" :key=\"i\" :d=\"path(e)\" fill=\"none\" stroke=\"var(--edge)\" stroke-width=\"1.5\" />\n <path v-if=\"drag\" :d=\"dragPath\" fill=\"none\" stroke=\"var(--action-primary)\" stroke-width=\"1.5\" stroke-dasharray=\"5 4\" />\n </svg>\n <NodeCard v-for=\"n in nodes\" :key=\"n.id\" v-bind=\"n\" :selected=\"selectedId === n.id\" :live=\"connectable\"\n :output-tone=\"drag && drag.from === n.id ? 'active' : 'default'\" :input-tones=\"inputTones(n.id)\"\n @click=\"!drag && emit('select', n.id)\" @menu=\"emit('menu', n.id)\"\n @output-down=\"start(n.id, $event)\"\n :style=\"{ position: 'absolute', left: n.x + 'px', top: n.y + 'px' }\" />\n </div>\n</template>\n","pipeline/ZoomControls.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Floating canvas zoom control (− / percentage / + / fit-to-view). */\nexport function ZoomControls({ value = 100, onChange, onFit, style }) {\n return (\n <div style={{ display: \"inline-flex\", alignItems: \"center\", gap: 2, background: \"var(--bg-surface)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 3, boxSizing: \"border-box\", ...style }}>\n <IconButton icon=\"minus\" size=\"sm\" onClick={() => onChange && onChange(Math.max(25, value - 10))} label=\"Zoom out\" />\n <span style={{ minWidth: 40, textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{value}%</span>\n <IconButton icon=\"plus\" size=\"sm\" onClick={() => onChange && onChange(Math.min(200, value + 10))} label=\"Zoom in\" />\n <span style={{ width: 1, alignSelf: \"stretch\", background: \"var(--border-subtle)\", margin: \"0 2px\" }} />\n <IconButton icon=\"target\" size=\"sm\" onClick={onFit} label=\"Fit to view\" />\n </div>\n );\n}\n","pipeline/ZoomControls.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Floating canvas zoom control (− / percentage / + / fit-to-view).\nconst props = defineProps({ modelValue: { type: Number, default: 100 }, style: Object })\nconst emit = defineEmits(['update:modelValue', 'fit'])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', alignItems: 'center', gap: '2px', background: 'var(--bg-surface)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-menu)', padding: '3px', boxSizing: 'border-box', ...style }\">\n <IconButton icon=\"minus\" size=\"sm\" label=\"Zoom out\" @click=\"emit('update:modelValue', Math.max(25, modelValue - 10))\" />\n <span style=\"min-width:40px;text-align:center;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ modelValue }}%</span>\n <IconButton icon=\"plus\" size=\"sm\" label=\"Zoom in\" @click=\"emit('update:modelValue', Math.min(200, modelValue + 10))\" />\n <span style=\"width:1px;align-self:stretch;background:var(--border-subtle);margin:0 2px\" />\n <IconButton icon=\"target\" size=\"sm\" label=\"Fit to view\" @click=\"emit('fit')\" />\n </div>\n</template>\n","pipeline/pipeline-catalog.js":"// The full component set of the pipeline editor, verbatim from the file. Shared by PalettePanel.vue; also re-exported by PalettePanel.jsx.\nexport const PIPELINE_CATALOG = [\n { label: 'Data Source', icon: 'table', hue: 'source', items: [{ label: 'Detail Sheet', icon: 'table' }, { label: 'Model', icon: 'layers' }, { label: 'Dimension', icon: 'grid-3x3' }] },\n { label: 'Data Join', icon: 'text-search', hue: 'join', items: [{ label: 'Lookup Data', icon: 'text-search' }, { label: 'Association Match', icon: 'shuffle' }] },\n { label: 'Data Transformation', icon: 'sliders-horizontal', hue: 'transform', items: [{ label: 'Field Value Calculation', icon: 'sliders-horizontal' }, { label: 'Field Split', icon: 'split' }, { label: 'Row to Column', icon: 'arrow-right-left' }, { label: 'Column to Row', icon: 'menu' }, { label: 'Multi-Condition Judge', icon: 'funnel' }, { label: 'Regional Calculation', icon: 'chart-no-axes-column' }, { label: 'Advanced Transformation', icon: 'trending-up' }, { label: 'JS Enhanced Processing', icon: 'code' }] },\n { label: 'Data Merge', icon: 'database', hue: 'merge', items: [{ label: 'Group Calculation', icon: 'database' }, { label: 'Concat Merge', icon: 'merge' }] },\n { label: 'Data Output', icon: 'table', hue: 'output', items: [{ label: 'Detail Sheet', icon: 'table' }, { label: 'Model', icon: 'layers' }, { label: 'Dimension', icon: 'grid-3x3' }] },\n]\n","pipeline/Port.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { portSkin } from './port-skin.js'\n// Round output/input port — 22px. tone: \"default\" | \"active\" (dragging from it) | \"target\" (drop here).\n// interactive lights it on hover and shows a grab cursor. Native listeners (@mousedown, @mouseup…) fall through to the span.\ndefineProps({ tone: { type: String, default: 'default' }, interactive: Boolean, style: Object })\nconst hover = ref(false)\n</script>\n<template>\n <span @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: '22px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', ...portSkin(tone, hover, interactive), ...style }\">\n <Icon :name=\"tone === 'target' ? 'plus' : 'arrow-right'\" :size=\"12\" :stroke-width=\"2.2\" />\n </span>\n</template>\n","pipeline/PortLabel.vue":"<script setup>\nimport { ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { portSkin } from './port-skin.js'\n// Labeled input port pill (\"A >\", \"B >\") — 28×22, same three tones as Port.\ndefineProps({ label: { type: String, default: 'A' }, tone: { type: String, default: 'default' }, interactive: Boolean, style: Object })\nconst hover = ref(false)\n</script>\n<template>\n <span @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n :style=\"{ width: '28px', height: '22px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '2px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', letterSpacing: 'var(--tracking-xs)', lineHeight: 1, ...portSkin(tone, hover, interactive), ...style }\">\n <Icon v-if=\"tone === 'target'\" name=\"plus\" :size=\"12\" :stroke-width=\"2.2\" />\n <template v-else>{{ label }}<Icon name=\"chevron-right\" :size=\"9\" :stroke-width=\"2.5\" /></template>\n </span>\n</template>\n","pipeline/port-skin.js":"// Shared port skin for Port.vue / PortLabel.vue / NodeCard.vue — mirrors portSkin() in NodeCard.jsx.\n// tone: \"default\" | \"active\" (a wire is being dragged from it) | \"target\" (valid drop here).\nexport function portSkin(tone, hover, interactive) {\n const hot = tone === 'active' || tone === 'target' || (interactive && hover)\n return {\n background: tone === 'target' ? 'var(--soft-bg)' : 'var(--bg-surface)',\n border: `2px solid ${hot ? 'var(--action-primary)' : 'var(--port-border)'}`,\n boxShadow: tone === 'active' || tone === 'target' ? '0 0 0 3px var(--focus-ring)' : 'none',\n color: hot ? 'var(--action-primary)' : 'var(--text-muted)',\n cursor: interactive ? (tone === 'active' ? 'grabbing' : 'grab') : 'default',\n transition: 'var(--t-hover)'\n }\n}\n"});