From 4650b3d7779d0df4d47d7ecfcf3f47088b604ac0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 01:17:20 +0000 Subject: [PATCH] =?UTF-8?q?Add=20=E9=87=91=E5=88=9A=E7=BB=8F=20book=20biog?= =?UTF-8?q?raphy=20page=20(=E4=B9=A6=E7=B1=8D=E8=AF=A6=E6=83=85=E9=A1=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the design's 老树 · 道德经.dc.html prototype into a reusable BookPage component (timeline reveal, translation comparison aside, readers' rings), and supplies it with real content for 《金刚经》: composition, Kumārajīva's and Xuanzang's translations, Huìnéng's awakening, the 868 CE Dunhuang scroll, its 1900 rediscovery and 1907 removal to London, through to today's reader annotations. Wired up at /books/jingangjing via minimal pathname-based routing in main.tsx, linked from 我的树's nav. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AvYL3A1wpWS51MUbBFqhgM --- README.md | 32 ++- src/BookPage.css | 395 ++++++++++++++++++++++++++++++++++++++ src/BookPage.tsx | 215 +++++++++++++++++++++ src/MyTree.tsx | 3 + src/books/diamondSutra.ts | 149 ++++++++++++++ src/books/types.ts | 61 ++++++ src/main.tsx | 18 +- 7 files changed, 867 insertions(+), 6 deletions(-) create mode 100644 src/BookPage.css create mode 100644 src/BookPage.tsx create mode 100644 src/books/diamondSutra.ts create mode 100644 src/books/types.ts diff --git a/README.md b/README.md index 58172d2..208264b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,21 @@ dark atmosphere. Everything respects `prefers-reduced-motion`. Per the design bible there are **no counts anywhere** — quantity is expressed by form, never a number badge — and cinnabar, the colour of the human hand, is used sparingly. +## 金剛經 · 一本书的传记 + +The second page implemented is a **书籍详情页** (book detail page) — a book's +biography, scrolling from its composition through the hands that carried it +forward, ending in the living 年轮 of today's readers. It ports the design's +`老树 · 道德经.dc.html` prototype into a reusable `BookPage` component, and +supplies it with real content for **《金刚经》** (the Diamond Sutra): from its +composition in India, through Kumārajīva's 402 CE translation and Xuanzang's +later retranslation, Huìnéng's awakening on hearing its central line, the 868 +CE Dunhuang scroll — the oldest complete, dated printed book known to survive — +its 1900 rediscovery and 1907 removal to London, and on to today's readers' +own 注/疑/证/驳 annotations. + +Visit it at `/books/jingangjing`, or follow the `金刚经` link from 我的树's nav. + ## Develop ```bash @@ -44,13 +59,20 @@ npm run preview # serve the production build | --- | --- | | `src/MyTree.tsx` | The 我的树 page component + its branch/annotation data | | `src/MyTree.css` | Page layout, the SVG tree, hover reveal, growth rings | +| `src/BookPage.tsx` | The 书籍详情页 component — any book's biography timeline | +| `src/BookPage.css` | Page layout, timeline reveal, comparison aside, rings | +| `src/books/types.ts` | Shared `BookData` shape a book's data must supply | +| `src/books/diamondSutra.ts` | 《金刚经》's data — timeline, translation comparison, rings | +| `src/main.tsx` | Renders 我的树 at `/`, a book page at `/books/:slug` | | `src/theme.css` | Design tokens (墨字朱批 palette), day/night themes, keyframes | -| `APP_NOTES.md` | Detailed notes on this page | +| `APP_NOTES.md` | Detailed notes on the 我的树 page | | `design/` | The original Claude Design handoff bundle — chat transcript + all page prototypes. The source of truth for the pages not yet built. | ## Not yet built -`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. +`design/project/` holds prototypes for the rest of the product: 首页书架 +(the bookshelf home — no book is on it yet), 阅读页 (the design's stated +priority — its "signature" screen with the time-slider), 落笔流程, 句子房间, 考据, +分享卡, 守树人. `我的树` links out to `/read` and `/inscribe`, and the book page's +CTAs link to `/books/jingangjing/read` and `/books/jingangjing/compare`, all as +placeholders for those routes. diff --git a/src/BookPage.css b/src/BookPage.css new file mode 100644 index 0000000..1b5f4c6 --- /dev/null +++ b/src/BookPage.css @@ -0,0 +1,395 @@ +/* 书籍详情页 · 一本书的传记 — a timeline biography of a book, from its + composition through the hands that carried it forward, ending in the + living 年轮 of today's readers. Ported from 老树 · 道德经.dc.html. */ + +.book-page { + position: relative; + z-index: 1; + min-height: 100vh; + font-family: 'Noto Sans SC', sans-serif; + color: var(--ink); +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 26px 44px; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + text-decoration: none; +} + +.brand-name { + font-family: 'Noto Serif SC', serif; + font-size: 17px; + font-weight: 500; + letter-spacing: 0.3em; + color: var(--ink); +} + +.brand-tag { + font-size: 12px; + color: var(--ink-light); + letter-spacing: 0.08em; + margin-left: 4px; +} + +.nav { + display: flex; + gap: 28px; + font-size: 13px; + color: var(--ink-light); +} + +.link-muted { + color: var(--ink-light); + text-decoration: none; + cursor: pointer; + transition: color 0.3s ease; +} +.link-muted:hover { + color: var(--ink); +} + +.link-cinnabar { + color: var(--cinnabar); + text-decoration: none; + transition: color 0.3s ease; +} +.link-cinnabar:hover { + color: var(--seal); +} + +.link-cinnabar-inline { + color: var(--cinnabar); + cursor: pointer; +} + +/* ---- hero ---- */ + +.book-hero { + max-width: 720px; + margin: 0 auto; + padding: 88px 44px 96px 44px; + position: relative; + box-sizing: border-box; +} + +.hero-side-quote { + position: absolute; + right: 44px; + top: 80px; + writing-mode: vertical-rl; + font-family: 'Noto Serif SC', serif; + font-size: 15px; + letter-spacing: 0.5em; + color: var(--ink-light); + opacity: 0.55; + line-height: 2; +} + +.eyebrow { + font-size: 12px; + color: var(--ink-light); + letter-spacing: 0.3em; + margin-bottom: 32px; +} + +.book-title { + font-family: 'Noto Serif SC', serif; + font-size: 60px; + font-weight: 400; + letter-spacing: 0.18em; + line-height: 1.3; + margin: 0; +} + +.book-meta { + margin-top: 20px; + font-family: Spectral, 'Noto Sans SC', serif; + font-feature-settings: 'onum'; + font-size: 13px; + color: var(--ink-light); + letter-spacing: 0.06em; +} + +.hero-quote { + margin-top: 40px; + font-family: 'Noto Serif SC', serif; + font-size: 21px; + letter-spacing: 0.05em; + max-width: 560px; +} + +.hero-cta { + margin-top: 36px; + font-size: 13px; + letter-spacing: 0.14em; +} + +/* ---- biography timeline ---- */ + +.biography { + max-width: 720px; + margin: 0 auto; + padding: 0 44px 60px 44px; + box-sizing: border-box; +} + +.biography-label { + font-size: 12px; + letter-spacing: 0.5em; + color: var(--ink-light); + margin-bottom: 56px; +} + +.timeline { + position: relative; + padding-left: 140px; +} + +.timeline::before { + content: ''; + position: absolute; + left: -20px; + top: 8px; + bottom: 8px; + width: 1px; + background: var(--line); +} + +.timeline-node { + position: relative; + margin-bottom: 76px; + transition: opacity 0.7s ease, transform 0.7s ease; +} + +.node-year { + position: absolute; + left: -140px; + top: 3px; + width: 106px; + text-align: right; + font-family: Spectral, 'Noto Sans SC', serif; + font-feature-settings: 'onum'; + font-size: 13px; + letter-spacing: 0.04em; +} + +.node-dot { + position: absolute; + left: -24px; + top: 6px; + width: 7px; + height: 7px; + border-radius: 50%; +} + +.node-who { + font-size: 14px; + font-weight: 500; + letter-spacing: 0.04em; + margin-bottom: 10px; +} + +.node-story { + font-family: 'Noto Serif SC', serif; + font-size: 14.5px; + line-height: 2.05; + letter-spacing: 0.02em; + color: var(--ink); +} + +.node-anchor { + margin-top: 16px; + font-family: 'Noto Serif SC', serif; + font-size: 15px; + line-height: 1.9; + color: var(--ink); + border-left: 2px solid var(--u-soft); + padding-left: 14px; +} + +/* ---- comparison aside: "经文在此改变" ---- */ + +.comparison { + margin-top: 20px; + border: 0.5px solid var(--line); + border-radius: 8px; + padding: 20px 22px; +} + +.comparison-label { + font-size: 11.5px; + letter-spacing: 0.24em; + color: var(--cinnabar); + margin-bottom: 16px; +} + +.comparison-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px 24px; + font-family: 'Noto Serif SC', serif; + font-size: 13.5px; + line-height: 1.9; +} + +.comparison-header { + font-size: 11px; + font-family: 'Noto Sans SC', sans-serif; + color: var(--ink-light); + letter-spacing: 0.1em; +} + +.comparison-row-pair { + display: contents; +} + +.comparison-note { + margin-top: 16px; + font-size: 12.5px; + color: var(--ink-light); + line-height: 1.8; +} + +.comparison-cta { + margin-top: 16px; + font-size: 12px; + letter-spacing: 0.1em; +} + +/* ---- today's rings: readers' living annotations ---- */ + +.today-rings { + margin-top: 20px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.ring-note { + border: 0.5px solid var(--line); + border-radius: 8px; + padding: 16px 18px; +} + +.ring-head { + display: flex; + align-items: baseline; + gap: 10px; + margin-bottom: 8px; +} + +.ring-name { + font-size: 13px; + font-weight: 500; +} + +.ring-badge { + font-size: 11px; + border: 0.5px solid; + border-radius: 4px; + padding: 1px 6px; +} + +.ring-tag { + font-size: 11px; + letter-spacing: 0.1em; +} + +.ring-date { + font-family: Spectral, serif; + font-feature-settings: 'onum'; + font-size: 12px; + color: var(--ink-light); + margin-left: auto; +} + +.ring-text { + font-family: 'Noto Serif SC', serif; + font-size: 13.5px; + line-height: 1.9; + color: var(--ink); +} + +.today-cta { + margin-top: 10px; + font-size: 13px; + letter-spacing: 0.14em; +} + +/* ---- ask the 守树人 ---- */ + +.ask-row { + margin-top: 14px; + text-align: right; +} + +.ask-link { + font-size: 12px; + letter-spacing: 0.08em; +} +.ask-link:hover { + color: var(--cinnabar); +} + +.ask-gate { + font-family: 'Noto Serif SC', serif; + font-size: 12.5px; + color: var(--ink-light); +} + +.book-footer { + text-align: center; + padding: 0 44px 96px 44px; + font-size: 12px; + letter-spacing: 0.2em; + color: var(--ink-light); +} + +.watcher-link { + position: fixed; + right: 28px; + bottom: 22px; + font-size: 12px; + letter-spacing: 0.1em; + color: var(--ink-light); + text-decoration: none; + transition: color 0.3s ease; + z-index: 20; +} +.watcher-link:hover { + color: var(--cinnabar); +} + +@media (max-width: 640px) { + .header, + .book-hero, + .biography { + padding-left: 24px; + padding-right: 24px; + } + .hero-side-quote { + display: none; + } + .book-title { + font-size: 40px; + } + .timeline { + padding-left: 96px; + } + .node-year { + left: -96px; + width: 76px; + font-size: 11.5px; + } + .comparison-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/BookPage.tsx b/src/BookPage.tsx new file mode 100644 index 0000000..2aaddc4 --- /dev/null +++ b/src/BookPage.tsx @@ -0,0 +1,215 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import './BookPage.css' +import type { BookData } from './books/types' + +/** + * 书籍详情页 · 一本书的传记 — a scrollable biography of a book: who carried + * the text forward across centuries, ending in the living 年轮 of readers' + * own annotations today. Ported from the design's + * 老树 · 道德经.dc.html prototype, generalised to take any book's data. + */ +export default function BookPage({ book }: { book: BookData }) { + const [night, setNight] = useState(false) + const [seen, setSeen] = useState>({}) + const [asking, setAsking] = useState(null) + const ioRef = useRef(null) + + useEffect(() => { + document.title = `老树 · ${book.title} · 一本书的传记` + }, [book.title]) + + const toggleNight = useCallback(() => { + setNight((prev) => { + const next = !prev + if (next) document.body.setAttribute('data-night', '1') + else document.body.removeAttribute('data-night') + return next + }) + }, []) + + useEffect(() => { + const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches + if (reduced || !window.IntersectionObserver) { + const all: Record = {} + book.timeline.forEach((n) => (all[n.id] = true)) + setSeen(all) + return + } + const io = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + const id = entry.target.getAttribute('data-node') + if (entry.isIntersecting && id) { + setSeen((prev) => (prev[id] ? prev : { ...prev, [id]: true })) + io.unobserve(entry.target) + } + }) + }, + { threshold: 0.12 }, + ) + ioRef.current = io + const observe = () => document.querySelectorAll('[data-node]').forEach((el) => io.observe(el)) + const t1 = setTimeout(observe, 150) + const t2 = setTimeout(observe, 1200) + return () => { + clearTimeout(t1) + clearTimeout(t2) + io.disconnect() + } + }, [book.timeline]) + + return ( +
+
+ + + 老树 + 千年经典的活体年轮库 + + +
+ +
+
{book.sideQuote}
+
{book.shelfCategory}
+

{book.title}

+
{book.metaLine}
+
{book.heroQuote}
+ +
+ +
+
一本书的传记
+
+ {book.timeline.map((node) => { + const isSeen = !!seen[node.id] + const showAsk = !node.noAsk && asking !== node.id + const showAsking = !node.noAsk && asking === node.id + return ( +
+
+ {node.year} +
+
+
{node.who}
+
{node.story}
+ {node.anchor &&
{node.anchor}
} + + {node.special && ( +
+
{book.comparison.label}
+
+
{book.comparison.columns[0]}
+
{book.comparison.columns[1]}
+ {book.comparison.rows.map((row, i) => ( +
+
{row[0]}
+
{row[1]}
+
+ ))} +
+
{book.comparison.note}
+ +
+ )} + + {node.today && ( +
+ {book.rings.map((r) => ( +
+
+ + {r.name} + + + {r.genre} + + + {r.state} + + {r.date} +
+
{r.text}
+
+ ))} + +
+ )} + + {showAsk && ( +
+ setAsking(node.id)}> + 与守树人细究此节 → + +
+ )} + {showAsking && ( +
+ 读是自由的。落笔与问路,需要一个名字。 → 留下名字 +
+ )} +
+ ) + })} +
+
+ +
{book.footerLine}
+ + + 问守树人 + +
+ ) +} diff --git a/src/MyTree.tsx b/src/MyTree.tsx index e773e3c..d8c9da8 100644 --- a/src/MyTree.tsx +++ b/src/MyTree.tsx @@ -178,6 +178,9 @@ export default function MyTree() { 老树