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.
This commit is contained in:
parent
35ec8a3904
commit
bf6bcfade2
6 changed files with 126 additions and 59 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "screenshot",
|
"id": "screenshot",
|
||||||
"name": "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.",
|
"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",
|
"author": "Silent Mode",
|
||||||
"icon": "📸",
|
"icon": "📸",
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,15 @@ body { background: var(--bg); color: var(--ink);
|
||||||
user-select: none; flex-wrap: wrap; }
|
user-select: none; flex-wrap: wrap; }
|
||||||
/* Older versions grouped the drawing tools in the middle of the bar — keep
|
/* Older versions grouped the drawing tools in the middle of the bar — keep
|
||||||
the drawing / swatch / width / undo cluster centred while the top-bar
|
the drawing / swatch / width / undo cluster centred while the top-bar
|
||||||
(back / max / sound / name → save / copy) stays edge-anchored. */
|
(back / name → save / copy / sound / max / close) stays edge-anchored. */
|
||||||
.toolbar { justify-content: center; }
|
.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;
|
.topbar .name { color: var(--dim); font-size: 11.5px; overflow: hidden;
|
||||||
text-overflow: ellipsis; white-space: nowrap; max-width: 200px; margin: 0 4px; }
|
text-overflow: ellipsis; white-space: nowrap; max-width: 200px; margin: 0 4px; }
|
||||||
.topbar .spacer, .toolbar .spacer { flex: 1; }
|
.topbar .spacer, .toolbar .spacer { flex: 1; }
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,17 @@
|
||||||
<button class="btn" id="toggle-max" title="Expand the sidebar to full window / restore">
|
<button class="btn" id="toggle-max" title="Expand the sidebar to full window / restore">
|
||||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4"/></svg>
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn" id="close-sidebar" title="Close the sidebar">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar rows are grouped so that when the sidebar is too narrow to
|
||||||
|
hold everything, whole categories wrap together (tools stay with
|
||||||
|
tools, swatches with swatches, widths with widths, history with
|
||||||
|
history) instead of one dot spilling to a lonely second row. -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
|
<div class="tgroup" role="group" aria-label="Tools">
|
||||||
<button class="tool active" data-tool="select" title="Select (no tool)">
|
<button class="tool active" data-tool="select" title="Select (no tool)">
|
||||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M3 2l10 5-4 1-1 4z"/></svg>
|
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M3 2l10 5-4 1-1 4z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -64,30 +72,31 @@
|
||||||
<button class="tool" data-tool="blur" title="Blur / mosaic redaction (B)">
|
<button class="tool" data-tool="blur" title="Blur / mosaic redaction (B)">
|
||||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="2.5" width="11" height="11"/><path d="M5 6h1M8 6h1M11 6h1M5 9h1M8 9h1M11 9h1M5 12h1M8 12h1M11 12h1"/></svg>
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="2.5" width="11" height="11"/><path d="M5 6h1M8 6h1M11 6h1M5 9h1M8 9h1M11 9h1M5 12h1M8 12h1M11 12h1"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="sep"></span>
|
<div class="tgroup" role="group" aria-label="Colours">
|
||||||
|
|
||||||
<button class="swatch active" data-color="#ff3b30" style="background:#ff3b30" title="Red"></button>
|
<button class="swatch active" data-color="#ff3b30" style="background:#ff3b30" title="Red"></button>
|
||||||
<button class="swatch" data-color="#d6ff3d" style="background:#d6ff3d" title="Acid"></button>
|
<button class="swatch" data-color="#d6ff3d" style="background:#d6ff3d" title="Acid"></button>
|
||||||
<button class="swatch" data-color="#ffcc00" style="background:#ffcc00" title="Yellow"></button>
|
<button class="swatch" data-color="#ffcc00" style="background:#ffcc00" title="Yellow"></button>
|
||||||
<button class="swatch" data-color="#0aa8ff" style="background:#0aa8ff" title="Blue"></button>
|
<button class="swatch" data-color="#0aa8ff" style="background:#0aa8ff" title="Blue"></button>
|
||||||
<button class="swatch" data-color="#ffffff" style="background:#ffffff" title="White"></button>
|
<button class="swatch" data-color="#ffffff" style="background:#ffffff" title="White"></button>
|
||||||
<button class="swatch" data-color="#000000" style="background:#000000" title="Black"></button>
|
<button class="swatch" data-color="#000000" style="background:#000000" title="Black"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="sep"></span>
|
<div class="tgroup" role="group" aria-label="Widths">
|
||||||
|
|
||||||
<button class="width" data-width="3" title="Thin"><span class="dot" style="width:4px;height:4px"></span></button>
|
<button class="width" data-width="3" title="Thin"><span class="dot" style="width:4px;height:4px"></span></button>
|
||||||
<button class="width active" data-width="5" title="Medium"><span class="dot" style="width:7px;height:7px"></span></button>
|
<button class="width active" data-width="5" title="Medium"><span class="dot" style="width:7px;height:7px"></span></button>
|
||||||
<button class="width" data-width="9" title="Thick"><span class="dot" style="width:11px;height:11px"></span></button>
|
<button class="width" data-width="9" title="Thick"><span class="dot" style="width:11px;height:11px"></span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="sep"></span>
|
<div class="tgroup" role="group" aria-label="History">
|
||||||
|
|
||||||
<button class="btn" id="undo" title="Undo (Ctrl+Z)" disabled>
|
<button class="btn" id="undo" title="Undo (Ctrl+Z)" disabled>
|
||||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 8c0-3 2-5 5-5s5 2 5 5-2 5-5 5"/><path d="M6 5L3 8l3 3"/></svg>
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 8c0-3 2-5 5-5s5 2 5 5-2 5-5 5"/><path d="M6 5L3 8l3 3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn" id="redo" title="Redo (Ctrl+Shift+Z)" disabled>
|
<button class="btn" id="redo" title="Redo (Ctrl+Shift+Z)" disabled>
|
||||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M13 8c0-3-2-5-5-5S3 5 3 8s2 5 5 5"/><path d="M10 5l3 3-3 3"/></svg>
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M13 8c0-3-2-5-5-5S3 5 3 8s2 5 5 5"/><path d="M10 5l3 3-3 3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="board" id="board">
|
<div class="board" id="board">
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,11 @@ function commitOver() {
|
||||||
over.style.pointerEvents = "none"; // draw layer never blocks input; base receives
|
over.style.pointerEvents = "none"; // draw layer never blocks input; base receives
|
||||||
base.addEventListener("pointerdown", (ev) => {
|
base.addEventListener("pointerdown", (ev) => {
|
||||||
if (state.tool === "select") return;
|
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);
|
base.setPointerCapture(ev.pointerId);
|
||||||
const p = pointToCanvas(ev);
|
const p = pointToCanvas(ev);
|
||||||
if (state.tool === "pen") { state.pen = [p]; }
|
if (state.tool === "pen") { state.pen = [p]; }
|
||||||
|
|
@ -475,39 +479,58 @@ base.addEventListener("pointerup", (ev) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- text tool -----------------------------------------------------------
|
// -- text tool -----------------------------------------------------------
|
||||||
|
// A little <input> 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) {
|
function openTextInput(pt) {
|
||||||
cancelTextInput();
|
cancelTextInput();
|
||||||
|
const size = Math.max(16, state.width * 4);
|
||||||
const el = document.createElement("input");
|
const el = document.createElement("input");
|
||||||
el.type = "text";
|
el.type = "text";
|
||||||
el.className = "text-input";
|
el.className = "text-input";
|
||||||
el.placeholder = "text…";
|
el.placeholder = "text…";
|
||||||
el.style.color = state.color;
|
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 rect = base.getBoundingClientRect();
|
||||||
const scale = stage._scale || 1;
|
const scale = stage._scale || 1;
|
||||||
const cssX = pt.x * scale + rect.left;
|
const cssX = pt.x * scale + rect.left;
|
||||||
const cssY = pt.y * scale + rect.top;
|
const cssY = pt.y * scale + rect.top;
|
||||||
el.style.left = cssX + "px";
|
el.style.left = cssX + "px";
|
||||||
el.style.top = (cssY - 14) + "px";
|
el.style.top = (cssY - size) + "px";
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
el.focus();
|
// Focus after a paint so Chromium reliably picks it up (the input goes
|
||||||
state.textInput = { x: pt.x, y: pt.y, el };
|
// 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) => {
|
el.addEventListener("keydown", (ev) => {
|
||||||
if (ev.key === "Enter") { commitTextInput(); ev.preventDefault(); }
|
if (ev.key === "Enter") { commitTextInput(); ev.preventDefault(); ev.stopPropagation(); }
|
||||||
else if (ev.key === "Escape") { cancelTextInput(); ev.preventDefault(); }
|
else if (ev.key === "Escape") { cancelTextInput(); ev.preventDefault(); ev.stopPropagation(); }
|
||||||
});
|
});
|
||||||
el.addEventListener("blur", commitTextInput);
|
el.addEventListener("blur", commitTextInput);
|
||||||
}
|
}
|
||||||
function commitTextInput() {
|
function commitTextInput() {
|
||||||
const ti = state.textInput;
|
const ti = state.textInput;
|
||||||
if (!ti) return;
|
if (!ti) return;
|
||||||
const value = ti.el.value.trim();
|
const value = ti.el.value;
|
||||||
|
const trimmed = value.trim();
|
||||||
ti.el.remove();
|
ti.el.remove();
|
||||||
state.textInput = null;
|
state.textInput = null;
|
||||||
if (!value) return;
|
if (!trimmed) return;
|
||||||
const size = Math.max(14, state.width * 4);
|
|
||||||
bctx.fillStyle = state.color;
|
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.textBaseline = "alphabetic";
|
||||||
bctx.fillText(value, ti.x, ti.y);
|
bctx.fillText(value, ti.x, ti.y);
|
||||||
pushSnapshot();
|
pushSnapshot();
|
||||||
|
|
@ -575,6 +598,16 @@ if (cancelCropBtn) cancelCropBtn.addEventListener("click", cancelCrop);
|
||||||
// -- top-bar navigation ----------------------------------------------
|
// -- top-bar navigation ----------------------------------------------
|
||||||
$("back").addEventListener("click", () => { playDiscard(); location.href = "panel.html"; });
|
$("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
|
// Sound-toggle button — mirrors the sidebar panel's, both persist via
|
||||||
// silentmode.storage so opening either surface reflects the same setting.
|
// silentmode.storage so opening either surface reflects the same setting.
|
||||||
const soundBtn = $("toggle-sound");
|
const soundBtn = $("toggle-sound");
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
}
|
}
|
||||||
header .max:hover { background: var(--btn-h); }
|
header .max:hover { background: var(--btn-h); }
|
||||||
header .max svg { width: 14px; height: 14px; display: block; }
|
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; }
|
main { flex: 1; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 12px; }
|
||||||
.modes { display: flex; gap: 6px; }
|
.modes { display: flex; gap: 6px; }
|
||||||
button.mode {
|
button.mode {
|
||||||
|
|
@ -115,6 +116,11 @@
|
||||||
<path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4"/>
|
<path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="max close" id="btn-close" title="Close the sidebar" aria-label="Close sidebar">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
||||||
|
<path d="M4 4l8 8M12 4l-8 8"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
|
|
@ -431,6 +437,14 @@
|
||||||
btnCopy.addEventListener("click", doCopy);
|
btnCopy.addEventListener("click", doCopy);
|
||||||
btnSave.addEventListener("click", doSave);
|
btnSave.addEventListener("click", doSave);
|
||||||
btnMax.addEventListener("click", toggleMax);
|
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
|
// 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
|
// OS-modal from the last decade AND opens over the tab area (out of the
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ contextBridge.exposeInMainWorld("silentmode", {
|
||||||
toggleMax:() => ipcRenderer.invoke("sidebar-toggle-max"),
|
toggleMax:() => ipcRenderer.invoke("sidebar-toggle-max"),
|
||||||
isMax: () => ipcRenderer.invoke("sidebar-is-max"),
|
isMax: () => ipcRenderer.invoke("sidebar-is-max"),
|
||||||
onMaxChange: (cb) => ipcRenderer.on("sidebar-max-change", (_e, max) => cb(!!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"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue