diff --git a/APP_NOTES.md b/APP_NOTES.md index 7edb729..663e0a2 100644 --- a/APP_NOTES.md +++ b/APP_NOTES.md @@ -1,16 +1,25 @@ -# 老树 · 我的树 +# 老树 · 我的树 & 首页书架 -The **我的树** (My Tree) personal-archive page from the 老树 design commission, -implemented as a real React + Vite + TypeScript app. +The **我的树** (My Tree) personal-archive page and the **首页书架** (Bookshelf) +timeline page from the 老树 design commission, implemented as a real React + +Vite + TypeScript app. -It's a port of the Claude Design prototype at `design/project/老树 · 我的树.dc.html`, -recreated pixel-for-pixel outside the proprietary `dc-runtime` format. +我的树 is a port of the Claude Design prototype at +`design/project/老树 · 我的树.dc.html`, recreated pixel-for-pixel outside the +proprietary `dc-runtime` format. 首页书架 is a port of +`design/project/老树 · 首页书架.dc.html`. 我的树 was originally hardcoded to a +single book (道德经); it's now generic over a `Book` (see `src/books/`), driven +by the `/tree/:bookId` route, so the shelf can link into any planted book's +tree. A second book, **《心经》** (the Buddhist Heart Sutra), was added this way +end to end: shelf entry, five annotated branches, and its own 周年回声. ## What the page is Each reader grows an abstract tree: -- **枝 (branches)** — chapters read, labelled with their number (十一, 八, 四十四…). +- **枝 (branches)** — chapters/passages read, labelled either by chapter number + (道德经: 十一, 八, 四十四…) or a two-character theme tag for a book with no + chapters (心经: 色空, 五蕴, 空相…). - **叶 (leaves)** — annotations left. A **朱砂 (cinnabar)** leaf is 入木 — enshrined, set alongside the ancients. A **青绿 (verdigris)** leaf is 青芽 — a sprout still awaiting time and peers. @@ -35,8 +44,18 @@ Design notes, all inside the 禁则 (no counts, no gradients, no glow, cinnabar - Quantity is shown by **form** (branches, leaves, rings), never a number badge. - **Cinnabar** marks the human hand: signatures, the active/hovered branch, genre badges. -- The 四十四 sprout's annotation is the same one echoed in **周年回声** below — the tree and - the memory are one story. +- The tree's one 青芽 sprout's annotation is the same one echoed in **周年回声** below — the + tree and the memory are one story. (道德经: 第四十四章; 心经: 「诸法空相」句.) + +## 首页书架 (Bookshelf) + +`/` lays every book on a horizontal timeline positioned by the age of its text +(`MIN=-600` to `MAX=2026`), not a grid — a book from 400 BCE sits far from one +from 1980. Planked spines grow up from the timeline on load (`growUp`); a live +book is an `` into `/tree/:bookId`, a not-yet-planted one is an inert span +with lowered opacity. Hovering any plank raises a caption card with its teaser +line and a "已生长约 N 年 · M 层年轮" meta line, matching the same 无计数原则 as +我的树 — these are decorative age/rings, not user-facing counts of anything. ## Run @@ -51,9 +70,15 @@ npm run preview # serve the production build | File | Purpose | | --- | --- | -| `src/MyTree.tsx` | The page component and its branch data | -| `src/MyTree.css` | Page layout + hover affordances | +| `src/main.tsx` | Router: `/` → Bookshelf, `/tree/:bookId` → MyTree | +| `src/Bookshelf.tsx` / `.css` | The 首页书架 timeline page | +| `src/MyTree.tsx` / `.css` | The 我的树 page, generic over a `Book` | +| `src/books/types.ts` | `Book` / `Branch` / `ShelfEntry` data model | +| `src/books/daodejing.ts`, `src/books/xinjing.ts` | Per-book data | +| `src/books/index.ts` | `BOOKS`, `PLACEHOLDER_BOOKS`, `getBook()` | | `src/theme.css` | Design tokens (墨字朱批 palette), day/night themes, keyframes | -Sibling links (回到阅读, 回去看看, 接续一年前的自己) point at routes for pages that -are elsewhere in the commission and not built in this project. +Sibling links (回到阅读, 回去看看, 接续一年前的自己, 考据) point at routes for pages +that are elsewhere in the commission and not built in this project. Visiting +`/tree/:bookId` for a placeholder (not-yet-planted) book id shows a small +"这本书还没有种下" state with a link back to the shelf, instead of a blank tree. diff --git a/README.md b/README.md index 58172d2..d661220 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,23 @@ 不是读书 App,不是笔记工具,不是 AI 聊天产品 —— 更接近一座**可以走进去的、还在生长的碑林**。 -This repository holds the 老树 product front-end. The first page implemented is +This repository holds the 老树 product front-end. The pages implemented so far are +**首页书架 (Bookshelf)** — a timeline of every planted book — and, per book, **我的树 (My Tree)** — a reader's personal archive, rendered as an abstract, slowly growing tree. +## 首页书架 (Bookshelf) + +The home page (`/`) lays every book on a timeline by the age of its text, not a +grid. Two books are planted (live) and walkable: + +- **《道德经》** (Taoism, ~5th century BCE) +- **《心经》** (Buddhism, the Heart Sutra, Xuanzang's 7th-century translation) + +论语 and 庄子 sit on the same timeline as **即将种下** (soon to be planted) +placeholders — ghosts of a growing library, not yet clickable. Hovering any plank +shows a teaser card; clicking a live one walks into that book's 我的树. + ## 我的树 (My Tree) Each reader grows a tree: @@ -42,15 +55,29 @@ npm run preview # serve the production build | Path | What | | --- | --- | -| `src/MyTree.tsx` | The 我的树 page component + its branch/annotation data | +| `src/Bookshelf.tsx` | The 首页书架 (`/`) timeline page | +| `src/Bookshelf.css` | Its layout, timeline ticks, plank hover cards | +| `src/MyTree.tsx` | The 我的树 (`/tree/:bookId`) page — generic, driven by a `Book` | | `src/MyTree.css` | Page layout, the SVG tree, hover reveal, growth rings | +| `src/books/types.ts` | Shared `Book` / `Branch` / `ShelfEntry` data model | +| `src/books/daodejing.ts` | 《道德经》's branches, annotations, shelf entry | +| `src/books/xinjing.ts` | 《心经》's branches, annotations, shelf entry | +| `src/books/index.ts` | Book registry (`BOOKS`, `PLACEHOLDER_BOOKS`, `getBook`) | | `src/theme.css` | Design tokens (墨字朱批 palette), day/night themes, keyframes | -| `APP_NOTES.md` | Detailed notes on this page | +| `APP_NOTES.md` | Detailed notes on these pages | | `design/` | The original Claude Design handoff bundle — chat transcript + all page prototypes. The source of truth for the pages not yet built. | +## Adding a book + +A book is just an entry in `src/books/`: a `Book` (title, reader, planted date, +branches with their annotations, the anniversary echo, and a shelf entry for the +timeline) registered in `src/books/index.ts`. A book without a real tree yet can +be added to `PLACEHOLDER_BOOKS` instead — it shows on the shelf as **即将种下** +and isn't clickable until it has real branch data. + ## Not yet built -`design/project/` holds prototypes for the rest of the product: 首页书架, 书籍详情页, +`design/project/` holds prototypes for the rest of the product: 书籍详情页, 阅读页 (the design's stated priority — its "signature" screen with the time-slider), 落笔流程, 句子房间, 考据, 分享卡, 守树人. `我的树` links out to `/read` and `/inscribe` as placeholders for those routes. diff --git a/package-lock.json b/package-lock.json index e07cb30..c6846f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.1.0", "dependencies": { "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.4" }, "devDependencies": { "@types/react": "^18.3.12", @@ -793,6 +794,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1627,6 +1637,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", diff --git a/package.json b/package.json index eade0c2..0cbd1db 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ }, "dependencies": { "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.4" }, "devDependencies": { "@types/react": "^18.3.12", diff --git a/src/Bookshelf.css b/src/Bookshelf.css new file mode 100644 index 0000000..55836ca --- /dev/null +++ b/src/Bookshelf.css @@ -0,0 +1,300 @@ +/* 首页书架 — books sit on a timeline by the age of their text, not a grid. + Same 墨字朱批 tokens and 禁则 as 我的树: no counts, no gradients, no glow. */ + +.shelf-page { + position: relative; + z-index: 1; + min-height: 100vh; + display: flex; + flex-direction: column; + font-family: 'Noto Sans SC', sans-serif; + color: var(--ink); +} + +.shelf-page .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 26px 44px; +} + +.shelf-page .brand { + display: flex; + align-items: center; + gap: 12px; + text-decoration: none; +} + +.shelf-page .brand-name { + font-family: 'Noto Serif SC', serif; + font-size: 17px; + font-weight: 500; + letter-spacing: 0.3em; + color: var(--ink); +} + +.shelf-page .tagline { + font-size: 12px; + color: var(--ink-light); + letter-spacing: 0.08em; + margin-left: 4px; +} + +.shelf-page .nav { + display: flex; + gap: 28px; + font-size: 13px; + color: var(--ink-light); +} + +.shelf-page .link-muted { + color: var(--ink-light); + text-decoration: none; + cursor: pointer; + transition: color 0.3s ease; +} +.shelf-page .link-muted:hover { + color: var(--ink); +} + +.shelf-stage { + position: relative; + flex: 1; + min-height: 600px; + padding: 0 44px; +} + +.intro { + position: absolute; + top: 9%; + left: 44px; + max-width: 520px; +} + +.intro-line { + font-family: 'Noto Serif SC', serif; + font-size: 27px; + letter-spacing: 0.08em; + line-height: 1.8; +} + +.intro-sub { + margin-top: 14px; + font-size: 13px; + line-height: 1.9; + color: var(--ink-light); + letter-spacing: 0.04em; +} + +.timeline { + position: absolute; + left: 64px; + right: 64px; + bottom: 26%; +} + +.timeline-rule { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: var(--line); +} + +.tick { + position: absolute; + bottom: -4px; + transform: translateX(-50%); +} + +.tick-mark { + width: 1px; + height: 6px; + background: var(--line); + margin: 0 auto; +} + +.tick-label { + margin-top: 8px; + font-family: Spectral, 'Noto Sans SC', serif; + font-feature-settings: 'onum'; + font-size: 11px; + white-space: nowrap; + color: var(--ink-light); +} + +.tick-now { + color: var(--cinnabar); +} + +.book { + position: absolute; + bottom: 2px; +} + +.plank-grow { + width: 100%; + height: 100%; + transform-origin: bottom; + animation: growUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both; +} +@keyframes growUp { + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); + } +} + +.plank { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + box-sizing: border-box; + border: 1px solid var(--ink); + background: var(--paper); + text-decoration: none; + color: var(--ink); + cursor: pointer; + transition: transform 0.3s ease, background-color 0.5s ease; +} +.plank:hover { + transform: translateY(-6px); +} + +.plank-dormant { + cursor: default; + opacity: 0.55; +} +.plank-dormant:hover { + transform: none; +} + +.plank-lines::before, +.plank-lines::after { + content: ''; + position: absolute; + top: 8px; + bottom: 8px; + width: 1px; + background: var(--line); +} +.plank-lines::before { + left: 5px; +} +.plank-lines::after { + right: 5px; +} + +.plank-title { + writing-mode: vertical-rl; + font-family: 'Noto Serif SC', serif; + letter-spacing: 0.4em; + line-height: 1; +} + +.plank-seal { + position: absolute; + bottom: 10px; + left: 50%; + margin-left: -3px; + width: 6px; + height: 6px; + background: var(--cinnabar); +} + +.hover-card { + position: absolute; + bottom: calc(100% + 20px); + left: 50%; + transform: translateX(-50%); + width: 264px; + background: var(--paper); + border: 0.5px solid var(--line); + border-radius: 8px; + padding: 18px 20px; + box-shadow: 0 10px 30px rgba(31, 29, 26, 0.08); + animation: cardIn 0.3s ease-out both; + z-index: 5; +} +@keyframes cardIn { + from { + opacity: 0; + transform: translate(-50%, 6px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } +} + +.hover-title { + font-family: 'Noto Serif SC', serif; + font-size: 17px; + letter-spacing: 0.12em; + margin-bottom: 8px; +} + +.hover-line { + font-family: 'Noto Serif SC', serif; + font-size: 13.5px; + line-height: 1.8; + color: var(--ink); + margin-bottom: 10px; +} + +.hover-meta { + font-family: Spectral, 'Noto Sans SC', serif; + font-feature-settings: 'onum'; + font-size: 12px; + color: var(--ink-light); + margin-bottom: 12px; +} + +.hover-cta { + font-size: 12px; + letter-spacing: 0.12em; +} +.cta-live { + color: var(--cinnabar); +} +.cta-dormant { + color: var(--verdigris); +} + +.shelf-caption { + position: absolute; + bottom: 8%; + font-size: 12px; + letter-spacing: 0.2em; + color: var(--ink-light); +} +.shelf-caption-left { + left: 64px; +} +.shelf-caption-right { + right: 64px; + letter-spacing: 0.12em; + text-align: right; +} + +@media (max-width: 640px) { + .shelf-page .header, + .shelf-stage { + padding-left: 24px; + padding-right: 24px; + } + .intro { + left: 24px; + max-width: calc(100% - 48px); + } + .timeline { + left: 32px; + right: 32px; + } +} diff --git a/src/Bookshelf.tsx b/src/Bookshelf.tsx new file mode 100644 index 0000000..47823c0 --- /dev/null +++ b/src/Bookshelf.tsx @@ -0,0 +1,155 @@ +import { useCallback, useState } from 'react' +import { Link } from 'react-router-dom' +import './Bookshelf.css' +import { SHELF_BOOKS } from './books' + +/** + * 首页书架 (Bookshelf) — every book sits on a timeline by the age of its text, + * not a bookshelf grid. Planted (live) books can be walked into; the rest are + * "即将种下" (soon to be planted) placeholders — ghosts of a growing library. + */ + +const MIN = -600 +const MAX = 2026 + +function pct(year: number): string { + return (((year - MIN) / (MAX - MIN)) * 100).toFixed(2) + '%' +} + +function half(width: string): string { + return '-' + parseInt(width, 10) / 2 + 'px' +} + +const TICK_YEARS = [-600, -400, -200, 0, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000] + +export default function Bookshelf() { + const [night, setNight] = useState(false) + const [hover, setHover] = useState(null) + + const toggleNight = useCallback(() => { + setNight((prev) => { + const next = !prev + if (next) document.body.setAttribute('data-night', '1') + else document.body.removeAttribute('data-night') + return next + }) + }, []) + + return ( +
+
+ + + 老树 + 千年经典的活体年轮库 + + +
+ +
+
+
一座还在生长的碑林。
+
+ AI 长出土壤,人在其上署名落笔,时间让字入木。 +
+ 免登录,全站可读。 +
+
+ +
+
+ + {TICK_YEARS.map((y) => ( +
+
+
{y < 0 ? `前 ${-y}` : String(y === 0 ? 1 : y)}
+
+ ))} +
+
+
+
+ + {SHELF_BOOKS.map((b, i) => { + const s = b.shelf + const isHovered = hover === b.id + const content = ( + <> +
+ + {s.shortTitle} + + {s.seal &&
} + + ) + return ( +
setHover(b.id)} + onMouseLeave={() => setHover(null)} + > +
+ {s.live ? ( + + {content} + + ) : ( + + {content} + + )} +
+ {isHovered && ( +
+
{s.shortTitle}
+
{s.line}
+
{s.meta}
+
+ {s.live ? '走进这本书 →' : '即将种下'} +
+
+ )} +
+ ) + })} +
+ +
越老的书,年轮越厚。
+
书架尚空。空不是缺陷,是等待被种下的余地。
+
+
+ ) +} diff --git a/src/MyTree.tsx b/src/MyTree.tsx index e773e3c..27c5534 100644 --- a/src/MyTree.tsx +++ b/src/MyTree.tsx @@ -1,9 +1,14 @@ import { useCallback, useState } from 'react' +import { Link, useParams } from 'react-router-dom' import './MyTree.css' +import { getBook } from './books' +import type { Book, Branch } from './books' /** - * One branch of the abstract tree. - * 枝 = a chapter read; 叶 (leaf) = an annotation left; + * 我的树 (My Tree) — a reader's personal archive for one book, rendered as an + * abstract, slowly growing tree. + * + * 枝 = a chapter/passage read; 叶 (leaf) = an annotation left; * 朱砂 leaf = 入木 (enshrined, alongside the ancients); * 青绿 leaf = 青芽 (a sprout, awaiting time & peers); * thread = someone continued your line across the years. @@ -11,123 +16,14 @@ import './MyTree.css' * Hovering / focusing a branch quietly surfaces what you wrote there — the * page's one concession to 探索欲, kept as an inline caption, never a bubble. */ -type LeafKind = '' | 'cinnabar' | 'verdigris' -type State = '入木' | '青芽' | '读过' - -type Branch = { - id: string - /** chapter label (Chinese numeral) shown on the branch */ - label: string - /** chapter name for the caption */ - chapter: string - /** SVG path drawn on as the branch grows */ - d: string - /** stroke width — upper (younger) branches read thinner */ - w: number - leaf: LeafKind - leafAt?: { x: number; y: number } - labelAt: { x: number; y: number; anchor: 'start' | 'end' } - drawDelay: string - leafDelay: string - state: State - /** 注/疑/证/驳 — only annotated branches carry a genre */ - genre?: string - /** the anchored line of scripture */ - source?: string - /** the annotation itself */ - note?: string - /** cinnabar signature + date */ - sign?: string - /** a verdigris "someone continued you" thread hangs off this branch */ - thread?: { d: string; at: { x: number; y: number }; label: string } -} -// Geometry hand-tuned in a 440×360 space for an organic, drawn look. -// Lower branches are longer/older; higher branches shorter/younger. -const BRANCHES: Branch[] = [ - { - id: 'ch11', - label: '十一', - chapter: '第十一章', - d: 'M 220 250 Q 276 234 332 231', - w: 1.5, - leaf: 'cinnabar', - leafAt: { x: 332, y: 231 }, - labelAt: { x: 342, y: 235, anchor: 'start' }, - drawDelay: '0.9s', - leafDelay: '1.7s', - state: '入木', - genre: '证', - source: '当其无,有器之用。', - note: '学陶那年最废的一只碗,是我捏得最厚的那只——几乎没有「无」,盛不下什么。', - sign: '沈一苇 · 2023 冬', - thread: { d: 'M 332 231 L 374 205', at: { x: 378, y: 201 }, label: '陈拾 · 3 年后接续' }, - }, - { - id: 'ch8', - label: '八', - chapter: '第八章', - d: 'M 220 214 Q 168 202 116 196', - w: 1.4, - leaf: '', - labelAt: { x: 106, y: 200, anchor: 'end' }, - drawDelay: '1.15s', - leafDelay: '1.7s', - state: '读过', - source: '上善若水。', - }, - { - id: 'ch44', - label: '四十四', - chapter: '第四十四章', - d: 'M 221 176 Q 268 166 312 162', - w: 1.3, - leaf: 'verdigris', - leafAt: { x: 312, y: 162 }, - labelAt: { x: 322, y: 166, anchor: 'start' }, - drawDelay: '1.4s', - leafDelay: '2.0s', - state: '青芽', - genre: '证', - source: '知足不辱,知止不殆。', - note: '辞掉第二份工作的那晚我写:知道停在哪里,比知道去哪里难。', - sign: '沈一苇 · 2025 春', - }, - { - id: 'ch22', - label: '二十二', - chapter: '第二十二章', - d: 'M 220 146 Q 178 138 138 134', - w: 1.2, - leaf: '', - labelAt: { x: 128, y: 138, anchor: 'end' }, - drawDelay: '1.65s', - leafDelay: '2.2s', - state: '读过', - source: '曲则全,枉则直。', - }, - { - id: 'ch64', - label: '六十四', - chapter: '第六十四章', - d: 'M 221 116 Q 256 108 288 105', - w: 1.1, - leaf: '', - labelAt: { x: 298, y: 109, anchor: 'start' }, - drawDelay: '1.9s', - leafDelay: '2.4s', - state: '读过', - source: '千里之行,始于足下。', - }, -] - -const LEAF_COLOR: Record, string> = { +const LEAF_COLOR: Record<'cinnabar' | 'verdigris', string> = { cinnabar: 'var(--cinnabar)', verdigris: 'var(--verdigris)', } /** A hovered branch's detail, or the resting hint. */ -function BranchDetail({ branch }: { branch: Branch | null }) { +function BranchDetail({ book, branch }: { book: Book; branch: Branch | null }) { if (!branch) { return
悬停一枝,看你在那一章写下的。
} @@ -139,7 +35,9 @@ function BranchDetail({ branch }: { branch: Branch | null }) { {branch.state !== '读过' && } {branch.state === '读过' ? '读过 · 尚未落笔' : branch.state} - 道德经 · {branch.chapter} + + {book.title} · {branch.chapter} + {branch.genre && {branch.genre}} {branch.source &&
{branch.source}
} @@ -149,7 +47,26 @@ function BranchDetail({ branch }: { branch: Branch | null }) { ) } +function NotPlanted() { + return ( +
+
+
我的树
+
这本书还没有种下
+
+ + 回书架看看 → + +
+
+
+ ) +} + export default function MyTree() { + const { bookId } = useParams<{ bookId: string }>() + const book = getBook(bookId) + // Night ("夜读") is a whole-page atmosphere; the prototype toggled a body attribute. const [night, setNight] = useState(false) const [active, setActive] = useState(null) @@ -163,12 +80,16 @@ export default function MyTree() { }) }, []) - const activeBranch = BRANCHES.find((b) => b.id === active) ?? null + if (!book) return + + const activeBranch = book.branches.find((b) => b.id === active) ?? null + const cinnabarCount = book.branches.filter((b) => b.leaf === 'cinnabar').length + const verdigrisCount = book.branches.filter((b) => b.leaf === 'verdigris').length return (
- + {/* three concentric rings — a tree's growth rings, the product's mark */} 老树 - +