hd-garden 0.9.2 → 0.9.4

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.
@@ -0,0 +1,72 @@
1
+ import type { Metadata } from "next";
2
+ import { notFound } from "next/navigation";
3
+ import { bookshelf } from "@garden/bookshelf";
4
+ import { Bookshelf } from "../../components/bookshelf";
5
+ import { pageAlternates } from "../../lib/page-alternates";
6
+ import { siteConfig } from "../../site.config";
7
+ import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
8
+ import type { BookshelfBook } from "../../components/bookshelf/types";
9
+
10
+ function hubBooks(): { shelfTitle: string; book: BookshelfBook }[] {
11
+ if (!bookshelf) return [];
12
+ const found: { shelfTitle: string; book: BookshelfBook }[] = [];
13
+ for (const row of bookshelf.catalog) {
14
+ for (const book of row.books) {
15
+ if ((book.posts?.length ?? 0) > 1) found.push({ shelfTitle: bookshelf.title, book });
16
+ }
17
+ }
18
+ return found;
19
+ }
20
+
21
+ export function generateStaticParams() {
22
+ return staticParams(
23
+ hubBooks().map(({ book }) => ({ bookId: book.id })),
24
+ { bookId: STATIC_PLACEHOLDER },
25
+ );
26
+ }
27
+
28
+ export async function generateMetadata({
29
+ params,
30
+ }: {
31
+ params: Promise<{ bookId: string }>;
32
+ }): Promise<Metadata> {
33
+ const { bookId } = await params;
34
+ const found = hubBooks().find((item) => item.book.id === bookId);
35
+ if (!found) return {};
36
+ return {
37
+ title: found.book.title,
38
+ description: found.book.posts?.find((post) => post.summary)?.summary,
39
+ alternates: pageAlternates(`/bookshelf/${found.book.id}`),
40
+ };
41
+ }
42
+
43
+ /** `/bookshelf/<id>` — one multi-note volume. Single-note books stay on the shelf. */
44
+ export default async function BookHubPage({
45
+ params,
46
+ }: {
47
+ params: Promise<{ bookId: string }>;
48
+ }) {
49
+ const { bookId } = await params;
50
+ const found = hubBooks().find((item) => item.book.id === bookId);
51
+ if (!found || !bookshelf) {
52
+ if (isStaticPlaceholder(bookId)) return null;
53
+ notFound();
54
+ }
55
+ return (
56
+ <main className="sh-main bookshelf-page" id="content">
57
+ <header className="sh-head">
58
+ {bookshelf.eyebrow ? <p className="sh-kicker">{bookshelf.eyebrow}</p> : null}
59
+ <h1 id="sh-title">{bookshelf.title}</h1>
60
+ {bookshelf.description ? <p className="sh-deck">{bookshelf.description}</p> : null}
61
+ </header>
62
+ <Bookshelf
63
+ embedded
64
+ catalog={bookshelf.catalog}
65
+ notes={bookshelf.notes}
66
+ title={bookshelf.title}
67
+ eyebrow={bookshelf.eyebrow ?? siteConfig.name}
68
+ initialHub={found.book.id}
69
+ />
70
+ </main>
71
+ );
72
+ }
@@ -13,7 +13,8 @@ export function bookshelfConfigPath(root = gardenSiteRoot()) {
13
13
  /**
14
14
  * The site shelf, or null when `content/bookshelf.yaml` is absent.
15
15
  * Absent means the package demo (click-test only). A file here is the live shelf:
16
- * each book's `post` is read from `content/posts` and turned into note HTML.
16
+ * each book's `post` or `posts` is read from `content/posts`. One note becomes
17
+ * the open-book HTML. Several notes stay on the book for `/bookshelf/<id>`.
17
18
  * `preparedPosts`, when passed, are the sync pipeline's posts (rewritten asset
18
19
  * URLs). Otherwise the markdown files are read directly.
19
20
  * Throws `BookshelfConfigError` when the file, a cover, or a post path is broken.
@@ -35,22 +36,33 @@ export function loadBookshelf(root = gardenSiteRoot(), preparedPosts) {
35
36
  title: post.title,
36
37
  description: post.description,
37
38
  cover: post.cover,
39
+ date: typeof post.date === "string" ? post.date : undefined,
38
40
  content: post.content,
39
41
  }))
40
42
  : readReferencedPosts(root, config);
41
43
  return bookshelfMountFromFile(config, sources);
42
44
  }
43
45
 
46
+ function referencedPostPaths(book) {
47
+ if (Array.isArray(book.posts) && book.posts.length > 0) {
48
+ return book.posts
49
+ .map((entry) => (typeof entry === "string" ? entry : entry?.path))
50
+ .filter((path) => typeof path === "string" && path.trim());
51
+ }
52
+ return book.post ? [book.post] : [];
53
+ }
54
+
44
55
  function readReferencedPosts(root, config) {
45
56
  const seen = new Set();
46
57
  const posts = [];
47
58
  for (const shelf of config.shelves) {
48
59
  for (const book of shelf.books) {
49
- if (!book.post) continue;
50
- const relative = normalizeBookshelfPostPath(book.post);
51
- if (seen.has(relative)) continue;
52
- seen.add(relative);
53
- posts.push(readPost(root, relative));
60
+ for (const entry of referencedPostPaths(book)) {
61
+ const relative = normalizeBookshelfPostPath(entry);
62
+ if (seen.has(relative)) continue;
63
+ seen.add(relative);
64
+ posts.push(readPost(root, relative));
65
+ }
54
66
  }
55
67
  }
56
68
  return posts;
@@ -85,6 +97,7 @@ function readPost(root, postPath) {
85
97
  title: optionalString(data.title),
86
98
  description: optionalString(data.description),
87
99
  cover: optionalString(data.cover),
100
+ date: optionalDate(data.date),
88
101
  content: parsed.content,
89
102
  };
90
103
  }
@@ -94,3 +107,8 @@ function optionalString(value) {
94
107
  const trimmed = value.trim();
95
108
  return trimmed ? trimmed : undefined;
96
109
  }
110
+
111
+ function optionalDate(value) {
112
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
113
+ return optionalString(value);
114
+ }
@@ -0,0 +1,142 @@
1
+ import Link from "next/link";
2
+ import { siteHref } from "../../lib/site-href";
3
+ import { ui } from "../../lib/i18n";
4
+ import type { BookshelfBook, BookshelfBookPost, BookshelfNoteKind, BookshelfSource } from "./types";
5
+
6
+ function day(value: string) {
7
+ const date = new Date(value);
8
+ if (Number.isNaN(date.getTime())) return value.slice(0, 10);
9
+ return new Intl.DateTimeFormat("en-CA", {
10
+ timeZone: "Asia/Shanghai",
11
+ year: "numeric",
12
+ month: "2-digit",
13
+ day: "2-digit",
14
+ }).format(date);
15
+ }
16
+
17
+ function kindLabel(kind: BookshelfNoteKind | undefined) {
18
+ const t = ui();
19
+ switch (kind) {
20
+ case "overview":
21
+ return t.bookHubOverview;
22
+ case "excerpt":
23
+ return t.bookHubExcerpt;
24
+ case undefined:
25
+ return "";
26
+ default: {
27
+ const unexpected: never = kind;
28
+ return unexpected;
29
+ }
30
+ }
31
+ }
32
+
33
+ function sourceHref(href: string) {
34
+ if (href.startsWith("/") && !href.startsWith("//")) return siteHref(href);
35
+ return href;
36
+ }
37
+
38
+ function isExternal(href: string) {
39
+ return /^(?:https?:)?\/\//iu.test(href);
40
+ }
41
+
42
+ /** Hub chips are Douban and PDF. The Chinese label is the zh shelf; English sites write Douban. */
43
+ function isDoubanChip(label: string) {
44
+ return label === "豆瓣" || /^douban$/iu.test(label);
45
+ }
46
+
47
+ function hubSources(sources: readonly BookshelfSource[] | undefined) {
48
+ const list = sources ?? [];
49
+ const douban = list.filter((source) => isDoubanChip(source.label));
50
+ const pdf = list.filter((source) => /pdf/iu.test(source.label));
51
+ return [...douban, ...pdf];
52
+ }
53
+
54
+ function NoteCard({ entry }: { entry: BookshelfBookPost }) {
55
+ const t = ui();
56
+ const label = kindLabel(entry.kind);
57
+ const href = entry.href ?? siteHref(`/blog/${entry.path}`);
58
+ const when = entry.date ? day(entry.date) : "";
59
+ const count = entry.words ? t.wordCount(entry.words) : "";
60
+ return (
61
+ <Link className="book-hub-note" href={href}>
62
+ {label ? <p className="book-hub-note-label">{label}</p> : null}
63
+ <h3>{entry.title}</h3>
64
+ {entry.summary ? <p className="book-hub-note-sum">{entry.summary}</p> : null}
65
+ {when || count ? (
66
+ <p className="book-hub-note-meta">
67
+ {when}
68
+ {when && count ? " · " : null}
69
+ {count}
70
+ </p>
71
+ ) : null}
72
+ </Link>
73
+ );
74
+ }
75
+
76
+ function SourceChips({ sources }: { sources: readonly BookshelfSource[] }) {
77
+ const shown = hubSources(sources);
78
+ if (!shown.length) return null;
79
+ const t = ui();
80
+ return (
81
+ <div className="book-hub-sources">
82
+ {shown.map((source) => {
83
+ const href = sourceHref(source.href);
84
+ const external = isExternal(source.href);
85
+ const pdf = /pdf/iu.test(source.label);
86
+ return (
87
+ <span className="book-hub-source" key={`${source.label}\u0000${source.href}`}>
88
+ <a
89
+ className="book-hub-chip"
90
+ href={href}
91
+ {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
92
+ >
93
+ {source.label}
94
+ <span className="ext" aria-hidden="true">↗</span>
95
+ </a>
96
+ {pdf ? <span className="book-hub-source-note">{t.bookHubPdfNote}</span> : null}
97
+ </span>
98
+ );
99
+ })}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ /** Book hub (本书). Multi-note volumes land here; a note card is a plain article link. */
105
+ export function BookHub({ shelfTitle, book }: { shelfTitle: string; book: BookshelfBook }) {
106
+ const t = ui();
107
+ const jacket = typeof book.cover === "string" ? book.cover : "";
108
+ const posts = book.posts ?? [];
109
+ return (
110
+ <main className="sh-main book-hub" id="content">
111
+ <p className="book-hub-crumb">
112
+ <Link href={siteHref("/columns")}>{t.homeColumns}</Link>
113
+ <span aria-hidden="true"> · </span>
114
+ <Link href={siteHref("/bookshelf")}>{shelfTitle}</Link>
115
+ <span aria-hidden="true"> · </span>
116
+ <span aria-current="page">{t.bookHubCurrent}</span>
117
+ </p>
118
+
119
+ <section className="book-hub-card" aria-labelledby="book-hub-title">
120
+ {jacket ? (
121
+ <div className="book-hub-cover">
122
+ <img src={jacket} alt={t.coverLabel(book.title)} width={148} />
123
+ </div>
124
+ ) : null}
125
+ <div className="book-hub-meta">
126
+ <h1 id="book-hub-title">{book.title}</h1>
127
+ {book.author ? <p className="book-hub-author">{book.author}</p> : null}
128
+ <SourceChips sources={book.sources ?? []} />
129
+ </div>
130
+ </section>
131
+
132
+ <h2 className="book-hub-notes-head">{t.bookHubNotes}</h2>
133
+ <ul className="book-hub-notes">
134
+ {posts.map((entry) => (
135
+ <li key={entry.path}>
136
+ <NoteCard entry={entry} />
137
+ </li>
138
+ ))}
139
+ </ul>
140
+ </main>
141
+ );
142
+ }
@@ -12,9 +12,20 @@ function withNoteLinks(catalog: BookshelfShelf[] | undefined) {
12
12
  if (!catalog) return catalog;
13
13
  return catalog.map((row) => ({
14
14
  ...row,
15
- books: row.books.map((book) =>
16
- book.post ? { ...book, href: siteHref(`/blog/${book.post}`) } : book,
17
- ),
15
+ books: row.books.map((book) => {
16
+ const posts = book.posts?.map((entry) => ({
17
+ ...entry,
18
+ href: siteHref(`/blog/${entry.path}`),
19
+ }));
20
+ const hub = book.hub ? siteHref(book.hub) : undefined;
21
+ if (!book.post && !posts?.length && !hub) return book;
22
+ return {
23
+ ...book,
24
+ ...(book.post ? { href: siteHref(`/blog/${book.post}`) } : {}),
25
+ ...(posts?.length ? { posts } : {}),
26
+ ...(hub ? { hub } : {}),
27
+ };
28
+ }),
18
29
  }));
19
30
  }
20
31
 
@@ -68,6 +79,7 @@ function shelfShell(brand: string, title: string) {
68
79
  <div class="note-view" id="noteView" hidden>
69
80
  <article class="note">
70
81
  <button type="button" class="back-btn" id="backBtn">← 返回书架</button>
82
+ <div class="hub-panel" id="hubPanel" hidden></div>
71
83
  <div class="note-hero">
72
84
  <div class="note-cover" id="noteCover"></div>
73
85
  <div class="note-meta">
@@ -90,7 +102,14 @@ function shelfShell(brand: string, title: string) {
90
102
  }
91
103
 
92
104
  /** Approved bookshelf. Pass `catalog` and `notes` for a site shelf; omit them for the package demo. */
93
- export function Bookshelf({ catalog, notes, title, eyebrow, embedded = false }: BookshelfProps) {
105
+ export function Bookshelf({
106
+ catalog,
107
+ notes,
108
+ title,
109
+ eyebrow,
110
+ embedded = false,
111
+ initialHub,
112
+ }: BookshelfProps) {
94
113
  const ref = useRef<HTMLDivElement>(null);
95
114
  useLayoutEffect(() => {
96
115
  const root = ref.current;
@@ -100,13 +119,18 @@ export function Bookshelf({ catalog, notes, title, eyebrow, embedded = false }:
100
119
  if (saved != null) window.scrollTo(0, saved);
101
120
  };
102
121
  applyScroll();
103
- const unmount = mountBookshelf(root, { catalog: withNoteLinks(catalog), notes });
122
+ const unmount = mountBookshelf(root, {
123
+ catalog: withNoteLinks(catalog),
124
+ notes,
125
+ initialHub,
126
+ shelfHref: siteHref("/bookshelf"),
127
+ });
104
128
  const frame = window.requestAnimationFrame(applyScroll);
105
129
  return () => {
106
130
  window.cancelAnimationFrame(frame);
107
131
  unmount?.();
108
132
  };
109
- }, [catalog, notes, title, eyebrow, embedded]);
133
+ }, [catalog, notes, title, eyebrow, embedded, initialHub]);
110
134
  return (
111
135
  <div
112
136
  className={embedded ? "bookshelf-demo is-site" : "bookshelf-demo"}
@@ -283,6 +283,7 @@
283
283
  overscroll-behavior-x: contain;
284
284
  scrollbar-width: none;
285
285
  }
286
+ .bookshelf-demo .spines.has-jackets { gap: 10px; }
286
287
  .bookshelf-demo .spines::-webkit-scrollbar { display: none; }
287
288
 
288
289
  .bookshelf-demo .spine {
@@ -353,14 +354,29 @@
353
354
  transition: opacity 220ms var(--ease-motion);
354
355
  }
355
356
 
356
- /* Mode C: temporary expand on spine wall */
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. */
357
+ /* Mode C: temporary expand on spine wall.
358
+ Demo spines (no .has-jacket) stay cloth until expand.
359
+ A real books[].cover is .has-jacket: the <img> is the face at rest,
360
+ on hover, and while extracting. bk-* must not paint a plate over it.
361
+ The old `.spine[class*="img-"] { background-image: none !important }`
362
+ was that plate. */
360
363
  .bookshelf-demo .spine.is-expanded,
361
- .bookshelf-demo .spine.is-extracting {
364
+ .bookshelf-demo .spine.is-extracting,
365
+ .bookshelf-demo .spine.has-jacket {
362
366
  background-color: transparent !important;
363
367
  }
368
+ .bookshelf-demo .spine.has-jacket {
369
+ padding: 0;
370
+ writing-mode: horizontal-tb;
371
+ text-orientation: mixed;
372
+ letter-spacing: 0;
373
+ background-image: none;
374
+ }
375
+ .bookshelf-demo .spine.has-jacket > span {
376
+ opacity: 0 !important;
377
+ visibility: hidden !important;
378
+ }
379
+ .bookshelf-demo .spine > .cover-art,
364
380
  .bookshelf-demo .spine.is-expanded > .cover-art,
365
381
  .bookshelf-demo .spine.is-extracting > .cover-art {
366
382
  position: absolute;
@@ -374,33 +390,34 @@
374
390
  z-index: 1;
375
391
  border-radius: inherit;
376
392
  }
377
- .bookshelf-demo .spine[class*="img-"] {
393
+ /* Demo catalog only: cloth at rest, jacket when the spine expands. */
394
+ .bookshelf-demo .spine[class*="img-"]:not(.has-jacket) {
378
395
  background-image: none !important;
379
396
  }
380
- .bookshelf-demo .spine.is-expanded[class*="img-"] {
397
+ .bookshelf-demo .spine.is-expanded[class*="img-"]:not(.has-jacket) {
381
398
  background-image: unset;
382
399
  }
383
- .bookshelf-demo .spine.is-expanded.img-clean-code { background-image: url("/covers/clean-code.jpg") !important; }
384
- .bookshelf-demo .spine.is-expanded.img-refactoring { background-image: url("/covers/refactoring.jpg") !important; }
385
- .bookshelf-demo .spine.is-expanded.img-design-patterns { background-image: url("/covers/design-patterns.jpg") !important; }
386
- .bookshelf-demo .spine.is-expanded.img-pragmatic-programmer { background-image: url("/covers/pragmatic-programmer.jpg") !important; }
387
- .bookshelf-demo .spine.is-expanded.img-continuous-delivery { background-image: url("/covers/continuous-delivery.jpg") !important; }
388
- .bookshelf-demo .spine.is-expanded.img-phoenix-project { background-image: url("/covers/phoenix-project.jpg") !important; }
389
- .bookshelf-demo .spine.is-expanded.img-mythical-man-month { background-image: url("/covers/mythical-man-month.jpg") !important; }
390
- .bookshelf-demo .spine.is-expanded.img-tdd { background-image: url("/covers/tdd.jpg") !important; }
391
- .bookshelf-demo .spine.is-expanded.img-lean-startup { background-image: url("/covers/lean-startup.jpg") !important; }
392
- .bookshelf-demo .spine.is-expanded.img-release-it { background-image: url("/covers/release-it.jpg") !important; }
393
- .bookshelf-demo .spine.is-expanded.img-sre { background-image: url("/covers/sre.jpg") !important; }
394
- .bookshelf-demo .spine.is-expanded.img-team-topologies { background-image: url("/covers/team-topologies.jpg") !important; }
395
- .bookshelf-demo .spine.is-expanded.img-accelerate { background-image: url("/covers/accelerate.jpg") !important; }
396
- .bookshelf-demo .spine.is-expanded.img-ddd { background-image: url("/covers/ddd.jpg") !important; }
397
- .bookshelf-demo .spine.is-expanded.img-microservices { background-image: url("/covers/microservices.jpg") !important; }
398
- .bookshelf-demo .spine.is-expanded.img-eaa { background-image: url("/covers/eaa.jpg") !important; }
399
- .bookshelf-demo .spine.is-expanded.img-ddia { background-image: url("/covers/ddia.jpg") !important; }
400
- .bookshelf-demo .spine.is-expanded.img-fundamentals-sa { background-image: url("/covers/fundamentals-sa.jpg") !important; }
401
- .bookshelf-demo .spine.is-expanded.img-implementing-ddd { background-image: url("/covers/implementing-ddd.jpg") !important; }
402
- .bookshelf-demo .spine.is-expanded.img-systems-performance { background-image: url("/covers/systems-performance.jpg") !important; }
403
- .bookshelf-demo .spine.is-expanded.img-clean-architecture { background-image: url("/covers/clean-architecture.jpg") !important; }
400
+ .bookshelf-demo .spine.is-expanded.img-clean-code:not(.has-jacket) { background-image: url("/covers/clean-code.jpg") !important; }
401
+ .bookshelf-demo .spine.is-expanded.img-refactoring:not(.has-jacket) { background-image: url("/covers/refactoring.jpg") !important; }
402
+ .bookshelf-demo .spine.is-expanded.img-design-patterns:not(.has-jacket) { background-image: url("/covers/design-patterns.jpg") !important; }
403
+ .bookshelf-demo .spine.is-expanded.img-pragmatic-programmer:not(.has-jacket) { background-image: url("/covers/pragmatic-programmer.jpg") !important; }
404
+ .bookshelf-demo .spine.is-expanded.img-continuous-delivery:not(.has-jacket) { background-image: url("/covers/continuous-delivery.jpg") !important; }
405
+ .bookshelf-demo .spine.is-expanded.img-phoenix-project:not(.has-jacket) { background-image: url("/covers/phoenix-project.jpg") !important; }
406
+ .bookshelf-demo .spine.is-expanded.img-mythical-man-month:not(.has-jacket) { background-image: url("/covers/mythical-man-month.jpg") !important; }
407
+ .bookshelf-demo .spine.is-expanded.img-tdd:not(.has-jacket) { background-image: url("/covers/tdd.jpg") !important; }
408
+ .bookshelf-demo .spine.is-expanded.img-lean-startup:not(.has-jacket) { background-image: url("/covers/lean-startup.jpg") !important; }
409
+ .bookshelf-demo .spine.is-expanded.img-release-it:not(.has-jacket) { background-image: url("/covers/release-it.jpg") !important; }
410
+ .bookshelf-demo .spine.is-expanded.img-sre:not(.has-jacket) { background-image: url("/covers/sre.jpg") !important; }
411
+ .bookshelf-demo .spine.is-expanded.img-team-topologies:not(.has-jacket) { background-image: url("/covers/team-topologies.jpg") !important; }
412
+ .bookshelf-demo .spine.is-expanded.img-accelerate:not(.has-jacket) { background-image: url("/covers/accelerate.jpg") !important; }
413
+ .bookshelf-demo .spine.is-expanded.img-ddd:not(.has-jacket) { background-image: url("/covers/ddd.jpg") !important; }
414
+ .bookshelf-demo .spine.is-expanded.img-microservices:not(.has-jacket) { background-image: url("/covers/microservices.jpg") !important; }
415
+ .bookshelf-demo .spine.is-expanded.img-eaa:not(.has-jacket) { background-image: url("/covers/eaa.jpg") !important; }
416
+ .bookshelf-demo .spine.is-expanded.img-ddia:not(.has-jacket) { background-image: url("/covers/ddia.jpg") !important; }
417
+ .bookshelf-demo .spine.is-expanded.img-fundamentals-sa:not(.has-jacket) { background-image: url("/covers/fundamentals-sa.jpg") !important; }
418
+ .bookshelf-demo .spine.is-expanded.img-implementing-ddd:not(.has-jacket) { background-image: url("/covers/implementing-ddd.jpg") !important; }
419
+ .bookshelf-demo .spine.is-expanded.img-systems-performance:not(.has-jacket) { background-image: url("/covers/systems-performance.jpg") !important; }
420
+ .bookshelf-demo .spine.is-expanded.img-clean-architecture:not(.has-jacket) { background-image: url("/covers/clean-architecture.jpg") !important; }
404
421
 
405
422
  .bookshelf-demo .spine.is-expanded {
406
423
  width: 100px;
@@ -435,7 +452,8 @@
435
452
  box-shadow var(--t-collapse) var(--ease-motion),
436
453
  background-size var(--t-collapse) var(--ease-motion);
437
454
  }
438
- /* C collapse: shrink while still "expanded" class — drop cover image to solid hue immediately, never flash a wide black plate */
455
+ /* C collapse: shrink while still "expanded". Real jackets keep .cover-art.
456
+ Demo cloth may drop the expanded photo; it must not clear a .has-jacket. */
439
457
  .bookshelf-demo .spine.is-expanded.is-collapsing {
440
458
  width: 28px;
441
459
  height: 150px;
@@ -449,10 +467,18 @@
449
467
  box-shadow:
450
468
  inset -1px 0 0 rgba(255,255,255,0.12),
451
469
  inset 1px 0 0 rgba(0,0,0,0.18);
452
- background-image: none !important;
453
470
  background-size: auto;
454
471
  z-index: 2;
455
472
  }
473
+ .bookshelf-demo .spine.is-expanded.is-collapsing:not(.has-jacket) {
474
+ background-image: none !important;
475
+ }
476
+ .bookshelf-demo .spine.has-jacket.is-expanded.is-collapsing {
477
+ writing-mode: horizontal-tb;
478
+ padding: 0;
479
+ background-color: transparent !important;
480
+ background-image: none;
481
+ }
456
482
  .bookshelf-demo .spine.is-expanded.is-collapsing.s-tall { width: 30px; height: 168px; }
457
483
  .bookshelf-demo .spine.is-expanded.is-collapsing.s-mid { width: 28px; height: 152px; }
458
484
  .bookshelf-demo .spine.is-expanded.is-collapsing.s-short { width: 24px; height: 136px; }
@@ -823,7 +849,24 @@
823
849
  opacity: 0 !important;
824
850
  }
825
851
 
826
- /* ux16: exact homeRect standing spine — full height, spine width, slides out/in */
852
+ /* ux16: exact homeRect standing spine — full height, spine width, slides out/in.
853
+ A real jacket fills the slot. bk-* cloth is only the filler / demo label. */
854
+ .bookshelf-demo .flyer.flyer-spine-slot.has-jacket {
855
+ background: none !important;
856
+ background-color: transparent !important;
857
+ box-shadow: none;
858
+ padding: 0;
859
+ }
860
+ .bookshelf-demo .flyer-spine-slot-img {
861
+ position: absolute;
862
+ inset: 0;
863
+ width: 100%;
864
+ height: 100%;
865
+ object-fit: contain;
866
+ object-position: center bottom;
867
+ display: block;
868
+ border-radius: inherit;
869
+ }
827
870
  .bookshelf-demo .flyer.flyer-spine-slot {
828
871
  position: fixed;
829
872
  margin: 0;
@@ -1161,35 +1204,6 @@
1161
1204
  .bookshelf-demo .s-tall { height: 148px; width: 26px; }
1162
1205
  .bookshelf-demo .s-mid { height: 134px; width: 24px; }
1163
1206
  .bookshelf-demo .s-short { height: 120px; width: 20px; }
1164
- /* resting spines: solid hue only; cover image only when expanded */
1165
- .bookshelf-demo .spine[class*="img-"] {
1166
- background-image: none !important;
1167
- }
1168
- .bookshelf-demo .spine.is-expanded[class*="img-"] {
1169
- background-image: unset;
1170
- }
1171
- .bookshelf-demo .spine.is-expanded.img-clean-code { background-image: url("/covers/clean-code.jpg") !important; }
1172
- .bookshelf-demo .spine.is-expanded.img-refactoring { background-image: url("/covers/refactoring.jpg") !important; }
1173
- .bookshelf-demo .spine.is-expanded.img-design-patterns { background-image: url("/covers/design-patterns.jpg") !important; }
1174
- .bookshelf-demo .spine.is-expanded.img-pragmatic-programmer { background-image: url("/covers/pragmatic-programmer.jpg") !important; }
1175
- .bookshelf-demo .spine.is-expanded.img-continuous-delivery { background-image: url("/covers/continuous-delivery.jpg") !important; }
1176
- .bookshelf-demo .spine.is-expanded.img-phoenix-project { background-image: url("/covers/phoenix-project.jpg") !important; }
1177
- .bookshelf-demo .spine.is-expanded.img-mythical-man-month { background-image: url("/covers/mythical-man-month.jpg") !important; }
1178
- .bookshelf-demo .spine.is-expanded.img-tdd { background-image: url("/covers/tdd.jpg") !important; }
1179
- .bookshelf-demo .spine.is-expanded.img-lean-startup { background-image: url("/covers/lean-startup.jpg") !important; }
1180
- .bookshelf-demo .spine.is-expanded.img-release-it { background-image: url("/covers/release-it.jpg") !important; }
1181
- .bookshelf-demo .spine.is-expanded.img-sre { background-image: url("/covers/sre.jpg") !important; }
1182
- .bookshelf-demo .spine.is-expanded.img-team-topologies { background-image: url("/covers/team-topologies.jpg") !important; }
1183
- .bookshelf-demo .spine.is-expanded.img-accelerate { background-image: url("/covers/accelerate.jpg") !important; }
1184
- .bookshelf-demo .spine.is-expanded.img-ddd { background-image: url("/covers/ddd.jpg") !important; }
1185
- .bookshelf-demo .spine.is-expanded.img-microservices { background-image: url("/covers/microservices.jpg") !important; }
1186
- .bookshelf-demo .spine.is-expanded.img-eaa { background-image: url("/covers/eaa.jpg") !important; }
1187
- .bookshelf-demo .spine.is-expanded.img-ddia { background-image: url("/covers/ddia.jpg") !important; }
1188
- .bookshelf-demo .spine.is-expanded.img-fundamentals-sa { background-image: url("/covers/fundamentals-sa.jpg") !important; }
1189
- .bookshelf-demo .spine.is-expanded.img-implementing-ddd { background-image: url("/covers/implementing-ddd.jpg") !important; }
1190
- .bookshelf-demo .spine.is-expanded.img-systems-performance { background-image: url("/covers/systems-performance.jpg") !important; }
1191
- .bookshelf-demo .spine.is-expanded.img-clean-architecture { background-image: url("/covers/clean-architecture.jpg") !important; }
1192
-
1193
1207
  .bookshelf-demo .spine.is-expanded {
1194
1208
  width: 90px;
1195
1209
  height: 136px;
@@ -1328,6 +1342,10 @@ body:has(.bookshelf-demo.is-site) .sh-header .sh-word {
1328
1342
  text-underline-offset: 3px;
1329
1343
  }
1330
1344
  .bookshelf-demo .note-read[hidden] { display: none; }
1345
+ .bookshelf-demo .hub-panel[hidden] { display: none !important; }
1346
+ .bookshelf-demo .note-view.is-hub .note-hero,
1347
+ .bookshelf-demo .note-view.is-hub .note-body { display: none !important; }
1348
+ .bookshelf-demo .hub-panel .book-hub { width: 100%; padding: 0; }
1331
1349
  .bookshelf-demo.is-site .note-read { color: var(--blue); }
1332
1350
  @media (max-width: 820px) {
1333
1351
  .bookshelf-demo.is-site .note { padding: 16px 20px 64px; }