feat(theseus/screenshot): 0.6.3 — per-tile delete, no Select button, text tool halo, right-anchor panel controls
Four issues from the user's report on 0.6.2:
- Recent captures had a global "clear all" but no way to drop a single
screenshot. Each tile now grows a small × button (visible on hover;
drops in behind the thumbnail preview so it never obstructs the
content). Clicking the × invokes clearRecent({name}) and removes both
the ring entry and the scratch PNG on disk. Bubble-guarded so the ×
click doesn't also trigger the tile's "load into preview" handler.
- Select tool button removed — clicking it did nothing visible, so users
read it as broken. The internal "select" mode still exists as the
no-tool state; you get back to it now by clicking the same drawing
tool a second time (toggle-off) or hitting Escape. The active-drawing-
tool button flips its border when armed.
- Text tool made unmistakable: input paints with a 2 px acid border, a
glowing acid halo, dark background, and the visible ink colour on the
text itself. Focus attempt is three-layered (sync, rAF, timer) to
outrun any Chromium build that drops the mid-pointer-event focus. Non-
Enter/Escape keys get stopPropagation so a stray document listener
can't steal the focus mid-typing.
- Panel header's sound / max / close cluster kept nudging inward when
the status text was empty. The parent's `justify-content: space-
between` distributed the row unevenly. Force-anchor the cluster with
`#btn-sound { margin-left: auto }` so the three window-control icons
hug the right edge regardless of what fills the middle.
Bundled but not shipped separately — parent session signs and pushes.
This commit is contained in:
parent
744574ba96
commit
8eda0433d7
4 changed files with 76 additions and 18 deletions
|
|
@ -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": "📸",
|
||||
|
|
|
|||
|
|
@ -44,9 +44,6 @@
|
|||
history) instead of one dot spilling to a lonely second row. -->
|
||||
<div class="toolbar">
|
||||
<div class="tgroup" role="group" aria-label="Tools">
|
||||
<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>
|
||||
</button>
|
||||
<button class="tool" data-tool="crop" title="Crop (C)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 1v11h11M1 4h11v11"/></svg>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
</style>
|
||||
|
|
@ -319,16 +333,27 @@
|
|||
return;
|
||||
}
|
||||
recentBox.classList.add("on");
|
||||
recentStrip.innerHTML = list.map((r) =>
|
||||
`<div class="tile" data-name="${r.name.replace(/"/g,""")}" title="${r.name.replace(/"/g,""")} · ${fmt(r.bytes || 0)}"></div>`
|
||||
).join("");
|
||||
recentStrip.innerHTML = list.map((r) => {
|
||||
const nameAttr = r.name.replace(/"/g, """);
|
||||
return `<div class="tile" data-name="${nameAttr}" title="${nameAttr} · ${fmt(r.bytes || 0)}">`
|
||||
+ `<span class="tdel" data-del="${nameAttr}" title="Delete this capture">×</span>`
|
||||
+ `</div>`;
|
||||
}).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 = `<img src="${b.dataUrl}" alt="">`;
|
||||
if (b && b.dataUrl) {
|
||||
// Keep the delete button; drop it back on top of the image.
|
||||
const del = tile.querySelector(".tdel");
|
||||
tile.innerHTML = `<img src="${b.dataUrl}" alt="">`;
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue