@hutusi/amytis 1.6.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 +12 -2
- package/README.md +14 -0
- package/TODO.md +24 -16
- package/bun.lock +8 -3
- package/content/about.mdx +1 -0
- package/content/about.zh.mdx +21 -0
- 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 +15 -0
- package/content/links.mdx +42 -0
- package/content/links.zh.mdx +41 -0
- 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/posts/2026-02-20-i18n-routing-considerations.mdx +150 -0
- package/content/posts/multimedia-showcase/index.mdx +261 -0
- package/content/privacy.mdx +32 -0
- package/content/privacy.zh.mdx +32 -0
- package/docs/ARCHITECTURE.md +16 -0
- package/docs/CONTRIBUTING.md +11 -0
- package/docs/DIGITAL_GARDEN.md +64 -0
- package/package.json +8 -3
- package/scripts/copy-assets.ts +1 -1
- 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 +146 -44
- package/src/app/[slug]/page.tsx +0 -10
- package/src/app/archive/page.tsx +38 -10
- package/src/app/books/[slug]/page.tsx +18 -0
- package/src/app/flows/[year]/[month]/[day]/page.tsx +51 -31
- 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/layout.tsx +47 -21
- 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 +134 -72
- package/src/app/posts/[slug]/page.tsx +8 -12
- package/src/app/search.json/route.ts +15 -1
- package/src/app/series/[slug]/page.tsx +18 -0
- package/src/app/subscribe/page.tsx +17 -0
- package/src/app/tags/[tag]/page.tsx +9 -26
- package/src/app/tags/page.tsx +3 -8
- package/src/components/AuthorCard.tsx +43 -0
- package/src/components/Backlinks.tsx +39 -0
- package/src/components/Comments.tsx +20 -4
- package/src/components/ExternalLinks.tsx +6 -2
- 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/Footer.tsx +35 -26
- package/src/components/KnowledgeGraph.tsx +324 -0
- package/src/components/LanguageProvider.tsx +0 -5
- package/src/components/LanguageSwitch.tsx +117 -6
- package/src/components/LocaleSwitch.tsx +33 -0
- package/src/components/MarkdownRenderer.tsx +13 -2
- package/src/components/Navbar.tsx +266 -17
- package/src/components/NoteContent.tsx +123 -0
- package/src/components/NoteSidebar.tsx +132 -0
- package/src/components/PostNavigation.tsx +55 -0
- package/src/components/PostSidebar.tsx +172 -126
- package/src/components/ReadingProgressBar.tsx +6 -21
- package/src/components/RecentNotesSection.tsx +6 -11
- package/src/components/RelatedPosts.tsx +1 -1
- package/src/components/Search.tsx +29 -5
- package/src/components/SelectedBooksSection.tsx +12 -6
- package/src/components/ShareBar.tsx +115 -0
- package/src/components/SimpleLayoutHeader.tsx +5 -14
- package/src/components/SubscribePage.tsx +298 -0
- package/src/components/TagContentTabs.tsx +102 -0
- package/src/components/TagPageHeader.tsx +7 -13
- package/src/components/TagSidebar.tsx +142 -0
- package/src/components/TagsIndexClient.tsx +156 -0
- package/src/hooks/useScrollY.ts +41 -0
- package/src/i18n/translations.ts +105 -1
- package/src/layouts/PostLayout.tsx +40 -8
- package/src/layouts/SimpleLayout.tsx +53 -15
- package/src/lib/markdown.ts +347 -18
- package/src/lib/remark-wikilinks.ts +59 -0
- package/src/lib/search-utils.ts +2 -1
- package/src/components/TableOfContents.tsx +0 -158
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
import type { Root, Text, Parent } from 'mdast';
|
|
3
|
+
import type { SlugRegistryEntry } from './markdown';
|
|
4
|
+
|
|
5
|
+
interface WikilinksOptions {
|
|
6
|
+
slugRegistry: Map<string, SlugRegistryEntry>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function remarkWikilinks({ slugRegistry }: WikilinksOptions) {
|
|
10
|
+
return (tree: Root) => {
|
|
11
|
+
visit(tree, 'text', (node: Text, index: number | undefined, parent: Parent | undefined) => {
|
|
12
|
+
if (!parent || index === undefined) return;
|
|
13
|
+
if (!node.value.includes('[[')) return;
|
|
14
|
+
|
|
15
|
+
// Create fresh regex each time to avoid lastIndex issue with 'g' flag
|
|
16
|
+
const WIKILINK = /\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g;
|
|
17
|
+
if (!WIKILINK.test(node.value)) return;
|
|
18
|
+
WIKILINK.lastIndex = 0;
|
|
19
|
+
|
|
20
|
+
const newNodes: (Text | { type: 'html'; value: string })[] = [];
|
|
21
|
+
let last = 0;
|
|
22
|
+
let match: RegExpExecArray | null;
|
|
23
|
+
|
|
24
|
+
while ((match = WIKILINK.exec(node.value)) !== null) {
|
|
25
|
+
if (match.index > last) {
|
|
26
|
+
newNodes.push({ type: 'text', value: node.value.slice(last, match.index) });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const slug = match[1].trim();
|
|
30
|
+
const display = match[2]?.trim() || slug;
|
|
31
|
+
const entry = slugRegistry.get(slug);
|
|
32
|
+
|
|
33
|
+
if (entry) {
|
|
34
|
+
newNodes.push({
|
|
35
|
+
type: 'html',
|
|
36
|
+
value: `<a href="${entry.url}" class="wikilink wikilink--resolved wikilink--${entry.type}">${display}</a>`,
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
newNodes.push({
|
|
40
|
+
type: 'html',
|
|
41
|
+
value: `<span class="wikilink wikilink--broken" title="[[${slug}]] not found">${display}</span>`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
last = match.index + match[0].length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (last < node.value.length) {
|
|
49
|
+
newNodes.push({ type: 'text', value: node.value.slice(last) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (newNodes.length > 1 || (newNodes.length === 1 && newNodes[0].type !== 'text')) {
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
(parent.children as any[]).splice(index, 1, ...newNodes);
|
|
55
|
+
return index + newNodes.length; // skip inserted nodes
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/lib/search-utils.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export type ContentType = 'All' | 'Post' | 'Flow' | 'Book';
|
|
1
|
+
export type ContentType = 'All' | 'Post' | 'Flow' | 'Book' | 'Note';
|
|
2
2
|
|
|
3
3
|
/** Derive content type from a Pagefind result URL. */
|
|
4
4
|
export function getResultType(url: string): Exclude<ContentType, 'All'> {
|
|
5
5
|
if (url.includes('/flows/')) return 'Flow';
|
|
6
6
|
if (url.includes('/books/')) return 'Book';
|
|
7
|
+
if (url.includes('/notes/')) return 'Note';
|
|
7
8
|
return 'Post';
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import { Heading } from '@/lib/markdown';
|
|
5
|
-
import { useLanguage } from '@/components/LanguageProvider';
|
|
6
|
-
|
|
7
|
-
export default function TableOfContents({ headings }: { headings: Heading[] }) {
|
|
8
|
-
const { t } = useLanguage();
|
|
9
|
-
const [activeId, setActiveId] = useState<string>('');
|
|
10
|
-
const [readProgress, setReadProgress] = useState(0);
|
|
11
|
-
|
|
12
|
-
// Track scroll position and active heading
|
|
13
|
-
const handleScroll = useCallback(() => {
|
|
14
|
-
// Calculate read progress
|
|
15
|
-
const scrollTop = window.scrollY;
|
|
16
|
-
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
17
|
-
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
|
18
|
-
setReadProgress(Math.min(100, Math.max(0, progress)));
|
|
19
|
-
|
|
20
|
-
// Find active heading
|
|
21
|
-
const headingElements = headings
|
|
22
|
-
.map(h => document.getElementById(h.id))
|
|
23
|
-
.filter(Boolean) as HTMLElement[];
|
|
24
|
-
|
|
25
|
-
if (headingElements.length === 0) return;
|
|
26
|
-
|
|
27
|
-
// Find the heading that's currently in view
|
|
28
|
-
const scrollPosition = scrollTop + 100; // Offset for navbar
|
|
29
|
-
|
|
30
|
-
let currentHeading = headingElements[0];
|
|
31
|
-
for (const heading of headingElements) {
|
|
32
|
-
if (heading.offsetTop <= scrollPosition) {
|
|
33
|
-
currentHeading = heading;
|
|
34
|
-
} else {
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (currentHeading) {
|
|
40
|
-
setActiveId(currentHeading.id);
|
|
41
|
-
}
|
|
42
|
-
}, [headings]);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
// Initial check on mount via animation frame to avoid cascading render error
|
|
46
|
-
const rafId = requestAnimationFrame(handleScroll);
|
|
47
|
-
|
|
48
|
-
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
49
|
-
return () => {
|
|
50
|
-
cancelAnimationFrame(rafId);
|
|
51
|
-
window.removeEventListener('scroll', handleScroll);
|
|
52
|
-
};
|
|
53
|
-
}, [handleScroll]);
|
|
54
|
-
|
|
55
|
-
// Smooth scroll to heading
|
|
56
|
-
const scrollToHeading = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
|
|
57
|
-
e.preventDefault();
|
|
58
|
-
const element = document.getElementById(id);
|
|
59
|
-
if (element) {
|
|
60
|
-
const offset = 80; // Navbar height
|
|
61
|
-
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
|
|
62
|
-
window.scrollTo({
|
|
63
|
-
top: elementPosition - offset,
|
|
64
|
-
behavior: 'smooth'
|
|
65
|
-
});
|
|
66
|
-
// Update URL without scrolling
|
|
67
|
-
history.pushState(null, '', `#${id}`);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (headings.length === 0) return null;
|
|
72
|
-
|
|
73
|
-
// Find active index for progress calculation
|
|
74
|
-
const activeIndex = headings.findIndex(h => h.id === activeId);
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<nav
|
|
78
|
-
className="hidden lg:block sticky top-28 self-start w-56 pl-6 max-h-[calc(100vh-8rem)] overflow-y-auto scrollbar-hide"
|
|
79
|
-
aria-label="Table of contents"
|
|
80
|
-
>
|
|
81
|
-
{/* Header with progress */}
|
|
82
|
-
<div className="flex items-center justify-between mb-4 pb-3 border-b border-muted/10">
|
|
83
|
-
<h2 className="text-[10px] font-sans font-bold uppercase tracking-widest text-muted">
|
|
84
|
-
{t('on_this_page')}
|
|
85
|
-
</h2>
|
|
86
|
-
<span className="text-[10px] font-mono text-muted/60">
|
|
87
|
-
{Math.round(readProgress)}%
|
|
88
|
-
</span>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{/* Progress bar */}
|
|
92
|
-
<div className="h-0.5 bg-muted/10 rounded-full overflow-hidden mb-5">
|
|
93
|
-
<div
|
|
94
|
-
className="h-full bg-accent/50 rounded-full transition-all duration-150"
|
|
95
|
-
style={{ width: `${readProgress}%` }}
|
|
96
|
-
/>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
{/* Headings list */}
|
|
100
|
-
<ul className="space-y-1 relative">
|
|
101
|
-
{/* Active indicator line */}
|
|
102
|
-
<div className="absolute left-0 top-0 bottom-0 w-px bg-muted/10" />
|
|
103
|
-
|
|
104
|
-
{headings.map((heading, index) => {
|
|
105
|
-
const isActive = heading.id === activeId;
|
|
106
|
-
const isPast = activeIndex > -1 && index < activeIndex;
|
|
107
|
-
const isH3 = heading.level === 3;
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<li
|
|
111
|
-
key={heading.id}
|
|
112
|
-
className={`relative ${isH3 ? 'pl-4' : ''}`}
|
|
113
|
-
>
|
|
114
|
-
{/* Active indicator */}
|
|
115
|
-
{isActive && (
|
|
116
|
-
<div
|
|
117
|
-
className="absolute left-0 w-0.5 bg-accent rounded-full transition-all duration-200"
|
|
118
|
-
style={{
|
|
119
|
-
top: '4px',
|
|
120
|
-
height: 'calc(100% - 8px)'
|
|
121
|
-
}}
|
|
122
|
-
/>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
<a
|
|
126
|
-
href={`#${heading.id}`}
|
|
127
|
-
onClick={(e) => scrollToHeading(e, heading.id)}
|
|
128
|
-
className={`block py-1.5 pl-4 text-sm leading-snug transition-all duration-200 ${
|
|
129
|
-
isActive
|
|
130
|
-
? 'text-accent font-medium'
|
|
131
|
-
: isPast
|
|
132
|
-
? 'text-foreground/60 hover:text-foreground'
|
|
133
|
-
: 'text-muted/70 hover:text-foreground'
|
|
134
|
-
}`}
|
|
135
|
-
aria-current={isActive ? 'location' : undefined}
|
|
136
|
-
>
|
|
137
|
-
{heading.text}
|
|
138
|
-
</a>
|
|
139
|
-
</li>
|
|
140
|
-
);
|
|
141
|
-
})}
|
|
142
|
-
</ul>
|
|
143
|
-
|
|
144
|
-
{/* Back to top */}
|
|
145
|
-
{readProgress > 20 && (
|
|
146
|
-
<button
|
|
147
|
-
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
|
148
|
-
className="mt-6 pt-4 border-t border-muted/10 w-full text-left text-xs text-muted hover:text-accent transition-colors flex items-center gap-1.5"
|
|
149
|
-
>
|
|
150
|
-
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
151
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
152
|
-
</svg>
|
|
153
|
-
{t('back_to_top')}
|
|
154
|
-
</button>
|
|
155
|
-
)}
|
|
156
|
-
</nav>
|
|
157
|
-
);
|
|
158
|
-
}
|