@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.
- package/CHANGELOG.md +49 -0
- package/GEMINI.md +6 -0
- package/README.md +14 -0
- package/TODO.md +15 -3
- package/bun.lock +5 -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/docs/ARCHITECTURE.md +8 -0
- package/docs/CONTRIBUTING.md +11 -0
- package/docs/DIGITAL_GARDEN.md +64 -0
- package/package.json +7 -3
- package/scripts/generate-knowledge-graph.ts +162 -0
- package/scripts/new-flow.ts +0 -5
- package/scripts/new-note.ts +53 -0
- package/site.config.ts +21 -1
- package/src/app/flows/[year]/[month]/[day]/page.tsx +32 -29
- package/src/app/flows/[year]/[month]/page.tsx +15 -13
- package/src/app/flows/[year]/page.tsx +22 -15
- package/src/app/flows/page/[page]/page.tsx +3 -9
- package/src/app/flows/page.tsx +3 -8
- package/src/app/globals.css +41 -0
- package/src/app/graph/page.tsx +19 -0
- package/src/app/notes/[slug]/page.tsx +128 -0
- package/src/app/notes/page/[page]/page.tsx +58 -0
- package/src/app/notes/page.tsx +31 -0
- package/src/app/page.tsx +0 -1
- package/src/app/posts/[slug]/page.tsx +4 -2
- package/src/app/search.json/route.ts +15 -1
- package/src/components/Backlinks.tsx +39 -0
- 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/MarkdownRenderer.tsx +13 -2
- package/src/components/Navbar.tsx +235 -9
- 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 +5 -1
- package/src/components/TagContentTabs.tsx +0 -1
- package/src/i18n/translations.ts +21 -1
- package/src/layouts/PostLayout.tsx +8 -3
- package/src/lib/markdown.ts +276 -3
- package/src/lib/remark-wikilinks.ts +59 -0
- 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
|
+
});
|
package/scripts/new-flow.ts
CHANGED
|
@@ -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:
|
|
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-
|
|
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-
|
|
100
|
-
{prev.
|
|
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-
|
|
112
|
-
{next.
|
|
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
|
-
|
|
48
|
-
<div className="
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
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/
|
|
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-
|
|
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
|
-
<
|
|
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}
|
package/src/app/flows/page.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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}
|
package/src/app/globals.css
CHANGED
|
@@ -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
|
+
}
|