From f138de3f042948c68bda0732f3f676c2fa02647c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 19:14:14 +0000 Subject: [PATCH] =?UTF-8?q?Add=20=E3=80=8A=E6=BA=90=E6=B0=8F=E7=89=A9?= =?UTF-8?q?=E8=AF=AD=E3=80=8Bas=20a=20second=20grown=20tree=20in=20?= =?UTF-8?q?=E6=88=91=E7=9A=84=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the per-book tree data (branches, annotations, echo) out of MyTree.tsx into src/books.ts so a book is just a data record, then add 《源氏物语》(紫式部, 1008) alongside 《道德经》: five 帖 spanning 桐壺 to 夢浮橋, real anchoring lines, 入木/青芽/读过 states, a continuation thread, and its own anniversary echo. A quiet book switcher below the reader's name moves between the two trees in place. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AotmBqgJa7s8hM7rwAG6vD --- APP_NOTES.md | 9 ++ README.md | 10 +- src/MyTree.css | 45 +++++++++ src/MyTree.tsx | 205 +++++++++++++-------------------------- src/books.ts | 254 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 384 insertions(+), 139 deletions(-) create mode 100644 src/books.ts diff --git a/APP_NOTES.md b/APP_NOTES.md index 7edb729..41e6061 100644 --- a/APP_NOTES.md +++ b/APP_NOTES.md @@ -8,6 +8,15 @@ recreated pixel-for-pixel outside the proprietary `dc-runtime` format. ## What the page is +A reader may grow more than one tree, one per book. Below the reader's name +sits a quiet switcher — book title + a one-line attribution (author, era) — +that swaps which tree is drawn, with no page navigation. Two books are grown +so far: 《道德经》(老子) and 《源氏物语》(紫式部, 五十四帖 of it, five +branches drawn). All book content — branches, leaf states, annotations, the +周年回声 — lives in `src/books.ts` as a `Book` record; `MyTree.tsx` itself +has no book-specific content, so adding a new themed book (a sutra, a +philosophy text, another epic) means adding one entry there. + Each reader grows an abstract tree: - **枝 (branches)** — chapters read, labelled with their number (十一, 八, 四十四…). diff --git a/README.md b/README.md index 58172d2..da567eb 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ growing tree. ## 我的树 (My Tree) +A reader may grow more than one tree — one per book. A switcher below the +reader's name moves between them without leaving the page. Two are grown so +far: **《道德经》** (老子, planted 2023) and **《源氏物语》** (紫式部, planted +2024), each with its own planting date, chapters, and annotations. Adding a +new themed book is a matter of adding one entry to `src/books.ts` — the page +component itself carries no book-specific content. + Each reader grows a tree: - **枝 (branches)** — chapters read, labelled with their number (十一, 八, 四十四…). @@ -42,7 +49,8 @@ npm run preview # serve the production build | Path | What | | --- | --- | -| `src/MyTree.tsx` | The 我的树 page component + its branch/annotation data | +| `src/MyTree.tsx` | The 我的树 page component (book-agnostic) | +| `src/books.ts` | Per-book tree data — branches, annotations, echo. Add a book here. | | `src/MyTree.css` | Page layout, the SVG tree, hover reveal, growth rings | | `src/theme.css` | Design tokens (墨字朱批 palette), day/night themes, keyframes | | `APP_NOTES.md` | Detailed notes on this page | diff --git a/src/MyTree.css b/src/MyTree.css index b2d034f..416d360 100644 --- a/src/MyTree.css +++ b/src/MyTree.css @@ -109,6 +109,51 @@ body[data-night='1']::before { letter-spacing: 0.08em; } +/* ---- book switcher — a reader may grow more than one tree ---- */ + +.book-switch { + display: flex; + justify-content: center; + gap: 36px; + margin-top: 30px; +} + +.book-tab { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + background: none; + border: none; + border-bottom: 1px solid transparent; + padding: 0 0 8px 0; + cursor: pointer; + font-family: 'Noto Serif SC', serif; + color: var(--ink-light); + transition: color 0.3s ease, border-color 0.3s ease; +} +.book-tab:hover { + color: var(--ink); +} +.book-tab.is-current { + color: var(--ink); + border-bottom-color: var(--cinnabar); +} +.book-tab-title { + font-size: 14px; + letter-spacing: 0.08em; +} +.book-tab-subtitle { + font-family: Spectral, 'Noto Sans SC', serif; + font-feature-settings: 'onum'; + font-size: 10.5px; + letter-spacing: 0.04em; + color: var(--ink-light); +} +.book-tab.is-current .book-tab-subtitle { + color: var(--verdigris); +} + /* ---- the tree ---- */ .tree-stage { diff --git a/src/MyTree.tsx b/src/MyTree.tsx index e773e3c..5cb92f2 100644 --- a/src/MyTree.tsx +++ b/src/MyTree.tsx @@ -1,8 +1,9 @@ import { useCallback, useState } from 'react' import './MyTree.css' +import { BOOKS, BOOK_ORDER, LEAF_COLOR, type Branch } from './books' /** - * One branch of the abstract tree. + * 我的树 renders one book's tree at a time. * 枝 = a chapter read; 叶 (leaf) = an annotation left; * 朱砂 leaf = 入木 (enshrined, alongside the ancients); * 青绿 leaf = 青芽 (a sprout, awaiting time & peers); @@ -10,124 +11,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. + * A reader may grow more than one tree — the switcher below the eyebrow + * moves between books without leaving the page. */ -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> = { - cinnabar: 'var(--cinnabar)', - verdigris: 'var(--verdigris)', -} +const READER_NAME = '沈一苇' /** A hovered branch's detail, or the resting hint. */ -function BranchDetail({ branch }: { branch: Branch | null }) { +function BranchDetail({ branch, bookTitle }: { branch: Branch | null; bookTitle: string }) { if (!branch) { return
悬停一枝,看你在那一章写下的。
} @@ -139,7 +30,9 @@ function BranchDetail({ branch }: { branch: Branch | null }) { {branch.state !== '读过' && } {branch.state === '读过' ? '读过 · 尚未落笔' : branch.state} - 道德经 · {branch.chapter} + + {bookTitle} · {branch.chapter} + {branch.genre && {branch.genre}} {branch.source &&
{branch.source}
} @@ -152,6 +45,7 @@ function BranchDetail({ branch }: { branch: Branch | null }) { export default function MyTree() { // Night ("夜读") is a whole-page atmosphere; the prototype toggled a body attribute. const [night, setNight] = useState(false) + const [bookId, setBookId] = useState(BOOK_ORDER[0]) const [active, setActive] = useState(null) const toggleNight = useCallback(() => { @@ -163,7 +57,14 @@ export default function MyTree() { }) }, []) - const activeBranch = BRANCHES.find((b) => b.id === active) ?? null + const book = BOOKS[bookId] + const activeBranch = book.branches.find((b) => b.id === active) ?? null + const echoBranch = book.branches.find((b) => b.id === book.echo.branchId) ?? null + + const selectBook = useCallback((id: string) => { + setBookId(id) + setActive(null) + }, []) return (
@@ -200,11 +101,37 @@ export default function MyTree() {
我的树
-
沈一苇
-
种于 2023 年冬
+
{READER_NAME}
+ +
+ {BOOK_ORDER.map((id) => { + const b = BOOKS[id] + const isCurrent = id === bookId + return ( + + ) + })} +
+ +
{book.planted}
setActive(null)}> - + {/* 年轮 — growth rings at the root, echoing the mark and the metaphor */} @@ -213,7 +140,7 @@ export default function MyTree() { - + {/* trunk, drawn upward from the ground on load */} - {BRANCHES.map((b) => { + {book.branches.map((b) => { const isActive = active === b.id return ( setActive(b.id)} onFocus={() => setActive(b.id)} onBlur={() => setActive(null)} @@ -288,7 +215,7 @@ export default function MyTree() {
- +
枝,是读过的章。叶,是留下的年轮。 @@ -296,20 +223,22 @@ export default function MyTree() { 朱砂叶已入木,与古人同列。细线,是有人接续了你。
-
-
周年回声
-
一年前的你,在「知足不辱,知止不殆」之下写过一枚青芽。
-
辞掉第二份工作的那晚我写:知道停在哪里,比知道去哪里难。
-
现在的你,还这么读吗?
- -
+ {echoBranch && ( +
+
周年回声
+
{book.echo.lead}
+
{book.echo.quote}
+
{book.echo.ask}
+ +
+ )}
) diff --git a/src/books.ts b/src/books.ts new file mode 100644 index 0000000..72d0efe --- /dev/null +++ b/src/books.ts @@ -0,0 +1,254 @@ +/** + * Per-book tree data for 我的树. + * + * Each book a reader has grown is its own tree: a trunk drawn in the same + * 440×360 space, with branches (chapters read) and leaves (annotations left). + * Adding a new book/theme means adding one entry here — the page component + * itself carries no book-specific content. + */ + +export type LeafKind = '' | 'cinnabar' | 'verdigris' +export type BranchState = '入木' | '青芽' | '读过' + +export type Branch = { + id: string + /** short label shown on the branch itself */ + label: string + /** full 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: BranchState + /** 注/疑/证/驳 — only annotated branches carry a genre */ + genre?: string + /** the anchored line of scripture/text */ + 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 } +} + +export type Echo = { + /** which branch id this echo recalls */ + branchId: string + lead: string + quote: string + ask: string +} + +export type Book = { + id: string + /** title shown in captions and the switcher */ + title: string + /** short line under the title in the switcher */ + subtitle: string + planted: string + branches: Branch[] + echo: Echo +} + +const DAODEJING: Book = { + id: 'daodejing', + title: '道德经', + subtitle: '老子 · 约公元前 4 世纪', + planted: '种于 2023 年冬', + branches: [ + { + 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: '千里之行,始于足下。', + }, + ], + echo: { + branchId: 'ch44', + lead: '一年前的你,在「知足不辱,知止不殆」之下写过一枚青芽。', + quote: '辞掉第二份工作的那晚我写:知道停在哪里,比知道去哪里难。', + ask: '现在的你,还这么读吗?', + }, +} + +/** + * 《源氏物语》— 紫式部,约成书于 1008 年前后,五十四帖。 + * Chapters kept by their canonical 帖名 rather than a number, since that's + * how the work is read and cited. Source lines are this reader's own + * rendering of well-known passages, not a copyrighted translation. + */ +const GENJI: Book = { + id: 'genji', + title: '源氏物语', + subtitle: '紫式部 · 约公元 1008 年', + planted: '种于 2024 年秋', + branches: [ + { + id: 'kiritsubo', + label: '桐壺', + chapter: '第一帖 · 桐壺', + d: 'M 220 250 Q 280 240 344 220', + w: 1.5, + leaf: 'cinnabar', + leafAt: { x: 344, y: 220 }, + labelAt: { x: 354, y: 224, anchor: 'start' }, + drawDelay: '0.9s', + leafDelay: '1.7s', + state: '入木', + genre: '证', + source: '世间少有的容色,却也不是最尊贵的身份。', + note: '我妈总说她一碗水端平,可我小时候就知道:弟弟感冒她请假守着,我感冒她只留一张退烧药的字条。桐壺更衣的恩宠也是这样——不是不爱,是爱不匀。', + sign: '沈一苇 · 2024 秋', + thread: { d: 'M 344 220 L 386 198', at: { x: 390, y: 194 }, label: '陆声 · 2 年后接续' }, + }, + { + id: 'wakamurasaki', + label: '若紫', + chapter: '第五帖 · 若紫', + d: 'M 220 214 Q 160 198 100 188', + w: 1.4, + leaf: '', + labelAt: { x: 90, y: 192, anchor: 'end' }, + drawDelay: '1.15s', + leafDelay: '1.7s', + state: '读过', + source: '看那雀儿,竟被犬君放跑了。', + }, + { + id: 'hotaru', + label: '蛍', + chapter: '第二十五帖 · 蛍', + d: 'M 221 176 Q 272 164 320 156', + w: 1.3, + leaf: 'verdigris', + leafAt: { x: 320, y: 156 }, + labelAt: { x: 330, y: 160, anchor: 'start' }, + drawDelay: '1.4s', + leafDelay: '2.0s', + state: '青芽', + genre: '疑', + source: '正史不过如实录一隅,物语反倒把心事写全了。', + note: '写公众号第三年,我越写越怕——我记的到底是那年真发生的事,还是我现在希望它发生的样子?源氏说物语比正史更真,我疑心他只是在给自己找台阶。', + sign: '沈一苇 · 2025 春', + }, + { + id: 'minori', + label: '御法', + chapter: '第四十帖 · 御法', + d: 'M 220 146 Q 172 134 128 126', + w: 1.2, + leaf: '', + labelAt: { x: 118, y: 130, anchor: 'end' }, + drawDelay: '1.65s', + leafDelay: '2.2s', + state: '读过', + source: '秋风一乱,才知露水原来这样轻。', + }, + { + id: 'yumenoukihashi', + label: '夢浮橋', + chapter: '第五十四帖 · 夢浮橋', + d: 'M 221 116 Q 260 106 296 100', + w: 1.1, + leaf: '', + labelAt: { x: 306, y: 104, anchor: 'start' }, + drawDelay: '1.9s', + leafDelay: '2.4s', + state: '读过', + source: '故事讲到这里,没有说完,就断了。', + }, + ], + echo: { + branchId: 'hotaru', + lead: '一年前的你,在「物语反倒把心事写全了」之下写过一枚青芽。', + quote: '我记的到底是那年真发生的事,还是我现在希望它发生的样子?源氏说物语比正史更真,我疑心他只是在给自己找台阶。', + ask: '现在的你,还这么疑吗?', + }, +} + +export const BOOKS: Record = { + daodejing: DAODEJING, + genji: GENJI, +} + +export const BOOK_ORDER = ['daodejing', 'genji'] + +export const LEAF_COLOR: Record, string> = { + cinnabar: 'var(--cinnabar)', + verdigris: 'var(--verdigris)', +}