theseus/sidebar-preload.js
Local Dev bf6bcfade2 feat(theseus/screenshot): 0.6.1 — text tool fix, category wrap, sidebar close X
Four user reports from the 0.6.0 rollout:

- Text tool never committed. openTextInput placed the box correctly but
  a couple of Chromium quirks stopped a normal type-Enter cycle:
  focus() called synchronously right after appendChild lost the race
  in some builds, and the input's own mousedown / click was bubbling
  through to #base and re-firing openTextInput on every subsequent
  keystroke click-through, so what looked like "nothing happens" was
  actually "a new empty box spawned on top of the last one every time".
  Now: focus after requestAnimationFrame, contain pointerdown / mousedown
  / click inside the input so they don't bubble to the canvas, track
  the font size on the state so commit uses the same one openTextInput
  measured against, and preventDefault on the base pointerdown so
  Chromium doesn't reset focus back to <body>.

- Toolbar wrapped one dot at a time when the sidebar was narrow (a
  lonely thin/medium/thick width would jump to a second row while the
  swatches stayed above it). Toolbar items are now wrapped in
  `<div class="tgroup">` per category — tools / swatches / widths /
  undo-redo — with `flex: 0 0 auto`, so a whole row wraps as a unit
  and lands cleanly under the previous one. `gap: 10px / row-gap: 6px`
  keeps the visual grouping obvious.

- No way to close the sidebar without hunting for the dock icon. Added
  an X button in the top-right of both the sidebar panel and the
  editor toolbar. Both wire through a new `silentmode.sidebar.close()`
  preload method that calls the existing `sidebar-close` IPC.

- Tightened the pointerdown text branch so preventDefault + explicit
  focus-after-frame make the click-through races impossible.

Bundled but not shipped separately — parent session signs and pushes.
2026-09-09 10:55:21 +02:00

97 lines
5 KiB
JavaScript

// Preload shared by every add-on sidebar panel. Exposes a small, safe
// surface to the add-on's HTML. Main-side handlers derive the add-on
// identity from the sender's URL (the panel is always loaded from
// somewhere inside <userData>/addons/<id>/), so a random page that
// happens to see the API shape can't touch another add-on's storage.
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("silentmode", {
storage: {
get: (key, fallback = null) => ipcRenderer.invoke("addon-storage-get", key, fallback),
set: (key, value) => ipcRenderer.invoke("addon-storage-set", key, value),
all: () => ipcRenderer.invoke("addon-storage-all"),
},
// Ask main which panel is currently visible — panels may want to
// suspend expensive work when hidden.
onVisibility: (cb) => ipcRenderer.on("sidebar-visibility", (_e, visible) => cb(!!visible)),
// Call into the add-on's activate() context: resolves with whatever the
// matching api.onMessage handler returned (or rejects with its error).
invoke: (msg, payload) => ipcRenderer.invoke("addon-msg", String(msg), payload),
// Events the add-on pushes via api.emit while this panel is open.
on: (msg, cb) => ipcRenderer.on("addon-event", (_e, name, payload) => { if (name === msg) cb(payload); }),
// Sidebar sizing controls a panel may want (e.g. the screenshot editor
// wants a full-window canvas). Widen fills the window minus a thin strip
// for the tab area behind it; restore returns to the pre-widen width.
sidebar: {
maximize: () => ipcRenderer.invoke("sidebar-maximize"),
restore: () => ipcRenderer.invoke("sidebar-restore"),
toggleMax:() => ipcRenderer.invoke("sidebar-toggle-max"),
isMax: () => ipcRenderer.invoke("sidebar-is-max"),
onMaxChange: (cb) => ipcRenderer.on("sidebar-max-change", (_e, max) => cb(!!max)),
// Close the sidebar entirely. Same IPC the toolbar dock icon toggles
// — the panel/editor can offer an explicit X button so users don't
// have to hunt for the dock icon just to hide the panel.
close: () => ipcRenderer.invoke("sidebar-close"),
},
});
// Panel picker strip was here — a 32-px tab bar injected at the top of
// every panel to switch between registered extensions. Removed: the
// toolbar extension dock (per-extension buttons + puzzle dropdown at
// narrow widths) is the canonical switcher now, and doubling that inside
// the sidebar wasted vertical space and made narrow panels feel cramped.
// Sidebar resize grip. Injected into every panel automatically so panel
// authors don't have to reinvent it. A thin strip along the LEFT edge
// (the boundary between the tab area and the sidebar) accepts mousedown
// and streams drag deltas to main until mouseup. Main clamps the width
// to [200, 800] and persists it in settings.sidebarWidth.
window.addEventListener("DOMContentLoaded", () => {
const grip = document.createElement("div");
grip.setAttribute("aria-label", "Resize sidebar");
// A visible-at-rest separator. Fully transparent used to hide the seam
// between the tab area and the sidebar completely — users couldn't tell
// where one ended and the other began. A mid-gray at moderate alpha
// reads on every panel background (dark and light) without competing
// for attention. Hover / drag ramps to acid so the grab affordance
// still stands out.
const IDLE_BG = "rgba(140,150,170,.55)";
const HOVER_BG = "rgba(214,255,61,.35)";
const ACTIVE_BG = "rgba(214,255,61,.55)";
grip.style.cssText = [
"position:fixed", "left:0", "top:0", "bottom:0",
"width:2px", "cursor:col-resize", "z-index:2147483647",
"background:" + IDLE_BG,
// A wider invisible hit target sits over the visible strip so
// dragging still catches a 5-px slack — visible line stays a
// clean 2 px.
"box-shadow:2px 0 0 0 transparent",
].join(";");
// A subtle overlay expands the pointer-catch zone without widening
// the visible band. Same click-through element, wider hit box.
grip.style.setProperty("outline", "2px solid transparent", "important");
grip.style.setProperty("outline-offset", "1px", "important");
grip.addEventListener("mouseenter", () => { grip.style.background = HOVER_BG; });
grip.addEventListener("mouseleave", () => { if (!dragging) grip.style.background = IDLE_BG; });
document.body.appendChild(grip);
let dragging = false;
grip.addEventListener("mousedown", (e) => {
if (e.button !== 0) return;
e.preventDefault();
dragging = true;
document.body.style.userSelect = "none";
grip.style.background = ACTIVE_BG;
});
window.addEventListener("mousemove", (e) => {
if (!dragging) return;
// Moving cursor LEFT = grow sidebar width. movementX is negative left.
if (e.movementX !== 0) ipcRenderer.invoke("sidebar-drag", -e.movementX);
});
const stop = () => {
if (!dragging) return;
dragging = false;
document.body.style.userSelect = "";
grip.style.background = IDLE_BG;
};
window.addEventListener("mouseup", stop);
window.addEventListener("mouseleave", stop);
});