@hutusi/amytis 1.7.0 → 1.9.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/.github/workflows/ci.yml +1 -1
- package/CHANGELOG.md +63 -0
- package/CLAUDE.md +9 -18
- package/GEMINI.md +6 -0
- package/README.md +44 -0
- package/TODO.md +15 -3
- package/bun.lock +5 -3
- package/content/about.mdx +64 -10
- package/content/about.zh.mdx +66 -9
- package/content/books/sample-book/index.mdx +3 -3
- package/content/flows/2026/02/05.md +0 -1
- package/content/flows/2026/02/10.mdx +2 -1
- package/content/flows/2026/02/15.md +2 -1
- package/content/flows/2026/02/18.mdx +2 -1
- package/content/flows/2026/02/20.md +0 -1
- package/content/notes/algorithms-and-data-structures.mdx +51 -0
- package/content/notes/digital-garden-philosophy.mdx +36 -0
- package/content/notes/react-server-components.mdx +49 -0
- package/content/notes/tailwind-v4.mdx +45 -0
- package/content/notes/zettelkasten-method.mdx +33 -0
- package/content/series/digital-garden/01-philosophy.mdx +25 -12
- package/docs/ARCHITECTURE.md +9 -1
- package/docs/CONTRIBUTING.md +26 -0
- package/docs/DIGITAL_GARDEN.md +72 -0
- package/imports/README.md +45 -0
- package/package.json +12 -5
- package/scripts/generate-knowledge-graph.ts +162 -0
- package/scripts/import-book.ts +176 -0
- package/scripts/new-flow-from-chat.ts +238 -0
- package/scripts/new-flow.ts +0 -5
- package/scripts/new-note.ts +53 -0
- package/scripts/sync-book-chapters.ts +210 -0
- package/site.config.ts +30 -7
- package/src/app/authors/[author]/page.tsx +3 -1
- package/src/app/books/[slug]/[chapter]/page.tsx +2 -1
- package/src/app/books/[slug]/page.tsx +6 -5
- package/src/app/flows/[year]/[month]/[day]/page.tsx +35 -29
- package/src/app/flows/[year]/[month]/page.tsx +18 -13
- package/src/app/flows/[year]/page.tsx +25 -15
- package/src/app/flows/page/[page]/page.tsx +5 -9
- package/src/app/flows/page.tsx +5 -8
- package/src/app/globals.css +41 -0
- package/src/app/graph/page.tsx +21 -0
- package/src/app/layout.tsx +4 -2
- package/src/app/notes/[slug]/page.tsx +129 -0
- package/src/app/notes/page/[page]/page.tsx +60 -0
- package/src/app/notes/page.tsx +33 -0
- package/src/app/page/[page]/page.tsx +1 -0
- package/src/app/page.tsx +4 -5
- package/src/app/posts/[slug]/page.tsx +5 -2
- package/src/app/posts/page/[page]/page.tsx +4 -1
- package/src/app/search.json/route.ts +17 -3
- package/src/app/series/[slug]/page/[page]/page.tsx +1 -0
- package/src/app/series/[slug]/page.tsx +3 -3
- package/src/app/sitemap.ts +1 -1
- package/src/app/tags/[tag]/page.tsx +3 -3
- package/src/components/Backlinks.tsx +39 -0
- package/src/components/BookMobileNav.tsx +11 -11
- package/src/components/BookSidebar.tsx +17 -25
- package/src/components/BrowserDetectionBanner.tsx +96 -0
- package/src/components/FeaturedStoriesSection.tsx +1 -1
- package/src/components/FlowCalendarSidebar.tsx +4 -2
- package/src/components/FlowContent.tsx +4 -3
- package/src/components/FlowHubTabs.tsx +50 -0
- package/src/components/FlowTimelineEntry.tsx +7 -9
- package/src/components/KnowledgeGraph.tsx +324 -0
- package/src/components/LanguageProvider.tsx +14 -5
- package/src/components/MarkdownRenderer.tsx +13 -2
- package/src/components/Navbar.tsx +237 -10
- package/src/components/NoteContent.tsx +123 -0
- package/src/components/NoteSidebar.tsx +132 -0
- package/src/components/RecentNotesSection.tsx +6 -11
- package/src/components/Search.tsx +7 -3
- package/src/components/TagContentTabs.tsx +0 -1
- package/src/i18n/translations.ts +43 -17
- package/src/layouts/BookLayout.tsx +3 -3
- package/src/layouts/PostLayout.tsx +8 -3
- package/src/lib/i18n.ts +83 -6
- package/src/lib/markdown.ts +306 -19
- package/src/lib/remark-wikilinks.ts +59 -0
- package/src/lib/search-utils.ts +2 -1
- package/tests/unit/static-params.test.ts +238 -0
- package/content/series/digital-garden/01-philosophy/index.mdx +0 -23
|
@@ -17,7 +17,7 @@ export default function BookMobileNav({ bookSlug, bookTitle, toc, chapters, curr
|
|
|
17
17
|
const { t } = useLanguage();
|
|
18
18
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
19
19
|
|
|
20
|
-
const currentIndex = chapters.findIndex(ch => ch.
|
|
20
|
+
const currentIndex = chapters.findIndex(ch => ch.id === currentChapter);
|
|
21
21
|
const prevChapter = currentIndex > 0 ? chapters[currentIndex - 1] : null;
|
|
22
22
|
const nextChapter = currentIndex < chapters.length - 1 ? chapters[currentIndex + 1] : null;
|
|
23
23
|
|
|
@@ -51,7 +51,7 @@ export default function BookMobileNav({ bookSlug, bookTitle, toc, chapters, curr
|
|
|
51
51
|
<div className="flex gap-3 mb-3">
|
|
52
52
|
{prevChapter ? (
|
|
53
53
|
<Link
|
|
54
|
-
href={`/books/${bookSlug}/${prevChapter.
|
|
54
|
+
href={`/books/${bookSlug}/${prevChapter.id}`}
|
|
55
55
|
className="flex-1 flex items-center gap-2 py-2.5 px-3 rounded-lg bg-muted/5 hover:bg-muted/10 no-underline transition-colors group"
|
|
56
56
|
>
|
|
57
57
|
<svg className="w-4 h-4 flex-shrink-0 text-muted group-hover:text-accent transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
@@ -67,7 +67,7 @@ export default function BookMobileNav({ bookSlug, bookTitle, toc, chapters, curr
|
|
|
67
67
|
)}
|
|
68
68
|
{nextChapter ? (
|
|
69
69
|
<Link
|
|
70
|
-
href={`/books/${bookSlug}/${nextChapter.
|
|
70
|
+
href={`/books/${bookSlug}/${nextChapter.id}`}
|
|
71
71
|
className="flex-1 flex items-center justify-end gap-2 py-2.5 px-3 rounded-lg bg-muted/5 hover:bg-muted/10 no-underline transition-colors group text-right"
|
|
72
72
|
>
|
|
73
73
|
<div className="min-w-0">
|
|
@@ -113,19 +113,19 @@ export default function BookMobileNav({ bookSlug, bookTitle, toc, chapters, curr
|
|
|
113
113
|
</div>
|
|
114
114
|
<ol className="space-y-1">
|
|
115
115
|
{item.chapters.map(ch => {
|
|
116
|
-
const isCurrent = ch.
|
|
117
|
-
const chIdx = chapters.findIndex(c => c.
|
|
116
|
+
const isCurrent = ch.id === currentChapter;
|
|
117
|
+
const chIdx = chapters.findIndex(c => c.id === ch.id);
|
|
118
118
|
const isPast = chIdx < currentIndex;
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
|
-
<li key={ch.
|
|
121
|
+
<li key={ch.id}>
|
|
122
122
|
{isCurrent ? (
|
|
123
123
|
<div className="flex items-center gap-3 py-1.5 px-2 rounded-lg bg-accent/5">
|
|
124
124
|
<span className="text-sm font-semibold text-accent truncate">{ch.title}</span>
|
|
125
125
|
</div>
|
|
126
126
|
) : (
|
|
127
127
|
<Link
|
|
128
|
-
href={`/books/${bookSlug}/${ch.
|
|
128
|
+
href={`/books/${bookSlug}/${ch.id}`}
|
|
129
129
|
className={`block py-1.5 px-2 rounded-lg text-sm no-underline hover:bg-muted/5 transition-colors ${
|
|
130
130
|
isPast ? 'text-foreground/70 hover:text-foreground' : 'text-muted hover:text-foreground'
|
|
131
131
|
}`}
|
|
@@ -140,19 +140,19 @@ export default function BookMobileNav({ bookSlug, bookTitle, toc, chapters, curr
|
|
|
140
140
|
</div>
|
|
141
141
|
);
|
|
142
142
|
} else {
|
|
143
|
-
const isCurrent = item.
|
|
144
|
-
const chIdx = chapters.findIndex(c => c.
|
|
143
|
+
const isCurrent = item.id === currentChapter;
|
|
144
|
+
const chIdx = chapters.findIndex(c => c.id === item.id);
|
|
145
145
|
const isPast = chIdx < currentIndex;
|
|
146
146
|
|
|
147
147
|
return (
|
|
148
|
-
<div key={item.
|
|
148
|
+
<div key={item.id}>
|
|
149
149
|
{isCurrent ? (
|
|
150
150
|
<div className="flex items-center gap-3 py-1.5 px-2 rounded-lg bg-accent/5">
|
|
151
151
|
<span className="text-sm font-semibold text-accent truncate">{item.title}</span>
|
|
152
152
|
</div>
|
|
153
153
|
) : (
|
|
154
154
|
<Link
|
|
155
|
-
href={`/books/${bookSlug}/${item.
|
|
155
|
+
href={`/books/${bookSlug}/${item.id}`}
|
|
156
156
|
className={`block py-1.5 px-2 rounded-lg text-sm no-underline hover:bg-muted/5 transition-colors ${
|
|
157
157
|
isPast ? 'text-foreground/70 hover:text-foreground' : 'text-muted hover:text-foreground'
|
|
158
158
|
}`}
|
|
@@ -16,7 +16,7 @@ interface BookSidebarProps {
|
|
|
16
16
|
|
|
17
17
|
export default function BookSidebar({ bookSlug, bookTitle, toc, chapters, currentChapter, headings = [] }: BookSidebarProps) {
|
|
18
18
|
const { t } = useLanguage();
|
|
19
|
-
const currentIndex = chapters.findIndex(ch => ch.
|
|
19
|
+
const currentIndex = chapters.findIndex(ch => ch.id === currentChapter);
|
|
20
20
|
const [headingsCollapsed, setHeadingsCollapsed] = useState(false);
|
|
21
21
|
const currentItemRef = useRef<HTMLLIElement>(null);
|
|
22
22
|
const sidebarRef = useRef<HTMLElement>(null);
|
|
@@ -27,7 +27,7 @@ export default function BookSidebar({ bookSlug, bookTitle, toc, chapters, curren
|
|
|
27
27
|
const initial: Record<string, boolean> = {};
|
|
28
28
|
for (const item of toc) {
|
|
29
29
|
if ('part' in item) {
|
|
30
|
-
const containsCurrent = item.chapters.some(ch => ch.
|
|
30
|
+
const containsCurrent = item.chapters.some(ch => ch.id === currentChapter);
|
|
31
31
|
initial[item.part] = !containsCurrent;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -96,7 +96,7 @@ export default function BookSidebar({ bookSlug, bookTitle, toc, chapters, curren
|
|
|
96
96
|
// Expand part containing current chapter when it changes
|
|
97
97
|
useEffect(() => {
|
|
98
98
|
for (const item of toc) {
|
|
99
|
-
if ('part' in item && item.chapters.some(ch => ch.
|
|
99
|
+
if ('part' in item && item.chapters.some(ch => ch.id === currentChapter)) {
|
|
100
100
|
setCollapsedParts(prev => ({ ...prev, [item.part]: false }));
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -156,23 +156,23 @@ export default function BookSidebar({ bookSlug, bookTitle, toc, chapters, curren
|
|
|
156
156
|
toc.forEach((item) => {
|
|
157
157
|
if ('part' in item) {
|
|
158
158
|
item.chapters.forEach(ch => {
|
|
159
|
-
chapterIndices.set(ch.
|
|
159
|
+
chapterIndices.set(ch.id, currentGlobalIdx++);
|
|
160
160
|
});
|
|
161
161
|
} else {
|
|
162
|
-
chapterIndices.set(item.
|
|
162
|
+
chapterIndices.set(item.id, currentGlobalIdx++);
|
|
163
163
|
}
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
// Helper to render a chapter link + inline headings if current
|
|
167
|
-
const renderChapterItem = (ch: { title: string;
|
|
168
|
-
const isCurrent = ch.
|
|
169
|
-
const idx = chapterIndices.get(ch.
|
|
167
|
+
const renderChapterItem = (ch: { title: string; id: string }) => {
|
|
168
|
+
const isCurrent = ch.id === currentChapter;
|
|
169
|
+
const idx = chapterIndices.get(ch.id) ?? 0;
|
|
170
170
|
const isPast = idx < currentIndex;
|
|
171
171
|
|
|
172
172
|
return (
|
|
173
|
-
<li key={ch.
|
|
173
|
+
<li key={ch.id} ref={isCurrent ? currentItemRef : undefined}>
|
|
174
174
|
<Link
|
|
175
|
-
href={`/books/${bookSlug}/${ch.
|
|
175
|
+
href={`/books/${bookSlug}/${ch.id}`}
|
|
176
176
|
className={`block py-2 px-3 rounded-lg text-sm no-underline transition-all duration-200 ${
|
|
177
177
|
isCurrent
|
|
178
178
|
? 'bg-accent/10 text-accent font-semibold border-l-2 border-accent'
|
|
@@ -196,27 +196,19 @@ export default function BookSidebar({ bookSlug, bookTitle, toc, chapters, curren
|
|
|
196
196
|
>
|
|
197
197
|
{/* Book Header */}
|
|
198
198
|
<div className="mb-6 pb-4 border-b border-muted/10">
|
|
199
|
-
<
|
|
200
|
-
<span className="text-[10px] font-sans font-bold uppercase tracking-widest text-accent
|
|
199
|
+
<div className="flex items-center justify-between mb-2">
|
|
200
|
+
<span className="text-[10px] font-sans font-bold uppercase tracking-widest text-accent">
|
|
201
201
|
{t('book')}
|
|
202
202
|
</span>
|
|
203
|
-
<h3 className="font-serif font-bold text-heading text-lg leading-snug group-hover:text-accent transition-colors">
|
|
204
|
-
{bookTitle}
|
|
205
|
-
</h3>
|
|
206
|
-
</Link>
|
|
207
|
-
|
|
208
|
-
{/* Progress */}
|
|
209
|
-
<div className="mt-3 flex items-center gap-3">
|
|
210
|
-
<div className="flex-1 h-1 bg-muted/10 rounded-full overflow-hidden">
|
|
211
|
-
<div
|
|
212
|
-
className="h-full bg-accent/60 rounded-full transition-all duration-500"
|
|
213
|
-
style={{ width: `${((currentIndex + 1) / chapters.length) * 100}%` }}
|
|
214
|
-
/>
|
|
215
|
-
</div>
|
|
216
203
|
<span className="text-xs font-mono text-muted whitespace-nowrap">
|
|
217
204
|
{currentIndex + 1}/{chapters.length}
|
|
218
205
|
</span>
|
|
219
206
|
</div>
|
|
207
|
+
<Link href={`/books/${bookSlug}`} className="group block no-underline">
|
|
208
|
+
<h3 className="font-serif font-bold text-heading text-lg leading-snug group-hover:text-accent transition-colors">
|
|
209
|
+
{bookTitle}
|
|
210
|
+
</h3>
|
|
211
|
+
</Link>
|
|
220
212
|
</div>
|
|
221
213
|
|
|
222
214
|
{/* TOC */}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
4
|
+
import { useLanguage } from '@/components/LanguageProvider';
|
|
5
|
+
|
|
6
|
+
const DISMISSED_KEY = 'browser-warning-dismissed';
|
|
7
|
+
|
|
8
|
+
function isOutdatedBrowser(): boolean {
|
|
9
|
+
// Detect Internet Explorer
|
|
10
|
+
if (/MSIE|Trident/.test(navigator.userAgent)) return true;
|
|
11
|
+
|
|
12
|
+
// CSS custom properties (Chrome 49+, Firefox 31+, Safari 9.1+)
|
|
13
|
+
if (!('CSS' in window) || !CSS.supports('color', 'var(--x)')) return true;
|
|
14
|
+
|
|
15
|
+
// CSS oklch() — required by Tailwind CSS v4 color system (Chrome 111+, Firefox 113+, Safari 15.4+)
|
|
16
|
+
// Note: CSS.supports() only handles property-value declarations, not at-rules,
|
|
17
|
+
// so @layer cannot be tested here. oklch already sets a higher minimum than @layer anyway.
|
|
18
|
+
if (!CSS.supports('color', 'oklch(0.5 0.1 0)')) return true;
|
|
19
|
+
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function subscribe() {
|
|
24
|
+
return () => {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSnapshot(): boolean {
|
|
28
|
+
try {
|
|
29
|
+
if (localStorage.getItem(DISMISSED_KEY)) return false;
|
|
30
|
+
} catch {
|
|
31
|
+
// localStorage unavailable (private browsing, sandboxed iframe, etc.)
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return isOutdatedBrowser();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getServerSnapshot(): boolean {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function BrowserDetectionBanner({ updateUrl }: { updateUrl?: string }) {
|
|
42
|
+
const { t } = useLanguage();
|
|
43
|
+
const isOutdated = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
44
|
+
const [dismissed, setDismissed] = useState(false);
|
|
45
|
+
|
|
46
|
+
if (!isOutdated || dismissed) return null;
|
|
47
|
+
|
|
48
|
+
const dismiss = () => {
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem(DISMISSED_KEY, '1');
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore — dismissal works for current session via state
|
|
53
|
+
}
|
|
54
|
+
setDismissed(true);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
role="alert"
|
|
60
|
+
className="bg-amber-100 dark:bg-amber-900/40 border-b border-amber-300 dark:border-amber-700 text-amber-900 dark:text-amber-100 px-4 py-2.5 flex items-center justify-center gap-3 text-sm"
|
|
61
|
+
>
|
|
62
|
+
<svg
|
|
63
|
+
className="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400"
|
|
64
|
+
viewBox="0 0 20 20"
|
|
65
|
+
fill="currentColor"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
>
|
|
68
|
+
<path
|
|
69
|
+
fillRule="evenodd"
|
|
70
|
+
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
|
|
71
|
+
clipRule="evenodd"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
<span>{t('browser_outdated')}</span>
|
|
75
|
+
{updateUrl && (
|
|
76
|
+
<a
|
|
77
|
+
href={updateUrl}
|
|
78
|
+
target="_blank"
|
|
79
|
+
rel="noopener noreferrer"
|
|
80
|
+
className="underline underline-offset-2 font-medium hover:opacity-75 transition-opacity shrink-0"
|
|
81
|
+
>
|
|
82
|
+
{t('browser_update')}
|
|
83
|
+
</a>
|
|
84
|
+
)}
|
|
85
|
+
<button
|
|
86
|
+
onClick={dismiss}
|
|
87
|
+
aria-label={t('browser_dismiss')}
|
|
88
|
+
className="ml-auto p-1 rounded hover:opacity-70 transition-opacity focus-ring shrink-0"
|
|
89
|
+
>
|
|
90
|
+
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
91
|
+
<path d="M4.293 4.293a1 1 0 011.414 0L8 6.586l2.293-2.293a1 1 0 111.414 1.414L9.414 8l2.293 2.293a1 1 0 01-1.414 1.414L8 9.414l-2.293 2.293a1 1 0 01-1.414-1.414L6.586 8 4.293 5.707a1 1 0 010-1.414z" />
|
|
92
|
+
</svg>
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -40,7 +40,7 @@ export default function FeaturedStoriesSection({ allFeatured, maxItems, scrollTh
|
|
|
40
40
|
return (
|
|
41
41
|
<section className="mb-24">
|
|
42
42
|
<div className="flex items-center justify-between mb-12">
|
|
43
|
-
<h2 className="text-3xl font-serif font-bold text-heading">{t('
|
|
43
|
+
<h2 className="text-3xl font-serif font-bold text-heading">{t('featured_articles')}</h2>
|
|
44
44
|
{allFeatured.length > maxItems && (
|
|
45
45
|
<button
|
|
46
46
|
onClick={handleShuffle}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useMemo } from 'react';
|
|
3
|
+
import { useState, useMemo, type ReactNode } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { useLanguage } from '@/components/LanguageProvider';
|
|
6
6
|
|
|
@@ -10,11 +10,12 @@ interface FlowCalendarSidebarProps {
|
|
|
10
10
|
tags?: Record<string, number>;
|
|
11
11
|
selectedTag?: string | null;
|
|
12
12
|
onTagSelect?: (tag: string) => void;
|
|
13
|
+
breadcrumb?: ReactNode;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
16
17
|
|
|
17
|
-
export default function FlowCalendarSidebar({ entryDates, currentDate, tags, selectedTag, onTagSelect }: FlowCalendarSidebarProps) {
|
|
18
|
+
export default function FlowCalendarSidebar({ entryDates, currentDate, tags, selectedTag, onTagSelect, breadcrumb }: FlowCalendarSidebarProps) {
|
|
18
19
|
const { t } = useLanguage();
|
|
19
20
|
const initialDate = currentDate ? new Date(currentDate + 'T00:00:00') : new Date();
|
|
20
21
|
const [viewYear, setViewYear] = useState(initialDate.getFullYear());
|
|
@@ -77,6 +78,7 @@ export default function FlowCalendarSidebar({ entryDates, currentDate, tags, sel
|
|
|
77
78
|
|
|
78
79
|
return (
|
|
79
80
|
<aside className="hidden lg:block sticky top-20 self-start w-[280px] max-h-[calc(100vh-6rem)]">
|
|
81
|
+
{breadcrumb && <div className="mb-4">{breadcrumb}</div>}
|
|
80
82
|
<div className="border border-muted/20 rounded-lg p-4">
|
|
81
83
|
{/* Month navigation */}
|
|
82
84
|
<div className="flex items-center justify-between mb-3">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useMemo } from 'react';
|
|
3
|
+
import { useState, useMemo, type ReactNode } from 'react';
|
|
4
4
|
import { useLanguage } from '@/components/LanguageProvider';
|
|
5
5
|
import FlowCalendarSidebar from '@/components/FlowCalendarSidebar';
|
|
6
6
|
import FlowTimelineEntry from '@/components/FlowTimelineEntry';
|
|
@@ -24,9 +24,10 @@ interface FlowContentProps {
|
|
|
24
24
|
totalPages: number;
|
|
25
25
|
basePath: string;
|
|
26
26
|
};
|
|
27
|
+
breadcrumb?: ReactNode;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export default function FlowContent({ flows, entryDates, tags, currentDate, pagination }: FlowContentProps) {
|
|
30
|
+
export default function FlowContent({ flows, entryDates, tags, currentDate, pagination, breadcrumb }: FlowContentProps) {
|
|
30
31
|
const { t } = useLanguage();
|
|
31
32
|
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
|
32
33
|
|
|
@@ -47,6 +48,7 @@ export default function FlowContent({ flows, entryDates, tags, currentDate, pagi
|
|
|
47
48
|
tags={tags}
|
|
48
49
|
selectedTag={selectedTag}
|
|
49
50
|
onTagSelect={handleTagSelect}
|
|
51
|
+
breadcrumb={breadcrumb}
|
|
50
52
|
/>
|
|
51
53
|
|
|
52
54
|
<div className="flex-1 min-w-0">
|
|
@@ -72,7 +74,6 @@ export default function FlowContent({ flows, entryDates, tags, currentDate, pagi
|
|
|
72
74
|
<FlowTimelineEntry
|
|
73
75
|
key={flow.slug}
|
|
74
76
|
date={flow.date}
|
|
75
|
-
title={flow.title}
|
|
76
77
|
excerpt={flow.excerpt}
|
|
77
78
|
tags={flow.tags}
|
|
78
79
|
slug={flow.slug}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { useLanguage } from './LanguageProvider';
|
|
6
|
+
|
|
7
|
+
interface FlowHubTabsProps {
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function FlowHubTabs({ subtitle }: FlowHubTabsProps) {
|
|
12
|
+
const pathname = usePathname();
|
|
13
|
+
const { t } = useLanguage();
|
|
14
|
+
|
|
15
|
+
// Normalize: strip trailing slash added by next.config trailingSlash:true
|
|
16
|
+
const path = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
|
17
|
+
|
|
18
|
+
const isFlowsActive = path === '/flows' || path.startsWith('/flows/page');
|
|
19
|
+
const isNotesActive = path === '/notes' || path.startsWith('/notes/page');
|
|
20
|
+
const isGraphActive = path.startsWith('/graph');
|
|
21
|
+
|
|
22
|
+
const tabs = [
|
|
23
|
+
{ href: '/flows', label: t('tab_daily_flow'), active: isFlowsActive },
|
|
24
|
+
{ href: '/notes', label: t('notes'), active: isNotesActive },
|
|
25
|
+
{ href: '/graph', label: t('tab_graph'), active: isGraphActive },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="mb-10">
|
|
30
|
+
<div className="flex items-end gap-8 border-b border-muted/20">
|
|
31
|
+
{tabs.map(tab => (
|
|
32
|
+
<Link
|
|
33
|
+
key={tab.href}
|
|
34
|
+
href={tab.href}
|
|
35
|
+
className={`pb-3 text-3xl font-bold no-underline border-b-2 -mb-px transition-colors ${
|
|
36
|
+
tab.active
|
|
37
|
+
? 'border-accent text-heading'
|
|
38
|
+
: 'border-transparent text-muted/30 hover:text-muted/60'
|
|
39
|
+
}`}
|
|
40
|
+
>
|
|
41
|
+
{tab.label}
|
|
42
|
+
</Link>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
{subtitle && (
|
|
46
|
+
<p className="mt-3 text-sm text-muted">{subtitle}</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -3,25 +3,23 @@ import Tag from './Tag';
|
|
|
3
3
|
|
|
4
4
|
interface FlowTimelineEntryProps {
|
|
5
5
|
date: string;
|
|
6
|
-
title: string;
|
|
7
6
|
excerpt: string;
|
|
8
7
|
tags: string[];
|
|
9
8
|
slug: string;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
export default function FlowTimelineEntry({ date,
|
|
11
|
+
export default function FlowTimelineEntry({ date, excerpt, tags, slug }: FlowTimelineEntryProps) {
|
|
13
12
|
return (
|
|
14
13
|
<article className="relative pl-6 pb-8 border-l-2 border-muted/20 last:pb-0">
|
|
15
14
|
{/* Timeline dot */}
|
|
16
15
|
<div className="absolute -left-[5px] top-1.5 w-2 h-2 rounded-full bg-accent" />
|
|
17
16
|
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</
|
|
23
|
-
|
|
24
|
-
<p className="text-sm text-muted leading-relaxed line-clamp-3">{excerpt}</p>
|
|
17
|
+
<Link href={`/flows/${slug}`} className="no-underline group">
|
|
18
|
+
<time className="text-sm font-mono text-accent group-hover:text-accent/70 transition-colors">{date}</time>
|
|
19
|
+
</Link>
|
|
20
|
+
{excerpt && (
|
|
21
|
+
<p className="mt-1.5 text-sm text-muted leading-relaxed line-clamp-3">{excerpt}</p>
|
|
22
|
+
)}
|
|
25
23
|
{tags.length > 0 && (
|
|
26
24
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
27
25
|
{tags.map(tag => (
|