hd-garden 0.9.1 → 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.
@@ -235,18 +235,22 @@
235
235
  z-index: 2;
236
236
  }
237
237
  }
238
- .bookshelf-demo .cover.is-placeholder {
239
- /* ux13+ux24: invisible seat — no gray/wood/bk plate while flyer is out */
238
+ .bookshelf-demo .cover.is-placeholder,
239
+ .bookshelf-demo .spine.is-placeholder {
240
+ /* Empty seat while the flyer owns the motion: shelf surface only.
241
+ A bk-* fill here is the gray/lavender plate behind the real cover. */
240
242
  opacity: 0 !important;
241
243
  visibility: hidden !important;
242
244
  background-color: transparent !important;
243
245
  background-image: none !important;
244
- filter: none;
246
+ filter: none !important;
245
247
  pointer-events: none;
246
- box-shadow: none;
248
+ box-shadow: none !important;
249
+ color: transparent !important;
247
250
  transform: none !important;
248
251
  }
249
- .bookshelf-demo .cover.is-placeholder > .cover-art {
252
+ .bookshelf-demo .cover.is-placeholder > .cover-art,
253
+ .bookshelf-demo .spine.is-placeholder > .cover-art {
250
254
  visibility: hidden !important;
251
255
  opacity: 0 !important;
252
256
  }
@@ -342,15 +346,7 @@
342
346
  z-index: 2;
343
347
  }
344
348
  }
345
- .bookshelf-demo .spine.is-placeholder {
346
- /* ux13: invisible seat — ban gray ghost plate */
347
- opacity: 0;
348
- filter: none;
349
- pointer-events: none;
350
- box-shadow: none;
351
- transform: none !important;
352
- }
353
- .bookshelf-demo .spine.is-placeholder span { opacity: 0; }
349
+ .bookshelf-demo .spine.is-placeholder span { opacity: 0 !important; visibility: hidden !important; }
354
350
  .bookshelf-demo .cover.is-placeholder.is-landing,
355
351
  .bookshelf-demo .spine.is-placeholder.is-landing {
356
352
  opacity: 0;
@@ -359,6 +355,25 @@
359
355
 
360
356
  /* Mode C: temporary expand on spine wall */
361
357
  /* resting spines: solid hue only; cover image only when expanded */
358
+ /* Expanded / extracting face must not fall back to bk-* (#513688 etc.).
359
+ The jacket is .cover-art. A missing image stays cream shelf, not a shell pad. */
360
+ .bookshelf-demo .spine.is-expanded,
361
+ .bookshelf-demo .spine.is-extracting {
362
+ background-color: transparent !important;
363
+ }
364
+ .bookshelf-demo .spine.is-expanded > .cover-art,
365
+ .bookshelf-demo .spine.is-extracting > .cover-art {
366
+ position: absolute;
367
+ inset: 0;
368
+ width: 100%;
369
+ height: 100%;
370
+ object-fit: contain;
371
+ object-position: center top;
372
+ display: block;
373
+ pointer-events: none;
374
+ z-index: 1;
375
+ border-radius: inherit;
376
+ }
362
377
  .bookshelf-demo .spine[class*="img-"] {
363
378
  background-image: none !important;
364
379
  }
@@ -559,15 +574,32 @@
559
574
  align-items: flex-start;
560
575
  margin-bottom: 36px;
561
576
  }
577
+ .bookshelf-demo .note-meta {
578
+ min-width: 0;
579
+ flex: 1 1 auto;
580
+ }
581
+ /* Settled note header: size the box to the picture. A fixed 2:3 frame with
582
+ background-size:cover center-crops landscape heroes (Obsidian → "sid"). */
562
583
  .bookshelf-demo .note-cover {
563
- width: 120px;
564
- height: 180px;
565
- flex-shrink: 0;
584
+ width: max-content;
585
+ max-width: min(420px, 46%);
586
+ height: auto;
587
+ flex: 0 0 auto;
566
588
  border-radius: 2px 3px 0 0;
567
- background-size: cover;
568
- background-position: center top;
569
- background-color: #2a2a2a;
589
+ background-color: transparent;
590
+ background-image: none;
570
591
  box-shadow: 0 8px 20px rgba(40,30,20,0.12);
592
+ line-height: 0;
593
+ }
594
+ .bookshelf-demo .note-cover-img {
595
+ display: block;
596
+ width: auto;
597
+ height: auto;
598
+ max-width: 100%;
599
+ max-height: 180px;
600
+ object-fit: contain;
601
+ object-position: center;
602
+ border-radius: inherit;
571
603
  }
572
604
  .bookshelf-demo .note-cat {
573
605
  margin: 0 0 8px;
@@ -713,21 +745,16 @@
713
745
  backface-visibility: hidden;
714
746
  }
715
747
 
716
- /* Thin cover board peeking under page block (thickness cue) */
748
+ /* ux27 plate ban: this used to be a full-spread shell (fallback #6a7a98 / #5a6a88,
749
+ and shell-hi of dark covers reads the same gray-lavender). It sat wider than
750
+ the jacket during lift and put-back. Fore-edge stays on .flyer-thickness. */
717
751
  .bookshelf-demo .flyer-board {
718
- position: absolute;
719
- inset: -2% -1.2% -3.5% -1.2%;
720
- border-radius: 3px 4px 2px 2px;
721
- /* ux26: shell = this book's cover dominant (via --shell); ban site-wide brown */
722
- background: linear-gradient(
723
- 180deg,
724
- var(--shell-hi, #6a7a98) 0%,
725
- var(--shell, #5a6a88) 55%,
726
- var(--shell-lo, #3a4a68) 100%
727
- );
728
- box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset;
729
- transform: translateZ(-8px);
730
- z-index: 0;
752
+ display: none !important;
753
+ background: none !important;
754
+ background-color: transparent !important;
755
+ box-shadow: none !important;
756
+ opacity: 0 !important;
757
+ visibility: hidden !important;
731
758
  }
732
759
 
733
760
  .bookshelf-demo .flyer-leaf {
@@ -759,7 +786,7 @@
759
786
  /* Cover on BACK of left leaf — camera-facing when leaf folded (180°) */
760
787
  .bookshelf-demo .flyer-face-cover {
761
788
  transform: rotateY(180deg);
762
- background-color: transparent; /* ux24: no preset wood plate */
789
+ background-color: transparent !important; /* ux24/ux27: bk-* must not fill behind the img */
763
790
  /* No background-image here — .img-* / inline applyCoverImage owns the photo.
764
791
  Gradients-only used to override img classes → flat brown/gray close flash. */
765
792
  box-shadow:
@@ -782,17 +809,18 @@
782
809
  }
783
810
  .bookshelf-demo .flyer-spine-label { pointer-events: none; }
784
811
 
785
- /* ux15 plate kept inert; ux16 uses full-height .flyer-spine-slot instead */
812
+ /* ux15 plate kept inert; ux16 uses full-height .flyer-spine-slot instead.
813
+ Never paint the lavender shell fallback if a leftover plate node is mounted. */
786
814
  .bookshelf-demo .flyer-spine-land-plate {
787
815
  position: absolute;
788
816
  inset: 0;
789
817
  pointer-events: none;
790
818
  border-radius: inherit;
791
819
  z-index: 1;
792
- display: none;
793
- }
794
- .bookshelf-demo .flyer-spine-land-plate:not([class*="bk-"]) {
795
- background: linear-gradient(180deg, var(--shell-hi, #6a7a98) 0%, var(--shell, #5a6a88) 100%);
820
+ display: none !important;
821
+ background: none !important;
822
+ background-color: transparent !important;
823
+ opacity: 0 !important;
796
824
  }
797
825
 
798
826
  /* ux16: exact homeRect standing spine — full height, spine width, slides out/in */
@@ -1002,6 +1030,18 @@
1002
1030
  overflow: auto;
1003
1031
  }
1004
1032
 
1033
+ /* Close: flyer owns the motion. The note used to stay at z-index 75, so the
1034
+ article sat on top of the return for the whole fade and the shrink was hidden. */
1035
+ .bookshelf-demo.is-return .stage { z-index: 80; }
1036
+ .bookshelf-demo.is-return .note-view,
1037
+ .bookshelf-demo.is-return .note-view.is-preparing,
1038
+ .bookshelf-demo.is-return .note-view.is-open { z-index: 60; }
1039
+ .bookshelf-demo.is-site.is-return .stage { z-index: 36; }
1040
+ .bookshelf-demo.is-site.is-return .open-backdrop { z-index: 30; }
1041
+ .bookshelf-demo.is-site.is-return .note-view,
1042
+ .bookshelf-demo.is-site.is-return .note-view.is-preparing,
1043
+ .bookshelf-demo.is-site.is-return .note-view.is-open { z-index: 31; }
1044
+
1005
1045
  .bookshelf-demo .shelf-view.is-dim {
1006
1046
  /* Backdrop covers shelf — dim is unused during open; keep inert */
1007
1047
  opacity: 1;
@@ -1059,7 +1099,7 @@
1059
1099
  inset: 0;
1060
1100
  border-radius: inherit;
1061
1101
  overflow: hidden;
1062
- background-color: transparent; /* ux24: no preset wood plate */
1102
+ background-color: transparent !important; /* ux24/ux27: no solid shell behind the img */
1063
1103
  background-size: cover;
1064
1104
  background-position: center top;
1065
1105
  background-repeat: no-repeat;
@@ -1162,7 +1202,13 @@
1162
1202
  .bookshelf-demo .spine.is-expanded.is-collapsing.s-mid { width: 24px; height: 134px; }
1163
1203
  .bookshelf-demo .spine.is-expanded.is-collapsing.s-short { width: 20px; height: 120px; }
1164
1204
  .bookshelf-demo .note-hero { flex-direction: column; gap: 16px; }
1165
- .bookshelf-demo .note-cover { width: 100px; height: 150px; }
1205
+ .bookshelf-demo .note-cover { width: max-content; max-width: 100%; }
1206
+ .bookshelf-demo .note-cover-img {
1207
+ width: auto;
1208
+ max-width: 100%;
1209
+ max-height: 180px;
1210
+ height: auto;
1211
+ }
1166
1212
  .bookshelf-demo .qa-hint { width: 100%; margin-left: 0; margin-top: 4px; }
1167
1213
  }
1168
1214
 
@@ -312,6 +312,30 @@ export function mountBookshelf(root, options = {}) {
312
312
  return Math.max(96, Math.round(w * (nat.h / nat.w)));
313
313
  }
314
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
+
315
339
  function applyFaceSize(el, book, { expanded = false } = {}) {
316
340
  if (!el || !book) return;
317
341
  const faceH = bookFaceHeight(book);
@@ -580,6 +604,7 @@ export function mountBookshelf(root, options = {}) {
580
604
  function restoreRestingSpine(el, bookId) {
581
605
  if (!el) return;
582
606
  el.classList.remove("is-expanded", "is-collapsing");
607
+ clearSeatCover(el);
583
608
  const found = bookById(bookId);
584
609
  if (found && found.book.cover && !found.book.filler) {
585
610
  applyFaceSize(el, found.book, { expanded: false });
@@ -621,6 +646,7 @@ export function mountBookshelf(root, options = {}) {
621
646
  el.classList.remove("is-collapsing");
622
647
  el.classList.add("is-expanded");
623
648
  applyFaceSize(el, found.book, { expanded: true });
649
+ mountSeatCover(el, found.book);
624
650
  }
625
651
  }
626
652
 
@@ -686,7 +712,20 @@ export function mountBookshelf(root, options = {}) {
686
712
  $("#noteTitle").textContent = book.title;
687
713
  $("#noteAuthor").textContent = book.author || "";
688
714
  const coverEl = $("#noteCover");
689
- coverEl.className = `note-cover ${bkClass(book)} ${coverClass(book)}`;
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
+ }
690
729
  const note = NOTES[book.note];
691
730
  const read = $("#noteRead");
692
731
  if (read) {
@@ -969,6 +1008,42 @@ export function mountBookshelf(root, options = {}) {
969
1008
  return { offset, ...pathPose(transform) };
970
1009
  }
971
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
+
972
1047
  // ux14 geometric core: ONE diagonal arc slot-center ↔ screen-center.
973
1048
  // X, Y, AND scale on the SAME beat — ban L-path (lift-only then slide-only).
974
1049
  // Transforms are lerpTS(T0→Tfull) samples so WAAPI stays collinear in tx/ty/s.
@@ -1279,6 +1354,7 @@ export function mountBookshelf(root, options = {}) {
1279
1354
  "is-expanded",
1280
1355
  "is-collapsing"
1281
1356
  );
1357
+ clearSeatCover(el);
1282
1358
  // Explicit removeProperty — kills commitStyles leftovers like opacity:0 / transform
1283
1359
  [
1284
1360
  "width",
@@ -1324,6 +1400,9 @@ export function mountBookshelf(root, options = {}) {
1324
1400
 
1325
1401
  const board = document.createElement("div");
1326
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";
1327
1406
 
1328
1407
  const left = document.createElement("div");
1329
1408
  left.className = "flyer-leaf flyer-leaf-left";
@@ -1401,7 +1480,6 @@ export function mountBookshelf(root, options = {}) {
1401
1480
  left.style.transform = "rotateY(180deg)";
1402
1481
  right.style.transform = "rotateY(0deg)";
1403
1482
  right.style.opacity = "0";
1404
- board.style.opacity = "0";
1405
1483
  leftThick.style.opacity = "0";
1406
1484
  rightThick.style.opacity = "0";
1407
1485
  bookEl.style.transform = "rotateX(8deg) translate3d(0,0,0)";
@@ -1475,13 +1553,9 @@ export function mountBookshelf(root, options = {}) {
1475
1553
  noteView.classList.add("is-preparing");
1476
1554
  noteView.classList.remove("is-open");
1477
1555
  noteView.style.opacity = "0";
1478
- const coverEl = $("#noteCover");
1479
- const bg = coverEl && getComputedStyle(coverEl).backgroundImage;
1480
- const m = bg && bg.match(/url\(["']?([^"')]+)["']?\)/);
1481
- if (m) {
1556
+ const img = $("#noteCover") && $("#noteCover").querySelector(".note-cover-img");
1557
+ if (img && !coverImgReady(img)) {
1482
1558
  try {
1483
- const img = new Image();
1484
- img.src = m[1];
1485
1559
  const DECODE_MS = 400;
1486
1560
  const decodeOrLoad = img.decode
1487
1561
  ? img.decode()
@@ -1528,6 +1602,7 @@ export function mountBookshelf(root, options = {}) {
1528
1602
  busy = true;
1529
1603
  const myFlow = ++flowSeq;
1530
1604
  document.body.classList.add("is-busy");
1605
+ root.classList.remove("is-return");
1531
1606
  openBook = { book, cat, asSpine, fromExpanded: !!fromExpanded, openSeatRect: null };
1532
1607
  openFromExpanded = !!fromExpanded;
1533
1608
 
@@ -1568,6 +1643,11 @@ export function mountBookshelf(root, options = {}) {
1568
1643
  sourceEl.style.transition = "none";
1569
1644
  sourceEl.style.transform = "none"; // kill :hover translateY / is-expanded lift
1570
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
+ }
1571
1651
  if (fromExpanded) {
1572
1652
  sourceEl.classList.remove("is-expanded", "is-collapsing");
1573
1653
  // Drop the expanded cover's width. Do NOT drop a resting spine's height:
@@ -1722,14 +1802,14 @@ export function mountBookshelf(root, options = {}) {
1722
1802
  { offset: 0, opacity: 0 },
1723
1803
  { offset: 0.14, opacity: 0 },
1724
1804
  { offset: 0.22, opacity: 1 },
1725
- { offset: 0.90, opacity: 1 },
1726
- { offset: 0.96, opacity: 0.35 },
1805
+ { offset: 0.82, opacity: 1 },
1806
+ { offset: 0.90, opacity: 0 },
1727
1807
  { offset: 1.0, opacity: 0 },
1728
1808
  ]
1729
1809
  : [
1730
1810
  { offset: 0, opacity: 1 },
1731
- { offset: 0.90, opacity: 1 },
1732
- { offset: 0.96, opacity: 0.35 },
1811
+ { offset: 0.82, opacity: 1 },
1812
+ { offset: 0.90, opacity: 0 },
1733
1813
  { offset: 1.0, opacity: 0 },
1734
1814
  ],
1735
1815
  opts
@@ -1755,6 +1835,7 @@ export function mountBookshelf(root, options = {}) {
1755
1835
  const aExtractCoverIn = null;
1756
1836
 
1757
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.
1758
1839
  const closedFaceKf = [
1759
1840
  { offset: 0, opacity: 0 },
1760
1841
  { offset: 0.28, opacity: 0 },
@@ -1762,7 +1843,11 @@ export function mountBookshelf(root, options = {}) {
1762
1843
  { offset: 0.52, opacity: 1 },
1763
1844
  { offset: 1.0, opacity: 1 },
1764
1845
  ];
1765
- const aBoard = dual._board ? dual._board.animate(closedFaceKf, opts) : null;
1846
+ if (dual._board) {
1847
+ dual._board.style.opacity = "0";
1848
+ dual._board.style.visibility = "hidden";
1849
+ }
1850
+ const aBoard = null;
1766
1851
  const aRightFace = dual._right.animate(
1767
1852
  [
1768
1853
  { offset: 0, opacity: 0, transform: "rotateY(0deg)" },
@@ -1803,20 +1888,9 @@ export function mountBookshelf(root, options = {}) {
1803
1888
  )
1804
1889
  : null;
1805
1890
 
1806
- // §12: note main rise offset 0.70–0.92 (≥1.80s clear). FORBIDDEN: ≥0.95 at 0.34.
1807
- const aNote = noteView.animate(
1808
- [
1809
- { offset: 0, opacity: 0 },
1810
- { offset: 0.65, opacity: 0 },
1811
- { offset: 0.70, opacity: 0.15 },
1812
- { offset: 0.78, opacity: 0.55 },
1813
- { offset: 0.85, opacity: 0.90 },
1814
- { offset: 0.90, opacity: 0.99 },
1815
- { offset: 0.92, opacity: 1 },
1816
- { offset: 1.0, opacity: 1 },
1817
- ],
1818
- opts
1819
- );
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);
1820
1894
 
1821
1895
  const aBack = openBackdrop
1822
1896
  ? openBackdrop.animate(
@@ -1961,6 +2035,9 @@ export function mountBookshelf(root, options = {}) {
1961
2035
  // Clock first. Shelf measure, freeze, and rAF used to run before this and
1962
2036
  // the 0.12 Tfull keyframe then held 672×504 for ~240ms. Stash from open is
1963
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");
1964
2041
  const closeMotion = { duration: CLOSE_MS, easing: "linear", fill: "forwards" };
1965
2042
  const closeOpts = { duration: CLOSE_MS, easing: "linear", fill: "forwards" };
1966
2043
  const stashEarly = openBook.pathTs;
@@ -1982,29 +2059,21 @@ export function mountBookshelf(root, options = {}) {
1982
2059
  heldEarly.style.visibility = "visible";
1983
2060
  heldEarly.style.zIndex = "70";
1984
2061
  heldEarly.style.opacity = "1";
2062
+ if (heldEarly._board) {
2063
+ heldEarly._board.style.opacity = "0";
2064
+ heldEarly._board.style.visibility = "hidden";
2065
+ }
1985
2066
  promote(heldEarly);
1986
2067
  earlyMove = heldEarly.animate(
1987
- [
1988
- pathKey(0, stashEarly.Tfull),
1989
- pathKey(0.28, stashEarly.Tp3),
1990
- pathKey(0.44, stashEarly.Tp2),
1991
- pathKey(0.60, stashEarly.Tp1),
1992
- pathKey(0.78, stashEarly.T0),
1993
- pathKey(0.90, stashEarly.T0),
1994
- pathKey(1, stashEarly.T0),
1995
- ],
2068
+ returnMoveKeys(stashEarly.Tfull, stashEarly.Tp1, stashEarly.T0),
1996
2069
  closeMotion
1997
2070
  );
1998
- earlyNote = noteView.animate(
1999
- [
2000
- { offset: 0, opacity: 1 },
2001
- { offset: 0.06, opacity: 0.85 },
2002
- { offset: 0.12, opacity: 0.45 },
2003
- { offset: 0.22, opacity: 0 },
2004
- { offset: 1.0, opacity: 0 },
2005
- ],
2006
- closeOpts
2007
- );
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
+ });
2008
2077
  if (heldEarly._left) {
2009
2078
  earlyLeft = heldEarly._left.animate(
2010
2079
  [
@@ -2058,6 +2127,7 @@ export function mountBookshelf(root, options = {}) {
2058
2127
  try { earlyNote && earlyNote.cancel(); } catch (_) {}
2059
2128
  try { earlyLeft && earlyLeft.cancel(); } catch (_) {}
2060
2129
  try { earlyRight && earlyRight.cancel(); } catch (_) {}
2130
+ root.classList.remove("is-return");
2061
2131
  noteView.classList.remove("is-open", "is-preparing");
2062
2132
  noteView.hidden = true;
2063
2133
  noteView.style.opacity = "";
@@ -2081,7 +2151,10 @@ export function mountBookshelf(root, options = {}) {
2081
2151
  ].forEach((p) => {
2082
2152
  try { slot.style.removeProperty(p); } catch (_) {}
2083
2153
  });
2084
- slot.classList.remove("is-collapsing", "is-placeholder", "is-expanded");
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");
2085
2158
  expandedId = null;
2086
2159
  // Spines: re-pin the resting face BEFORE reading the seat. A cleared inline
2087
2160
  // height is the shorter size-class box, and put-back would keep it.
@@ -2116,13 +2189,13 @@ export function mountBookshelf(root, options = {}) {
2116
2189
  });
2117
2190
  shelfView.classList.add("is-frozen");
2118
2191
 
2119
- // ux13: prepare shelf+flyer UNDER opaque note — ban route hard-cut then mid-air resume.
2120
- // Keep note is-open + opacity 1 until reverse clock starts.
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.
2121
2194
  noteView.classList.add("is-open");
2122
2195
  noteView.classList.remove("is-preparing");
2123
2196
  noteView.hidden = false;
2124
- noteView.style.opacity = "1";
2125
- noteView.style.zIndex = "75";
2197
+ if (!earlyNote) noteView.style.opacity = "1";
2198
+ noteView.style.zIndex = "";
2126
2199
 
2127
2200
  unparkShelf();
2128
2201
  shelfView.style.opacity = "1";
@@ -2217,7 +2290,10 @@ export function mountBookshelf(root, options = {}) {
2217
2290
  flyer._right.style.transform = "rotateY(-8deg)";
2218
2291
  flyer._right.style.opacity = "1";
2219
2292
  }
2220
- if (flyer._board) flyer._board.style.opacity = "1";
2293
+ if (flyer._board) {
2294
+ flyer._board.style.opacity = "0";
2295
+ flyer._board.style.visibility = "hidden";
2296
+ }
2221
2297
  if (flyer._leftThick) flyer._leftThick.style.opacity = "1";
2222
2298
  if (flyer._rightThick) flyer._rightThick.style.opacity = "1";
2223
2299
  if (flyer._gutter) flyer._gutter.style.opacity = "1";
@@ -2280,29 +2356,13 @@ export function mountBookshelf(root, options = {}) {
2280
2356
 
2281
2357
  resetVeil();
2282
2358
 
2283
- // ux13: note fades ON the book layer while reverse shrink starts — ban instant note unmount.
2284
- const aNoteOut = earlyNote || noteView.animate(
2285
- [
2286
- { offset: 0, opacity: 1 },
2287
- { offset: 0.06, opacity: 0.85 },
2288
- { offset: 0.12, opacity: 0.45 },
2289
- { offset: 0.22, opacity: 0 },
2290
- { offset: 1.0, opacity: 0 },
2291
- ],
2292
- closeOpts
2293
- );
2359
+ // Note fades out in the first ~230ms. Shrink already started on frame 0.
2360
+ const aNoteOut = earlyNote || noteView.animate(noteOutKeys(), closeOpts);
2294
2361
 
2295
- // Shrink starts at offset 0. The old 0.12 Tfull key was ~228ms parked at 672×504.
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.
2296
2364
  const aFlyerMove = earlyMove || flyer.animate(
2297
- [
2298
- pathKey(0, Tfull),
2299
- pathKey(0.28, Tp3),
2300
- pathKey(0.44, Tp2),
2301
- pathKey(0.60, Tp1),
2302
- pathKey(0.78, T0),
2303
- pathKey(0.90, T0),
2304
- pathKey(1, T0),
2305
- ],
2365
+ returnMoveKeys(Tfull, Tp1, T0),
2306
2366
  closeMotion
2307
2367
  );
2308
2368
  // Reverse hinge on SAME layer while shrink (open was 0.32→0.68 hinge).
@@ -2336,7 +2396,7 @@ export function mountBookshelf(root, options = {}) {
2336
2396
  { offset: 0.28, opacity: 0 },
2337
2397
  { offset: 1.0, opacity: 0 },
2338
2398
  ];
2339
- const aBoardClose = flyer._board ? flyer._board.animate(closedFaceOut, closeOpts) : null;
2399
+ const aBoardClose = null;
2340
2400
  const aThickLClose = flyer._leftThick ? flyer._leftThick.animate(closedFaceOut, closeOpts) : null;
2341
2401
  const aThickRClose = flyer._rightThick ? flyer._rightThick.animate(closedFaceOut, closeOpts) : null;
2342
2402
  const aGutterClose = flyer._gutter
@@ -2377,9 +2437,11 @@ export function mountBookshelf(root, options = {}) {
2377
2437
  const aFlyerFade = flyer.animate(
2378
2438
  targetMode === "spines"
2379
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.
2380
2442
  { offset: 0, opacity: 1 },
2381
- { offset: 0.50, opacity: 1 },
2382
- { offset: 0.58, opacity: 0 },
2443
+ { offset: 0.68, opacity: 1 },
2444
+ { offset: 0.80, opacity: 0 },
2383
2445
  { offset: 1.0, opacity: 0 },
2384
2446
  ]
2385
2447
  : [
@@ -2411,10 +2473,10 @@ export function mountBookshelf(root, options = {}) {
2411
2473
  ? spineSlotClose.animate(
2412
2474
  [
2413
2475
  { offset: 0, transform: outClose, opacity: 0 },
2414
- { offset: 0.50, transform: outClose, opacity: 0 },
2415
- { offset: 0.58, transform: outClose, opacity: 1 },
2416
- { offset: 0.78, transform: "translate3d(0px, 0px, 0)", opacity: 1 },
2417
- { offset: 0.90, transform: "translate3d(0px, 0px, 0)", opacity: 1 },
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 },
2418
2480
  { offset: 1.0, transform: "translate3d(0px, 0px, 0)", opacity: 0 },
2419
2481
  ],
2420
2482
  closeMotion
@@ -2429,8 +2491,8 @@ export function mountBookshelf(root, options = {}) {
2429
2491
  targetMode === "spines"
2430
2492
  ? [
2431
2493
  { offset: 0, opacity: 1 },
2432
- { offset: 0.50, opacity: 1 },
2433
- { offset: 0.58, opacity: 0 },
2494
+ { offset: 0.68, opacity: 1 },
2495
+ { offset: 0.80, opacity: 0 },
2434
2496
  { offset: 1, opacity: 0 },
2435
2497
  ]
2436
2498
  : [
@@ -2452,6 +2514,13 @@ export function mountBookshelf(root, options = {}) {
2452
2514
  slot.style.backgroundColor = "";
2453
2515
  }
2454
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
+
2455
2524
  const aSlotIn = slot.animate(
2456
2525
  [
2457
2526
  { offset: 0, opacity: 0 },
@@ -2510,6 +2579,7 @@ export function mountBookshelf(root, options = {}) {
2510
2579
  flyer.style.opacity = "0";
2511
2580
  slot.style.opacity = "1";
2512
2581
  slot.style.transform = "none";
2582
+ root.classList.remove("is-return");
2513
2583
  noteView.classList.remove("is-open", "is-preparing");
2514
2584
  noteView.hidden = true;
2515
2585
  noteView.style.opacity = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-garden",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "private": false,
5
5
  "description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
6
6
  "license": "MIT",