@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/GEMINI.md +12 -2
  3. package/README.md +14 -0
  4. package/TODO.md +24 -16
  5. package/bun.lock +8 -3
  6. package/content/about.mdx +1 -0
  7. package/content/about.zh.mdx +21 -0
  8. package/content/flows/2026/02/05.md +0 -1
  9. package/content/flows/2026/02/10.mdx +2 -1
  10. package/content/flows/2026/02/15.md +2 -1
  11. package/content/flows/2026/02/18.mdx +2 -1
  12. package/content/flows/2026/02/20.md +15 -0
  13. package/content/links.mdx +42 -0
  14. package/content/links.zh.mdx +41 -0
  15. package/content/notes/algorithms-and-data-structures.mdx +51 -0
  16. package/content/notes/digital-garden-philosophy.mdx +36 -0
  17. package/content/notes/react-server-components.mdx +49 -0
  18. package/content/notes/tailwind-v4.mdx +45 -0
  19. package/content/notes/zettelkasten-method.mdx +33 -0
  20. package/content/posts/2026-02-20-i18n-routing-considerations.mdx +150 -0
  21. package/content/posts/multimedia-showcase/index.mdx +261 -0
  22. package/content/privacy.mdx +32 -0
  23. package/content/privacy.zh.mdx +32 -0
  24. package/docs/ARCHITECTURE.md +16 -0
  25. package/docs/CONTRIBUTING.md +11 -0
  26. package/docs/DIGITAL_GARDEN.md +64 -0
  27. package/package.json +8 -3
  28. package/scripts/copy-assets.ts +1 -1
  29. package/scripts/generate-knowledge-graph.ts +162 -0
  30. package/scripts/new-flow.ts +0 -5
  31. package/scripts/new-note.ts +53 -0
  32. package/site.config.ts +146 -44
  33. package/src/app/[slug]/page.tsx +0 -10
  34. package/src/app/archive/page.tsx +38 -10
  35. package/src/app/books/[slug]/page.tsx +18 -0
  36. package/src/app/flows/[year]/[month]/[day]/page.tsx +51 -31
  37. package/src/app/flows/[year]/[month]/page.tsx +15 -13
  38. package/src/app/flows/[year]/page.tsx +22 -15
  39. package/src/app/flows/page/[page]/page.tsx +3 -9
  40. package/src/app/flows/page.tsx +3 -8
  41. package/src/app/globals.css +41 -0
  42. package/src/app/graph/page.tsx +19 -0
  43. package/src/app/layout.tsx +47 -21
  44. package/src/app/notes/[slug]/page.tsx +128 -0
  45. package/src/app/notes/page/[page]/page.tsx +58 -0
  46. package/src/app/notes/page.tsx +31 -0
  47. package/src/app/page.tsx +134 -72
  48. package/src/app/posts/[slug]/page.tsx +8 -12
  49. package/src/app/search.json/route.ts +15 -1
  50. package/src/app/series/[slug]/page.tsx +18 -0
  51. package/src/app/subscribe/page.tsx +17 -0
  52. package/src/app/tags/[tag]/page.tsx +9 -26
  53. package/src/app/tags/page.tsx +3 -8
  54. package/src/components/AuthorCard.tsx +43 -0
  55. package/src/components/Backlinks.tsx +39 -0
  56. package/src/components/Comments.tsx +20 -4
  57. package/src/components/ExternalLinks.tsx +6 -2
  58. package/src/components/FlowCalendarSidebar.tsx +4 -2
  59. package/src/components/FlowContent.tsx +4 -3
  60. package/src/components/FlowHubTabs.tsx +50 -0
  61. package/src/components/FlowTimelineEntry.tsx +7 -9
  62. package/src/components/Footer.tsx +35 -26
  63. package/src/components/KnowledgeGraph.tsx +324 -0
  64. package/src/components/LanguageProvider.tsx +0 -5
  65. package/src/components/LanguageSwitch.tsx +117 -6
  66. package/src/components/LocaleSwitch.tsx +33 -0
  67. package/src/components/MarkdownRenderer.tsx +13 -2
  68. package/src/components/Navbar.tsx +266 -17
  69. package/src/components/NoteContent.tsx +123 -0
  70. package/src/components/NoteSidebar.tsx +132 -0
  71. package/src/components/PostNavigation.tsx +55 -0
  72. package/src/components/PostSidebar.tsx +172 -126
  73. package/src/components/ReadingProgressBar.tsx +6 -21
  74. package/src/components/RecentNotesSection.tsx +6 -11
  75. package/src/components/RelatedPosts.tsx +1 -1
  76. package/src/components/Search.tsx +29 -5
  77. package/src/components/SelectedBooksSection.tsx +12 -6
  78. package/src/components/ShareBar.tsx +115 -0
  79. package/src/components/SimpleLayoutHeader.tsx +5 -14
  80. package/src/components/SubscribePage.tsx +298 -0
  81. package/src/components/TagContentTabs.tsx +102 -0
  82. package/src/components/TagPageHeader.tsx +7 -13
  83. package/src/components/TagSidebar.tsx +142 -0
  84. package/src/components/TagsIndexClient.tsx +156 -0
  85. package/src/hooks/useScrollY.ts +41 -0
  86. package/src/i18n/translations.ts +105 -1
  87. package/src/layouts/PostLayout.tsx +40 -8
  88. package/src/layouts/SimpleLayout.tsx +53 -15
  89. package/src/lib/markdown.ts +347 -18
  90. package/src/lib/remark-wikilinks.ts +59 -0
  91. package/src/lib/search-utils.ts +2 -1
  92. 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
+ }
@@ -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
- }