app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
Publish 1.0.50
10 files changed
+250−54
dashboard/kicad_dashboard.py+18−1dashboard/ui/assets/icons/settings-cog.svg+1dashboard/ui/assets/shots/taskbar-both.pngdashboard/ui/assets/shots/taskbar-flash.pngdashboard/ui/assets/shots/taskbar-none.pngdashboard/ui/assets/shots/taskbar-overlay.pngdashboard/ui/assets/shots/taskbar-progress.pngdashboard/ui/index.html+216−50package.json+8−2page.json+7−1dashboard/kicad_dashboard.py+18−1@@ -49,7 +49,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse -VERSION = "2.0.0"+VERSION = "2.1.0" AI_THREAD = "kicad-dashboard" DEFAULT_PORT = 8931 UI_DIR = Path(__file__).resolve().parent / "ui"@@ -893,6 +893,8 @@ def do_action(target, name, args): if mode not in ("progress", "flash", "both", "none"): return {"success": False, "error": f"unknown mode {mode}"} return ab(target, "kicad_set_settings", {"activityIndicator": mode}, timeout=60)+ if name == "restore_settings":+ return ab(target, "kicad_set_settings", {"restoreDefaults": True}, timeout=60) if name == "part_tour": return part_tour(target, args) if name == "surface":@@ -951,6 +953,21 @@ class Handler(BaseHTTPRequestHandler): if path == "/health": return self._json({"ok": True, "app": "kicad-dashboard", "version": VERSION, "sseClients": HUB.count()})+ am = re.search(r"/assets/(.+)$", path)+ if am:+ # Settings screenshots + the shared Adom cog. Real captures only+ # (settings-layout skill); anything else under here 404s. Matched+ # anywhere in the path because the adom.cloud proxy mounts the+ # app under a sub-path.+ rel = am.group(1)+ f = (UI_DIR / "assets" / rel).resolve()+ if not str(f).startswith(str((UI_DIR / "assets").resolve())) or not f.is_file():+ return self._json({"error": "no such asset"}, 404)+ mime = {"svg": "image/svg+xml", "png": "image/png", "jpg": "image/jpeg",+ "webp": "image/webp"}.get(f.suffix.lstrip(".").lower(),+ "application/octet-stream")+ return self._send(200, mime, f.read_bytes(),+ {"Cache-Control": "no-cache"}) if path == "/console": with _console_lock: return self._json({"lines": list(CONSOLE)})
dashboard/ui/assets/icons/settings-cog.svgadded+1@@ -0,0 +1 @@+<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/></svg>
dashboard/ui/assets/shots/taskbar-both.pngadded⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-flash.pngadded⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-none.pngadded⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-overlay.pngadded⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-progress.pngadded⋯ 1 unchanged line ⋯
dashboard/ui/index.html+216−50@@ -9,7 +9,8 @@ :root{ --bg:#0d1117;--surface:#161b22;--elevated:#1c2128;--overlay:#21262d;--border:#30363d; --text:#e6edf3;--text-body:#c9d1d9;--text-2:#8b949e;- --accent:#00b8b1;--accent-hover:#00d4cb;--green:#3fb950;--red:#f85149;--yellow:#d29922;--blue:#64abff;+ --accent:#00b8b1;--accent-hover:#00d4cb;--on-accent:#05221f;--green:#3fb950;--red:#f85149;--yellow:#d29922;--blue:#64abff;+ --brand-heading:'Familjen Grotesk',sans-serif;--brand-body:'Satoshi',sans-serif; } *{box-sizing:border-box;margin:0;padding:0} @font-face{font-family:'Familjen Grotesk';src:url('https://adom.inc/fonts/FamiljenGrotesk-SemiBold.woff2') format('woff2');font-weight:600}@@ -83,16 +84,67 @@ input:focus-visible{outline:none;border-color:var(--accent) !important} #toasts{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);display:flex;flex-direction:column-reverse;gap:8px;z-index:99999} .toast{background:var(--overlay);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:8px;padding:10px 14px;color:var(--text);font-size:14px;box-shadow:0 8px 24px rgba(0,0,0,.4)} .toast.ok{border-left-color:var(--green)}.toast.err{border-left-color:var(--red)}-dialog{margin:auto;background:var(--overlay);color:var(--text-body);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:520px;width:92vw}-dialog::backdrop{background:rgba(0,0,0,.55)}-.setrow{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:12px 0;border-bottom:1px solid var(--border)}-.toggle{width:42px;height:24px;border-radius:999px;background:var(--elevated);border:1px solid var(--border);position:relative;cursor:pointer;flex:none;transition:border-color .15s ease}-.toggle:hover{border-color:var(--accent)}-.toggle:active::after{width:22px}-.toggle.is-on:active::after{left:17px}-.toggle::after{content:'';position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:999px;background:var(--text-2);transition:all .15s ease}-.toggle.is-on{background:var(--accent);border-color:var(--accent)}-.toggle.is-on::after{left:21px;background:#05221f}+dialog{margin:auto;background:var(--surface);color:var(--text-body);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:520px;width:92vw}+/* The backdrop law (settings-layout skill): dim 20-30%, NEVER blur, one value+ for every dialog in the app - settings often change the very thing behind+ the modal and the user must be able to watch it change. */+dialog::backdrop{background:rgba(0,0,0,.25)}++/* ---- Settings modal: the Adom settings surface (settings-layout skill) ---- */+dialog.settings{width:min(560px,92vw);max-width:none;padding:0;overflow:hidden;border-radius:12px;background:var(--surface)}+dialog.settings:focus{outline:none}+.settings-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px 12px 18px;border-bottom:1px solid var(--border);cursor:move;user-select:none}+.settings-head h2{margin:0;font:600 15px/1.3 var(--brand-heading);color:var(--text);display:flex;align-items:center;gap:10px}+.settings-head .card-status{font-family:var(--brand-body)}+.settings-body{max-height:70vh;overflow:auto;padding:18px;scrollbar-width:thin;scrollbar-color:rgba(139,148,158,.35) transparent}+.settings-body::-webkit-scrollbar{width:8px}+.settings-body::-webkit-scrollbar-track{background:transparent}+.settings-body::-webkit-scrollbar-thumb{background:rgba(139,148,158,.35);border-radius:999px;border:2px solid transparent;background-clip:padding-box}+.settings-body::-webkit-scrollbar-thumb:hover{background:var(--accent);background-clip:padding-box}+.settings-card{background:var(--elevated);border:1px solid var(--border);border-left:3px solid color-mix(in srgb,var(--accent) 45%,transparent);border-radius:10px;padding:.9rem 1.1rem 1rem;margin-bottom:1rem}+.card-title{font:600 13px/1.3 var(--brand-heading);color:var(--text);display:flex;align-items:center;gap:10px;margin:0 0 .45rem}+.card-status{font-size:11px;color:var(--text-2);font-weight:400;font-family:var(--brand-body)}+.card-status.ok{color:var(--green)}+.card-desc{font-family:var(--brand-body);font-size:12px;line-height:1.55;color:var(--text-2);max-width:60ch;margin:0 0 .55rem}+.card-desc code{font:11px 'JetBrains Mono',monospace;background:rgba(110,118,129,.2);padding:1px 4px;border-radius:4px}+.divider{height:1px;background:var(--border);margin:.55rem 0}+.wrapper{padding:.5rem 0;display:flex;flex-direction:column;gap:.25rem}+.name{margin:.125rem 0;font-family:var(--brand-heading);font-size:.9375rem;font-weight:600}+.prefix{color:var(--text)}+.title{color:#8fd3d4}+.default-hint{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2)}+.setting-shot{margin:0 0 .75rem}+.setting-shot img{display:block;max-width:100%;height:auto;border:1px solid var(--border);border-radius:8px;background:var(--surface)}+.setting-shot figcaption{margin-top:.35rem;font-family:var(--brand-body);font-size:.75rem;color:var(--text-2)}+.choice-set{border:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:.75rem}+.choice{flex:1 1 200px;display:flex;flex-direction:column;gap:.35rem;padding:.6rem;cursor:pointer;position:relative;background:var(--surface);border:1px solid var(--border);border-radius:8px;transition:border-color .15s ease,background .15s ease,transform .06s ease}+.choice input{position:absolute;opacity:0;pointer-events:none}+.choice img{display:block;width:100%;max-width:168px;height:auto;border-radius:4px;background:var(--bg);image-rendering:auto}+.choice-name{font-family:var(--brand-heading);font-size:.875rem;font-weight:600;color:var(--text)}+.choice-desc{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2);line-height:1.5}+.choice:hover{border-color:var(--accent);background:var(--elevated)}+.choice:active{transform:translateY(1px) scale(.995)}+.choice[data-selected='true']{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}+.choice:focus-within{outline:2px solid var(--accent);outline-offset:2px}+.setting-row{display:flex;align-items:center;gap:10px}+.set-check-label{font-family:var(--brand-body);font-size:.875rem;color:var(--text-body)}+.toggle{background:none;border:none;padding:2px;cursor:pointer;flex-shrink:0;transition:transform .06s ease}+.toggle-track{display:block;width:32px;height:18px;background:var(--overlay);border:1px solid var(--border);border-radius:9px;position:relative;transition:background .15s ease,border-color .15s ease}+.toggle:hover .toggle-track{border-color:var(--accent)}+.toggle:active{transform:translateY(1px) scale(.97)}+.toggle.active .toggle-track{background:var(--accent);border-color:var(--accent)}+.toggle-thumb{position:absolute;top:2px;left:2px;width:12px;height:12px;background:#fff;border-radius:50%;transition:transform .18s ease,width .12s ease}+.toggle.active .toggle-thumb{transform:translateX(14px)}+.toggle:active .toggle-thumb{width:16px}+.toggle.active:active .toggle-thumb{transform:translateX(10px)}+.toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:10px}+.settings-footer{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:.5rem}+.footer-note{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2);margin:0;max-width:60ch}+.restore-btn{width:fit-content;display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .85rem;background:transparent;border:1px solid var(--border);border-radius:.375rem;color:var(--text-body);font-family:var(--brand-heading);font-size:.8125rem;font-weight:600;cursor:pointer;transition:background .15s ease,border-color .15s ease,color .15s ease,transform .06s ease}+.restore-btn:hover{background:var(--elevated)}+.restore-btn:active{transform:translateY(1px) scale(.98)}+.restore-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}+.restore-btn.armed{color:var(--red);border-color:rgba(255,107,107,.5);background:rgba(255,107,107,.08)} @media (max-width:720px){.meta{display:none}.grid{grid-template-columns:1fr 1fr}} @media (max-width:480px){.grid{grid-template-columns:1fr}.brand span{display:none}.subject{display:none}.vsep{display:none}header{gap:8px}} #tt{position:fixed;z-index:99999;background:var(--overlay);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:12.5px;color:var(--text-body);max-width:260px;pointer-events:none;opacity:0;transform:translateY(3px);transition:opacity .18s ease,transform .18s ease}@@ -123,7 +175,7 @@ main a:hover{border-bottom-color:var(--accent)} <div class="subject" id="subject">connecting...</div> <div class="meta" id="meta"></div> <div class="actions">- <button class="iconbtn" id="btnSettings" data-tip="Settings: bridge defaults like the Adom overlay badge on KiCad taskbar buttons (sticky per box)" aria-label="Settings"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.51 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.26.6.86 1 1.51 1H21a2 2 0 1 1 0 4h-.09c-.65 0-1.25.4-1.51 1z"/></svg></button>+ <button class="iconbtn" id="btnSettings" title="Settings" aria-label="Settings"><!-- the shared Adom cog (mdi:cog), inlined so it follows currentColor; file copy in assets/icons/settings-cog.svg --><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/></svg></button> <button class="iconbtn" id="btnRefresh" data-tip="Re-read every facet's status from the bridge right now (also auto-refreshes every 15s)" aria-label="Refresh"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6"/></svg></button> </div> </header>@@ -171,28 +223,72 @@ main a:hover{border-bottom-color:var(--accent)} <div id="prompts"></div> </main> -<dialog id="dlgSettings">- <h2 style="margin:0 0 8px">Settings</h2>- <div class="setrow">- <div>- <div class="led-label">Activity indicator</div>- <div class="note">How the KiCad taskbar button shows "Adom is driving": a progress bar from the measured verb timings (ends by itself), the OS orange flash (nags until clicked), both, or none.</div>- </div>- <div id="segIndicator" style="display:flex;gap:6px">- <button class="btn" data-mode="progress">progress</button>- <button class="btn" data-mode="flash">flash</button>- <button class="btn" data-mode="both">both</button>- <button class="btn" data-mode="none">none</button>- </div>+<dialog id="dlgSettings" class="settings" tabindex="-1" aria-labelledby="settingsTitle">+ <div class="settings-head" id="settingsHead">+ <h2 id="settingsTitle">KiCad Bridge Settings <span class="card-status" id="settingsBox"></span></h2>+ <button class="iconbtn" id="btnCloseSettings" aria-label="Close settings" title="Close"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button> </div>- <div class="setrow">- <div>- <div class="led-label">Adom overlay badges</div>- <div class="led-sub">Stamp the Adom favicon on every bridge-driven KiCad taskbar button, so you can see at a glance which windows Adom is controlling. Never applied to KiCad windows you launched yourself.</div>- </div>- <div class="toggle" id="tglOverlay" role="switch" aria-checked="true" tabindex="0"></div>+ <div class="settings-body" id="settingsBody">++ <section class="settings-card" id="card-activity">+ <h3 class="card-title">Taskbar activity <span class="card-status" id="stActivity"></span></h3>+ <p class="card-desc">While a verb is driving KiCad, the bridge marks the KiCad taskbar button so you can tell the window is busy and who is busy in it. The progress bar is painted through the Windows taskbar API (<code>ITaskbarList3</code>) and sized from this box's own measured verb timings: it fills over the time that verb usually takes here, creeps past the end when a run is overdue, and shows an indeterminate marquee when nothing has been measured yet. The flash is Windows' own attention highlight. The bridge never triggers it, it only decides whether to cancel it while a bar is showing.</p>+ <p class="card-desc">The difference is what each one asks of you. A flash stays lit until you click the button, so it reads as something you have to deal with. A bar removes itself the moment Adom stops driving, so when the bar and the overlay badge are both gone, the window is yours again. Read-only verbs such as status checks never mark activity in any mode.</p>+ <div class="wrapper">+ <h4 class="name" title="activityIndicator"><span class="prefix">Taskbar: </span><span class="title">Activity indicator</span></h4>+ <fieldset class="choice-set" role="radiogroup" aria-labelledby="activityLabel">+ <legend id="activityLabel" class="set-check-label" style="padding:0;margin-bottom:.35rem">What the KiCad taskbar button does while Adom is driving it. Each picture is a capture of this machine's taskbar.</legend>+ <label class="choice" data-mode="progress" data-selected="false">+ <input type="radio" name="activityIndicator" value="progress">+ <img src="assets/shots/taskbar-progress.png" width="168" height="56" loading="lazy" alt="The KiCad taskbar button with a blue progress bar about two thirds full under its icon, neighbours untouched">+ <span class="choice-name">Progress bar</span>+ <span class="choice-desc">Fills over the verb's usual duration on this box, then clears itself. Cancels the OS flash.</span>+ </label>+ <label class="choice" data-mode="flash" data-selected="false">+ <input type="radio" name="activityIndicator" value="flash">+ <img src="assets/shots/taskbar-flash.png" width="168" height="56" loading="lazy" alt="The same KiCad taskbar button tinted salmon with a highlighted underline, no progress bar">+ <span class="choice-name">Attention flash</span>+ <span class="choice-desc">Windows' own highlight, left alone. Says something happened, not how long is left, and stays until you click it.</span>+ </label>+ <label class="choice" data-mode="both" data-selected="false">+ <input type="radio" name="activityIndicator" value="both">+ <img src="assets/shots/taskbar-both.png" width="168" height="56" loading="lazy" alt="The same KiCad taskbar button tinted salmon and carrying the blue progress bar at the same time">+ <span class="choice-name">Bar and flash</span>+ <span class="choice-desc">Both at once. The bar still clears itself; the tint stays until you click.</span>+ </label>+ <label class="choice" data-mode="none" data-selected="false">+ <input type="radio" name="activityIndicator" value="none">+ <img src="assets/shots/taskbar-none.png" width="168" height="56" loading="lazy" alt="The same KiCad taskbar button with no bar and no highlight, only the running dot">+ <span class="choice-name">Nothing</span>+ <span class="choice-desc">The button never changes. You rely on the overlay badge below, or on nothing.</span>+ </label>+ </fieldset>+ <span class="default-hint">Default: Progress bar</span>+ </div>+ </section>++ <section class="settings-card" id="card-overlay">+ <h3 class="card-title">Overlay badge <span class="card-status" id="stOverlay"></span></h3>+ <p class="card-desc">The Adom mark is stamped in the corner of a KiCad window's taskbar button while a verb is running against that window, and comes off by itself five minutes after the last verb, so a badge always means "Adom is driving this right now". It is a per-window overlay through the same taskbar API, so it never changes the button's own icon or how Windows groups it, and it is never stamped by read-only verbs like status checks. A bridge restart clears every badge, because a freshly started bridge is driving nothing.</p>+ <figure class="setting-shot">+ <img src="assets/shots/taskbar-overlay.png" width="168" height="56" loading="lazy" alt="The KiCad taskbar button with the small teal Adom mark in its top right corner">+ <figcaption>The badge on the KiCad PCB Editor button, captured on this machine while a verb was driving it.</figcaption>+ </figure>+ <div class="wrapper">+ <h4 class="name" title="overlayBadges"><span class="prefix">Taskbar: </span><span class="title">Adom overlay badge</span></h4>+ <div class="setting-row">+ <button class="toggle" id="tglOverlay" role="switch" aria-checked="true" aria-label="Show the Adom overlay badge while driving"><span class="toggle-track"><span class="toggle-thumb"></span></span></button>+ <span class="set-check-label">Show the badge while Adom is driving</span>+ </div>+ <span class="default-hint">Default: On</span>+ </div>+ </section>++ <footer class="settings-footer">+ <p class="footer-note">Settings are stored on the selected box in <code>%USERPROFILE%\.adom\kicad-bridge-settings.json</code> and apply immediately. Restoring returns every option above to its default.</p>+ <button class="restore-btn" id="btnRestore" type="button"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path id="restoreIcon" fill="currentColor" d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18m-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8z"/></svg><span id="restoreLabel">Restore defaults</span></button>+ </footer> </div>- <div class="btnrow" style="justify-content:flex-end"><button class="btn" id="btnCloseSettings">Done</button></div> </dialog> <dialog id="dlgSetup">@@ -320,25 +416,95 @@ $('#btnPartTour').onclick=async()=>{ else{toast('part installed and project generated','ok');$('#partLabel').textContent=(d.steps||[]).join(' · ');refresh(true);} }; -// Settings-$('#btnSettings').onclick=()=>{const s=(lastStatus&&lastStatus.settings)||{};- const t=$('#tglOverlay');const on=s.overlayBadges!==false;t.classList.toggle('is-on',on);t.setAttribute('aria-checked',String(on));- const mode=s.activityIndicator||'progress';- document.querySelectorAll('#segIndicator .btn').forEach(b=>{b.classList.toggle('primary',b.dataset.mode===mode);});- $('#dlgSettings').showModal();};-$('#btnCloseSettings').onclick=()=>$('#dlgSettings').close();-document.querySelectorAll('#segIndicator .btn').forEach(b=>{b.onclick=async()=>{- const r=await act('set_activity_indicator',{mode:b.dataset.mode});- // fusion's bug: a 200 with an error field toasted success. Check the KEY.- if(r&&!r.error){document.querySelectorAll('#segIndicator .btn').forEach(x=>x.classList.toggle('primary',x===b));toast('activity indicator: '+b.dataset.mode,'ok');}- else toast('failed: '+((r&&r.error)||'no response'),'err');-};});-$('#tglOverlay').onclick=async(e)=>{- const t=e.currentTarget;const now=!t.classList.contains('is-on');- t.classList.toggle('is-on',now);t.setAttribute('aria-checked',String(now));- await act('set_overlay_badges',{overlayBadges:now});- toast('overlay badges '+(now?'on':'off')+' (sticky)','ok');+// ---- Settings (settings-layout skill: modal, four dismissals, optimistic saves) ----+const SETTINGS_DEFAULTS={activityIndicator:'progress',overlayBadges:true};+const MODE_LABEL={progress:'Progress bar',flash:'Attention flash',both:'Bar and flash',none:'Nothing'};+const dlg=$('#dlgSettings');+// Local mirror: the box's settings file is the truth, but the page reflects a+// click before the round trip and survives a stalled one (skill: local-first).+let settingsView=Object.assign({},SETTINGS_DEFAULTS,JSON.parse(localStorage.getItem('kdSettings')||'{}'));+function rememberSettings(){localStorage.setItem('kdSettings',JSON.stringify(settingsView));}+function renderSettings(){+ const mode=MODE_LABEL[settingsView.activityIndicator]?settingsView.activityIndicator:'progress';+ document.querySelectorAll('#dlgSettings .choice').forEach(c=>{+ const sel=c.dataset.mode===mode;c.dataset.selected=String(sel);c.querySelector('input').checked=sel;});+ $('#stActivity').textContent=MODE_LABEL[mode];$('#stActivity').className='card-status'+(mode!=='none'?' ok':'');+ const on=settingsView.overlayBadges!==false;+ const t=$('#tglOverlay');t.classList.toggle('active',on);t.setAttribute('aria-checked',String(on));+ $('#stOverlay').textContent=on?'on':'off';$('#stOverlay').className='card-status'+(on?' ok':'');+ $('#settingsBox').textContent=curTarget?('on '+curTarget):'';+}+// Bounded, optimistic save: the DOM already shows the click; a failed or+// stalled round trip toasts and reverts to what the box last reported.+async function saveSetting(action,args,revert){+ const ctrl=new AbortController();const timer=setTimeout(()=>ctrl.abort(),5000);+ try{+ const r=await fetch('api/action',{method:'POST',headers:{'Content-Type':'application/json'},+ body:JSON.stringify({action,target:curTarget||undefined,args}),signal:ctrl.signal}).then(x=>x.json());+ const d=(r&&r.data)||r||{};+ if(d.error||d.success===false)throw new Error(d.error||'the bridge refused the change');+ if(d.settings){Object.assign(settingsView,d.settings);rememberSettings();renderSettings();}+ return true;+ }catch(e){+ toast('setting not saved: '+(e.name==='AbortError'?'the box did not answer within 5s':e.message),'err');+ if(revert){Object.assign(settingsView,revert);rememberSettings();renderSettings();}+ return false;+ }finally{clearTimeout(timer);}+}+function openSettings(){+ if(lastStatus&&lastStatus.settings&&Object.keys(lastStatus.settings).length){Object.assign(settingsView,lastStatus.settings);rememberSettings();}+ renderSettings();+ dlg.style.margin='';dlg.style.left='';dlg.style.top='';+ dlg.showModal();+ // Open at the TOP: showModal autofocuses the first focusable child, which+ // scrolls to it. Focus the dialog itself and reset scroll, again next frame.+ dlg.focus();const body=$('#settingsBody');body.scrollTop=0;requestAnimationFrame(()=>{body.scrollTop=0;});+}+$('#btnSettings').onclick=openSettings;+$('#btnCloseSettings').onclick=()=>dlg.close();+// Click OUTSIDE closes: the backdrop is part of the dialog element, so gate on+// the pointer being outside the dialog's own box, or padding clicks close it.+dlg.addEventListener('click',e=>{+ if(e.target!==dlg)return;+ const r=dlg.getBoundingClientRect();+ const inside=e.clientX>=r.left&&e.clientX<=r.right&&e.clientY>=r.top&&e.clientY<=r.bottom;+ if(!inside)dlg.close();+});+// Draggable by its header, clamped to the viewport.+(()=>{const head=$('#settingsHead');let drag=null;+ head.addEventListener('pointerdown',e=>{if(e.target.closest('button'))return;+ const r=dlg.getBoundingClientRect();drag={dx:e.clientX-r.left,dy:e.clientY-r.top};head.setPointerCapture(e.pointerId);});+ head.addEventListener('pointermove',e=>{if(!drag)return;+ const r=dlg.getBoundingClientRect();+ const x=Math.max(0,Math.min(e.clientX-drag.dx,innerWidth-r.width));+ const y=Math.max(0,Math.min(e.clientY-drag.dy,innerHeight-r.height));+ dlg.style.margin='0';dlg.style.left=x+'px';dlg.style.top=y+'px';});+ head.addEventListener('pointerup',()=>{drag=null;});head.addEventListener('pointercancel',()=>{drag=null;});+})();+document.querySelectorAll('#dlgSettings .choice input').forEach(inp=>{inp.addEventListener('change',()=>{+ const prev={activityIndicator:settingsView.activityIndicator};+ settingsView.activityIndicator=inp.value;rememberSettings();renderSettings();+ saveSetting('set_activity_indicator',{mode:inp.value},prev).then(ok=>{if(ok)toast('taskbar activity: '+MODE_LABEL[inp.value],'ok');});+});});+$('#tglOverlay').onclick=()=>{+ const prev={overlayBadges:settingsView.overlayBadges!==false};+ const now=!prev.overlayBadges;+ settingsView.overlayBadges=now;rememberSettings();renderSettings();+ saveSetting('set_overlay_badges',{overlayBadges:now},prev).then(ok=>{if(ok)toast('overlay badge '+(now?'on':'off'),'ok');}); };+// Two-click Restore defaults, auto-disarming after 4s. The button IS the confirmation.+(()=>{const b=$('#btnRestore');let armed=false,timer=null;+ const RESTORE='M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18m-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8z';+ const ALERT='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8';+ function disarm(){armed=false;b.classList.remove('armed');$('#restoreLabel').textContent='Restore defaults';$('#restoreIcon').setAttribute('d',RESTORE);clearTimeout(timer);}+ b.onclick=()=>{+ if(!armed){armed=true;b.classList.add('armed');$('#restoreLabel').textContent='Click again to confirm';$('#restoreIcon').setAttribute('d',ALERT);timer=setTimeout(disarm,4000);return;}+ disarm();+ const prev=Object.assign({},settingsView);+ Object.assign(settingsView,SETTINGS_DEFAULTS);rememberSettings();renderSettings();+ saveSetting('restore_settings',{},prev).then(ok=>{if(ok)toast('settings restored to defaults','ok');});+ };+})(); // Setup: walk the not-green facets in order, one action per click. const SETUP_ORDER=[
package.json+8−2@@ -1,6 +1,6 @@ { "slug": "kicad-bridge",- "version": "1.0.49",+ "version": "1.0.50", "type": "app", "description": "Skills for your container so your AI knows how to drive the KiCad bridge. The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [@@ -40,6 +40,12 @@ "dashboard/kicad_dashboard.py", "dashboard/ui/index.html", "dashboard/ui/kicad-logo.png",+ "dashboard/ui/assets/shots/taskbar-overlay.png",+ "dashboard/ui/assets/shots/taskbar-both.png",+ "dashboard/ui/assets/shots/taskbar-flash.png",+ "dashboard/ui/assets/shots/taskbar-progress.png",+ "dashboard/ui/assets/shots/taskbar-none.png",+ "dashboard/ui/assets/icons/settings-cog.svg", "kicad-bridge-hero-v6.png", "docs/img/dock-kicad.svg" ],@@ -77,4 +83,4 @@ "prompt": "Open the KiCad dashboard and tell me if anything is red" } ]-}+}
page.json+7−1@@ -4,7 +4,7 @@ "slug": "kicad-bridge", "title": "KiCad - the KiCad Bridge", "brief": "Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",- "version": "1.0.49",+ "version": "1.0.50", "tags": [ "kicad", "pcb",@@ -99,6 +99,12 @@ "dashboard/kicad_dashboard.py", "dashboard/ui/index.html", "dashboard/ui/kicad-logo.png",+ "dashboard/ui/assets/shots/taskbar-overlay.png",+ "dashboard/ui/assets/shots/taskbar-both.png",+ "dashboard/ui/assets/shots/taskbar-flash.png",+ "dashboard/ui/assets/shots/taskbar-progress.png",+ "dashboard/ui/assets/shots/taskbar-none.png",+ "dashboard/ui/assets/icons/settings-cog.svg", "kicad-bridge-hero-v6.png", "docs/img/dock-kicad.svg" ],