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_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) {
|
||||
return Math.max(lo, Math.min(hi, v));
|
||||
@@ -906,11 +907,12 @@
|
||||
function applyFabPos(fab, side, y) {
|
||||
fab.style.top = y + "px";
|
||||
fab.style.bottom = "auto";
|
||||
fab.classList.toggle("left", side === "left");
|
||||
if (side === "left") {
|
||||
fab.style.left = FAB_MARGIN + "px";
|
||||
fab.style.left = FAB_EDGE + "px";
|
||||
fab.style.right = "auto";
|
||||
} else {
|
||||
fab.style.right = FAB_MARGIN + "px";
|
||||
fab.style.right = FAB_EDGE + "px";
|
||||
fab.style.left = "auto";
|
||||
}
|
||||
}
|
||||
@@ -932,9 +934,9 @@
|
||||
}
|
||||
|
||||
function placeFab(fab) {
|
||||
const s = fab.offsetWidth || 52;
|
||||
const h = fab.offsetHeight || 44;
|
||||
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 y = p ? clamp(p.yRatio * window.innerHeight, FAB_MARGIN, maxY) : maxY - 8;
|
||||
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) return;
|
||||
e.preventDefault();
|
||||
const s = fab.offsetWidth;
|
||||
fab.style.left = clamp(e.clientX - offX, 0, window.innerWidth - s) + "px";
|
||||
const w = fab.offsetWidth, h = fab.offsetHeight;
|
||||
fab.style.left = clamp(e.clientX - offX, 0, window.innerWidth - w) + "px";
|
||||
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.addEventListener("pointerup", (e) => {
|
||||
@@ -970,10 +972,10 @@
|
||||
fab.style.transition = "";
|
||||
if (!moved) return; // tap — let the click handler toggle the panel
|
||||
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 side = r.left + s / 2 < window.innerWidth / 2 ? "left" : "right";
|
||||
const y = clamp(r.top, FAB_MARGIN, window.innerHeight - s - FAB_MARGIN);
|
||||
const side = r.left + w / 2 < window.innerWidth / 2 ? "left" : "right";
|
||||
const y = clamp(r.top, FAB_MARGIN, window.innerHeight - h - FAB_MARGIN);
|
||||
fab.style.transition = "left .2s ease, right .2s ease, top .2s ease";
|
||||
applyFabPos(fab, side, y);
|
||||
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) {
|
||||
if (!root) return;
|
||||
const ring = root.getElementById("ring");
|
||||
const prog = ring.firstElementChild;
|
||||
ring.classList.add("active");
|
||||
prog.style.transition = "none";
|
||||
prog.style.strokeDashoffset = RING_C; // empty
|
||||
prog.getBoundingClientRect(); // reflow so the next assignment animates
|
||||
prog.style.transition = "stroke-dashoffset " + ms + "ms linear";
|
||||
prog.style.strokeDashoffset = "0"; // fills over ms
|
||||
const fill = root.getElementById("fill");
|
||||
fill.style.transition = "none";
|
||||
fill.style.height = "0";
|
||||
fill.getBoundingClientRect(); // reflow so the next assignment animates
|
||||
fill.style.transition = "height " + ms + "ms linear";
|
||||
fill.style.height = "100%";
|
||||
}
|
||||
|
||||
function stopRing() {
|
||||
if (!root) return;
|
||||
const ring = root.getElementById("ring");
|
||||
if (!ring) return;
|
||||
const prog = ring.firstElementChild;
|
||||
ring.classList.remove("active");
|
||||
prog.style.transition = "none";
|
||||
prog.style.strokeDashoffset = RING_C;
|
||||
const fill = root.getElementById("fill");
|
||||
if (!fill) return;
|
||||
fill.style.transition = "none";
|
||||
fill.style.height = "0";
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
@@ -1227,10 +1224,7 @@
|
||||
// ============================================================
|
||||
|
||||
const TEMPLATE = `
|
||||
<button id="fab" aria-label="Manga bookmarks">
|
||||
<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>
|
||||
<button id="fab" aria-label="Manga bookmarks"><span id="fill"></span></button>
|
||||
<div id="backdrop"></div>
|
||||
<aside id="panel" role="dialog" aria-label="Manga bookmarks">
|
||||
<header>
|
||||
@@ -1259,23 +1253,18 @@
|
||||
* { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; }
|
||||
#fab {
|
||||
position: fixed;
|
||||
width: 52px; height: 52px; border-radius: 50%; border: none;
|
||||
background: #6d28d9; color: #fff; font-size: 22px;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,.4); z-index: 2147483646; cursor: pointer;
|
||||
touch-action: none; user-select: none;
|
||||
display: flex; align-items: center; justify-content: center; padding: 0;
|
||||
width: 7px; height: 44px; border: none; padding: 0;
|
||||
border-radius: 4px 0 0 4px;
|
||||
background: #6d28d9; opacity: .45;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.4); z-index: 2147483646; cursor: pointer;
|
||||
touch-action: none; user-select: none; overflow: hidden;
|
||||
}
|
||||
#fab:active { transform: scale(.94); }
|
||||
#ring {
|
||||
position: absolute; inset: 0; transform: rotate(-90deg);
|
||||
opacity: 0; transition: opacity .15s; pointer-events: none;
|
||||
#fab.left { border-radius: 0 4px 4px 0; }
|
||||
#fab:active { opacity: 1; }
|
||||
#fill {
|
||||
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 {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.5);
|
||||
opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 2147483646;
|
||||
|
||||
Reference in New Issue
Block a user