@hutusi/amytis 1.7.0 → 1.8.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/GEMINI.md +6 -0
  3. package/README.md +14 -0
  4. package/TODO.md +15 -3
  5. package/bun.lock +5 -3
  6. package/content/flows/2026/02/05.md +0 -1
  7. package/content/flows/2026/02/10.mdx +2 -1
  8. package/content/flows/2026/02/15.md +2 -1
  9. package/content/flows/2026/02/18.mdx +2 -1
  10. package/content/flows/2026/02/20.md +0 -1
  11. package/content/notes/algorithms-and-data-structures.mdx +51 -0
  12. package/content/notes/digital-garden-philosophy.mdx +36 -0
  13. package/content/notes/react-server-components.mdx +49 -0
  14. package/content/notes/tailwind-v4.mdx +45 -0
  15. package/content/notes/zettelkasten-method.mdx +33 -0
  16. package/docs/ARCHITECTURE.md +8 -0
  17. package/docs/CONTRIBUTING.md +11 -0
  18. package/docs/DIGITAL_GARDEN.md +64 -0
  19. package/package.json +7 -3
  20. package/scripts/generate-knowledge-graph.ts +162 -0
  21. package/scripts/new-flow.ts +0 -5
  22. package/scripts/new-note.ts +53 -0
  23. package/site.config.ts +21 -1
  24. package/src/app/flows/[year]/[month]/[day]/page.tsx +32 -29
  25. package/src/app/flows/[year]/[month]/page.tsx +15 -13
  26. package/src/app/flows/[year]/page.tsx +22 -15
  27. package/src/app/flows/page/[page]/page.tsx +3 -9
  28. package/src/app/flows/page.tsx +3 -8
  29. package/src/app/globals.css +41 -0
  30. package/src/app/graph/page.tsx +19 -0
  31. package/src/app/notes/[slug]/page.tsx +128 -0
  32. package/src/app/notes/page/[page]/page.tsx +58 -0
  33. package/src/app/notes/page.tsx +31 -0
  34. package/src/app/page.tsx +0 -1
  35. package/src/app/posts/[slug]/page.tsx +4 -2
  36. package/src/app/search.json/route.ts +15 -1
  37. package/src/components/Backlinks.tsx +39 -0
  38. package/src/components/FlowCalendarSidebar.tsx +4 -2
  39. package/src/components/FlowContent.tsx +4 -3
  40. package/src/components/FlowHubTabs.tsx +50 -0
  41. package/src/components/FlowTimelineEntry.tsx +7 -9
  42. package/src/components/KnowledgeGraph.tsx +324 -0
  43. package/src/components/MarkdownRenderer.tsx +13 -2
  44. package/src/components/Navbar.tsx +235 -9
  45. package/src/components/NoteContent.tsx +123 -0
  46. package/src/components/NoteSidebar.tsx +132 -0
  47. package/src/components/RecentNotesSection.tsx +6 -11
  48. package/src/components/Search.tsx +5 -1
  49. package/src/components/TagContentTabs.tsx +0 -1
  50. package/src/i18n/translations.ts +21 -1
  51. package/src/layouts/PostLayout.tsx +8 -3
  52. package/src/lib/markdown.ts +276 -3
  53. package/src/lib/remark-wikilinks.ts +59 -0
  54. package/src/lib/search-utils.ts +2 -1
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Generate public/knowledge-graph.json for the Knowledge Graph visualization.
3
+ *
4
+ * Nodes: all posts, all notes, and flows that appear as wikilink source/target.
5
+ * Edges: wikilink edges (from backlink index) + series membership edges.
6
+ *
7
+ * Run: NODE_ENV=production bun scripts/generate-knowledge-graph.ts
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { getAllPosts, getAllNotes, getAllFlows, getSeriesData } from '../src/lib/markdown';
13
+
14
+ interface GraphNode {
15
+ id: string;
16
+ title: string;
17
+ type: 'post' | 'note' | 'flow' | 'series';
18
+ url: string;
19
+ connections: number;
20
+ }
21
+
22
+ interface GraphEdge {
23
+ source: string;
24
+ target: string;
25
+ type: 'wikilink' | 'series';
26
+ }
27
+
28
+ function extractWikilinks(content: string): string[] {
29
+ const slugs: string[] = [];
30
+ const re = /\[\[([^\]|]+?)(?:\|[^\]]+?)?\]\]/g;
31
+ let match;
32
+ while ((match = re.exec(content)) !== null) {
33
+ slugs.push(match[1].trim());
34
+ }
35
+ return slugs;
36
+ }
37
+
38
+ async function main() {
39
+ console.log('Generating knowledge graph…');
40
+
41
+ const posts = getAllPosts();
42
+ const notes = getAllNotes();
43
+ const flows = getAllFlows();
44
+
45
+ // Build id→node map
46
+ const nodeMap = new Map<string, GraphNode>();
47
+ const edges: GraphEdge[] = [];
48
+
49
+ // Add all posts and notes
50
+ for (const post of posts) {
51
+ nodeMap.set(post.slug, { id: post.slug, title: post.title, type: 'post', url: `/posts/${post.slug}`, connections: 0 });
52
+ }
53
+ for (const note of notes) {
54
+ nodeMap.set(note.slug, { id: note.slug, title: note.title, type: 'note', url: `/notes/${note.slug}`, connections: 0 });
55
+ }
56
+
57
+ // Track which flows appear in wikilinks (to avoid including ALL flows)
58
+ const linkedFlowSlugs = new Set<string>();
59
+
60
+ // Scan all content for wikilinks
61
+ const allContent: Array<{ slug: string; title: string; type: 'post' | 'note' | 'flow'; content: string; url: string }> = [
62
+ ...posts.map(p => ({ slug: p.slug, title: p.title, type: 'post' as const, content: p.content, url: `/posts/${p.slug}` })),
63
+ ...notes.map(n => ({ slug: n.slug, title: n.title, type: 'note' as const, content: n.content, url: `/notes/${n.slug}` })),
64
+ ...flows.map(f => ({ slug: f.slug, title: f.title, type: 'flow' as const, content: f.content, url: `/flows/${f.slug}` })),
65
+ ];
66
+
67
+ // Build wikilink edges (deduplicate per source document)
68
+ for (const item of allContent) {
69
+ const targets = extractWikilinks(item.content);
70
+ const seenTargets = new Set<string>();
71
+ for (const target of targets) {
72
+ if (target === item.slug) continue; // skip self
73
+ if (seenTargets.has(target)) continue; // skip duplicate within this doc
74
+ seenTargets.add(target);
75
+ edges.push({ source: item.slug, target, type: 'wikilink' });
76
+
77
+ // Ensure source exists in nodeMap
78
+ if (!nodeMap.has(item.slug)) {
79
+ nodeMap.set(item.slug, { id: item.slug, title: item.title, type: item.type, url: item.url, connections: 0 });
80
+ if (item.type === 'flow') linkedFlowSlugs.add(item.slug);
81
+ }
82
+ // Track referenced flows
83
+ const targetFlow = flows.find(f => f.slug === target);
84
+ if (targetFlow) {
85
+ linkedFlowSlugs.add(target);
86
+ if (!nodeMap.has(target)) {
87
+ nodeMap.set(target, { id: target, title: targetFlow.title, type: 'flow', url: `/flows/${target}`, connections: 0 });
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ // Add series nodes + series membership edges
94
+ const seriesSlugsSet = new Set<string>();
95
+ for (const post of posts) {
96
+ if (post.series) seriesSlugsSet.add(post.series);
97
+ }
98
+
99
+ for (const seriesSlug of seriesSlugsSet) {
100
+ const seriesData = getSeriesData(seriesSlug);
101
+ const seriesId = `series:${seriesSlug}`;
102
+ nodeMap.set(seriesId, {
103
+ id: seriesId,
104
+ title: seriesData?.title || seriesSlug,
105
+ type: 'series',
106
+ url: `/series/${seriesSlug}`,
107
+ connections: 0,
108
+ });
109
+ // Add edges from series to each post
110
+ for (const post of posts) {
111
+ if (post.series === seriesSlug) {
112
+ edges.push({ source: seriesId, target: post.slug, type: 'series' });
113
+ }
114
+ }
115
+ }
116
+
117
+ // Compute connection counts
118
+ for (const edge of edges) {
119
+ const src = nodeMap.get(edge.source);
120
+ if (src) src.connections++;
121
+ const tgt = nodeMap.get(edge.target);
122
+ if (tgt) tgt.connections++;
123
+ }
124
+
125
+ // Filter out nodes with no edges (isolated) to keep graph clean
126
+ const connectedIds = new Set<string>();
127
+ for (const edge of edges) {
128
+ connectedIds.add(edge.source);
129
+ connectedIds.add(edge.target);
130
+ }
131
+
132
+ // Always include all notes and posts (they are the knowledge base)
133
+ const nodes = Array.from(nodeMap.values()).filter(n =>
134
+ n.type === 'note' || n.type === 'post' || n.type === 'series' || connectedIds.has(n.id)
135
+ );
136
+
137
+ // Filter edges to only include those with both source and target in nodes
138
+ const validIds = new Set(nodes.map(n => n.id));
139
+ const validEdges = edges.filter(e => validIds.has(e.source) && validIds.has(e.target));
140
+
141
+ // Recompute connection counts from validEdges only (pre-filter counts were inflated)
142
+ const connectionCounts = new Map<string, number>();
143
+ for (const edge of validEdges) {
144
+ connectionCounts.set(edge.source, (connectionCounts.get(edge.source) ?? 0) + 1);
145
+ connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
146
+ }
147
+ for (const node of nodes) {
148
+ node.connections = connectionCounts.get(node.id) ?? 0;
149
+ }
150
+
151
+ const graphData = { nodes, edges: validEdges };
152
+
153
+ const outputPath = path.join(process.cwd(), 'public', 'knowledge-graph.json');
154
+ fs.writeFileSync(outputPath, JSON.stringify(graphData, null, 2));
155
+
156
+ console.log(`✓ Written ${nodes.length} nodes, ${validEdges.length} edges → ${outputPath}`);
157
+ }
158
+
159
+ main().catch(err => {
160
+ console.error('Error generating knowledge graph:', err);
161
+ process.exit(1);
162
+ });
@@ -2,14 +2,12 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
 
4
4
  const args = process.argv.slice(2);
5
- const title = args.filter(arg => !arg.startsWith('--'))[0];
6
5
  const useMdx = args.includes('--mdx');
7
6
 
8
7
  const now = new Date();
9
8
  const year = String(now.getFullYear());
10
9
  const month = String(now.getMonth() + 1).padStart(2, '0');
11
10
  const day = String(now.getDate()).padStart(2, '0');
12
- const dateStr = `${year}-${month}-${day}`;
13
11
 
14
12
  const ext = useMdx ? '.mdx' : '.md';
15
13
  const dirPath = path.join(process.cwd(), 'content', 'flows', year, month);
@@ -34,10 +32,7 @@ if (!fs.existsSync(dirPath)) {
34
32
  fs.mkdirSync(dirPath, { recursive: true });
35
33
  }
36
34
 
37
- const flowTitle = title || dateStr;
38
-
39
35
  const content = `---
40
- title: "${flowTitle}"
41
36
  tags: []
42
37
  ---
43
38
 
@@ -0,0 +1,53 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const args = process.argv.slice(2);
5
+ const titleArg = args.filter(arg => !arg.startsWith('--'))[0];
6
+ const useMd = args.includes('--md');
7
+
8
+ if (!titleArg) {
9
+ console.error('Usage: bun run new-note "Note Title"');
10
+ process.exit(1);
11
+ }
12
+
13
+ // Slugify title
14
+ const slug = titleArg
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
17
+ .replace(/^-|-$/g, '');
18
+
19
+ const now = new Date();
20
+ const dateStr = now.toISOString().split('T')[0];
21
+ const ext = useMd ? '.md' : '.mdx';
22
+
23
+ const notesDir = path.join(process.cwd(), 'content', 'notes');
24
+ const targetPath = path.join(notesDir, `${slug}${ext}`);
25
+
26
+ const altExt = useMd ? '.mdx' : '.md';
27
+ const altPath = path.join(notesDir, `${slug}${altExt}`);
28
+
29
+ if (fs.existsSync(targetPath)) {
30
+ console.error(`Error: Note already exists at ${targetPath}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ if (fs.existsSync(altPath)) {
35
+ console.error(`Error: Note already exists at ${altPath}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ if (!fs.existsSync(notesDir)) {
40
+ fs.mkdirSync(notesDir, { recursive: true });
41
+ }
42
+
43
+ const content = `---
44
+ title: "${titleArg}"
45
+ date: "${dateStr}"
46
+ tags: []
47
+ aliases: []
48
+ ---
49
+
50
+ `;
51
+
52
+ fs.writeFileSync(targetPath, content);
53
+ console.log(`Created new note: ${targetPath}`);
package/site.config.ts CHANGED
@@ -1,8 +1,17 @@
1
+ export interface NavChildItem {
2
+ name: string;
3
+ url: string;
4
+ external?: boolean;
5
+ dividerBefore?: boolean; // render a separator line before this item
6
+ }
7
+
1
8
  export interface NavItem {
2
9
  name: string;
3
10
  url: string;
4
11
  weight: number;
12
+ external?: boolean;
5
13
  dropdown?: string[];
14
+ children?: NavChildItem[]; // static sub-links rendered as a dropdown
6
15
  }
7
16
 
8
17
  // Defined up-front so footer.connect can reference these URLs without duplication
@@ -34,6 +43,12 @@ export const siteConfig = {
34
43
  { name: "Series", url: "/series", weight: 3, dropdown: ["digital-garden", "markdown-showcase", "ai-nexus-weekly"] },
35
44
  { name: "Books", url: "/books", weight: 4, dropdown: [] },
36
45
  { name: "About", url: "/about", weight: 5 },
46
+ { name: "More", url: "", weight: 6, children: [
47
+ { name: "Archive", url: "/archive" },
48
+ { name: "Tags", url: "/tags" },
49
+ { name: "Links", url: "/links" },
50
+ { name: "Subscribe", url: "/subscribe", dividerBefore: true },
51
+ ]},
37
52
  ] as NavItem[],
38
53
 
39
54
  // ── Footer ────────────────────────────────────────────────────────────────
@@ -93,6 +108,10 @@ export const siteConfig = {
93
108
  enabled: true,
94
109
  name: { en: "Flow", zh: "随笔" },
95
110
  },
111
+ notes: {
112
+ enabled: true,
113
+ name: { en: "Notes", zh: "笔记" },
114
+ },
96
115
  },
97
116
 
98
117
  // ── Homepage ──────────────────────────────────────────────────────────────
@@ -115,8 +134,9 @@ export const siteConfig = {
115
134
  // ── Content ───────────────────────────────────────────────────────────────
116
135
  pagination: {
117
136
  posts: 5,
118
- series: 1,
137
+ series: 5,
119
138
  flows: 20,
139
+ notes: 20,
120
140
  },
121
141
  posts: {
122
142
  toc: true,
@@ -1,10 +1,11 @@
1
- import { getAllFlows, getFlowBySlug, getAdjacentFlows } from '@/lib/markdown';
1
+ import { getAllFlows, getFlowBySlug, getAdjacentFlows, buildSlugRegistry, getBacklinks } from '@/lib/markdown';
2
2
  import { siteConfig } from '../../../../../../site.config';
3
3
  import { Metadata } from 'next';
4
4
  import { notFound } from 'next/navigation';
5
5
  import { t, resolveLocale } from '@/lib/i18n';
6
6
  import FlowCalendarSidebar from '@/components/FlowCalendarSidebar';
7
7
  import MarkdownRenderer from '@/components/MarkdownRenderer';
8
+ import Backlinks from '@/components/Backlinks';
8
9
  import ShareBar from '@/components/ShareBar';
9
10
  import Link from 'next/link';
10
11
 
@@ -50,56 +51,59 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
50
51
  const allFlows = getAllFlows();
51
52
  const entryDates = allFlows.map(f => f.date);
52
53
  const { prev, next } = getAdjacentFlows(flow.slug);
54
+ const slugRegistry = buildSlugRegistry();
55
+ const backlinks = getBacklinks(flow.slug);
53
56
  const flowUrl = `${siteConfig.baseUrl}/flows/${year}/${month}/${day}`;
54
57
 
58
+ const breadcrumb = (
59
+ <nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-1.5 text-sm text-muted">
60
+ <Link href="/flows" className="hover:text-accent no-underline shrink-0">
61
+ {t('all_flows')}
62
+ </Link>
63
+ <span className="text-muted/40" aria-hidden="true">›</span>
64
+ <Link href={`/flows/${year}`} className="hover:text-accent no-underline shrink-0">
65
+ {year}
66
+ </Link>
67
+ <span className="text-muted/40" aria-hidden="true">›</span>
68
+ <Link href={`/flows/${year}/${month}`} className="hover:text-accent no-underline shrink-0">
69
+ {month}
70
+ </Link>
71
+ <span className="text-muted/40" aria-hidden="true">›</span>
72
+ <span className="text-foreground shrink-0">{day}</span>
73
+ </nav>
74
+ );
75
+
55
76
  return (
56
77
  <div className="layout-main">
57
- {/* Breadcrumb navigation */}
58
- <nav className="flex items-center gap-1.5 text-sm text-muted mb-6">
59
- <Link href="/flows" className="hover:text-accent no-underline">
60
- {t('all_flows')}
61
- </Link>
62
- <span className="text-muted/40">›</span>
63
- <Link href={`/flows/${year}`} className="hover:text-accent no-underline">
64
- {year}
65
- </Link>
66
- <span className="text-muted/40">›</span>
67
- <Link href={`/flows/${year}/${month}`} className="hover:text-accent no-underline">
68
- {month}
69
- </Link>
70
- <span className="text-muted/40">›</span>
71
- <span className="text-foreground">{day}</span>
72
- </nav>
73
-
74
78
  <div className="flex gap-10">
75
- <FlowCalendarSidebar entryDates={entryDates} currentDate={flow.date} />
79
+ <FlowCalendarSidebar entryDates={entryDates} currentDate={flow.date} breadcrumb={breadcrumb} />
76
80
 
77
81
  <article className="flex-1 min-w-0">
78
82
  {/* Header */}
79
83
  <header className="mb-8">
80
- <time className="text-sm font-mono text-accent" data-pagefind-meta="date[content]">{flow.date}</time>
81
- <h1 className="mt-2 text-3xl md:text-4xl font-serif font-bold text-heading">{flow.title}</h1>
84
+ <time className="text-base font-mono text-accent" data-pagefind-meta="date[content]">{flow.date}</time>
82
85
  </header>
83
86
 
84
87
  {/* Content */}
85
88
  <div className="prose prose-lg dark:prose-invert max-w-none">
86
- <MarkdownRenderer content={flow.content} />
89
+ <MarkdownRenderer content={flow.content} slugRegistry={slugRegistry} />
87
90
  </div>
88
91
 
92
+ <Backlinks backlinks={backlinks} />
93
+
89
94
  <ShareBar url={flowUrl} title={flow.title} className="mt-8 mb-2" />
90
95
 
91
96
  {/* Prev/Next navigation */}
92
- <nav className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-2 gap-4">
97
+ <nav aria-label="Post navigation" className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-2 gap-4">
93
98
  {prev ? (
94
99
  <Link
95
100
  href={`/flows/${prev.slug}`}
96
101
  className="group text-left no-underline"
97
102
  >
98
103
  <span className="text-xs text-muted">{t('older')}</span>
99
- <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
100
- {prev.title}
104
+ <div className="text-sm font-mono text-heading group-hover:text-accent transition-colors">
105
+ {prev.date}
101
106
  </div>
102
- <span className="text-xs font-mono text-muted">{prev.date}</span>
103
107
  </Link>
104
108
  ) : <div />}
105
109
  {next ? (
@@ -108,10 +112,9 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
108
112
  className="group text-right no-underline"
109
113
  >
110
114
  <span className="text-xs text-muted">{t('newer')}</span>
111
- <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
112
- {next.title}
115
+ <div className="text-sm font-mono text-heading group-hover:text-accent transition-colors">
116
+ {next.date}
113
117
  </div>
114
- <span className="text-xs font-mono text-muted">{next.date}</span>
115
118
  </Link>
116
119
  ) : <div />}
117
120
  </nav>
@@ -39,6 +39,20 @@ export default async function FlowsMonthPage({ params }: { params: Promise<{ yea
39
39
  const tags = getFlowTags();
40
40
  const monthLabel = new Date(parseInt(year), parseInt(month) - 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
41
41
 
42
+ const breadcrumb = (
43
+ <nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-sm text-muted">
44
+ <Link href="/flows" className="hover:text-accent no-underline">
45
+ {t('all_flows')}
46
+ </Link>
47
+ <span className="text-muted/40" aria-hidden="true">›</span>
48
+ <Link href={`/flows/${year}`} className="hover:text-accent no-underline">
49
+ {year}
50
+ </Link>
51
+ <span className="text-muted/40" aria-hidden="true">›</span>
52
+ <span className="text-foreground">{month}</span>
53
+ </nav>
54
+ );
55
+
42
56
  return (
43
57
  <div className="layout-main">
44
58
  <PageHeader
@@ -48,24 +62,12 @@ export default async function FlowsMonthPage({ params }: { params: Promise<{ yea
48
62
  subtitleParams={{ count: flows.length }}
49
63
  />
50
64
 
51
- {/* Breadcrumb navigation */}
52
- <nav className="flex items-center gap-1.5 text-sm text-muted mb-6">
53
- <Link href="/flows" className="hover:text-accent no-underline">
54
- {t('all_flows')}
55
- </Link>
56
- <span className="text-muted/40">›</span>
57
- <Link href={`/flows/${year}`} className="hover:text-accent no-underline">
58
- {year}
59
- </Link>
60
- <span className="text-muted/40">›</span>
61
- <span className="text-foreground">{month}</span>
62
- </nav>
63
-
64
65
  <FlowContent
65
66
  flows={flows}
66
67
  entryDates={entryDates}
67
68
  tags={tags}
68
69
  currentDate={`${year}-${month}-01`}
70
+ breadcrumb={breadcrumb}
69
71
  />
70
72
  </div>
71
73
  );
@@ -44,38 +44,45 @@ export default async function FlowsYearPage({ params }: { params: Promise<{ year
44
44
  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
45
45
  ];
46
46
 
47
- return (
48
- <div className="layout-main">
49
- <PageHeader
50
- titleKey="flows_in_year"
51
- titleParams={{ year }}
52
- subtitleKey="flow_subtitle"
53
- subtitleParams={{ count: flows.length }}
54
- />
55
-
56
- {/* Month navigation pills */}
57
- <div className="flex flex-wrap items-center gap-2 mb-6">
58
- <Link href="/flows" className="text-sm text-muted hover:text-accent no-underline">
59
- ← {t('all_flows')}
47
+ const breadcrumb = (
48
+ <div className="space-y-2">
49
+ <nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-sm text-muted">
50
+ <Link href="/flows" className="hover:text-accent no-underline">
51
+ {t('all_flows')}
60
52
  </Link>
61
- <span className="text-muted/30">|</span>
53
+ <span className="text-muted/40" aria-hidden="true">›</span>
54
+ <span className="text-foreground">{year}</span>
55
+ </nav>
56
+ <div className="flex flex-wrap gap-1.5">
62
57
  {sortedMonths.map(m => (
63
58
  <Link
64
59
  key={m}
65
60
  href={`/flows/${year}/${m}`}
66
- className="inline-flex items-center gap-1 px-3 py-1 text-xs rounded-full border border-muted/20 text-foreground hover:border-accent hover:text-accent no-underline transition-colors"
61
+ className="inline-flex items-center gap-1 px-2.5 py-0.5 text-xs rounded-full border border-muted/20 text-foreground hover:border-accent hover:text-accent no-underline transition-colors"
67
62
  >
68
63
  {monthNames[parseInt(m, 10) - 1]}
69
64
  <span className="text-muted text-[10px]">({monthCounts[m]})</span>
70
65
  </Link>
71
66
  ))}
72
67
  </div>
68
+ </div>
69
+ );
70
+
71
+ return (
72
+ <div className="layout-main">
73
+ <PageHeader
74
+ titleKey="flows_in_year"
75
+ titleParams={{ year }}
76
+ subtitleKey="flow_subtitle"
77
+ subtitleParams={{ count: flows.length }}
78
+ />
73
79
 
74
80
  <FlowContent
75
81
  flows={flows}
76
82
  entryDates={entryDates}
77
83
  tags={tags}
78
84
  currentDate={`${year}-01-01`}
85
+ breadcrumb={breadcrumb}
79
86
  />
80
87
  </div>
81
88
  );
@@ -2,9 +2,9 @@ import { getAllFlows, getFlowTags } from '@/lib/markdown';
2
2
  import { siteConfig } from '../../../../../site.config';
3
3
  import { Metadata } from 'next';
4
4
  import { notFound } from 'next/navigation';
5
- import { t, resolveLocale } from '@/lib/i18n';
6
- import PageHeader from '@/components/PageHeader';
5
+ import { t, tWith, resolveLocale } from '@/lib/i18n';
7
6
  import FlowContent from '@/components/FlowContent';
7
+ import FlowHubTabs from '@/components/FlowHubTabs';
8
8
 
9
9
  const PAGE_SIZE = siteConfig.pagination.flows;
10
10
 
@@ -45,13 +45,7 @@ export default async function FlowsPaginatedPage({ params }: { params: Promise<{
45
45
 
46
46
  return (
47
47
  <div className="layout-main">
48
- <PageHeader
49
- titleKey="flow"
50
- subtitleKey="page_of_total"
51
- subtitleParams={{ page, total: totalPages }}
52
- className="mb-12"
53
- />
54
-
48
+ <FlowHubTabs subtitle={tWith('page_of_total', { page, total: totalPages })} />
55
49
  <FlowContent
56
50
  flows={flows}
57
51
  entryDates={entryDates}
@@ -1,9 +1,9 @@
1
1
  import { getAllFlows, getFlowTags } from '@/lib/markdown';
2
2
  import { siteConfig } from '../../../site.config';
3
3
  import { Metadata } from 'next';
4
- import { t, resolveLocale } from '@/lib/i18n';
5
- import PageHeader from '@/components/PageHeader';
4
+ import { t, tWith, resolveLocale } from '@/lib/i18n';
6
5
  import FlowContent from '@/components/FlowContent';
6
+ import FlowHubTabs from '@/components/FlowHubTabs';
7
7
 
8
8
  const PAGE_SIZE = siteConfig.pagination.flows;
9
9
 
@@ -21,12 +21,7 @@ export default function FlowsPage() {
21
21
 
22
22
  return (
23
23
  <div className="layout-main">
24
- <PageHeader
25
- titleKey="flow"
26
- subtitleKey="flow_subtitle"
27
- subtitleParams={{ count: allFlows.length }}
28
- />
29
-
24
+ <FlowHubTabs subtitle={tWith('flow_subtitle', { count: allFlows.length })} />
30
25
  <FlowContent
31
26
  flows={flows}
32
27
  entryDates={entryDates}
@@ -54,6 +54,47 @@
54
54
  --accent-hover: #f59e0b;
55
55
  }
56
56
 
57
+ /* Wikilink styles */
58
+ .wikilink {
59
+ text-decoration: none;
60
+ transition: color 0.15s;
61
+ }
62
+ .wikilink::before,
63
+ .wikilink::after {
64
+ font-family: var(--font-mono);
65
+ font-size: 0.72em;
66
+ opacity: 0.45;
67
+ vertical-align: 0.08em;
68
+ transition: opacity 0.15s;
69
+ }
70
+ .wikilink::before { content: '[['; }
71
+ .wikilink::after { content: ']]'; }
72
+ .wikilink--resolved {
73
+ color: var(--accent);
74
+ }
75
+ .wikilink--resolved:hover,
76
+ .wikilink--resolved:focus-visible {
77
+ color: var(--accent-hover);
78
+ text-decoration: underline;
79
+ outline: none;
80
+ }
81
+ .wikilink--resolved:focus-visible {
82
+ outline: 2px solid var(--accent);
83
+ outline-offset: 2px;
84
+ border-radius: 2px;
85
+ }
86
+ .wikilink--resolved:hover::before,
87
+ .wikilink--resolved:hover::after,
88
+ .wikilink--resolved:focus-visible::before,
89
+ .wikilink--resolved:focus-visible::after {
90
+ opacity: 0.75;
91
+ }
92
+ .wikilink--broken {
93
+ color: var(--muted);
94
+ text-decoration: underline dashed;
95
+ cursor: default;
96
+ }
97
+
57
98
  /* PrismJS Syntax Highlighting Custom Theme */
58
99
  code[class*="language-"],
59
100
  pre[class*="language-"] {
@@ -0,0 +1,19 @@
1
+ import { Metadata } from 'next';
2
+ import { t, resolveLocale } from '@/lib/i18n';
3
+ import { siteConfig } from '../../../site.config';
4
+ import FlowHubTabs from '@/components/FlowHubTabs';
5
+ import KnowledgeGraph from '@/components/KnowledgeGraph';
6
+
7
+ export const metadata: Metadata = {
8
+ title: `${t('tab_graph')} | ${resolveLocale(siteConfig.title)}`,
9
+ description: t('graph_subtitle'),
10
+ };
11
+
12
+ export default function GraphPage() {
13
+ return (
14
+ <div className="layout-main">
15
+ <FlowHubTabs subtitle={t('graph_subtitle')} />
16
+ <KnowledgeGraph />
17
+ </div>
18
+ );
19
+ }