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.
- package/app/bookshelf/[bookId]/page.tsx +72 -0
- package/app/bookshelf/load.mjs +24 -6
- package/app/components/bookshelf/BookHub.tsx +142 -0
- package/app/components/bookshelf/Bookshelf.tsx +30 -6
- package/app/components/bookshelf/bookshelf.css +78 -60
- package/app/components/bookshelf/catalog.mjs +74 -14
- package/app/components/bookshelf/catalog.ts +4 -0
- package/app/components/bookshelf/engine.ts +406 -109
- package/app/components/bookshelf/index.ts +3 -0
- package/app/components/bookshelf/schema.mjs +70 -1
- package/app/components/bookshelf/schema.ts +52 -10
- package/app/components/bookshelf/types.ts +42 -3
- package/app/lib/i18n.ts +10 -0
- package/app/sitemap.ts +8 -0
- package/app/styles/shell.css +37 -0
- package/docs/handbook/config.md +23 -5
- package/docs/handbook/en/config.md +23 -5
- package/package.json +1 -1
- package/scripts/bookshelf-covers.mjs +62 -0
- package/scripts/build-content.mjs +15 -1
|
@@ -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
|
+
}
|
package/app/bookshelf/load.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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({
|
|
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, {
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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"
|
|
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; }
|