hd-garden 0.9.3 → 0.10.0
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/components/SiteChrome.tsx +3 -17
- package/app/components/SiteHeader.tsx +3 -3
- package/app/garden-modules.d.ts +0 -6
- package/app/layout.tsx +1 -2
- package/app/lib/i18n.ts +2 -12
- package/app/sitemap.ts +0 -11
- package/app/styles/shell.css +0 -37
- package/docs/handbook/config.md +2 -59
- package/docs/handbook/en/config.md +2 -59
- package/package.json +1 -2
- package/scripts/build-content.mjs +1 -31
- package/scripts/handbook-facts.mjs +1 -2
- package/ui/garden/site-nav.mjs +0 -12
- package/vite.config.ts +3 -12
- package/app/bookshelf/[bookId]/page.tsx +0 -55
- package/app/bookshelf/load.mjs +0 -114
- package/app/bookshelf/load.ts +0 -16
- package/app/bookshelf/page.tsx +0 -52
- package/app/components/bookshelf/BookHub.tsx +0 -142
- package/app/components/bookshelf/Bookshelf.tsx +0 -128
- package/app/components/bookshelf/bookshelf.css +0 -1334
- package/app/components/bookshelf/catalog.mjs +0 -176
- package/app/components/bookshelf/catalog.ts +0 -27
- package/app/components/bookshelf/demo-data.ts +0 -93
- package/app/components/bookshelf/density.ts +0 -32
- package/app/components/bookshelf/engine.ts +0 -2773
- package/app/components/bookshelf/index.ts +0 -15
- package/app/components/bookshelf/model.ts +0 -16
- package/app/components/bookshelf/schema.mjs +0 -155
- package/app/components/bookshelf/schema.ts +0 -110
- package/app/components/bookshelf/types.ts +0 -89
- package/public/covers/accelerate.jpg +0 -0
- package/public/covers/clean-architecture.jpg +0 -0
- package/public/covers/clean-code.jpg +0 -0
- package/public/covers/continuous-delivery.jpg +0 -0
- package/public/covers/ddd.jpg +0 -0
- package/public/covers/ddia.jpg +0 -0
- package/public/covers/design-patterns.jpg +0 -0
- package/public/covers/eaa.jpg +0 -0
- package/public/covers/fundamentals-sa.jpg +0 -0
- package/public/covers/implementing-ddd.jpg +0 -0
- package/public/covers/lean-startup.jpg +0 -0
- package/public/covers/microservices.jpg +0 -0
- package/public/covers/mythical-man-month.jpg +0 -0
- package/public/covers/phoenix-project.jpg +0 -0
- package/public/covers/pragmatic-programmer.jpg +0 -0
- package/public/covers/refactoring.jpg +0 -0
- package/public/covers/release-it.jpg +0 -0
- package/public/covers/sre.jpg +0 -0
- package/public/covers/systems-performance.jpg +0 -0
- package/public/covers/tdd.jpg +0 -0
- package/public/covers/team-topologies.jpg +0 -0
- package/scripts/bookshelf-covers.mjs +0 -62
package/app/bookshelf/load.mjs
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import matter from "gray-matter";
|
|
4
|
-
import { parse as parseYaml } from "yaml";
|
|
5
|
-
import { gardenSiteRoot } from "../../scripts/garden-paths.mjs";
|
|
6
|
-
import { bookshelfMountFromFile, normalizeBookshelfPostPath } from "../components/bookshelf/catalog.mjs";
|
|
7
|
-
import { BookshelfConfigError, parseBookshelfConfig } from "../components/bookshelf/schema.mjs";
|
|
8
|
-
|
|
9
|
-
export function bookshelfConfigPath(root = gardenSiteRoot()) {
|
|
10
|
-
return path.join(root, "content", "bookshelf.yaml");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The site shelf, or null when `content/bookshelf.yaml` is absent.
|
|
15
|
-
* Absent means the package demo (click-test only). A file here is the live shelf:
|
|
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>`.
|
|
18
|
-
* `preparedPosts`, when passed, are the sync pipeline's posts (rewritten asset
|
|
19
|
-
* URLs). Otherwise the markdown files are read directly.
|
|
20
|
-
* Throws `BookshelfConfigError` when the file, a cover, or a post path is broken.
|
|
21
|
-
*/
|
|
22
|
-
export function loadBookshelf(root = gardenSiteRoot(), preparedPosts) {
|
|
23
|
-
const file = bookshelfConfigPath(root);
|
|
24
|
-
if (!fs.existsSync(file)) return null;
|
|
25
|
-
let parsed;
|
|
26
|
-
try {
|
|
27
|
-
parsed = parseYaml(fs.readFileSync(file, "utf8"));
|
|
28
|
-
} catch (error) {
|
|
29
|
-
const detail = error instanceof Error ? error.message : "无法解析";
|
|
30
|
-
throw new BookshelfConfigError("invalid-root", `content/bookshelf.yaml 无法解析:${detail}`);
|
|
31
|
-
}
|
|
32
|
-
const config = parseBookshelfConfig(parsed);
|
|
33
|
-
const sources = preparedPosts
|
|
34
|
-
? preparedPosts.map((post) => ({
|
|
35
|
-
path: post.path,
|
|
36
|
-
title: post.title,
|
|
37
|
-
description: post.description,
|
|
38
|
-
cover: post.cover,
|
|
39
|
-
date: typeof post.date === "string" ? post.date : undefined,
|
|
40
|
-
content: post.content,
|
|
41
|
-
}))
|
|
42
|
-
: readReferencedPosts(root, config);
|
|
43
|
-
return bookshelfMountFromFile(config, sources);
|
|
44
|
-
}
|
|
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
|
-
|
|
55
|
-
function readReferencedPosts(root, config) {
|
|
56
|
-
const seen = new Set();
|
|
57
|
-
const posts = [];
|
|
58
|
-
for (const shelf of config.shelves) {
|
|
59
|
-
for (const book of shelf.books) {
|
|
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
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return posts;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function readPost(root, postPath) {
|
|
72
|
-
const relative = normalizeBookshelfPostPath(postPath);
|
|
73
|
-
const postsRoot = path.resolve(root, "content", "posts");
|
|
74
|
-
const file = `${path.resolve(postsRoot, ...relative.split("/"))}.md`;
|
|
75
|
-
if (!file.startsWith(`${postsRoot}${path.sep}`)) {
|
|
76
|
-
throw new BookshelfConfigError("post", `post「${postPath}」必须是 content/posts 下的路径,不带 .md。`);
|
|
77
|
-
}
|
|
78
|
-
if (!fs.existsSync(file)) {
|
|
79
|
-
throw new BookshelfConfigError("post", `content/bookshelf.yaml 引用的博文 ${relative} 不存在。`);
|
|
80
|
-
}
|
|
81
|
-
let parsed;
|
|
82
|
-
try {
|
|
83
|
-
parsed = matter(fs.readFileSync(file, "utf8"), {
|
|
84
|
-
engines: {
|
|
85
|
-
yaml: {
|
|
86
|
-
parse: (input) => parseYaml(input) ?? {},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
} catch (error) {
|
|
91
|
-
const detail = error instanceof Error ? error.message : "无法解析";
|
|
92
|
-
throw new BookshelfConfigError("post", `博文 ${relative} 无法解析:${detail}`);
|
|
93
|
-
}
|
|
94
|
-
const data = parsed.data && typeof parsed.data === "object" ? parsed.data : {};
|
|
95
|
-
return {
|
|
96
|
-
path: relative,
|
|
97
|
-
title: optionalString(data.title),
|
|
98
|
-
description: optionalString(data.description),
|
|
99
|
-
cover: optionalString(data.cover),
|
|
100
|
-
date: optionalDate(data.date),
|
|
101
|
-
content: parsed.content,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function optionalString(value) {
|
|
106
|
-
if (typeof value !== "string") return undefined;
|
|
107
|
-
const trimmed = value.trim();
|
|
108
|
-
return trimmed ? trimmed : undefined;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function optionalDate(value) {
|
|
112
|
-
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
113
|
-
return optionalString(value);
|
|
114
|
-
}
|
package/app/bookshelf/load.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { BookshelfMount } from "../components/bookshelf/catalog";
|
|
2
|
-
import { bookshelfConfigPath as configPath, loadBookshelf as readShelf } from "./load.mjs";
|
|
3
|
-
|
|
4
|
-
export type { BookshelfMount };
|
|
5
|
-
|
|
6
|
-
export function bookshelfConfigPath(root?: string) {
|
|
7
|
-
return configPath(root) as string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The site shelf, or null when `content/bookshelf.yaml` is absent.
|
|
12
|
-
* Absent means the package demo. See `load.mjs` for the file contract.
|
|
13
|
-
*/
|
|
14
|
-
export function loadBookshelf(root?: string): BookshelfMount | null {
|
|
15
|
-
return readShelf(root) as BookshelfMount | null;
|
|
16
|
-
}
|
package/app/bookshelf/page.tsx
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { bookshelf } from "@garden/bookshelf";
|
|
3
|
-
import { Bookshelf } from "../components/bookshelf";
|
|
4
|
-
import { siteLang } from "../lib/i18n";
|
|
5
|
-
import { pageAlternates } from "../lib/page-alternates";
|
|
6
|
-
import { siteConfig } from "../site.config";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* `/bookshelf` contract:
|
|
10
|
-
* - `content/bookshelf.yaml` present → that shelf, with each `post` rendered
|
|
11
|
-
* into `notes`, inside the site header. Indexed like the other pages.
|
|
12
|
-
* - file absent → package demo (Chinese click-test only, noindex, no site chrome).
|
|
13
|
-
* English builds skip the demo so a locale=en site stays free of that markup.
|
|
14
|
-
*/
|
|
15
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
16
|
-
const shelf = bookshelf;
|
|
17
|
-
if (shelf) {
|
|
18
|
-
return {
|
|
19
|
-
title: shelf.title,
|
|
20
|
-
description: shelf.description,
|
|
21
|
-
alternates: pageAlternates("/bookshelf"),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
if (siteLang === "zh") {
|
|
25
|
-
return { title: { absolute: "花园 · 读书笔记" }, robots: { index: false, follow: false } };
|
|
26
|
-
}
|
|
27
|
-
return { title: "Bookshelf", robots: { index: false, follow: false } };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default function BookshelfPage() {
|
|
31
|
-
const shelf = bookshelf;
|
|
32
|
-
if (shelf) {
|
|
33
|
-
return (
|
|
34
|
-
<main className="sh-main bookshelf-page" id="content">
|
|
35
|
-
<header className="sh-head">
|
|
36
|
-
{shelf.eyebrow ? <p className="sh-kicker">{shelf.eyebrow}</p> : null}
|
|
37
|
-
<h1 id="sh-title">{shelf.title}</h1>
|
|
38
|
-
{shelf.description ? <p className="sh-deck">{shelf.description}</p> : null}
|
|
39
|
-
</header>
|
|
40
|
-
<Bookshelf
|
|
41
|
-
embedded
|
|
42
|
-
catalog={shelf.catalog}
|
|
43
|
-
notes={shelf.notes}
|
|
44
|
-
title={shelf.title}
|
|
45
|
-
eyebrow={shelf.eyebrow ?? siteConfig.name}
|
|
46
|
-
/>
|
|
47
|
-
</main>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (siteLang !== "zh") return null;
|
|
51
|
-
return <Bookshelf />;
|
|
52
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useLayoutEffect, useRef } from "react";
|
|
4
|
-
import { siteHref } from "../../lib/site-href";
|
|
5
|
-
import { mountBookshelf } from "./engine";
|
|
6
|
-
import type { BookshelfProps, BookshelfShelf } from "./types";
|
|
7
|
-
import "./bookshelf.css";
|
|
8
|
-
|
|
9
|
-
const SHELF_SCROLL_KEY = "garden-bookshelf-scroll";
|
|
10
|
-
|
|
11
|
-
function withNoteLinks(catalog: BookshelfShelf[] | undefined) {
|
|
12
|
-
if (!catalog) return catalog;
|
|
13
|
-
return catalog.map((row) => ({
|
|
14
|
-
...row,
|
|
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
|
-
}),
|
|
29
|
-
}));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function readSavedScroll() {
|
|
33
|
-
try {
|
|
34
|
-
const raw = sessionStorage.getItem(SHELF_SCROLL_KEY);
|
|
35
|
-
if (raw == null) return null;
|
|
36
|
-
sessionStorage.removeItem(SHELF_SCROLL_KEY);
|
|
37
|
-
const y = Number(raw);
|
|
38
|
-
return Number.isFinite(y) ? y : null;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const DEMO_BRAND = "花园 · 读书笔记";
|
|
45
|
-
const DEMO_TITLE = "读书笔记";
|
|
46
|
-
|
|
47
|
-
function escapeHtml(value: string) {
|
|
48
|
-
return value
|
|
49
|
-
.replaceAll("&", "&")
|
|
50
|
-
.replaceAll("<", "<")
|
|
51
|
-
.replaceAll(">", ">")
|
|
52
|
-
.replaceAll('"', """);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function shelfShell(brand: string, title: string) {
|
|
56
|
-
return `
|
|
57
|
-
<div class="shelf-view" id="shelfView">
|
|
58
|
-
<div class="page">
|
|
59
|
-
<header class="top">
|
|
60
|
-
<div class="brand">${escapeHtml(brand)}</div>
|
|
61
|
-
<h1>${escapeHtml(title)}</h1>
|
|
62
|
-
<p class="lede is-debug" id="lede" hidden>调试说明:页级疏密;悬停/点按展一本;再点进笔记。</p>
|
|
63
|
-
<div class="qa-bar is-debug" id="qaBar" role="group" aria-label="演示控制" hidden>
|
|
64
|
-
<span class="qa-label">页级态</span>
|
|
65
|
-
<button type="button" class="qa-btn" data-mode="auto" id="modeAuto">自动</button>
|
|
66
|
-
<button type="button" class="qa-btn" data-mode="covers" id="modeCovers">A 全封面</button>
|
|
67
|
-
<button type="button" class="qa-btn" data-mode="spines" id="modeSpines">B 全立脊</button>
|
|
68
|
-
<span class="qa-hint" id="modeHint"></span>
|
|
69
|
-
</div>
|
|
70
|
-
</header>
|
|
71
|
-
<main id="bays"></main>
|
|
72
|
-
<footer class="foot is-debug" id="footBar" hidden>
|
|
73
|
-
<span>交互 demo · 抽→翻→笔记 · 对称返回</span>
|
|
74
|
-
<span id="footMode">—</span>
|
|
75
|
-
</footer>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<div class="note-view" id="noteView" hidden>
|
|
80
|
-
<article class="note">
|
|
81
|
-
<button type="button" class="back-btn" id="backBtn">← 返回书架</button>
|
|
82
|
-
<div class="note-hero">
|
|
83
|
-
<div class="note-cover" id="noteCover"></div>
|
|
84
|
-
<div class="note-meta">
|
|
85
|
-
<p class="note-cat" id="noteCat"></p>
|
|
86
|
-
<h1 id="noteTitle"></h1>
|
|
87
|
-
<p class="note-author" id="noteAuthor"></p>
|
|
88
|
-
<a class="note-read" id="noteRead" hidden>阅读全文</a>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
<div class="note-body" id="noteBody"></div>
|
|
92
|
-
</article>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
<div class="open-backdrop" id="openBackdrop" aria-hidden="true"></div>
|
|
96
|
-
<div class="open-veil" id="openVeil" aria-hidden="true">
|
|
97
|
-
<div class="open-veil-blob" id="openVeilBlob"></div>
|
|
98
|
-
</div>
|
|
99
|
-
<div class="stage" id="stage" aria-hidden="true"></div>
|
|
100
|
-
`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Approved bookshelf. Pass `catalog` and `notes` for a site shelf; omit them for the package demo. */
|
|
104
|
-
export function Bookshelf({ catalog, notes, title, eyebrow, embedded = false }: BookshelfProps) {
|
|
105
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
106
|
-
useLayoutEffect(() => {
|
|
107
|
-
const root = ref.current;
|
|
108
|
-
if (!root) return;
|
|
109
|
-
const saved = embedded ? readSavedScroll() : null;
|
|
110
|
-
const applyScroll = () => {
|
|
111
|
-
if (saved != null) window.scrollTo(0, saved);
|
|
112
|
-
};
|
|
113
|
-
applyScroll();
|
|
114
|
-
const unmount = mountBookshelf(root, { catalog: withNoteLinks(catalog), notes });
|
|
115
|
-
const frame = window.requestAnimationFrame(applyScroll);
|
|
116
|
-
return () => {
|
|
117
|
-
window.cancelAnimationFrame(frame);
|
|
118
|
-
unmount?.();
|
|
119
|
-
};
|
|
120
|
-
}, [catalog, notes, title, eyebrow, embedded]);
|
|
121
|
-
return (
|
|
122
|
-
<div
|
|
123
|
-
className={embedded ? "bookshelf-demo is-site" : "bookshelf-demo"}
|
|
124
|
-
ref={ref}
|
|
125
|
-
dangerouslySetInnerHTML={{ __html: shelfShell(eyebrow || DEMO_BRAND, title || DEMO_TITLE) }}
|
|
126
|
-
/>
|
|
127
|
-
);
|
|
128
|
-
}
|