coding-friend-cli 1.1.1 → 1.2.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/README.md +15 -0
- package/dist/{chunk-KZT4AFDW.js → chunk-5HZJX47M.js} +1 -1
- package/dist/{chunk-AQXTNLQD.js → chunk-6OI37OZX.js} +9 -1
- package/dist/chunk-R6ZYK4UX.js +128 -0
- package/dist/dev-LZASFXZZ.js +243 -0
- package/dist/{host-JBTJCWM2.js → host-BK6DYFWF.js} +2 -2
- package/dist/index.js +26 -5
- package/dist/{init-E6CL3UZQ.js → init-2UKYE2KV.js} +2 -2
- package/dist/{mcp-MWESK6UX.js → mcp-CH4SKZSX.js} +2 -2
- package/dist/postinstall.js +1 -1
- package/dist/{statusline-7D6YU5YM.js → statusline-ARI7I5YM.js} +1 -1
- package/dist/{update-IH3G4SN5.js → update-5A2OP6EY.js} +58 -37
- package/lib/learn-host/.prettierignore +3 -0
- package/lib/learn-host/.prettierrc +8 -0
- package/lib/learn-host/CHANGELOG.md +9 -0
- package/lib/learn-host/eslint.config.mjs +6 -0
- package/lib/learn-host/next-env.d.ts +1 -1
- package/lib/learn-host/next.config.ts +1 -0
- package/lib/learn-host/package-lock.json +6039 -391
- package/lib/learn-host/package.json +30 -15
- package/lib/learn-host/public/_pagefind/fragment/en_1172b3c.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_118ad1c.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_32ab3d8.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_441f1e1.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_4452de4.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_4ae396d.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_58ee89d.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_6dd2225.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_765a297.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_7a4cc4a.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_8050261.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_83eaedf.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_925bc5f.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_95f3dd5.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_96d7a02.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_971f951.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_a446c32.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_a5ee367.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_b11c248.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_b13c52e.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_b5bd69b.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_b625d7d.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_bf63915.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_c52b25b.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_c9db556.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_d1537ee.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_d2e6412.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_d2f47a4.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_d361292.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_d727ec8.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_e11cd8f.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_e481f19.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_eee2805.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/fragment/en_f4de6c4.pf_fragment +0 -0
- package/lib/learn-host/public/_pagefind/index/en_1ecb9d5.pf_index +0 -0
- package/lib/learn-host/public/_pagefind/index/en_37e362b.pf_index +0 -0
- package/lib/learn-host/public/_pagefind/index/en_538eee7.pf_index +0 -0
- package/lib/learn-host/public/_pagefind/index/en_5751dc8.pf_index +0 -0
- package/lib/learn-host/public/_pagefind/index/en_67f794d.pf_index +0 -0
- package/lib/learn-host/public/_pagefind/index/en_7458f81.pf_index +0 -0
- package/lib/learn-host/public/_pagefind/index/en_e21f7e1.pf_index +0 -0
- package/lib/learn-host/public/_pagefind/pagefind-entry.json +1 -0
- package/lib/learn-host/public/_pagefind/pagefind-highlight.js +1064 -0
- package/lib/learn-host/public/_pagefind/pagefind-modular-ui.css +214 -0
- package/lib/learn-host/public/_pagefind/pagefind-modular-ui.js +8 -0
- package/lib/learn-host/public/_pagefind/pagefind-ui.css +1 -0
- package/lib/learn-host/public/_pagefind/pagefind-ui.js +2 -0
- package/lib/learn-host/public/_pagefind/pagefind.en_104569cceb.pf_meta +0 -0
- package/lib/learn-host/public/_pagefind/pagefind.en_1075df6f16.pf_meta +0 -0
- package/lib/learn-host/public/_pagefind/pagefind.en_139f35f6e5.pf_meta +0 -0
- package/lib/learn-host/public/_pagefind/pagefind.en_46bfc9f7e1.pf_meta +0 -0
- package/lib/learn-host/public/_pagefind/pagefind.en_76b8937bbc.pf_meta +0 -0
- package/lib/learn-host/public/_pagefind/pagefind.en_83cbfb0fd5.pf_meta +0 -0
- package/lib/learn-host/public/_pagefind/pagefind.en_b1d168d536.pf_meta +0 -0
- package/lib/learn-host/public/_pagefind/pagefind.js +6 -0
- package/lib/learn-host/public/_pagefind/wasm.en.pagefind +0 -0
- package/lib/learn-host/public/_pagefind/wasm.unknown.pagefind +0 -0
- package/lib/learn-host/public/logo.svg +1 -0
- package/lib/learn-host/src/app/[category]/[slug]/page.tsx +36 -32
- package/lib/learn-host/src/app/[category]/page.tsx +2 -3
- package/lib/learn-host/src/app/apple-icon.svg +1 -0
- package/lib/learn-host/src/app/globals.css +74 -14
- package/lib/learn-host/src/app/icon.svg +1 -0
- package/lib/learn-host/src/app/layout.tsx +29 -9
- package/lib/learn-host/src/app/page.tsx +9 -11
- package/lib/learn-host/src/components/Breadcrumbs.tsx +12 -4
- package/lib/learn-host/src/components/DocCard.tsx +28 -10
- package/lib/learn-host/src/components/MarkdownRenderer.tsx +6 -2
- package/lib/learn-host/src/components/MobileNav.tsx +43 -35
- package/lib/learn-host/src/components/PagefindSearch.tsx +177 -54
- package/lib/learn-host/src/components/Sidebar.tsx +27 -29
- package/lib/learn-host/src/components/TableOfContents.tsx +62 -0
- package/lib/learn-host/src/components/TagBadge.tsx +1 -1
- package/lib/learn-host/src/components/ThemeToggle.tsx +36 -9
- package/lib/learn-host/src/components/layout/Footer.tsx +41 -0
- package/lib/learn-host/src/components/layout/Header.tsx +117 -0
- package/lib/learn-host/src/lib/docs.ts +98 -8
- package/lib/learn-host/src/lib/types.ts +7 -1
- package/lib/learn-host/tsconfig.json +8 -2
- package/lib/learn-host/tsconfig.tsbuildinfo +1 -0
- package/lib/learn-mcp/CHANGELOG.md +7 -0
- package/lib/learn-mcp/package.json +1 -1
- package/package.json +13 -5
- package/dist/chunk-VHZQ6KEU.js +0 -73
- package/lib/learn-host/src/app/search/page.tsx +0 -19
- package/lib/learn-host/src/components/SearchBar.tsx +0 -36
|
@@ -1,55 +1,63 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
4
5
|
import { useState } from "react";
|
|
5
6
|
import type { CategoryInfo } from "@/lib/types";
|
|
6
|
-
import SearchBar from "./SearchBar";
|
|
7
|
-
import ThemeToggle from "./ThemeToggle";
|
|
8
7
|
|
|
9
|
-
export default function MobileNav({
|
|
8
|
+
export default function MobileNav({
|
|
9
|
+
categories,
|
|
10
|
+
}: {
|
|
11
|
+
categories: CategoryInfo[];
|
|
12
|
+
}) {
|
|
13
|
+
const pathname = usePathname();
|
|
10
14
|
const [open, setOpen] = useState(false);
|
|
11
15
|
|
|
12
16
|
return (
|
|
13
|
-
<div className="
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
17
|
+
<div className="dark:bg-navy-950 border-b border-slate-200 bg-slate-50 dark:border-[#a0a0a01c]">
|
|
18
|
+
<button
|
|
19
|
+
onClick={() => setOpen(!open)}
|
|
20
|
+
className="flex w-full cursor-pointer items-center justify-between px-4 py-3 text-sm font-medium text-slate-700 dark:text-slate-300"
|
|
21
|
+
>
|
|
22
|
+
<span>Navigation</span>
|
|
23
|
+
<svg
|
|
24
|
+
className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`}
|
|
25
|
+
fill="none"
|
|
26
|
+
viewBox="0 0 24 24"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
>
|
|
29
|
+
<path
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
strokeLinejoin="round"
|
|
32
|
+
strokeWidth={2}
|
|
33
|
+
d="M19 9l-7 7-7-7"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
</button>
|
|
35
37
|
|
|
36
38
|
{open && (
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
<nav className="max-h-[60vh] space-y-0.5 overflow-y-auto px-4 pb-4">
|
|
40
|
+
{categories.map((cat) => {
|
|
41
|
+
const isActive = pathname === `/${cat.name}/`;
|
|
42
|
+
return (
|
|
41
43
|
<Link
|
|
42
44
|
key={cat.name}
|
|
43
45
|
href={`/${cat.name}/`}
|
|
44
46
|
onClick={() => setOpen(false)}
|
|
45
|
-
className=
|
|
47
|
+
className={`flex items-center justify-between rounded-md px-3 py-1.5 text-sm capitalize ${
|
|
48
|
+
isActive
|
|
49
|
+
? "font-medium text-violet-600 dark:text-violet-400"
|
|
50
|
+
: "text-slate-600 dark:text-slate-400"
|
|
51
|
+
}`}
|
|
46
52
|
>
|
|
47
|
-
<span
|
|
48
|
-
<span className="text-xs text-
|
|
53
|
+
<span>{cat.name.replace(/[_-]/g, " ")}</span>
|
|
54
|
+
<span className="text-xs text-slate-400 dark:text-slate-500">
|
|
55
|
+
{cat.docCount}
|
|
56
|
+
</span>
|
|
49
57
|
</Link>
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
</
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</nav>
|
|
53
61
|
)}
|
|
54
62
|
</div>
|
|
55
63
|
);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import { Command } from "cmdk";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
|
7
|
+
import { DialogTitle, DialogDescription } from "@radix-ui/react-dialog";
|
|
4
8
|
|
|
5
9
|
interface PagefindResult {
|
|
6
10
|
id: string;
|
|
@@ -8,7 +12,6 @@ interface PagefindResult {
|
|
|
8
12
|
url: string;
|
|
9
13
|
meta: { title?: string };
|
|
10
14
|
excerpt: string;
|
|
11
|
-
sub_results?: { url: string; title: string; excerpt: string }[];
|
|
12
15
|
}>;
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -29,13 +32,32 @@ interface Pagefind {
|
|
|
29
32
|
) => Promise<{ results: PagefindResult[] } | null>;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
function normalizePagefindUrl(url: string): string {
|
|
36
|
+
return url.replace(/\.html$/, "/");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pagefind excerpts only contain <mark> tags for highlighting — safe to render
|
|
40
|
+
// This is a trusted source (local pagefind index), not user input
|
|
41
|
+
function ExcerptMarkup({ html }: { html: string }) {
|
|
42
|
+
return (
|
|
43
|
+
<p
|
|
44
|
+
className="mt-0.5 line-clamp-2 text-sm text-slate-500 dark:text-slate-400 [&_mark]:bg-transparent [&_mark]:!text-yellow-600 dark:[&_mark]:!text-yellow-200"
|
|
45
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function PagefindSearch() {
|
|
51
|
+
const [open, setOpen] = useState(false);
|
|
52
|
+
const [query, setQuery] = useState("");
|
|
34
53
|
const [results, setResults] = useState<SearchResult[]>([]);
|
|
35
54
|
const [loading, setLoading] = useState(false);
|
|
36
55
|
const [ready, setReady] = useState(false);
|
|
37
56
|
const pagefindRef = useRef<Pagefind | null>(null);
|
|
57
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
58
|
+
const router = useRouter();
|
|
38
59
|
|
|
60
|
+
// Load pagefind
|
|
39
61
|
useEffect(() => {
|
|
40
62
|
async function load() {
|
|
41
63
|
try {
|
|
@@ -47,26 +69,60 @@ export default function PagefindSearch({ initialQuery = "" }: { initialQuery?: s
|
|
|
47
69
|
pagefindRef.current = pf;
|
|
48
70
|
setReady(true);
|
|
49
71
|
} catch {
|
|
50
|
-
// Pagefind not available (dev mode
|
|
72
|
+
// Pagefind not available (dev mode)
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
load();
|
|
54
76
|
}, []);
|
|
55
77
|
|
|
78
|
+
// Keyboard shortcut: Cmd+K / Ctrl+K
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
81
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
setOpen((prev) => !prev);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
87
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
// Click outside to close
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!open) return;
|
|
93
|
+
function handlePointerDown(e: PointerEvent) {
|
|
94
|
+
if (dialogRef.current && !dialogRef.current.contains(e.target as Node)) {
|
|
95
|
+
setOpen(false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
99
|
+
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
100
|
+
}, [open]);
|
|
101
|
+
|
|
102
|
+
// Reset state when dialog closes
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!open) {
|
|
105
|
+
setQuery("");
|
|
106
|
+
setResults([]);
|
|
107
|
+
}
|
|
108
|
+
}, [open]);
|
|
109
|
+
|
|
56
110
|
const doSearch = useCallback(async (q: string) => {
|
|
57
111
|
const pf = pagefindRef.current;
|
|
58
112
|
if (!pf || !q.trim()) {
|
|
59
113
|
setResults([]);
|
|
114
|
+
setLoading(false);
|
|
60
115
|
return;
|
|
61
116
|
}
|
|
62
117
|
|
|
118
|
+
const response = await pf.debouncedSearch(q, {}, 200);
|
|
119
|
+
if (!response) return;
|
|
120
|
+
|
|
63
121
|
setLoading(true);
|
|
122
|
+
setResults([]);
|
|
64
123
|
try {
|
|
65
|
-
const response = await pf.debouncedSearch(q, {}, 200);
|
|
66
|
-
if (!response) return; // debounced away
|
|
67
|
-
|
|
68
124
|
const items: SearchResult[] = [];
|
|
69
|
-
for (const result of response.results.slice(0,
|
|
125
|
+
for (const result of response.results.slice(0, 10)) {
|
|
70
126
|
const data = await result.data();
|
|
71
127
|
items.push({
|
|
72
128
|
id: result.id,
|
|
@@ -82,53 +138,120 @@ export default function PagefindSearch({ initialQuery = "" }: { initialQuery?: s
|
|
|
82
138
|
}, []);
|
|
83
139
|
|
|
84
140
|
useEffect(() => {
|
|
85
|
-
if (ready) doSearch(query);
|
|
86
|
-
}, [query, ready, doSearch]);
|
|
141
|
+
if (ready && open) doSearch(query);
|
|
142
|
+
}, [query, ready, open, doSearch]);
|
|
87
143
|
|
|
88
144
|
return (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
<>
|
|
146
|
+
{/* Search trigger button */}
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => setOpen(true)}
|
|
149
|
+
className="flex cursor-pointer items-center gap-1.5 rounded-lg p-2 text-slate-500 transition-colors duration-200 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
150
|
+
aria-label="Search docs"
|
|
151
|
+
>
|
|
152
|
+
<svg
|
|
153
|
+
className="h-5 w-5"
|
|
154
|
+
fill="none"
|
|
155
|
+
viewBox="0 0 24 24"
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
>
|
|
158
|
+
<path
|
|
159
|
+
strokeLinecap="round"
|
|
160
|
+
strokeLinejoin="round"
|
|
161
|
+
strokeWidth={2}
|
|
162
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
163
|
+
/>
|
|
164
|
+
</svg>
|
|
165
|
+
<kbd className="dark:bg-navy-800/80 hidden items-center gap-0.5 rounded border border-slate-300 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 sm:inline-flex dark:border-[#a0a0a01c]">
|
|
166
|
+
<span className="text-xs">⌘</span>K
|
|
167
|
+
</kbd>
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
{/* cmdk dialog */}
|
|
171
|
+
<Command.Dialog
|
|
172
|
+
open={open}
|
|
173
|
+
onOpenChange={setOpen}
|
|
174
|
+
shouldFilter={false}
|
|
175
|
+
loop
|
|
176
|
+
label="Search documentation"
|
|
177
|
+
overlayClassName="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
178
|
+
contentClassName="fixed inset-0 z-50 flex items-start justify-center pt-[15vh] pointer-events-none"
|
|
179
|
+
ref={dialogRef}
|
|
180
|
+
className="dark:bg-navy-900/80 pointer-events-auto mx-4 w-full max-w-lg overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-[#a0a0a01c]"
|
|
181
|
+
>
|
|
182
|
+
<VisuallyHidden>
|
|
183
|
+
<DialogTitle>Search documentation</DialogTitle>
|
|
184
|
+
<DialogDescription>
|
|
185
|
+
Search through the documentation pages
|
|
186
|
+
</DialogDescription>
|
|
187
|
+
</VisuallyHidden>
|
|
188
|
+
{/* Search input */}
|
|
189
|
+
<div className="flex items-center gap-3 border-b border-slate-200 px-4 dark:border-[#a0a0a01c]">
|
|
190
|
+
<svg
|
|
191
|
+
className="h-6 w-6 shrink-0 text-slate-400 dark:text-slate-500"
|
|
192
|
+
fill="none"
|
|
193
|
+
viewBox="0 0 24 24"
|
|
194
|
+
stroke="currentColor"
|
|
195
|
+
>
|
|
196
|
+
<path
|
|
197
|
+
strokeLinecap="round"
|
|
198
|
+
strokeLinejoin="round"
|
|
199
|
+
strokeWidth={2}
|
|
200
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
201
|
+
/>
|
|
202
|
+
</svg>
|
|
203
|
+
<Command.Input
|
|
204
|
+
value={query}
|
|
205
|
+
onValueChange={setQuery}
|
|
206
|
+
placeholder="Search documentation..."
|
|
207
|
+
className="flex-1 bg-transparent py-3 text-sm text-slate-900 placeholder-slate-400 outline-none dark:text-white dark:placeholder-slate-500"
|
|
208
|
+
/>
|
|
209
|
+
<kbd className="rounded border border-slate-300 px-1.5 py-0.5 text-xs text-slate-400 dark:border-[#a0a0a01c] dark:text-slate-500">
|
|
210
|
+
ESC
|
|
211
|
+
</kbd>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Results */}
|
|
215
|
+
<Command.List className="max-h-[50vh] overflow-y-auto">
|
|
216
|
+
{loading && (
|
|
217
|
+
<Command.Loading className="px-4 py-6 text-center text-sm text-slate-400">
|
|
218
|
+
Searching...
|
|
219
|
+
</Command.Loading>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{!ready && query.trim() && (
|
|
223
|
+
<p className="px-4 py-6 text-center text-sm text-slate-400">
|
|
224
|
+
Search index not available. Run a production build first.
|
|
225
|
+
</p>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
<Command.Empty className="px-4 py-6 text-center text-sm text-slate-400">
|
|
229
|
+
{query.trim() ? (
|
|
230
|
+
<>No results found for “{query}”</>
|
|
231
|
+
) : (
|
|
232
|
+
"Start typing to search..."
|
|
233
|
+
)}
|
|
234
|
+
</Command.Empty>
|
|
235
|
+
|
|
236
|
+
{!loading &&
|
|
237
|
+
results.map((entry) => (
|
|
238
|
+
<Command.Item
|
|
239
|
+
key={entry.id}
|
|
240
|
+
value={entry.id}
|
|
241
|
+
onSelect={() => {
|
|
242
|
+
setOpen(false);
|
|
243
|
+
router.push(normalizePagefindUrl(entry.url));
|
|
244
|
+
}}
|
|
245
|
+
className="dark:data-[selected=true]:bg-navy-800 cursor-pointer px-4 py-3 transition-colors data-[selected=true]:bg-slate-100"
|
|
246
|
+
>
|
|
247
|
+
<div className="text-base font-medium text-slate-900 dark:text-white">
|
|
248
|
+
{entry.title}
|
|
249
|
+
</div>
|
|
250
|
+
<ExcerptMarkup html={entry.excerpt} />
|
|
251
|
+
</Command.Item>
|
|
252
|
+
))}
|
|
253
|
+
</Command.List>
|
|
254
|
+
</Command.Dialog>
|
|
255
|
+
</>
|
|
133
256
|
);
|
|
134
257
|
}
|
|
@@ -1,44 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
2
5
|
import type { CategoryInfo } from "@/lib/types";
|
|
3
|
-
import ThemeToggle from "./ThemeToggle";
|
|
4
|
-
import SearchBar from "./SearchBar";
|
|
5
6
|
|
|
6
|
-
export default function Sidebar({
|
|
7
|
-
|
|
7
|
+
export default function Sidebar({
|
|
8
|
+
categories,
|
|
9
|
+
}: {
|
|
10
|
+
categories: CategoryInfo[];
|
|
11
|
+
}) {
|
|
12
|
+
const pathname = usePathname();
|
|
8
13
|
|
|
9
14
|
return (
|
|
10
|
-
<aside className="
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</p>
|
|
19
|
-
</Link>
|
|
20
|
-
|
|
21
|
-
<SearchBar />
|
|
22
|
-
|
|
23
|
-
<nav className="mt-6 space-y-1">
|
|
24
|
-
{categories.map((cat) => (
|
|
15
|
+
<aside className="dark:bg-navy-950 fixed top-14 left-0 z-10 hidden h-[calc(100vh-3.5rem)] w-64 shrink-0 border-r border-slate-200 bg-slate-50 md:flex md:flex-col lg:w-[300px] dark:border-[#a0a0a01c]">
|
|
16
|
+
<nav
|
|
17
|
+
className="scrollbar-none flex-1 space-y-1 overflow-y-auto p-4"
|
|
18
|
+
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
|
19
|
+
>
|
|
20
|
+
{categories.map((cat) => {
|
|
21
|
+
const isActive = pathname === `/${cat.name}/`;
|
|
22
|
+
return (
|
|
25
23
|
<Link
|
|
26
24
|
key={cat.name}
|
|
27
25
|
href={`/${cat.name}/`}
|
|
28
|
-
className=
|
|
26
|
+
className={`flex items-center justify-between rounded-full px-3 py-2 text-sm capitalize transition-colors duration-200 ${
|
|
27
|
+
isActive
|
|
28
|
+
? "font-medium text-violet-600 dark:text-violet-400"
|
|
29
|
+
: "dark:hover:bg-navy-800/50 text-slate-600 hover:bg-slate-200/50 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
30
|
+
}`}
|
|
29
31
|
>
|
|
30
|
-
<span
|
|
31
|
-
<span className="
|
|
32
|
+
<span>{cat.name.replace(/[_-]/g, " ")}</span>
|
|
33
|
+
<span className="dark:bg-navy-800 rounded-full bg-slate-200 px-1.5 py-0.5 text-xs text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-[#a0a0a01c]">
|
|
32
34
|
{cat.docCount}
|
|
33
35
|
</span>
|
|
34
36
|
</Link>
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
</
|
|
38
|
-
|
|
39
|
-
<div className="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200 dark:border-gray-700">
|
|
40
|
-
<ThemeToggle />
|
|
41
|
-
</div>
|
|
37
|
+
);
|
|
38
|
+
})}
|
|
39
|
+
</nav>
|
|
42
40
|
</aside>
|
|
43
41
|
);
|
|
44
42
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import type { TocItem } from "@/lib/types";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
headings: TocItem[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function TableOfContents({ headings }: Props) {
|
|
11
|
+
const [activeId, setActiveId] = useState("");
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const observer = new IntersectionObserver(
|
|
15
|
+
(entries) => {
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.isIntersecting) {
|
|
18
|
+
setActiveId(entry.target.id);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{ rootMargin: "-80px 0px -60% 0px", threshold: 0.1 },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
for (const heading of headings) {
|
|
26
|
+
const el = document.getElementById(heading.id);
|
|
27
|
+
if (el) observer.observe(el);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, [headings]);
|
|
32
|
+
|
|
33
|
+
if (headings.length === 0) return null;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<aside className="sticky top-16 hidden h-[calc(100vh-4rem)] w-56 shrink-0 overflow-y-auto lg:block">
|
|
37
|
+
<div className="p-4">
|
|
38
|
+
<h4 className="mb-3 text-xs font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">
|
|
39
|
+
On this page
|
|
40
|
+
</h4>
|
|
41
|
+
<ul className="space-y-1.5">
|
|
42
|
+
{headings.map((h) => (
|
|
43
|
+
<li key={h.id}>
|
|
44
|
+
<a
|
|
45
|
+
href={`#${h.id}`}
|
|
46
|
+
className={`block text-xs transition-colors duration-200 ${
|
|
47
|
+
h.level === 3 ? "pl-3" : ""
|
|
48
|
+
} ${
|
|
49
|
+
activeId === h.id
|
|
50
|
+
? "font-medium text-violet-600 dark:text-violet-400"
|
|
51
|
+
: "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
52
|
+
}`}
|
|
53
|
+
>
|
|
54
|
+
{h.text}
|
|
55
|
+
</a>
|
|
56
|
+
</li>
|
|
57
|
+
))}
|
|
58
|
+
</ul>
|
|
59
|
+
</div>
|
|
60
|
+
</aside>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -4,7 +4,7 @@ export default function TagBadge({ tag }: { tag: string }) {
|
|
|
4
4
|
return (
|
|
5
5
|
<Link
|
|
6
6
|
href={`/search/?q=${encodeURIComponent(tag)}`}
|
|
7
|
-
className="inline-block px-2 py-0.5 text-xs
|
|
7
|
+
className="inline-block rounded-full border border-slate-300 px-2.5 py-0.5 text-xs text-violet-600 transition-colors hover:border-violet-400 hover:bg-violet-50 dark:border-slate-600 dark:text-violet-400 dark:hover:border-violet-500 dark:hover:bg-violet-900/20"
|
|
8
8
|
>
|
|
9
9
|
{tag}
|
|
10
10
|
</Link>
|
|
@@ -1,28 +1,55 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useTheme } from "next-themes";
|
|
4
|
-
import {
|
|
4
|
+
import { useSyncExternalStore } from "react";
|
|
5
|
+
|
|
6
|
+
const emptySubscribe = () => () => {};
|
|
7
|
+
const getSnapshot = () => true;
|
|
8
|
+
const getServerSnapshot = () => false;
|
|
5
9
|
|
|
6
10
|
export default function ThemeToggle() {
|
|
7
11
|
const { theme, setTheme } = useTheme();
|
|
8
|
-
const
|
|
12
|
+
const mounted = useSyncExternalStore(
|
|
13
|
+
emptySubscribe,
|
|
14
|
+
getSnapshot,
|
|
15
|
+
getServerSnapshot,
|
|
16
|
+
);
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
if (!mounted) return <div className="w-9 h-9" />;
|
|
18
|
+
if (!mounted) return <div className="h-9 w-9" />;
|
|
12
19
|
|
|
13
20
|
return (
|
|
14
21
|
<button
|
|
15
22
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
16
|
-
className="
|
|
23
|
+
className="dark:hover:bg-navy-800 cursor-pointer rounded-lg p-2 transition-colors hover:bg-slate-100"
|
|
17
24
|
aria-label="Toggle theme"
|
|
18
25
|
>
|
|
19
26
|
{theme === "dark" ? (
|
|
20
|
-
<svg
|
|
21
|
-
|
|
27
|
+
<svg
|
|
28
|
+
className="h-5 w-5"
|
|
29
|
+
fill="none"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
>
|
|
33
|
+
<path
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
strokeWidth={2}
|
|
37
|
+
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
|
38
|
+
/>
|
|
22
39
|
</svg>
|
|
23
40
|
) : (
|
|
24
|
-
<svg
|
|
25
|
-
|
|
41
|
+
<svg
|
|
42
|
+
className="h-5 w-5"
|
|
43
|
+
fill="none"
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
>
|
|
47
|
+
<path
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeLinejoin="round"
|
|
50
|
+
strokeWidth={2}
|
|
51
|
+
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
|
52
|
+
/>
|
|
26
53
|
</svg>
|
|
27
54
|
)}
|
|
28
55
|
</button>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
|
|
3
|
+
export default function Footer() {
|
|
4
|
+
return (
|
|
5
|
+
<footer className="dark:bg-navy-950 fixed right-0 bottom-0 left-0 z-40 border-t border-slate-200 bg-slate-50 md:left-64 lg:left-[300px] dark:border-[#a0a0a01c]">
|
|
6
|
+
<div className="flex flex-row flex-wrap items-center gap-1 px-6 py-3 text-center text-xs text-slate-500 dark:text-slate-500">
|
|
7
|
+
<div className="flex items-center gap-2">
|
|
8
|
+
<Image src="/logo.svg" alt="Coding Friend" width={20} height={20} />
|
|
9
|
+
<span>
|
|
10
|
+
Powered by{" "}
|
|
11
|
+
<a
|
|
12
|
+
href="https://github.com/dinhanhthi/coding-friend"
|
|
13
|
+
target="_blank"
|
|
14
|
+
rel="noopener noreferrer"
|
|
15
|
+
className="text-violet-600 hover:text-violet-500 dark:text-violet-400 dark:hover:text-violet-300"
|
|
16
|
+
>
|
|
17
|
+
Coding Friend
|
|
18
|
+
</a>
|
|
19
|
+
, developed by{" "}
|
|
20
|
+
<a
|
|
21
|
+
href="https://dinhanhthi.com"
|
|
22
|
+
target="_blank"
|
|
23
|
+
rel="noopener noreferrer"
|
|
24
|
+
className="text-violet-600 hover:text-violet-500 dark:text-violet-400 dark:hover:text-violet-300"
|
|
25
|
+
>
|
|
26
|
+
Anh-Thi Dinh
|
|
27
|
+
</a>
|
|
28
|
+
.
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
Learning notes hosted locally with{" "}
|
|
33
|
+
<code className="rounded border border-slate-300 px-1 py-0.5 text-xs dark:border-slate-600">
|
|
34
|
+
cf host
|
|
35
|
+
</code>
|
|
36
|
+
.
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</footer>
|
|
40
|
+
);
|
|
41
|
+
}
|