hd-garden 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/bookshelf/load.mjs +96 -0
- package/app/bookshelf/load.ts +12 -19
- package/app/bookshelf/page.tsx +43 -10
- package/app/components/SiteChrome.tsx +14 -6
- package/app/components/SiteHeader.tsx +3 -3
- package/app/components/bookshelf/Bookshelf.tsx +64 -9
- package/app/components/bookshelf/bookshelf.css +211 -44
- package/app/components/bookshelf/catalog.mjs +116 -0
- package/app/components/bookshelf/catalog.ts +23 -0
- package/app/components/bookshelf/engine.ts +402 -117
- package/app/components/bookshelf/schema.mjs +86 -0
- package/app/components/bookshelf/schema.ts +25 -87
- package/app/components/bookshelf/types.ts +20 -3
- package/app/garden-modules.d.ts +6 -0
- package/app/layout.tsx +2 -1
- package/app/lib/i18n.ts +2 -2
- package/app/sitemap.ts +3 -0
- package/docs/handbook/config.md +41 -2
- package/docs/handbook/en/config.md +41 -2
- package/package.json +1 -1
- package/scripts/build-content.mjs +17 -1
- package/scripts/handbook-facts.mjs +2 -1
- package/ui/garden/site-nav.mjs +12 -0
- package/vite.config.ts +12 -3
|
@@ -38,8 +38,11 @@ function injectExtraCovers(catalog) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function mountBookshelf(root, options = {}) {
|
|
41
|
+
// A passed catalog is a site shelf. Demo notes and demo copy stay on the
|
|
42
|
+
// package preview, which mounts this with no catalog.
|
|
41
43
|
const CATALOG = options.catalog ?? DEMO_CATALOG;
|
|
42
|
-
const NOTES = options.notes ?? DEMO_NOTES;
|
|
44
|
+
const NOTES = options.notes ?? (options.catalog ? {} : DEMO_NOTES);
|
|
45
|
+
const usingDemo = !options.catalog;
|
|
43
46
|
const removeExtraCovers = injectExtraCovers(CATALOG);
|
|
44
47
|
|
|
45
48
|
// ux26: per-book cover dominant (= existing bk-* spine colors) for open-shell + page wash
|
|
@@ -100,8 +103,11 @@ export function mountBookshelf(root, options = {}) {
|
|
|
100
103
|
|
|
101
104
|
function bookShellHex(book) {
|
|
102
105
|
if (!book) return "#5a6a88";
|
|
103
|
-
if (book.filler === "sand") return COVER_SHELL.sand;
|
|
106
|
+
if (book.filler === "sand" || book.filler === true) return COVER_SHELL.sand;
|
|
104
107
|
if (book.filler === "cream") return COVER_SHELL.cream;
|
|
108
|
+
// Site books carry the sampled cover color. Demo jackets keep COVER_SHELL.
|
|
109
|
+
const color = typeof book.color === "string" ? book.color.trim() : "";
|
|
110
|
+
if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) return color;
|
|
105
111
|
return COVER_SHELL[book.id] || "#5a6a88";
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -171,6 +177,9 @@ export function mountBookshelf(root, options = {}) {
|
|
|
171
177
|
function noteSummaryText(book) {
|
|
172
178
|
const key = book && book.note;
|
|
173
179
|
const note = key ? NOTES[key] : null;
|
|
180
|
+
if (note && typeof note.summary === "string" && note.summary.trim()) {
|
|
181
|
+
return note.summary.trim().replace(/\s+/g, " ");
|
|
182
|
+
}
|
|
174
183
|
if (note && note.body) {
|
|
175
184
|
let text = plainFromHtml(note.body);
|
|
176
185
|
// Prefer first real sentence-ish chunk after stripping headings noise
|
|
@@ -179,6 +188,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
179
188
|
if (text.length >= 24) return text;
|
|
180
189
|
}
|
|
181
190
|
const who = book && book.author ? ` · ${book.author}` : "";
|
|
191
|
+
if (!usingDemo) return (book && (book.author || book.title)) || "";
|
|
182
192
|
return `《${book.title}》的读书笔记摘要${who}。正式站会换成完整笔记;Demo 先占这一页。`;
|
|
183
193
|
}
|
|
184
194
|
|
|
@@ -190,7 +200,11 @@ export function mountBookshelf(root, options = {}) {
|
|
|
190
200
|
|
|
191
201
|
function rightPageHtml(book) {
|
|
192
202
|
const summary = escapeHtml(noteSummaryText(book));
|
|
193
|
-
|
|
203
|
+
const href = book && typeof book.href === "string" ? book.href : "";
|
|
204
|
+
const more = href
|
|
205
|
+
? `<a class="flyer-page-more" href="${escapeHtml(href)}">阅读全文</a>`
|
|
206
|
+
: "";
|
|
207
|
+
return `<div class="flyer-page-kicker">读书笔记</div><p class="flyer-page-summary">${summary}</p>${more}`;
|
|
194
208
|
}
|
|
195
209
|
|
|
196
210
|
|
|
@@ -298,6 +312,30 @@ export function mountBookshelf(root, options = {}) {
|
|
|
298
312
|
return Math.max(96, Math.round(w * (nat.h / nat.w)));
|
|
299
313
|
}
|
|
300
314
|
|
|
315
|
+
// Real jacket on the expanded spine. CSS background-image is stripped by
|
|
316
|
+
// `.spine[class*="img-"] { background-image: none !important }` and by the
|
|
317
|
+
// collapse rule, which left bk-* (#513688) as a solid pad at extract start.
|
|
318
|
+
function mountSeatCover(el, book) {
|
|
319
|
+
if (!el || !book || !book.cover || book.filler) return;
|
|
320
|
+
el.style.backgroundColor = "transparent";
|
|
321
|
+
let art = el.querySelector(":scope > .cover-art");
|
|
322
|
+
if (!art) {
|
|
323
|
+
art = document.createElement("img");
|
|
324
|
+
art.className = "cover-art";
|
|
325
|
+
art.alt = "";
|
|
326
|
+
art.draggable = false;
|
|
327
|
+
el.appendChild(art);
|
|
328
|
+
}
|
|
329
|
+
const src = coverSrc(book);
|
|
330
|
+
if (art.getAttribute("src") !== src) art.src = src;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function clearSeatCover(el) {
|
|
334
|
+
if (!el || !el.classList.contains("spine")) return;
|
|
335
|
+
el.querySelectorAll(":scope > .cover-art").forEach((node) => node.remove());
|
|
336
|
+
try { el.style.removeProperty("background-color"); } catch (_) {}
|
|
337
|
+
}
|
|
338
|
+
|
|
301
339
|
function applyFaceSize(el, book, { expanded = false } = {}) {
|
|
302
340
|
if (!el || !book) return;
|
|
303
341
|
const faceH = bookFaceHeight(book);
|
|
@@ -323,12 +361,13 @@ export function mountBookshelf(root, options = {}) {
|
|
|
323
361
|
return null;
|
|
324
362
|
}
|
|
325
363
|
|
|
326
|
-
// ux21: mobile
|
|
327
|
-
//
|
|
364
|
+
// ux21 lock: mobile resting = all covers (全封面).
|
|
365
|
+
// Match CSS mobile shelf: (hover:none) OR max-width 640.
|
|
366
|
+
// DevTools「手机 393」often keeps hover:hover — width gate is required or autoDecide→spines.
|
|
328
367
|
function isMobilePrimary() {
|
|
329
368
|
return (
|
|
330
|
-
window.matchMedia("(hover: none)").matches
|
|
331
|
-
window.matchMedia("(
|
|
369
|
+
window.matchMedia("(hover: none)").matches ||
|
|
370
|
+
window.matchMedia("(max-width: 640px)").matches
|
|
332
371
|
);
|
|
333
372
|
}
|
|
334
373
|
|
|
@@ -565,6 +604,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
565
604
|
function restoreRestingSpine(el, bookId) {
|
|
566
605
|
if (!el) return;
|
|
567
606
|
el.classList.remove("is-expanded", "is-collapsing");
|
|
607
|
+
clearSeatCover(el);
|
|
568
608
|
const found = bookById(bookId);
|
|
569
609
|
if (found && found.book.cover && !found.book.filler) {
|
|
570
610
|
applyFaceSize(el, found.book, { expanded: false });
|
|
@@ -606,6 +646,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
606
646
|
el.classList.remove("is-collapsing");
|
|
607
647
|
el.classList.add("is-expanded");
|
|
608
648
|
applyFaceSize(el, found.book, { expanded: true });
|
|
649
|
+
mountSeatCover(el, found.book);
|
|
609
650
|
}
|
|
610
651
|
}
|
|
611
652
|
|
|
@@ -671,12 +712,37 @@ export function mountBookshelf(root, options = {}) {
|
|
|
671
712
|
$("#noteTitle").textContent = book.title;
|
|
672
713
|
$("#noteAuthor").textContent = book.author || "";
|
|
673
714
|
const coverEl = $("#noteCover");
|
|
674
|
-
|
|
715
|
+
// Intrinsic <img>, not the .img-* background. That background is
|
|
716
|
+
// background-size:cover inside a fixed 2:3 frame and clips landscape art.
|
|
717
|
+
coverEl.className = "note-cover";
|
|
718
|
+
coverEl.style.backgroundImage = "none";
|
|
719
|
+
coverEl.style.backgroundColor = "transparent";
|
|
720
|
+
coverEl.replaceChildren();
|
|
721
|
+
if (book.cover) {
|
|
722
|
+
const img = document.createElement("img");
|
|
723
|
+
img.className = "note-cover-img";
|
|
724
|
+
img.alt = "";
|
|
725
|
+
img.draggable = false;
|
|
726
|
+
img.src = coverSrc(book);
|
|
727
|
+
coverEl.appendChild(img);
|
|
728
|
+
}
|
|
675
729
|
const note = NOTES[book.note];
|
|
730
|
+
const read = $("#noteRead");
|
|
731
|
+
if (read) {
|
|
732
|
+
if (book.href) {
|
|
733
|
+
read.href = book.href;
|
|
734
|
+
read.hidden = false;
|
|
735
|
+
} else {
|
|
736
|
+
read.hidden = true;
|
|
737
|
+
read.removeAttribute("href");
|
|
738
|
+
}
|
|
739
|
+
}
|
|
676
740
|
if (note) {
|
|
677
741
|
$("#noteBody").innerHTML = note.body;
|
|
678
|
-
} else {
|
|
742
|
+
} else if (usingDemo) {
|
|
679
743
|
$("#noteBody").innerHTML = `<p class="placeholder">这本还没有完整笔记正文。Demo 占位:返回书架继续抽翻验收。</p><p>${book.title}${book.author ? " · " + book.author : ""}</p>`;
|
|
744
|
+
} else {
|
|
745
|
+
$("#noteBody").innerHTML = `<p class="placeholder">这本还没有笔记正文。</p><p>${escapeHtml(book.title)}${book.author ? " · " + escapeHtml(book.author) : ""}</p>`;
|
|
680
746
|
}
|
|
681
747
|
}
|
|
682
748
|
|
|
@@ -727,15 +793,23 @@ export function mountBookshelf(root, options = {}) {
|
|
|
727
793
|
return `translate(${tx}px, ${ty}px) scale(${sx}, ${sy})${extra ? " " + extra : ""}`;
|
|
728
794
|
}
|
|
729
795
|
|
|
730
|
-
// Closed book: cover on RIGHT half.
|
|
796
|
+
// Closed book: cover on RIGHT half.
|
|
797
|
+
// Covers stay uniform (sx ≈ sy). A narrow spine must NOT share that one scale:
|
|
798
|
+
// min(spineW / halfSpread, spineH / spreadH) ≈ 0.09 crushes height to ~45px.
|
|
799
|
+
// Spine slot: scale X and Y apart so the flyer box height stays the resting spine.
|
|
731
800
|
function closedCoverTransform(slotRect, mid) {
|
|
732
801
|
const halfW = mid.width / 2;
|
|
733
802
|
const sx = slotRect.width / halfW;
|
|
734
803
|
const sy = slotRect.height / mid.height;
|
|
804
|
+
const narrow = slotRect.width / Math.max(1, slotRect.height) < 0.42;
|
|
805
|
+
if (narrow) {
|
|
806
|
+
const tx = slotRect.left - mid.left - halfW * sx;
|
|
807
|
+
const ty = slotRect.top - mid.top;
|
|
808
|
+
return `translate(${tx.toFixed(2)}px, ${ty.toFixed(2)}px) scale(${sx.toFixed(4)}, ${sy.toFixed(4)})`;
|
|
809
|
+
}
|
|
735
810
|
const s = Math.min(sx, sy);
|
|
736
811
|
const scaledHalfW = halfW * s;
|
|
737
812
|
const scaledH = mid.height * s;
|
|
738
|
-
// Center scaled right-half over slot
|
|
739
813
|
const tx = slotRect.left - mid.left - halfW * s + (slotRect.width - scaledHalfW) / 2;
|
|
740
814
|
const ty = slotRect.top - mid.top + (slotRect.height - scaledH) / 2;
|
|
741
815
|
return `translate(${tx.toFixed(2)}px, ${ty.toFixed(2)}px) scale(${s.toFixed(4)})`;
|
|
@@ -879,19 +953,27 @@ export function mountBookshelf(root, options = {}) {
|
|
|
879
953
|
// Parse translate/scale from our path strings; lerp components so grow ≠ hard jump (PATH-SHAKE ~2.2s).
|
|
880
954
|
function parseTS(transform) {
|
|
881
955
|
const s = String(transform || "");
|
|
882
|
-
let tx = 0, ty = 0,
|
|
956
|
+
let tx = 0, ty = 0, sx = 1, sy = 1;
|
|
883
957
|
let m = s.match(/translate3d\(\s*([\d.-]+)px\s*,\s*([\d.-]+)px/);
|
|
884
958
|
if (m) { tx = +m[1]; ty = +m[2]; }
|
|
885
959
|
else {
|
|
886
960
|
m = s.match(/translate\(\s*([\d.-]+)px\s*,\s*([\d.-]+)px/);
|
|
887
961
|
if (m) { tx = +m[1]; ty = +m[2]; }
|
|
888
962
|
}
|
|
889
|
-
m = s.match(/scale\(\s*([\d.-]+)
|
|
890
|
-
if (m)
|
|
891
|
-
|
|
963
|
+
m = s.match(/scale\(\s*([\d.-]+)(?:\s*,\s*([\d.-]+))?\s*\)/);
|
|
964
|
+
if (m) {
|
|
965
|
+
sx = +m[1];
|
|
966
|
+
sy = m[2] != null ? +m[2] : sx;
|
|
967
|
+
}
|
|
968
|
+
return { tx, ty, sx, sy };
|
|
892
969
|
}
|
|
893
970
|
function formatTS(p) {
|
|
894
|
-
|
|
971
|
+
const sx = p.sx != null ? p.sx : p.s;
|
|
972
|
+
const sy = p.sy != null ? p.sy : sx;
|
|
973
|
+
const scale = Math.abs(sx - sy) < 1e-4
|
|
974
|
+
? `scale(${sx.toFixed(4)})`
|
|
975
|
+
: `scale(${sx.toFixed(4)}, ${sy.toFixed(4)})`;
|
|
976
|
+
return `translate(${p.tx.toFixed(2)}px, ${p.ty.toFixed(2)}px) ${scale}`;
|
|
895
977
|
}
|
|
896
978
|
function lerpTS(a, b, t) {
|
|
897
979
|
const u = Math.max(0, Math.min(1, t));
|
|
@@ -900,10 +982,68 @@ export function mountBookshelf(root, options = {}) {
|
|
|
900
982
|
return formatTS({
|
|
901
983
|
tx: pa.tx + (pb.tx - pa.tx) * u,
|
|
902
984
|
ty: pa.ty + (pb.ty - pa.ty) * u,
|
|
903
|
-
|
|
985
|
+
sx: pa.sx + (pb.sx - pa.sx) * u,
|
|
986
|
+
sy: pa.sy + (pb.sy - pa.sy) * u,
|
|
904
987
|
});
|
|
905
988
|
}
|
|
906
989
|
|
|
990
|
+
// Pose for the flyer move. Same tx/ty/sx/sy as the ux26 string, but on the
|
|
991
|
+
// individual translate and scale properties. A combined transform matrix
|
|
992
|
+
// decomposes badly when sx ≠ sy (slot height pin) and the flight stutters.
|
|
993
|
+
function pathPose(transform) {
|
|
994
|
+
const p = parseTS(transform);
|
|
995
|
+
return {
|
|
996
|
+
translate: `${p.tx.toFixed(2)}px ${p.ty.toFixed(2)}px`,
|
|
997
|
+
scale: `${p.sx.toFixed(4)} ${p.sy.toFixed(4)}`,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function applyPathPose(el, transform) {
|
|
1001
|
+
if (!el) return;
|
|
1002
|
+
const pose = pathPose(transform);
|
|
1003
|
+
el.style.transform = "none";
|
|
1004
|
+
el.style.translate = pose.translate;
|
|
1005
|
+
el.style.scale = pose.scale;
|
|
1006
|
+
}
|
|
1007
|
+
function pathKey(offset, transform) {
|
|
1008
|
+
return { offset, ...pathPose(transform) };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Close samples. T0 still carries the separate sx/sy height pin.
|
|
1012
|
+
// The old first stop was Tp3 (only 25% of the way home) at 0.28, so the
|
|
1013
|
+
// book was still nearly full-size for the first ~1s and read as overlap.
|
|
1014
|
+
function returnMoveKeys(Tfull, Tp1, T0) {
|
|
1015
|
+
return [
|
|
1016
|
+
pathKey(0, Tfull),
|
|
1017
|
+
pathKey(0.12, lerpTS(Tfull, T0, 0.35)),
|
|
1018
|
+
pathKey(0.32, lerpTS(Tfull, T0, 0.62)),
|
|
1019
|
+
pathKey(0.52, Tp1),
|
|
1020
|
+
pathKey(0.74, T0),
|
|
1021
|
+
pathKey(0.88, T0),
|
|
1022
|
+
pathKey(1, T0),
|
|
1023
|
+
];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Note stays out until the flyer leaves, then a short handoff.
|
|
1027
|
+
// Rising from 0.65 while the flyer stayed solid until 0.90 was the double exposure.
|
|
1028
|
+
function noteInKeys() {
|
|
1029
|
+
return [
|
|
1030
|
+
{ offset: 0, opacity: 0 },
|
|
1031
|
+
{ offset: 0.82, opacity: 0 },
|
|
1032
|
+
{ offset: 0.90, opacity: 1 },
|
|
1033
|
+
{ offset: 1, opacity: 1 },
|
|
1034
|
+
];
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Article is gone by ~0.12 (230ms). Shrink has already started on frame 0.
|
|
1038
|
+
function noteOutKeys() {
|
|
1039
|
+
return [
|
|
1040
|
+
{ offset: 0, opacity: 1 },
|
|
1041
|
+
{ offset: 0.05, opacity: 0.35 },
|
|
1042
|
+
{ offset: 0.12, opacity: 0 },
|
|
1043
|
+
{ offset: 1, opacity: 0 },
|
|
1044
|
+
];
|
|
1045
|
+
}
|
|
1046
|
+
|
|
907
1047
|
// ux14 geometric core: ONE diagonal arc slot-center ↔ screen-center.
|
|
908
1048
|
// X, Y, AND scale on the SAME beat — ban L-path (lift-only then slide-only).
|
|
909
1049
|
// Transforms are lerpTS(T0→Tfull) samples so WAAPI stays collinear in tx/ty/s.
|
|
@@ -1214,6 +1354,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1214
1354
|
"is-expanded",
|
|
1215
1355
|
"is-collapsing"
|
|
1216
1356
|
);
|
|
1357
|
+
clearSeatCover(el);
|
|
1217
1358
|
// Explicit removeProperty — kills commitStyles leftovers like opacity:0 / transform
|
|
1218
1359
|
[
|
|
1219
1360
|
"width",
|
|
@@ -1259,6 +1400,9 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1259
1400
|
|
|
1260
1401
|
const board = document.createElement("div");
|
|
1261
1402
|
board.className = "flyer-board";
|
|
1403
|
+
// ux27: never a solid shell behind the jacket. CSS also forces this off.
|
|
1404
|
+
board.style.opacity = "0";
|
|
1405
|
+
board.style.visibility = "hidden";
|
|
1262
1406
|
|
|
1263
1407
|
const left = document.createElement("div");
|
|
1264
1408
|
left.className = "flyer-leaf flyer-leaf-left";
|
|
@@ -1328,7 +1472,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1328
1472
|
right.style.transform = "rotateY(-8deg)";
|
|
1329
1473
|
bookEl.style.transform = "rotateX(8deg) translate3d(0,0,0)";
|
|
1330
1474
|
gutter.style.opacity = "1";
|
|
1331
|
-
flyer
|
|
1475
|
+
applyPathPose(flyer, "translate(0.00px, -4.00px) scale(1)");
|
|
1332
1476
|
flyer.style.opacity = "1";
|
|
1333
1477
|
} else {
|
|
1334
1478
|
// ux10: closed extract must look like ONE cover/spine — hide board + right page
|
|
@@ -1336,12 +1480,11 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1336
1480
|
left.style.transform = "rotateY(180deg)";
|
|
1337
1481
|
right.style.transform = "rotateY(0deg)";
|
|
1338
1482
|
right.style.opacity = "0";
|
|
1339
|
-
board.style.opacity = "0";
|
|
1340
1483
|
leftThick.style.opacity = "0";
|
|
1341
1484
|
rightThick.style.opacity = "0";
|
|
1342
1485
|
bookEl.style.transform = "rotateX(8deg) translate3d(0,0,0)";
|
|
1343
1486
|
gutter.style.opacity = "0";
|
|
1344
|
-
flyer
|
|
1487
|
+
applyPathPose(flyer, closedCoverTransform(atRect, mid));
|
|
1345
1488
|
flyer.style.opacity = "1";
|
|
1346
1489
|
}
|
|
1347
1490
|
stage.appendChild(flyer);
|
|
@@ -1351,7 +1494,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1351
1494
|
function promote(el) {
|
|
1352
1495
|
if (!el) return;
|
|
1353
1496
|
el.classList.add("is-ready");
|
|
1354
|
-
el.style.willChange = "
|
|
1497
|
+
el.style.willChange = "translate, scale, opacity";
|
|
1355
1498
|
}
|
|
1356
1499
|
|
|
1357
1500
|
function demote(el) {
|
|
@@ -1410,13 +1553,9 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1410
1553
|
noteView.classList.add("is-preparing");
|
|
1411
1554
|
noteView.classList.remove("is-open");
|
|
1412
1555
|
noteView.style.opacity = "0";
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
const m = bg && bg.match(/url\(["']?([^"')]+)["']?\)/);
|
|
1416
|
-
if (m) {
|
|
1556
|
+
const img = $("#noteCover") && $("#noteCover").querySelector(".note-cover-img");
|
|
1557
|
+
if (img && !coverImgReady(img)) {
|
|
1417
1558
|
try {
|
|
1418
|
-
const img = new Image();
|
|
1419
|
-
img.src = m[1];
|
|
1420
1559
|
const DECODE_MS = 400;
|
|
1421
1560
|
const decodeOrLoad = img.decode
|
|
1422
1561
|
? img.decode()
|
|
@@ -1463,6 +1602,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1463
1602
|
busy = true;
|
|
1464
1603
|
const myFlow = ++flowSeq;
|
|
1465
1604
|
document.body.classList.add("is-busy");
|
|
1605
|
+
root.classList.remove("is-return");
|
|
1466
1606
|
openBook = { book, cat, asSpine, fromExpanded: !!fromExpanded, openSeatRect: null };
|
|
1467
1607
|
openFromExpanded = !!fromExpanded;
|
|
1468
1608
|
|
|
@@ -1503,20 +1643,28 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1503
1643
|
sourceEl.style.transition = "none";
|
|
1504
1644
|
sourceEl.style.transform = "none"; // kill :hover translateY / is-expanded lift
|
|
1505
1645
|
sourceEl.classList.add("is-extracting"); // CSS: suppress :hover transform while extracting
|
|
1646
|
+
// Keep the real jacket on the seat until the flyer exists. Dropping
|
|
1647
|
+
// is-expanded used to reveal the bk-* fill for the frames before the flyer.
|
|
1648
|
+
if (sourceEl.classList.contains("spine") && book && book.cover) {
|
|
1649
|
+
mountSeatCover(sourceEl, book);
|
|
1650
|
+
}
|
|
1506
1651
|
if (fromExpanded) {
|
|
1507
1652
|
sourceEl.classList.remove("is-expanded", "is-collapsing");
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
"flex-shrink",
|
|
1514
|
-
].forEach((p) => {
|
|
1653
|
+
// Drop the expanded cover's width. Do NOT drop a resting spine's height:
|
|
1654
|
+
// clearing it falls through to .s-mid/.s-short (152/136), shorter than the
|
|
1655
|
+
// face height the shelf is already showing, and that short box becomes
|
|
1656
|
+
// homeRect for extract and put-back.
|
|
1657
|
+
["width", "min-width", "min-height", "flex-shrink"].forEach((p) => {
|
|
1515
1658
|
try { sourceEl.style.removeProperty(p); } catch (_) {}
|
|
1516
1659
|
});
|
|
1660
|
+
if (sourceEl.classList.contains("spine") && book && book.cover && !book.filler) {
|
|
1661
|
+
applyFaceSize(sourceEl, book);
|
|
1662
|
+
} else {
|
|
1663
|
+
try { sourceEl.style.removeProperty("height"); } catch (_) {}
|
|
1664
|
+
}
|
|
1517
1665
|
expandedId = null;
|
|
1518
1666
|
touchExpandedId = null;
|
|
1519
|
-
void sourceEl.offsetWidth; // reflow to
|
|
1667
|
+
void sourceEl.offsetWidth; // reflow to resting spine (transform already none)
|
|
1520
1668
|
} else {
|
|
1521
1669
|
// Even spine-direct: pointer may still be hovering → :hover lift would skew freeze.
|
|
1522
1670
|
void sourceEl.offsetWidth;
|
|
@@ -1636,13 +1784,13 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1636
1784
|
// Same dual: single diagonal to full — no hold-then-slide second segment.
|
|
1637
1785
|
const aDualMove = dual.animate(
|
|
1638
1786
|
[
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1787
|
+
pathKey(0, T0),
|
|
1788
|
+
pathKey(0.12, Tp1),
|
|
1789
|
+
pathKey(0.28, Tp2),
|
|
1790
|
+
pathKey(0.44, Tp3),
|
|
1791
|
+
pathKey(0.58, Tfull),
|
|
1792
|
+
pathKey(0.78, Tfull),
|
|
1793
|
+
pathKey(1, Tfull),
|
|
1646
1794
|
],
|
|
1647
1795
|
optsMotion
|
|
1648
1796
|
);
|
|
@@ -1654,14 +1802,14 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1654
1802
|
{ offset: 0, opacity: 0 },
|
|
1655
1803
|
{ offset: 0.14, opacity: 0 },
|
|
1656
1804
|
{ offset: 0.22, opacity: 1 },
|
|
1657
|
-
{ offset: 0.
|
|
1658
|
-
{ offset: 0.
|
|
1805
|
+
{ offset: 0.82, opacity: 1 },
|
|
1806
|
+
{ offset: 0.90, opacity: 0 },
|
|
1659
1807
|
{ offset: 1.0, opacity: 0 },
|
|
1660
1808
|
]
|
|
1661
1809
|
: [
|
|
1662
1810
|
{ offset: 0, opacity: 1 },
|
|
1663
|
-
{ offset: 0.
|
|
1664
|
-
{ offset: 0.
|
|
1811
|
+
{ offset: 0.82, opacity: 1 },
|
|
1812
|
+
{ offset: 0.90, opacity: 0 },
|
|
1665
1813
|
{ offset: 1.0, opacity: 0 },
|
|
1666
1814
|
],
|
|
1667
1815
|
opts
|
|
@@ -1687,6 +1835,7 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1687
1835
|
const aExtractCoverIn = null;
|
|
1688
1836
|
|
|
1689
1837
|
// Pose fade on SAME layer while diagonal continues — ban origin-swapping phase.
|
|
1838
|
+
// Board stays out: fading it in painted a wide shell behind the cover by ~1350ms.
|
|
1690
1839
|
const closedFaceKf = [
|
|
1691
1840
|
{ offset: 0, opacity: 0 },
|
|
1692
1841
|
{ offset: 0.28, opacity: 0 },
|
|
@@ -1694,7 +1843,11 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1694
1843
|
{ offset: 0.52, opacity: 1 },
|
|
1695
1844
|
{ offset: 1.0, opacity: 1 },
|
|
1696
1845
|
];
|
|
1697
|
-
|
|
1846
|
+
if (dual._board) {
|
|
1847
|
+
dual._board.style.opacity = "0";
|
|
1848
|
+
dual._board.style.visibility = "hidden";
|
|
1849
|
+
}
|
|
1850
|
+
const aBoard = null;
|
|
1698
1851
|
const aRightFace = dual._right.animate(
|
|
1699
1852
|
[
|
|
1700
1853
|
{ offset: 0, opacity: 0, transform: "rotateY(0deg)" },
|
|
@@ -1735,20 +1888,9 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1735
1888
|
)
|
|
1736
1889
|
: null;
|
|
1737
1890
|
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
{ offset: 0, opacity: 0 },
|
|
1742
|
-
{ offset: 0.65, opacity: 0 },
|
|
1743
|
-
{ offset: 0.70, opacity: 0.15 },
|
|
1744
|
-
{ offset: 0.78, opacity: 0.55 },
|
|
1745
|
-
{ offset: 0.85, opacity: 0.90 },
|
|
1746
|
-
{ offset: 0.90, opacity: 0.99 },
|
|
1747
|
-
{ offset: 0.92, opacity: 1 },
|
|
1748
|
-
{ offset: 1.0, opacity: 1 },
|
|
1749
|
-
],
|
|
1750
|
-
opts
|
|
1751
|
-
);
|
|
1891
|
+
// Note stays at 0 while the flyer owns the flight, then hands off by unlock (0.90).
|
|
1892
|
+
// FORBIDDEN: note body readable while the flyer is still solid (double exposure).
|
|
1893
|
+
const aNote = noteView.animate(noteInKeys(), opts);
|
|
1752
1894
|
|
|
1753
1895
|
const aBack = openBackdrop
|
|
1754
1896
|
? openBackdrop.animate(
|
|
@@ -1802,6 +1944,9 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1802
1944
|
try { a.cancel(); } catch (_) {}
|
|
1803
1945
|
});
|
|
1804
1946
|
} catch (_) {}
|
|
1947
|
+
// commitStyles does not keep the individual scale/translate pose. Park at
|
|
1948
|
+
// the open spread so close shrinks from 672×504 instead of jumping there.
|
|
1949
|
+
applyPathPose(dual, Tfull);
|
|
1805
1950
|
dual.style.opacity = "0";
|
|
1806
1951
|
dual.style.pointerEvents = "none";
|
|
1807
1952
|
dual.style.visibility = "visible";
|
|
@@ -1887,6 +2032,74 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1887
2032
|
return;
|
|
1888
2033
|
}
|
|
1889
2034
|
|
|
2035
|
+
// Clock first. Shelf measure, freeze, and rAF used to run before this and
|
|
2036
|
+
// the 0.12 Tfull keyframe then held 672×504 for ~240ms. Stash from open is
|
|
2037
|
+
// enough to start the return; height pin is already in those samples.
|
|
2038
|
+
// is-return puts the stage above the note so the shrink is not hidden
|
|
2039
|
+
// under an opaque article (that read as ~1s of overlap, then a jump).
|
|
2040
|
+
root.classList.add("is-return");
|
|
2041
|
+
const closeMotion = { duration: CLOSE_MS, easing: "linear", fill: "forwards" };
|
|
2042
|
+
const closeOpts = { duration: CLOSE_MS, easing: "linear", fill: "forwards" };
|
|
2043
|
+
const stashEarly = openBook.pathTs;
|
|
2044
|
+
let earlyMove = null;
|
|
2045
|
+
let earlyNote = null;
|
|
2046
|
+
let earlyLeft = null;
|
|
2047
|
+
let earlyRight = null;
|
|
2048
|
+
const heldEarly = openBook.heldFlyer;
|
|
2049
|
+
if (
|
|
2050
|
+
heldEarly &&
|
|
2051
|
+
heldEarly.isConnected &&
|
|
2052
|
+
stashEarly &&
|
|
2053
|
+
stashEarly.Tfull &&
|
|
2054
|
+
stashEarly.T0 &&
|
|
2055
|
+
stashEarly.Tp1 &&
|
|
2056
|
+
stashEarly.Tp2 &&
|
|
2057
|
+
stashEarly.Tp3
|
|
2058
|
+
) {
|
|
2059
|
+
heldEarly.style.visibility = "visible";
|
|
2060
|
+
heldEarly.style.zIndex = "70";
|
|
2061
|
+
heldEarly.style.opacity = "1";
|
|
2062
|
+
if (heldEarly._board) {
|
|
2063
|
+
heldEarly._board.style.opacity = "0";
|
|
2064
|
+
heldEarly._board.style.visibility = "hidden";
|
|
2065
|
+
}
|
|
2066
|
+
promote(heldEarly);
|
|
2067
|
+
earlyMove = heldEarly.animate(
|
|
2068
|
+
returnMoveKeys(stashEarly.Tfull, stashEarly.Tp1, stashEarly.T0),
|
|
2069
|
+
closeMotion
|
|
2070
|
+
);
|
|
2071
|
+
earlyNote = noteView.animate(noteOutKeys(), closeOpts);
|
|
2072
|
+
// Backup if the fade is dropped: the article must not still be up after ~230ms.
|
|
2073
|
+
wait(Math.round(CLOSE_MS * 0.12)).then(() => {
|
|
2074
|
+
if (myFlow !== flowSeq || !noteView) return;
|
|
2075
|
+
noteView.style.opacity = "0";
|
|
2076
|
+
});
|
|
2077
|
+
if (heldEarly._left) {
|
|
2078
|
+
earlyLeft = heldEarly._left.animate(
|
|
2079
|
+
[
|
|
2080
|
+
{ offset: 0, transform: "rotateY(8deg)" },
|
|
2081
|
+
{ offset: 0.08, transform: "rotateY(20deg)" },
|
|
2082
|
+
{ offset: 0.16, transform: "rotateY(70deg)" },
|
|
2083
|
+
{ offset: 0.24, transform: "rotateY(140deg)" },
|
|
2084
|
+
{ offset: 0.32, transform: "rotateY(180deg)" },
|
|
2085
|
+
{ offset: 1.0, transform: "rotateY(180deg)" },
|
|
2086
|
+
],
|
|
2087
|
+
closeMotion
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
if (heldEarly._right) {
|
|
2091
|
+
earlyRight = heldEarly._right.animate(
|
|
2092
|
+
[
|
|
2093
|
+
{ offset: 0, opacity: 1, transform: "rotateY(-8deg)" },
|
|
2094
|
+
{ offset: 0.20, opacity: 1, transform: "rotateY(-4deg)" },
|
|
2095
|
+
{ offset: 0.32, opacity: 0, transform: "rotateY(0deg)" },
|
|
2096
|
+
{ offset: 1.0, opacity: 0, transform: "rotateY(0deg)" },
|
|
2097
|
+
],
|
|
2098
|
+
closeMotion
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1890
2103
|
const mid = midRect(book);
|
|
1891
2104
|
const originX = mid.left + mid.width / 2;
|
|
1892
2105
|
const originY = mid.top + mid.height / 2;
|
|
@@ -1910,8 +2123,14 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1910
2123
|
slot = $(slotSel);
|
|
1911
2124
|
}
|
|
1912
2125
|
if (!slot) {
|
|
2126
|
+
try { earlyMove && earlyMove.cancel(); } catch (_) {}
|
|
2127
|
+
try { earlyNote && earlyNote.cancel(); } catch (_) {}
|
|
2128
|
+
try { earlyLeft && earlyLeft.cancel(); } catch (_) {}
|
|
2129
|
+
try { earlyRight && earlyRight.cancel(); } catch (_) {}
|
|
2130
|
+
root.classList.remove("is-return");
|
|
1913
2131
|
noteView.classList.remove("is-open", "is-preparing");
|
|
1914
2132
|
noteView.hidden = true;
|
|
2133
|
+
noteView.style.opacity = "";
|
|
1915
2134
|
openBook = null;
|
|
1916
2135
|
busy = false;
|
|
1917
2136
|
document.body.classList.remove("is-busy");
|
|
@@ -1932,9 +2151,17 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1932
2151
|
].forEach((p) => {
|
|
1933
2152
|
try { slot.style.removeProperty(p); } catch (_) {}
|
|
1934
2153
|
});
|
|
1935
|
-
slot.classList.remove("is-collapsing", "is-
|
|
2154
|
+
slot.classList.remove("is-collapsing", "is-expanded");
|
|
2155
|
+
// Keep the seat empty until handover. Dropping is-placeholder here let the
|
|
2156
|
+
// bk-* fill paint a solid rectangle under the returning cover.
|
|
2157
|
+
slot.classList.add("is-placeholder");
|
|
1936
2158
|
expandedId = null;
|
|
1937
|
-
|
|
2159
|
+
// Spines: re-pin the resting face BEFORE reading the seat. A cleared inline
|
|
2160
|
+
// height is the shorter size-class box, and put-back would keep it.
|
|
2161
|
+
if (targetMode === "spines" && book && book.cover && !book.filler) {
|
|
2162
|
+
applyFaceSize(slot, book);
|
|
2163
|
+
}
|
|
2164
|
+
void slot.offsetWidth; // reflow to resting spine after class/inline clear
|
|
1938
2165
|
const naturalW = slot.offsetWidth;
|
|
1939
2166
|
const naturalH = slot.offsetHeight;
|
|
1940
2167
|
slot.style.flexShrink = "0";
|
|
@@ -1962,13 +2189,13 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1962
2189
|
});
|
|
1963
2190
|
shelfView.classList.add("is-frozen");
|
|
1964
2191
|
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
2192
|
+
// Note stays in the tree so its fade can finish, but must not cover the flyer.
|
|
2193
|
+
// is-return (set before the clock) owns stacking. Do not pin z-index 75.
|
|
1967
2194
|
noteView.classList.add("is-open");
|
|
1968
2195
|
noteView.classList.remove("is-preparing");
|
|
1969
2196
|
noteView.hidden = false;
|
|
1970
|
-
noteView.style.opacity = "1";
|
|
1971
|
-
noteView.style.zIndex = "
|
|
2197
|
+
if (!earlyNote) noteView.style.opacity = "1";
|
|
2198
|
+
noteView.style.zIndex = "";
|
|
1972
2199
|
|
|
1973
2200
|
unparkShelf();
|
|
1974
2201
|
shelfView.style.opacity = "1";
|
|
@@ -1990,6 +2217,17 @@ export function mountBookshelf(root, options = {}) {
|
|
|
1990
2217
|
frozenHome.width = natW;
|
|
1991
2218
|
frozenHome.height = natH;
|
|
1992
2219
|
frozenHome.top = Math.round(bottom - natH);
|
|
2220
|
+
} else if (targetMode === "spines" && naturalH > 0) {
|
|
2221
|
+
// Seat and flyer use the resting spine just measured, not a short
|
|
2222
|
+
// cover-frame stash from extract.
|
|
2223
|
+
const bottom = frozenHome.top + frozenHome.height;
|
|
2224
|
+
frozenHome.width = naturalW;
|
|
2225
|
+
frozenHome.height = naturalH;
|
|
2226
|
+
frozenHome.top = Math.round(bottom - naturalH);
|
|
2227
|
+
if (openBook) {
|
|
2228
|
+
openBook.homeRect = copyRect(frozenHome);
|
|
2229
|
+
openBook.spineHome = copyRect(frozenHome);
|
|
2230
|
+
}
|
|
1993
2231
|
}
|
|
1994
2232
|
slot.style.width = frozenHome.width + "px";
|
|
1995
2233
|
slot.style.minWidth = frozenHome.width + "px";
|
|
@@ -2027,8 +2265,10 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2027
2265
|
}
|
|
2028
2266
|
flyer.style.zIndex = "70";
|
|
2029
2267
|
flyer.style.visibility = "visible";
|
|
2030
|
-
|
|
2031
|
-
|
|
2268
|
+
if (!earlyMove) {
|
|
2269
|
+
flyer.style.opacity = "0";
|
|
2270
|
+
applyPathPose(flyer, Tfull); // start at open end pose (transform-aligned)
|
|
2271
|
+
}
|
|
2032
2272
|
promote(flyer);
|
|
2033
2273
|
if (flyer._cover) {
|
|
2034
2274
|
flyer._cover.classList.add("is-cover-ready");
|
|
@@ -2050,7 +2290,10 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2050
2290
|
flyer._right.style.transform = "rotateY(-8deg)";
|
|
2051
2291
|
flyer._right.style.opacity = "1";
|
|
2052
2292
|
}
|
|
2053
|
-
if (flyer._board)
|
|
2293
|
+
if (flyer._board) {
|
|
2294
|
+
flyer._board.style.opacity = "0";
|
|
2295
|
+
flyer._board.style.visibility = "hidden";
|
|
2296
|
+
}
|
|
2054
2297
|
if (flyer._leftThick) flyer._leftThick.style.opacity = "1";
|
|
2055
2298
|
if (flyer._rightThick) flyer._rightThick.style.opacity = "1";
|
|
2056
2299
|
if (flyer._gutter) flyer._gutter.style.opacity = "1";
|
|
@@ -2098,13 +2341,10 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2098
2341
|
`translate3d(${spineOutClose.x.toFixed(2)}px, ${spineOutClose.y.toFixed(2)}px, 0)`;
|
|
2099
2342
|
}
|
|
2100
2343
|
|
|
2101
|
-
//
|
|
2102
|
-
|
|
2103
|
-
new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))),
|
|
2104
|
-
wait(32),
|
|
2105
|
-
]);
|
|
2344
|
+
// Start the reverse on this frame. A pre-clock rAF left the spread parked at
|
|
2345
|
+
// 672×504 while the note was still up (the ~240ms close-start hitch).
|
|
2106
2346
|
void readyPulse;
|
|
2107
|
-
flyer.style.opacity = "1";
|
|
2347
|
+
if (!earlyMove) flyer.style.opacity = "1";
|
|
2108
2348
|
|
|
2109
2349
|
// ux14: TIME-REVERSE of open pathTs — ban separately computed return / end-segment seek.
|
|
2110
2350
|
const PUT_MS = 380;
|
|
@@ -2113,41 +2353,20 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2113
2353
|
const xfadeFrom = 1 - XFADE_MS / CLOSE_MS; // ~0.89
|
|
2114
2354
|
const slotInAt = Math.max(0.86, xfadeFrom - 0.04);
|
|
2115
2355
|
const flyerFadeAt = Math.min(0.97, slotInAt + 0.06);
|
|
2116
|
-
const closeOpts = { duration: CLOSE_MS, easing: "linear", fill: "forwards" };
|
|
2117
|
-
const closeMotion = { duration: CLOSE_MS, easing: "linear", fill: "forwards" };
|
|
2118
2356
|
|
|
2119
2357
|
resetVeil();
|
|
2120
2358
|
|
|
2121
|
-
//
|
|
2122
|
-
const aNoteOut = noteView.animate(
|
|
2123
|
-
[
|
|
2124
|
-
{ offset: 0, opacity: 1 },
|
|
2125
|
-
{ offset: 0.06, opacity: 0.85 },
|
|
2126
|
-
{ offset: 0.12, opacity: 0.45 },
|
|
2127
|
-
{ offset: 0.22, opacity: 0 },
|
|
2128
|
-
{ offset: 1.0, opacity: 0 },
|
|
2129
|
-
],
|
|
2130
|
-
closeOpts
|
|
2131
|
-
);
|
|
2359
|
+
// Note fades out in the first ~230ms. Shrink already started on frame 0.
|
|
2360
|
+
const aNoteOut = earlyNote || noteView.animate(noteOutKeys(), closeOpts);
|
|
2132
2361
|
|
|
2133
|
-
//
|
|
2134
|
-
//
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
[
|
|
2138
|
-
{ offset: 0, transform: Tfull },
|
|
2139
|
-
{ offset: 0.12, transform: Tfull },
|
|
2140
|
-
{ offset: 0.28, transform: Tp3 },
|
|
2141
|
-
{ offset: 0.44, transform: Tp2 },
|
|
2142
|
-
{ offset: 0.60, transform: Tp1 },
|
|
2143
|
-
{ offset: 0.78, transform: T0 },
|
|
2144
|
-
{ offset: 0.90, transform: T0 },
|
|
2145
|
-
{ offset: 1.0, transform: T0 },
|
|
2146
|
-
],
|
|
2362
|
+
// Shrink starts at offset 0 and is already well under way by ~400ms.
|
|
2363
|
+
// A first stop on Tp3 (25% home) at 0.28 left the book looking full for ~1s.
|
|
2364
|
+
const aFlyerMove = earlyMove || flyer.animate(
|
|
2365
|
+
returnMoveKeys(Tfull, Tp1, T0),
|
|
2147
2366
|
closeMotion
|
|
2148
2367
|
);
|
|
2149
2368
|
// Reverse hinge on SAME layer while shrink (open was 0.32→0.68 hinge).
|
|
2150
|
-
const aLeftClose = flyer._left
|
|
2369
|
+
const aLeftClose = earlyLeft || (flyer._left
|
|
2151
2370
|
? flyer._left.animate(
|
|
2152
2371
|
[
|
|
2153
2372
|
{ offset: 0, transform: "rotateY(8deg)" },
|
|
@@ -2159,8 +2378,8 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2159
2378
|
],
|
|
2160
2379
|
closeMotion
|
|
2161
2380
|
)
|
|
2162
|
-
: null;
|
|
2163
|
-
const aRightClose = flyer._right
|
|
2381
|
+
: null);
|
|
2382
|
+
const aRightClose = earlyRight || (flyer._right
|
|
2164
2383
|
? flyer._right.animate(
|
|
2165
2384
|
[
|
|
2166
2385
|
{ offset: 0, opacity: 1, transform: "rotateY(-8deg)" },
|
|
@@ -2170,14 +2389,14 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2170
2389
|
],
|
|
2171
2390
|
closeMotion
|
|
2172
2391
|
)
|
|
2173
|
-
: null;
|
|
2392
|
+
: null);
|
|
2174
2393
|
const closedFaceOut = [
|
|
2175
2394
|
{ offset: 0, opacity: 1 },
|
|
2176
2395
|
{ offset: 0.18, opacity: 0.7 },
|
|
2177
2396
|
{ offset: 0.28, opacity: 0 },
|
|
2178
2397
|
{ offset: 1.0, opacity: 0 },
|
|
2179
2398
|
];
|
|
2180
|
-
const aBoardClose =
|
|
2399
|
+
const aBoardClose = null;
|
|
2181
2400
|
const aThickLClose = flyer._leftThick ? flyer._leftThick.animate(closedFaceOut, closeOpts) : null;
|
|
2182
2401
|
const aThickRClose = flyer._rightThick ? flyer._rightThick.animate(closedFaceOut, closeOpts) : null;
|
|
2183
2402
|
const aGutterClose = flyer._gutter
|
|
@@ -2218,9 +2437,11 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2218
2437
|
const aFlyerFade = flyer.animate(
|
|
2219
2438
|
targetMode === "spines"
|
|
2220
2439
|
? [
|
|
2440
|
+
// Stay through the shrink. Cutting at 0.58 dropped a still-large
|
|
2441
|
+
// cover and made the first ~1.1s look like overlap, then a pop.
|
|
2221
2442
|
{ offset: 0, opacity: 1 },
|
|
2222
|
-
{ offset: 0.
|
|
2223
|
-
{ offset: 0.
|
|
2443
|
+
{ offset: 0.68, opacity: 1 },
|
|
2444
|
+
{ offset: 0.80, opacity: 0 },
|
|
2224
2445
|
{ offset: 1.0, opacity: 0 },
|
|
2225
2446
|
]
|
|
2226
2447
|
: [
|
|
@@ -2252,10 +2473,10 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2252
2473
|
? spineSlotClose.animate(
|
|
2253
2474
|
[
|
|
2254
2475
|
{ offset: 0, transform: outClose, opacity: 0 },
|
|
2255
|
-
{ offset: 0.
|
|
2256
|
-
{ offset: 0.
|
|
2257
|
-
{ offset: 0.
|
|
2258
|
-
{ offset: 0.
|
|
2476
|
+
{ offset: 0.66, transform: outClose, opacity: 0 },
|
|
2477
|
+
{ offset: 0.74, transform: outClose, opacity: 1 },
|
|
2478
|
+
{ offset: 0.86, transform: "translate3d(0px, 0px, 0)", opacity: 1 },
|
|
2479
|
+
{ offset: 0.94, transform: "translate3d(0px, 0px, 0)", opacity: 1 },
|
|
2259
2480
|
{ offset: 1.0, transform: "translate3d(0px, 0px, 0)", opacity: 0 },
|
|
2260
2481
|
],
|
|
2261
2482
|
closeMotion
|
|
@@ -2270,8 +2491,8 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2270
2491
|
targetMode === "spines"
|
|
2271
2492
|
? [
|
|
2272
2493
|
{ offset: 0, opacity: 1 },
|
|
2273
|
-
{ offset: 0.
|
|
2274
|
-
{ offset: 0.
|
|
2494
|
+
{ offset: 0.68, opacity: 1 },
|
|
2495
|
+
{ offset: 0.80, opacity: 0 },
|
|
2275
2496
|
{ offset: 1, opacity: 0 },
|
|
2276
2497
|
]
|
|
2277
2498
|
: [
|
|
@@ -2293,6 +2514,13 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2293
2514
|
slot.style.backgroundColor = "";
|
|
2294
2515
|
}
|
|
2295
2516
|
|
|
2517
|
+
// Seat may show its real face only as the flyer hands off. Until then the
|
|
2518
|
+
// placeholder ban (transparent, invisible) blocks the solid bk plate.
|
|
2519
|
+
wait(Math.round(CLOSE_MS * Math.max(0, slotInAt - 0.02))).then(() => {
|
|
2520
|
+
if (myFlow !== flowSeq || !slot.isConnected) return;
|
|
2521
|
+
slot.classList.remove("is-placeholder");
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2296
2524
|
const aSlotIn = slot.animate(
|
|
2297
2525
|
[
|
|
2298
2526
|
{ offset: 0, opacity: 0 },
|
|
@@ -2347,10 +2575,11 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2347
2575
|
try { a.cancel(); } catch (_) {}
|
|
2348
2576
|
});
|
|
2349
2577
|
} catch (_) {}
|
|
2350
|
-
flyer
|
|
2578
|
+
applyPathPose(flyer, T0); // stay locked on frozen home until slot fully owns pixels
|
|
2351
2579
|
flyer.style.opacity = "0";
|
|
2352
2580
|
slot.style.opacity = "1";
|
|
2353
2581
|
slot.style.transform = "none";
|
|
2582
|
+
root.classList.remove("is-return");
|
|
2354
2583
|
noteView.classList.remove("is-open", "is-preparing");
|
|
2355
2584
|
noteView.hidden = true;
|
|
2356
2585
|
noteView.style.opacity = "";
|
|
@@ -2376,6 +2605,21 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2376
2605
|
openFromExpanded = false;
|
|
2377
2606
|
restoreSlot(slot, book);
|
|
2378
2607
|
try { slot.classList.remove("is-extracting"); } catch (_) {}
|
|
2608
|
+
// Pointer is often still over the returned spine. :hover is translateY(-8px)
|
|
2609
|
+
// scale(1.06), which measures ~8–11px taller than neighbors. Hold that off
|
|
2610
|
+
// until the pointer leaves; drop immediately if it is not hovering.
|
|
2611
|
+
if (targetMode === "spines" && slot) {
|
|
2612
|
+
slot.classList.add("is-seat");
|
|
2613
|
+
const releaseSeat = () => {
|
|
2614
|
+
slot.classList.remove("is-seat");
|
|
2615
|
+
slot.removeEventListener("pointerleave", releaseSeat);
|
|
2616
|
+
};
|
|
2617
|
+
slot.addEventListener("pointerleave", releaseSeat);
|
|
2618
|
+
requestAnimationFrame(() => {
|
|
2619
|
+
if (!slot.isConnected) return;
|
|
2620
|
+
if (!slot.matches(":hover")) releaseSeat();
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2379
2623
|
// ux23: drop close-path inline cover pin; resting .img-* CSS owns the seat again
|
|
2380
2624
|
if (targetMode === "covers" && slot) {
|
|
2381
2625
|
["background-image", "background-size", "background-position", "background-repeat", "background-color"].forEach((p) => {
|
|
@@ -2445,6 +2689,47 @@ export function mountBookshelf(root, options = {}) {
|
|
|
2445
2689
|
|
|
2446
2690
|
listen($("#backBtn"), "click", () => closeBookFlow());
|
|
2447
2691
|
|
|
2692
|
+
// Browser Back from the full note restores this page from bfcache, still open.
|
|
2693
|
+
// Land on the shelf at the scroll we saved, without replaying the close clock.
|
|
2694
|
+
listen(window, "pageshow", (event) => {
|
|
2695
|
+
if (!event.persisted || !openBook) return;
|
|
2696
|
+
const home = scrollLock
|
|
2697
|
+
? { x: scrollLock.x, y: scrollLock.y }
|
|
2698
|
+
: { x: scrollHome.x, y: scrollHome.y };
|
|
2699
|
+
try { releaseScrollLock(); } catch (_) {}
|
|
2700
|
+
openBook = null;
|
|
2701
|
+
openFromExpanded = false;
|
|
2702
|
+
expandedId = null;
|
|
2703
|
+
touchExpandedId = null;
|
|
2704
|
+
busy = false;
|
|
2705
|
+
flowSeq += 1;
|
|
2706
|
+
document.body.classList.remove("is-busy");
|
|
2707
|
+
noteView.classList.remove("is-open", "is-preparing");
|
|
2708
|
+
noteView.hidden = true;
|
|
2709
|
+
noteView.style.opacity = "";
|
|
2710
|
+
noteView.style.zIndex = "";
|
|
2711
|
+
if (stage) stage.replaceChildren();
|
|
2712
|
+
resetVeil();
|
|
2713
|
+
resetBackdrop();
|
|
2714
|
+
unparkShelf();
|
|
2715
|
+
shelfView.classList.remove("is-frozen", "is-dim", "is-parked");
|
|
2716
|
+
shelfView.style.opacity = "";
|
|
2717
|
+
try { shelfView.style.removeProperty("min-height"); } catch (_) {}
|
|
2718
|
+
renderShelf();
|
|
2719
|
+
writeScroll(home);
|
|
2720
|
+
requestAnimationFrame(() => writeScroll(home));
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
// Leaving for the post page: remember the shelf scroll so Back lands on it.
|
|
2724
|
+
listen(root, "click", (event) => {
|
|
2725
|
+
const target = event.target;
|
|
2726
|
+
if (!target || !target.closest) return;
|
|
2727
|
+
const link = target.closest("a.flyer-page-more, a.note-read");
|
|
2728
|
+
if (!link || !root.contains(link)) return;
|
|
2729
|
+
const y = scrollLock ? scrollLock.y : readScroll().y;
|
|
2730
|
+
try { sessionStorage.setItem("garden-bookshelf-scroll", String(y)); } catch (_) {}
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2448
2733
|
// Escape closes note
|
|
2449
2734
|
listen(window, "keydown", (e) => {
|
|
2450
2735
|
if (e.key === "Escape" && openBook && !busy) closeBookFlow();
|