feat: replace the FAB with a 7x44 edge tab
The circular button covered artwork exactly while reading. The tab keeps the drag-and-snap behaviour and the mangabm:fabpos key; only its box changes. The dwell countdown becomes a bottom-up fill of the tab, since a sliver has no room for a ring. Also fixes a latent bug the new shape exposes: placeFab and makeDraggable used offsetWidth for the vertical clamp too. Invisible on a 52px circle, off by 37px on a 7x44 tab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -897,7 +897,8 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const FAB_KEY = "mangabm:fabpos";
|
const FAB_KEY = "mangabm:fabpos";
|
||||||
const FAB_MARGIN = 12;
|
const FAB_MARGIN = 12; // vertical breathing room at the top and bottom
|
||||||
|
const FAB_EDGE = 0; // horizontal: an edge tab sits flush against the side
|
||||||
|
|
||||||
function clamp(v, lo, hi) {
|
function clamp(v, lo, hi) {
|
||||||
return Math.max(lo, Math.min(hi, v));
|
return Math.max(lo, Math.min(hi, v));
|
||||||
@@ -906,11 +907,12 @@
|
|||||||
function applyFabPos(fab, side, y) {
|
function applyFabPos(fab, side, y) {
|
||||||
fab.style.top = y + "px";
|
fab.style.top = y + "px";
|
||||||
fab.style.bottom = "auto";
|
fab.style.bottom = "auto";
|
||||||
|
fab.classList.toggle("left", side === "left");
|
||||||
if (side === "left") {
|
if (side === "left") {
|
||||||
fab.style.left = FAB_MARGIN + "px";
|
fab.style.left = FAB_EDGE + "px";
|
||||||
fab.style.right = "auto";
|
fab.style.right = "auto";
|
||||||
} else {
|
} else {
|
||||||
fab.style.right = FAB_MARGIN + "px";
|
fab.style.right = FAB_EDGE + "px";
|
||||||
fab.style.left = "auto";
|
fab.style.left = "auto";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -932,9 +934,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function placeFab(fab) {
|
function placeFab(fab) {
|
||||||
const s = fab.offsetWidth || 52;
|
const h = fab.offsetHeight || 44;
|
||||||
const p = loadFabPos();
|
const p = loadFabPos();
|
||||||
const maxY = window.innerHeight - s - FAB_MARGIN;
|
const maxY = window.innerHeight - h - FAB_MARGIN;
|
||||||
const side = p && p.side === "left" ? "left" : "right";
|
const side = p && p.side === "left" ? "left" : "right";
|
||||||
const y = p ? clamp(p.yRatio * window.innerHeight, FAB_MARGIN, maxY) : maxY - 8;
|
const y = p ? clamp(p.yRatio * window.innerHeight, FAB_MARGIN, maxY) : maxY - 8;
|
||||||
applyFabPos(fab, side, y);
|
applyFabPos(fab, side, y);
|
||||||
@@ -958,10 +960,10 @@
|
|||||||
if (!moved && (Math.abs(e.clientX - sx) > 6 || Math.abs(e.clientY - sy) > 6)) moved = true;
|
if (!moved && (Math.abs(e.clientX - sx) > 6 || Math.abs(e.clientY - sy) > 6)) moved = true;
|
||||||
if (!moved) return;
|
if (!moved) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const s = fab.offsetWidth;
|
const w = fab.offsetWidth, h = fab.offsetHeight;
|
||||||
fab.style.left = clamp(e.clientX - offX, 0, window.innerWidth - s) + "px";
|
fab.style.left = clamp(e.clientX - offX, 0, window.innerWidth - w) + "px";
|
||||||
fab.style.right = "auto";
|
fab.style.right = "auto";
|
||||||
fab.style.top = clamp(e.clientY - offY, 0, window.innerHeight - s) + "px";
|
fab.style.top = clamp(e.clientY - offY, 0, window.innerHeight - h) + "px";
|
||||||
fab.style.bottom = "auto";
|
fab.style.bottom = "auto";
|
||||||
});
|
});
|
||||||
fab.addEventListener("pointerup", (e) => {
|
fab.addEventListener("pointerup", (e) => {
|
||||||
@@ -970,10 +972,10 @@
|
|||||||
fab.style.transition = "";
|
fab.style.transition = "";
|
||||||
if (!moved) return; // tap — let the click handler toggle the panel
|
if (!moved) return; // tap — let the click handler toggle the panel
|
||||||
fab.dataset.dragged = "1"; // suppress the click that trails this drag
|
fab.dataset.dragged = "1"; // suppress the click that trails this drag
|
||||||
const s = fab.offsetWidth;
|
const w = fab.offsetWidth, h = fab.offsetHeight;
|
||||||
const r = fab.getBoundingClientRect();
|
const r = fab.getBoundingClientRect();
|
||||||
const side = r.left + s / 2 < window.innerWidth / 2 ? "left" : "right";
|
const side = r.left + w / 2 < window.innerWidth / 2 ? "left" : "right";
|
||||||
const y = clamp(r.top, FAB_MARGIN, window.innerHeight - s - FAB_MARGIN);
|
const y = clamp(r.top, FAB_MARGIN, window.innerHeight - h - FAB_MARGIN);
|
||||||
fab.style.transition = "left .2s ease, right .2s ease, top .2s ease";
|
fab.style.transition = "left .2s ease, right .2s ease, top .2s ease";
|
||||||
applyFabPos(fab, side, y);
|
applyFabPos(fab, side, y);
|
||||||
saveFabPos(side, y / window.innerHeight);
|
saveFabPos(side, y / window.innerHeight);
|
||||||
@@ -981,31 +983,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Countdown ring around the FAB (dwell-timer progress)
|
// Dwell-timer progress: the tab fills bottom-up. A 7px sliver has no room
|
||||||
|
// for the ring this replaced.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const RING_C = 144.5; // circumference: 2π·23
|
|
||||||
|
|
||||||
function startRing(ms) {
|
function startRing(ms) {
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
const ring = root.getElementById("ring");
|
const fill = root.getElementById("fill");
|
||||||
const prog = ring.firstElementChild;
|
fill.style.transition = "none";
|
||||||
ring.classList.add("active");
|
fill.style.height = "0";
|
||||||
prog.style.transition = "none";
|
fill.getBoundingClientRect(); // reflow so the next assignment animates
|
||||||
prog.style.strokeDashoffset = RING_C; // empty
|
fill.style.transition = "height " + ms + "ms linear";
|
||||||
prog.getBoundingClientRect(); // reflow so the next assignment animates
|
fill.style.height = "100%";
|
||||||
prog.style.transition = "stroke-dashoffset " + ms + "ms linear";
|
|
||||||
prog.style.strokeDashoffset = "0"; // fills over ms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopRing() {
|
function stopRing() {
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
const ring = root.getElementById("ring");
|
const fill = root.getElementById("fill");
|
||||||
if (!ring) return;
|
if (!fill) return;
|
||||||
const prog = ring.firstElementChild;
|
fill.style.transition = "none";
|
||||||
ring.classList.remove("active");
|
fill.style.height = "0";
|
||||||
prog.style.transition = "none";
|
|
||||||
prog.style.strokeDashoffset = RING_C;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePanel() {
|
function togglePanel() {
|
||||||
@@ -1227,10 +1224,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const TEMPLATE = `
|
const TEMPLATE = `
|
||||||
<button id="fab" aria-label="Manga bookmarks">
|
<button id="fab" aria-label="Manga bookmarks"><span id="fill"></span></button>
|
||||||
<svg id="ring" viewBox="0 0 52 52" aria-hidden="true"><circle class="prog" cx="26" cy="26" r="23"></circle></svg>
|
|
||||||
<span class="fab-icon">📑</span>
|
|
||||||
</button>
|
|
||||||
<div id="backdrop"></div>
|
<div id="backdrop"></div>
|
||||||
<aside id="panel" role="dialog" aria-label="Manga bookmarks">
|
<aside id="panel" role="dialog" aria-label="Manga bookmarks">
|
||||||
<header>
|
<header>
|
||||||
@@ -1259,23 +1253,18 @@
|
|||||||
* { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; }
|
* { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; }
|
||||||
#fab {
|
#fab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 52px; height: 52px; border-radius: 50%; border: none;
|
width: 7px; height: 44px; border: none; padding: 0;
|
||||||
background: #6d28d9; color: #fff; font-size: 22px;
|
border-radius: 4px 0 0 4px;
|
||||||
box-shadow: 0 4px 14px rgba(0,0,0,.4); z-index: 2147483646; cursor: pointer;
|
background: #6d28d9; opacity: .45;
|
||||||
touch-action: none; user-select: none;
|
box-shadow: 0 2px 8px rgba(0,0,0,.4); z-index: 2147483646; cursor: pointer;
|
||||||
display: flex; align-items: center; justify-content: center; padding: 0;
|
touch-action: none; user-select: none; overflow: hidden;
|
||||||
}
|
}
|
||||||
#fab:active { transform: scale(.94); }
|
#fab.left { border-radius: 0 4px 4px 0; }
|
||||||
#ring {
|
#fab:active { opacity: 1; }
|
||||||
position: absolute; inset: 0; transform: rotate(-90deg);
|
#fill {
|
||||||
opacity: 0; transition: opacity .15s; pointer-events: none;
|
position: absolute; left: 0; right: 0; bottom: 0; height: 0;
|
||||||
|
background: #c4b5fd; pointer-events: none;
|
||||||
}
|
}
|
||||||
#ring.active { opacity: 1; }
|
|
||||||
#ring .prog {
|
|
||||||
fill: none; stroke: #c4b5fd; stroke-width: 3; stroke-linecap: round;
|
|
||||||
stroke-dasharray: 144.5; stroke-dashoffset: 144.5;
|
|
||||||
}
|
|
||||||
.fab-icon { position: relative; z-index: 1; line-height: 1; }
|
|
||||||
#backdrop {
|
#backdrop {
|
||||||
position: fixed; inset: 0; background: rgba(0,0,0,.5);
|
position: fixed; inset: 0; background: rgba(0,0,0,.5);
|
||||||
opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 2147483646;
|
opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 2147483646;
|
||||||
|
|||||||
Reference in New Issue
Block a user