theseus/sidebar-preload.js
Local Dev 523832cd72 feat(theseus/screenshot): 0.4.0 — editor lives inside the sidebar, maximizable
User report: the sidebar preview lands correctly, but the moment the editor
opens in its own tab the picture is blank. Rather than chase that class of
handoff race again, put the editor in the same webContents as the panel:
the sidebar view navigates panel.html ↔ editor.html in place. Same
document object, same silentmode.storage surface, no cross-tab __pending
transfer at all.

- panel.html "Edit" button now calls silentmode.invoke("arm", …) — the
  add-on rewrites __pending with the currently-previewed capture's bytes,
  and the panel does location.href = "editor.html?name=…". Sidebar view
  loads the editor with the same preload; editor.js's storage-based load
  path pulls the pending entry out and paints.
- editor.html gains a "Back" arrow (returns to panel.html) and a
  maximize / restore icon.
- discard() now navigates to panel.html instead of closeTab() — there is
  no tab to close.
- Manifest drops the "open-tab" capability entirely (no more full-tab
  editor); keeps sidebar-panel + capture-tab.

Framework: new silentmode.sidebar.{maximize, restore, toggleMax, isMax,
onMaxChange}. main.js honours them via new sidebar-maximize / -restore /
-toggle-max / -is-max IPCs, remembering the pre-maximize width so a
restore drops back exactly. The sidebar drag-grip auto-exits maximize
mode on any user drag, so pulling the edge always lands on the pre-max
value plus/minus the delta. sidebar-preload exposes the surface;
chrome.html renderer is untouched — this is a per-panel affordance.

Editor tools (crop / arrow / rect / ellipse / pen / text / mosaic /
undo / redo / copy / save) unchanged. Save still goes through Chromium's
<a download> path, so the file lands in Downloads and appears in the
download chip like any other save.

Bundled but not shipped — leaving version bump + deploy to parent session.
2026-09-08 22:18:41 +02:00

77 lines
3.9 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)),
},
});
// 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");
grip.style.cssText = [
"position:fixed", "left:0", "top:0", "bottom:0",
"width:5px", "cursor:col-resize", "z-index:2147483647",
"background:transparent",
].join(";");
// A faint highlight on hover so the affordance is visible.
grip.addEventListener("mouseenter", () => { grip.style.background = "rgba(214,255,61,.20)"; });
grip.addEventListener("mouseleave", () => { if (!dragging) grip.style.background = "transparent"; });
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 = "rgba(214,255,61,.35)";
});
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 = "transparent";
};
window.addEventListener("mouseup", stop);
window.addEventListener("mouseleave", stop);
});