From bf6bcfade2a95ce9ab29f5bd7ece4be71a77971c Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 9 Sep 2026 10:55:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(theseus/screenshot):=200.6.1=20=E2=80=94?= =?UTF-8?q?=20text=20tool=20fix,=20category=20wrap,=20sidebar=20close=20X?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 . - 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 `
` 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. --- bundled-addons/screenshot/addon.json | 2 +- bundled-addons/screenshot/editor.css | 11 ++- bundled-addons/screenshot/editor.html | 99 +++++++++++++++------------ bundled-addons/screenshot/editor.js | 55 ++++++++++++--- bundled-addons/screenshot/panel.html | 14 ++++ sidebar-preload.js | 4 ++ 6 files changed, 126 insertions(+), 59 deletions(-) diff --git a/bundled-addons/screenshot/addon.json b/bundled-addons/screenshot/addon.json index c7d41da..5f06560 100644 --- a/bundled-addons/screenshot/addon.json +++ b/bundled-addons/screenshot/addon.json @@ -1,7 +1,7 @@ { "id": "screenshot", "name": "Screenshot", - "version": "0.6.0", + "version": "0.6.1", "description": "Capture the current tab β€” visible viewport, full page, or a rectangle you draw. Preview + annotate editor (crop, arrow, rect, ellipse, pen, text, blur/mosaic redaction, undo, copy, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.", "author": "Silent Mode", "icon": "πŸ“Έ", diff --git a/bundled-addons/screenshot/editor.css b/bundled-addons/screenshot/editor.css index 4591a12..cf165e1 100644 --- a/bundled-addons/screenshot/editor.css +++ b/bundled-addons/screenshot/editor.css @@ -25,8 +25,15 @@ body { background: var(--bg); color: var(--ink); user-select: none; flex-wrap: wrap; } /* Older versions grouped the drawing tools in the middle of the bar β€” keep the drawing / swatch / width / undo cluster centred while the top-bar - (back / max / sound / name β†’ save / copy) stays edge-anchored. */ -.toolbar { justify-content: center; } + (back / name β†’ save / copy / sound / max / close) stays edge-anchored. */ +.toolbar { justify-content: center; gap: 10px; row-gap: 6px; } +/* Each visible group of tools wraps as a unit β€” when the sidebar gets + narrow, the whole swatch row moves to the next line instead of a + single dot spilling by itself. */ +.tgroup { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; } + +/* Close-sidebar button β€” reads as danger without shouting. */ +#close-sidebar:hover { border-color: rgba(255,91,91,.55); color: var(--danger); } .topbar .name { color: var(--dim); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; margin: 0 4px; } .topbar .spacer, .toolbar .spacer { flex: 1; } diff --git a/bundled-addons/screenshot/editor.html b/bundled-addons/screenshot/editor.html index 3d1e176..7c798e7 100644 --- a/bundled-addons/screenshot/editor.html +++ b/bundled-addons/screenshot/editor.html @@ -37,57 +37,66 @@ +
+
- - - - - - - - +
+ + + + + + + + +
- +
+ + + + + + +
- - - - - - +
+ + + +
- - - - - - - - - - +
+ + +
diff --git a/bundled-addons/screenshot/editor.js b/bundled-addons/screenshot/editor.js index 902743f..3e641a5 100644 --- a/bundled-addons/screenshot/editor.js +++ b/bundled-addons/screenshot/editor.js @@ -422,7 +422,11 @@ function commitOver() { over.style.pointerEvents = "none"; // draw layer never blocks input; base receives base.addEventListener("pointerdown", (ev) => { if (state.tool === "select") return; - if (state.tool === "text") { openTextInput(pointToCanvas(ev)); return; } + if (state.tool === "text") { + ev.preventDefault(); + openTextInput(pointToCanvas(ev)); + return; + } base.setPointerCapture(ev.pointerId); const p = pointToCanvas(ev); if (state.tool === "pen") { state.pen = [p]; } @@ -475,39 +479,58 @@ base.addEventListener("pointerup", (ev) => { }); // -- text tool ----------------------------------------------------------- +// A little floats over the canvas at the click point; Enter commits +// as fillText, Escape drops. Chases a few browser quirks: +// - Focus after the DOM mutation, not before: some Chromium builds +// drop focus() when the element hasn't yet been laid out. +// - Contain the input's own pointer events so a mousedown inside it +// doesn't bubble to the canvas and immediately re-fire openTextInput, +// spawning a fresh empty box. +// - Track height instead of a hard-coded 14 px offset so tall fonts sit +// on the click's baseline rather than 14 px above it. function openTextInput(pt) { cancelTextInput(); + const size = Math.max(16, state.width * 4); const el = document.createElement("input"); el.type = "text"; el.className = "text-input"; el.placeholder = "text…"; el.style.color = state.color; - el.style.font = `${Math.max(14, state.width * 4)}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`; + el.style.font = `${size}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`; + el.style.lineHeight = "1.15"; const rect = base.getBoundingClientRect(); const scale = stage._scale || 1; const cssX = pt.x * scale + rect.left; const cssY = pt.y * scale + rect.top; el.style.left = cssX + "px"; - el.style.top = (cssY - 14) + "px"; + el.style.top = (cssY - size) + "px"; document.body.appendChild(el); - el.focus(); - state.textInput = { x: pt.x, y: pt.y, el }; + // Focus after a paint so Chromium reliably picks it up (the input goes + // from `display:absolute` to laid-out; focus() called synchronously + // right after appendChild races that in some builds). + requestAnimationFrame(() => el.focus()); + state.textInput = { x: pt.x, y: pt.y, size, el }; + // Don't let the input's own pointerdown / mousedown / click bubble to + // the canvas β€” otherwise every keystroke click-through re-fires + // openTextInput on the base and spawns duplicate boxes. + const swallow = (ev) => ev.stopPropagation(); + for (const t of ["pointerdown", "mousedown", "click"]) el.addEventListener(t, swallow); el.addEventListener("keydown", (ev) => { - if (ev.key === "Enter") { commitTextInput(); ev.preventDefault(); } - else if (ev.key === "Escape") { cancelTextInput(); ev.preventDefault(); } + if (ev.key === "Enter") { commitTextInput(); ev.preventDefault(); ev.stopPropagation(); } + else if (ev.key === "Escape") { cancelTextInput(); ev.preventDefault(); ev.stopPropagation(); } }); el.addEventListener("blur", commitTextInput); } function commitTextInput() { const ti = state.textInput; if (!ti) return; - const value = ti.el.value.trim(); + const value = ti.el.value; + const trimmed = value.trim(); ti.el.remove(); state.textInput = null; - if (!value) return; - const size = Math.max(14, state.width * 4); + if (!trimmed) return; bctx.fillStyle = state.color; - bctx.font = `${size}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`; + bctx.font = `${ti.size}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`; bctx.textBaseline = "alphabetic"; bctx.fillText(value, ti.x, ti.y); pushSnapshot(); @@ -575,6 +598,16 @@ if (cancelCropBtn) cancelCropBtn.addEventListener("click", cancelCrop); // -- top-bar navigation ---------------------------------------------- $("back").addEventListener("click", () => { playDiscard(); location.href = "panel.html"; }); +// Close the sidebar entirely β€” same IPC the dock icon toggles. We stop +// on the panel side (see panel.html); the editor mirrors the affordance +// so users don't need to navigate back before hiding the addon. +const closeSbBtn = document.getElementById("close-sidebar"); +if (closeSbBtn && window.silentmode?.sidebar) { + closeSbBtn.addEventListener("click", () => { + try { window.silentmode.sidebar.close(); } catch {} + }); +} + // Sound-toggle button β€” mirrors the sidebar panel's, both persist via // silentmode.storage so opening either surface reflects the same setting. const soundBtn = $("toggle-sound"); diff --git a/bundled-addons/screenshot/panel.html b/bundled-addons/screenshot/panel.html index f2038db..c5f46a2 100644 --- a/bundled-addons/screenshot/panel.html +++ b/bundled-addons/screenshot/panel.html @@ -30,6 +30,7 @@ } header .max:hover { background: var(--btn-h); } header .max svg { width: 14px; height: 14px; display: block; } + header .max.close:hover { border-color: rgba(255,91,91,.55); color: var(--err); } main { flex: 1; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 12px; } .modes { display: flex; gap: 6px; } button.mode { @@ -115,6 +116,11 @@ +
@@ -431,6 +437,14 @@ btnCopy.addEventListener("click", doCopy); btnSave.addEventListener("click", doSave); btnMax.addEventListener("click", toggleMax); + document.getElementById("btn-close").addEventListener("click", async () => { + // Explicit close from the sidebar itself β€” same IPC the toolbar dock + // icon toggles. No need to un-maximize first; setSidebar(false) on the + // main side flips visibility and the maximize state is remembered so + // the next open lands where it left off. + try { await window.silentmode.sidebar.close(); } + catch (e) { setStatus("Close failed: " + (e && e.message || e), "err"); } + }); // Inline confirmation for "clear all" β€” the native confirm() looks like an // OS-modal from the last decade AND opens over the tab area (out of the diff --git a/sidebar-preload.js b/sidebar-preload.js index d426909..598a14e 100644 --- a/sidebar-preload.js +++ b/sidebar-preload.js @@ -27,6 +27,10 @@ contextBridge.exposeInMainWorld("silentmode", { 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"), }, });