diff --git a/bundled-addons/screenshot/addon.json b/bundled-addons/screenshot/addon.json index bb7d04b..0dc3e7c 100644 --- a/bundled-addons/screenshot/addon.json +++ b/bundled-addons/screenshot/addon.json @@ -1,7 +1,7 @@ { "id": "screenshot", "name": "Screenshot", - "version": "0.6.2", + "version": "0.6.3", "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.html b/bundled-addons/screenshot/editor.html index 90a5eb6..4c43841 100644 --- a/bundled-addons/screenshot/editor.html +++ b/bundled-addons/screenshot/editor.html @@ -44,9 +44,6 @@ history) instead of one dot spilling to a lonely second row. -->
- diff --git a/bundled-addons/screenshot/editor.js b/bundled-addons/screenshot/editor.js index e22594a..3e82c45 100644 --- a/bundled-addons/screenshot/editor.js +++ b/bundled-addons/screenshot/editor.js @@ -298,8 +298,14 @@ function cancelCrop() { clearOver(); updateCropUI(); } +// Clicking an already-active tool toggles it off (back to "select" = no +// tool). The dedicated "Select" button used to live here but read as +// broken to users β€” clicking it did nothing visible. Toggle-off gives the +// same "put the pen down" affordance without a mystery button. for (const b of document.querySelectorAll(".tool")) { - b.addEventListener("click", () => setTool(b.dataset.tool)); + b.addEventListener("click", () => { + setTool(state.tool === b.dataset.tool ? "select" : b.dataset.tool); + }); } for (const b of document.querySelectorAll(".swatch")) { b.addEventListener("click", () => { @@ -494,10 +500,19 @@ function openTextInput(pt) { const el = document.createElement("input"); el.type = "text"; el.className = "text-input"; - el.placeholder = "text…"; + el.placeholder = "type, then Enter"; + el.autocomplete = "off"; + el.setAttribute("autocorrect", "off"); + el.setAttribute("spellcheck", "false"); + // Visual weight β€” a bright acid halo around the box + the actual + // ink colour on the text itself, so users see something clearly + // happened when they clicked. el.style.color = state.color; el.style.font = `${size}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`; el.style.lineHeight = "1.15"; + el.style.background = "rgba(11,14,20,0.94)"; + el.style.border = "2px solid var(--acid, #d6ff3d)"; + el.style.boxShadow = "0 0 0 3px rgba(214,255,61,.25), 0 4px 14px rgba(0,0,0,.45)"; const rect = base.getBoundingClientRect(); const scale = stage._scale || 1; const cssX = pt.x * scale + rect.left; @@ -505,19 +520,24 @@ function openTextInput(pt) { el.style.left = cssX + "px"; el.style.top = (cssY - size) + "px"; document.body.appendChild(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). + // Three-way focus attempt β€” Chromium is racy about focusing an element + // that appeared mid-pointer-event. Synchronous focus() first (works on + // most builds), then a paint tick, then a short timer as belt-and-braces. + el.focus(); requestAnimationFrame(() => el.focus()); + setTimeout(() => { if (state.textInput && state.textInput.el === el) el.focus(); }, 30); 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. + // the canvas β€” otherwise every 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(); ev.stopPropagation(); } + if (ev.key === "Enter") { commitTextInput(); ev.preventDefault(); ev.stopPropagation(); } else if (ev.key === "Escape") { cancelTextInput(); ev.preventDefault(); ev.stopPropagation(); } + // Every other key stays inside the input β€” belt against a stray + // document-level keydown handler stealing focus. + else ev.stopPropagation(); }); el.addEventListener("blur", commitTextInput); } diff --git a/bundled-addons/screenshot/panel.html b/bundled-addons/screenshot/panel.html index c5f46a2..317ea11 100644 --- a/bundled-addons/screenshot/panel.html +++ b/bundled-addons/screenshot/panel.html @@ -18,12 +18,16 @@ body { background: var(--bg); color: var(--ink); font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; display: flex; flex-direction: column; } - header { display: flex; align-items: center; justify-content: space-between; + header { display: flex; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--line); background: var(--panel); gap: 8px; } header .t { font-weight: 600; display: flex; gap: 8px; align-items: center; min-width: 0; } header .t .em { font-size: 15px; } header .m { color: var(--dim); font-size: 11.5px; min-height: 15px; flex: 1; text-align: right; } + /* Force the sound / max / close cluster to hug the right edge no matter + what else fills the header. justify-content on the parent kept nudging + them mid-row when the status text was empty. */ + header #btn-sound { margin-left: auto; } header .max { border: 1px solid var(--line); border-radius: 6px; background: var(--btn); color: var(--ink); cursor: pointer; padding: 4px 6px; font: inherit; line-height: 0; @@ -102,6 +106,16 @@ } .recent .tile img { width: 100%; height: 100%; object-fit: cover; background: #fff; } .recent .tile:hover { border-color: var(--acid); } + .recent .tile .tdel { + position: absolute; top: 2px; right: 2px; + width: 18px; height: 18px; line-height: 16px; text-align: center; + background: rgba(11,14,20,.85); color: #fff; border-radius: 50%; + font-size: 14px; font-weight: 700; cursor: pointer; + opacity: 0; transition: opacity 120ms, background 120ms, color 120ms; + user-select: none; + } + .recent .tile:hover .tdel { opacity: 1; } + .recent .tile .tdel:hover { background: #ff5b5b; color: #101418; } .recent .clear { background: none; border: 0; color: var(--dim); font: inherit; cursor: pointer; text-transform: none; letter-spacing: 0; } .recent .clear:hover { color: var(--err); } @@ -319,16 +333,27 @@ return; } recentBox.classList.add("on"); - recentStrip.innerHTML = list.map((r) => - `
` - ).join(""); + recentStrip.innerHTML = list.map((r) => { + const nameAttr = r.name.replace(/"/g, """); + return `
` + + `Γ—` + + `
`; + }).join(""); for (const tile of recentStrip.querySelectorAll(".tile")) { const name = tile.dataset.name; try { const b = await window.silentmode.invoke("getBytes", { name }); - if (b && b.dataUrl) tile.innerHTML = ``; + if (b && b.dataUrl) { + // Keep the delete button; drop it back on top of the image. + const del = tile.querySelector(".tdel"); + tile.innerHTML = ``; + if (del) tile.appendChild(del); + } } catch {} - tile.addEventListener("click", async () => { + tile.addEventListener("click", async (ev) => { + // The per-tile X sits inside the tile; don't count its click as + // a tile click. + if (ev.target && ev.target.classList.contains("tdel")) return; try { const b = await window.silentmode.invoke("getBytes", { name }); if (!b) return; @@ -343,6 +368,22 @@ } }); } + // Per-tile delete β€” no confirmation strip (single-item destroy is + // small enough to be safe with just the X; the sweeping "clear all" + // still gates on the inline red confirmation). + for (const del of recentStrip.querySelectorAll(".tdel")) { + del.addEventListener("click", async (ev) => { + ev.stopPropagation(); + const nm = del.dataset.del; + try { + await window.silentmode.invoke("clearRecent", { name: nm }); + if (last && last.name === nm) { last = null; renderPreview(); } + await refreshRecent(); + } catch (e) { + setStatus("Delete failed: " + (e && e.message || e), "err"); + } + }); + } } catch (e) { console.warn("refreshRecent failed:", e); }