coding-friend-cli 1.3.0 → 1.5.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 (42) hide show
  1. package/README.md +4 -0
  2. package/dist/chunk-5OALHHKR.js +77 -0
  3. package/dist/{chunk-CSF4FAHL.js → chunk-DDISNOEK.js} +15 -1
  4. package/dist/{update-GGCBM7U4.js → chunk-GTTSBOHM.js} +6 -2
  5. package/dist/{chunk-HRVSKMNA.js → chunk-JWAJ4XPK.js} +11 -0
  6. package/dist/chunk-MRTR7TJ4.js +27 -0
  7. package/dist/{chunk-6OI37OZX.js → chunk-WHCJT7E2.js} +25 -1
  8. package/dist/{chunk-Q4DKU5IG.js → chunk-X645ACZJ.js} +1 -1
  9. package/dist/{dev-VMN2JHA6.js → dev-WINMWRZK.js} +5 -13
  10. package/dist/{host-2WINWEW7.js → host-VR5POAVU.js} +3 -3
  11. package/dist/index.js +18 -10
  12. package/dist/{init-CTCDQKIQ.js → init-K4EVPAHK.js} +30 -4
  13. package/dist/install-BEFMUMKE.js +93 -0
  14. package/dist/{mcp-43HCE2KD.js → mcp-JCQUGUPJ.js} +3 -3
  15. package/dist/postinstall.js +1 -1
  16. package/dist/statusline-3MQQDRCI.js +55 -0
  17. package/dist/uninstall-BHYS52L3.js +202 -0
  18. package/dist/update-TALQ7TAO.js +17 -0
  19. package/lib/learn-host/CHANGELOG.md +6 -0
  20. package/lib/learn-host/package.json +1 -1
  21. package/lib/learn-host/src/app/globals.css +34 -4
  22. package/lib/learn-host/src/app/layout.tsx +2 -13
  23. package/lib/learn-host/src/app/page.tsx +45 -15
  24. package/lib/learn-host/src/components/Breadcrumbs.tsx +2 -5
  25. package/lib/learn-host/src/components/CodeBlock.tsx +56 -0
  26. package/lib/learn-host/src/components/DocCard.tsx +4 -6
  27. package/lib/learn-host/src/components/LayoutShell.tsx +39 -0
  28. package/lib/learn-host/src/components/MarkdownRenderer.tsx +2 -0
  29. package/lib/learn-host/src/components/MobileNav.tsx +1 -1
  30. package/lib/learn-host/src/components/Sidebar.tsx +1 -1
  31. package/lib/learn-host/src/components/TableOfContents.tsx +2 -2
  32. package/lib/learn-host/src/components/TagBadge.tsx +1 -1
  33. package/lib/learn-host/src/components/layout/Footer.tsx +8 -4
  34. package/lib/learn-host/src/components/layout/Header.tsx +2 -2
  35. package/lib/learn-host/src/lib/docs.ts +11 -2
  36. package/lib/learn-mcp/CHANGELOG.md +4 -0
  37. package/lib/learn-mcp/README.md +18 -0
  38. package/lib/learn-mcp/package.json +1 -1
  39. package/lib/learn-mcp/src/index.ts +1 -1
  40. package/package.json +1 -1
  41. package/dist/statusline-ARI7I5YM.js +0 -64
  42. package/lib/learn-host/next-env.d.ts +0 -6
@@ -0,0 +1,202 @@
1
+ import {
2
+ isMarketplaceRegistered,
3
+ isPluginInstalled
4
+ } from "./chunk-MRTR7TJ4.js";
5
+ import {
6
+ hasShellCompletion,
7
+ removeShellCompletion
8
+ } from "./chunk-DDISNOEK.js";
9
+ import {
10
+ commandExists,
11
+ run
12
+ } from "./chunk-UFGNO6CW.js";
13
+ import {
14
+ claudeSettingsPath,
15
+ devStatePath,
16
+ globalConfigDir,
17
+ marketplaceCachePath,
18
+ marketplaceClonePath
19
+ } from "./chunk-WHCJT7E2.js";
20
+ import {
21
+ log
22
+ } from "./chunk-6DUFTBTO.js";
23
+ import {
24
+ readJson,
25
+ writeJson
26
+ } from "./chunk-IUTXHCP7.js";
27
+
28
+ // src/commands/uninstall.ts
29
+ import { existsSync, rmSync } from "fs";
30
+ import { confirm } from "@inquirer/prompts";
31
+ import chalk from "chalk";
32
+ var MARKETPLACE_NAME = "coding-friend-marketplace";
33
+ var PLUGIN_NAME = "coding-friend";
34
+ var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
35
+ function hasStatuslineReference() {
36
+ const settings = readJson(claudeSettingsPath());
37
+ if (!settings) return false;
38
+ const statusLine = settings.statusLine;
39
+ return !!statusLine?.command?.includes(PLUGIN_NAME);
40
+ }
41
+ function removeStatuslineReference() {
42
+ const settingsPath = claudeSettingsPath();
43
+ const settings = readJson(settingsPath);
44
+ if (!settings) return false;
45
+ const statusLine = settings.statusLine;
46
+ if (!statusLine?.command?.includes(PLUGIN_NAME)) return false;
47
+ delete settings.statusLine;
48
+ writeJson(settingsPath, settings);
49
+ return true;
50
+ }
51
+ function detect() {
52
+ return {
53
+ pluginInstalled: isPluginInstalled(),
54
+ marketplaceRegistered: isMarketplaceRegistered(),
55
+ cacheExists: existsSync(marketplaceCachePath()),
56
+ cloneExists: existsSync(marketplaceClonePath()),
57
+ statuslineConfigured: hasStatuslineReference(),
58
+ shellCompletionExists: hasShellCompletion(),
59
+ globalConfigExists: existsSync(globalConfigDir()),
60
+ devModeActive: existsSync(devStatePath())
61
+ };
62
+ }
63
+ function displayDetection(d) {
64
+ const check = (v) => v ? chalk.green("\u2714") : chalk.dim("\u2013");
65
+ console.log();
66
+ console.log(" Detected components:");
67
+ console.log(` ${check(d.pluginInstalled)} Plugin registration`);
68
+ console.log(` ${check(d.marketplaceRegistered)} Marketplace registration`);
69
+ console.log(` ${check(d.cacheExists)} Plugin cache`);
70
+ console.log(` ${check(d.cloneExists)} Marketplace clone`);
71
+ console.log(` ${check(d.statuslineConfigured)} Statusline reference`);
72
+ console.log(` ${check(d.shellCompletionExists)} Shell tab completion`);
73
+ console.log(
74
+ ` ${check(d.globalConfigExists)} Global config (~/.coding-friend/)`
75
+ );
76
+ console.log();
77
+ }
78
+ function nothingToRemove(d) {
79
+ return !d.pluginInstalled && !d.marketplaceRegistered && !d.cacheExists && !d.cloneExists && !d.statuslineConfigured && !d.shellCompletionExists && !d.globalConfigExists;
80
+ }
81
+ async function uninstallCommand() {
82
+ console.log(`
83
+ === ${chalk.red("Coding Friend Uninstall")} ===`);
84
+ if (!commandExists("claude")) {
85
+ log.error("Claude CLI not found. Cannot uninstall plugin without it.");
86
+ log.dim(
87
+ "Install Claude CLI first: https://docs.anthropic.com/en/docs/claude-code"
88
+ );
89
+ return;
90
+ }
91
+ const detection = detect();
92
+ if (detection.devModeActive) {
93
+ log.warn("Dev mode is currently active.");
94
+ log.dim(`Run ${chalk.bold("cf dev off")} first, then try again.`);
95
+ return;
96
+ }
97
+ if (nothingToRemove(detection)) {
98
+ log.info("Nothing to uninstall \u2014 Coding Friend is not installed.");
99
+ return;
100
+ }
101
+ displayDetection(detection);
102
+ const proceed = await confirm({
103
+ message: "This will remove Coding Friend from Claude Code. Continue?",
104
+ default: false
105
+ });
106
+ if (!proceed) {
107
+ log.info("Uninstall cancelled.");
108
+ return;
109
+ }
110
+ let removeConfig = false;
111
+ if (detection.globalConfigExists) {
112
+ removeConfig = await confirm({
113
+ message: "Also remove ~/.coding-friend/ config directory? (global config, custom skills)",
114
+ default: false
115
+ });
116
+ }
117
+ console.log();
118
+ let errors = 0;
119
+ if (detection.pluginInstalled) {
120
+ log.step("Uninstalling plugin...");
121
+ const result = run("claude", ["plugin", "uninstall", PLUGIN_ID]);
122
+ if (result === null) {
123
+ const fallback = run("claude", ["plugin", "uninstall", PLUGIN_NAME]);
124
+ if (fallback === null) {
125
+ log.warn(
126
+ "Could not uninstall plugin via CLI (may already be removed)."
127
+ );
128
+ errors++;
129
+ }
130
+ }
131
+ }
132
+ if (detection.marketplaceRegistered) {
133
+ log.step("Removing marketplace...");
134
+ const result = run("claude", [
135
+ "plugin",
136
+ "marketplace",
137
+ "remove",
138
+ MARKETPLACE_NAME
139
+ ]);
140
+ if (result === null) {
141
+ log.warn(
142
+ "Could not remove marketplace via CLI (may already be removed)."
143
+ );
144
+ errors++;
145
+ }
146
+ }
147
+ if (detection.cacheExists) {
148
+ log.step("Removing plugin cache...");
149
+ try {
150
+ rmSync(marketplaceCachePath(), { recursive: true, force: true });
151
+ } catch {
152
+ log.warn("Could not remove plugin cache directory.");
153
+ errors++;
154
+ }
155
+ }
156
+ if (detection.cloneExists) {
157
+ log.step("Removing marketplace clone...");
158
+ try {
159
+ rmSync(marketplaceClonePath(), { recursive: true, force: true });
160
+ } catch {
161
+ log.warn("Could not remove marketplace clone directory.");
162
+ errors++;
163
+ }
164
+ }
165
+ if (detection.statuslineConfigured) {
166
+ log.step("Cleaning statusline...");
167
+ if (!removeStatuslineReference()) {
168
+ log.warn("Could not clean statusline from settings.");
169
+ errors++;
170
+ }
171
+ }
172
+ if (detection.shellCompletionExists) {
173
+ log.step("Removing shell completion...");
174
+ if (!removeShellCompletion()) {
175
+ log.warn("Could not remove shell completion.");
176
+ errors++;
177
+ }
178
+ }
179
+ if (removeConfig) {
180
+ log.step("Removing global config...");
181
+ try {
182
+ rmSync(globalConfigDir(), { recursive: true, force: true });
183
+ } catch {
184
+ log.warn("Could not remove ~/.coding-friend/ directory.");
185
+ errors++;
186
+ }
187
+ }
188
+ console.log();
189
+ if (errors === 0) {
190
+ log.success("Coding Friend has been completely uninstalled.");
191
+ } else {
192
+ log.warn(`Uninstalled with ${errors} warning(s). Check messages above.`);
193
+ }
194
+ if (!removeConfig && detection.globalConfigExists) {
195
+ log.dim("Global config kept at ~/.coding-friend/");
196
+ }
197
+ console.log();
198
+ log.dim("Restart Claude Code to complete the uninstall.");
199
+ }
200
+ export {
201
+ uninstallCommand
202
+ };
@@ -0,0 +1,17 @@
1
+ import {
2
+ getInstalledVersion,
3
+ getLatestVersion,
4
+ semverCompare,
5
+ updateCommand
6
+ } from "./chunk-GTTSBOHM.js";
7
+ import "./chunk-DDISNOEK.js";
8
+ import "./chunk-UFGNO6CW.js";
9
+ import "./chunk-WHCJT7E2.js";
10
+ import "./chunk-6DUFTBTO.js";
11
+ import "./chunk-IUTXHCP7.js";
12
+ export {
13
+ getInstalledVersion,
14
+ getLatestVersion,
15
+ semverCompare,
16
+ updateCommand
17
+ };
@@ -1,5 +1,11 @@
1
1
  # Changelog (Learn Host)
2
2
 
3
+ ## v0.1.0 (2026-03-03)
4
+
5
+ - Add copy button to code blocks ([#ac47c74](https://github.com/dinhanhthi/coding-friend/commit/ac47c74))
6
+ - Redesign home page with elegant layout and updated color scheme ([#2d53836](https://github.com/dinhanhthi/coding-friend/commit/2d53836))
7
+ - Improve TableOfContents heading styling with border ([#2267508](https://github.com/dinhanhthi/coding-friend/commit/2267508))
8
+
3
9
  ## v0.0.2 (2026-03-01)
4
10
 
5
11
  - Update header styling for consistency with ecosystem redesign
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-learn-host",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p 3333",
@@ -11,10 +11,10 @@
11
11
  --color-navy-900: #2a2d38;
12
12
  --color-navy-950: #23262e;
13
13
 
14
- /* Violet accent (from website) */
15
- --color-accent: #7c3aed;
16
- --color-accent-light: #a78bfa;
17
- --color-accent-dark: #6d28d9;
14
+ /* Amber accent (matching logo gradient) */
15
+ --color-accent: #f59e0b;
16
+ --color-accent-light: #fbbf24;
17
+ --color-accent-dark: #d97706;
18
18
 
19
19
  --color-surface: #2a2d38;
20
20
  --color-surface-secondary: #31353f;
@@ -78,6 +78,36 @@ pre {
78
78
  background: #0d1117;
79
79
  }
80
80
 
81
+ /* Code block copy button */
82
+ .code-block-wrapper {
83
+ position: relative;
84
+ }
85
+
86
+ .code-copy-btn {
87
+ position: absolute;
88
+ top: 0.5rem;
89
+ right: 0.5rem;
90
+ z-index: 10;
91
+ cursor: pointer;
92
+ border-radius: 0.375rem;
93
+ padding: 0.375rem;
94
+ color: #94a3b8;
95
+ opacity: 0;
96
+ transition:
97
+ opacity 0.2s,
98
+ color 0.2s,
99
+ background-color 0.2s;
100
+ }
101
+
102
+ .code-block-wrapper:hover .code-copy-btn {
103
+ opacity: 1;
104
+ }
105
+
106
+ .code-copy-btn:hover {
107
+ background: rgba(255, 255, 255, 0.1);
108
+ color: #e2e8f0;
109
+ }
110
+
81
111
  :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
82
112
  font-weight: 500;
83
113
  }
@@ -2,8 +2,7 @@ import type { Metadata } from "next";
2
2
  import { ThemeProvider } from "next-themes";
3
3
  import { getAllCategories } from "@/lib/docs";
4
4
  import Header from "@/components/layout/Header";
5
- import Footer from "@/components/layout/Footer";
6
- import Sidebar from "@/components/Sidebar";
5
+ import LayoutShell from "@/components/LayoutShell";
7
6
  import MobileNav from "@/components/MobileNav";
8
7
  import "./globals.css";
9
8
  import "highlight.js/styles/github-dark.css";
@@ -40,17 +39,7 @@ export default function RootLayout({
40
39
  <div data-pagefind-ignore className="md:hidden">
41
40
  <MobileNav categories={categories} />
42
41
  </div>
43
- <div className="flex">
44
- <div data-pagefind-ignore>
45
- <Sidebar categories={categories} />
46
- </div>
47
- <div className="flex min-w-0 flex-1 justify-center md:pl-64 lg:pl-[300px]">
48
- <main className="min-h-screen w-full max-w-5xl p-6 pb-24 md:p-8 md:pb-24">
49
- {children}
50
- </main>
51
- </div>
52
- </div>
53
- <Footer />
42
+ <LayoutShell categories={categories}>{children}</LayoutShell>
54
43
  </ThemeProvider>
55
44
  </body>
56
45
  </html>
@@ -1,6 +1,7 @@
1
1
  import { getAllCategories, getAllDocs, getAllTags } from "@/lib/docs";
2
2
  import DocCard from "@/components/DocCard";
3
3
  import TagBadge from "@/components/TagBadge";
4
+ import Image from "next/image";
4
5
  import Link from "next/link";
5
6
 
6
7
  export default function HomePage() {
@@ -10,28 +11,53 @@ export default function HomePage() {
10
11
  const recentDocs = docs.slice(0, 10);
11
12
 
12
13
  return (
13
- <div>
14
- <h1 className="mb-2 text-3xl font-bold">Learning Notes</h1>
15
- <p className="mb-8 text-slate-500 dark:text-slate-400">
16
- {docs.length} docs across {categories.length} categories
17
- </p>
14
+ <div className="mx-auto">
15
+ {/* Hero */}
16
+ <section className="mb-12 flex flex-col items-center pt-8 text-center md:pt-14">
17
+ <Image
18
+ src="/logo.svg"
19
+ alt="Learning Notes"
20
+ width={64}
21
+ height={64}
22
+ className="mb-5"
23
+ />
24
+ <h1 className="mb-3 text-4xl font-bold tracking-tight text-slate-900 md:text-5xl dark:text-white">
25
+ Learning Notes
26
+ </h1>
27
+ <p className="max-w-lg text-lg text-slate-500 dark:text-slate-400">
28
+ A personal knowledge base with{" "}
29
+ <span className="font-semibold text-amber-600 dark:text-amber-400">
30
+ {docs.length}
31
+ </span>{" "}
32
+ docs across{" "}
33
+ <span className="font-semibold text-amber-600 dark:text-amber-400">
34
+ {categories.length}
35
+ </span>{" "}
36
+ categories
37
+ </p>
38
+ </section>
18
39
 
19
40
  {/* Categories */}
20
- <section className="mb-8">
21
- <h2 className="mb-3 text-lg font-semibold">Categories</h2>
22
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
41
+ <section className="mb-12">
42
+ <h2 className="mb-5 text-xl font-semibold text-slate-900 dark:text-white">
43
+ Categories
44
+ </h2>
45
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
23
46
  {categories.map((cat) => (
24
47
  <Link
25
48
  key={cat.name}
26
49
  href={`/${cat.name}/`}
27
- className="rounded-lg border border-slate-200 p-3 transition-colors hover:border-violet-400/50 dark:border-[#a0a0a01c] dark:hover:border-violet-400/50"
50
+ className="group dark:bg-navy-800/50 relative cursor-pointer overflow-hidden rounded-xl border border-slate-200 bg-white p-4 transition-all duration-200 hover:-translate-y-0.5 hover:border-amber-300 hover:shadow-md hover:shadow-amber-100/50 dark:border-[#a0a0a01c] dark:hover:border-amber-500/40 dark:hover:shadow-amber-900/20"
28
51
  >
29
- <div className="font-medium text-slate-900 capitalize dark:text-slate-100">
52
+ <div className="mb-1 font-medium text-slate-900 capitalize dark:text-slate-100">
30
53
  {cat.name.replace(/[_-]/g, " ")}
31
54
  </div>
32
55
  <div className="text-sm text-slate-500 dark:text-slate-400">
33
56
  {cat.docCount} {cat.docCount === 1 ? "doc" : "docs"}
34
57
  </div>
58
+ <div className="dark:bg-navy-950 absolute right-3 bottom-3 flex h-7 w-7 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-500 dark:text-slate-400">
59
+ {cat.docCount}
60
+ </div>
35
61
  </Link>
36
62
  ))}
37
63
  </div>
@@ -39,10 +65,12 @@ export default function HomePage() {
39
65
 
40
66
  {/* Tags */}
41
67
  {tags.length > 0 && (
42
- <section className="mb-8">
43
- <h2 className="mb-3 text-lg font-semibold">Tags</h2>
68
+ <section className="mb-12">
69
+ <h2 className="mb-5 text-xl font-semibold text-slate-900 dark:text-white">
70
+ Tags
71
+ </h2>
44
72
  <div className="flex flex-wrap gap-2">
45
- {tags.slice(0, 20).map(({ tag }) => (
73
+ {tags.slice(0, 24).map(({ tag }) => (
46
74
  <TagBadge key={tag} tag={tag} />
47
75
  ))}
48
76
  </div>
@@ -51,8 +79,10 @@ export default function HomePage() {
51
79
 
52
80
  {/* Recent Docs */}
53
81
  <section>
54
- <h2 className="mb-3 text-lg font-semibold">Recently Updated</h2>
55
- <div className="grid gap-3">
82
+ <h2 className="mb-5 text-xl font-semibold text-slate-900 dark:text-white">
83
+ Recently Updated
84
+ </h2>
85
+ <div className="grid gap-3 sm:grid-cols-2">
56
86
  {recentDocs.map((doc) => (
57
87
  <DocCard key={`${doc.category}/${doc.slug}`} doc={doc} />
58
88
  ))}
@@ -8,10 +8,7 @@ interface Crumb {
8
8
  export default function Breadcrumbs({ crumbs }: { crumbs: Crumb[] }) {
9
9
  return (
10
10
  <nav className="mb-4 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
11
- <Link
12
- href="/"
13
- className="hover:text-violet-600 dark:hover:text-violet-400"
14
- >
11
+ <Link href="/" className="hover:text-amber-600 dark:hover:text-amber-400">
15
12
  Home
16
13
  </Link>
17
14
  {crumbs.map((crumb, i) => (
@@ -20,7 +17,7 @@ export default function Breadcrumbs({ crumbs }: { crumbs: Crumb[] }) {
20
17
  {crumb.href ? (
21
18
  <Link
22
19
  href={crumb.href}
23
- className="capitalize hover:text-violet-600 dark:hover:text-violet-400"
20
+ className="capitalize hover:text-amber-600 dark:hover:text-amber-400"
24
21
  >
25
22
  {crumb.label}
26
23
  </Link>
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useRef, useState } from "react";
4
+
5
+ export default function CodeBlock(props: React.ComponentProps<"pre">) {
6
+ const preRef = useRef<HTMLPreElement>(null);
7
+ const [copied, setCopied] = useState(false);
8
+
9
+ const handleCopy = async () => {
10
+ const text = preRef.current?.querySelector("code")?.textContent ?? "";
11
+ await navigator.clipboard.writeText(text);
12
+ setCopied(true);
13
+ setTimeout(() => setCopied(false), 2000);
14
+ };
15
+
16
+ return (
17
+ <div className="code-block-wrapper">
18
+ <button
19
+ onClick={handleCopy}
20
+ className="code-copy-btn"
21
+ aria-label="Copy to clipboard"
22
+ >
23
+ {copied ? (
24
+ <svg
25
+ className="h-4 w-4 text-emerald-400"
26
+ fill="none"
27
+ viewBox="0 0 24 24"
28
+ stroke="currentColor"
29
+ >
30
+ <path
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round"
33
+ strokeWidth={2}
34
+ d="M5 13l4 4L19 7"
35
+ />
36
+ </svg>
37
+ ) : (
38
+ <svg
39
+ className="h-4 w-4"
40
+ fill="none"
41
+ viewBox="0 0 24 24"
42
+ stroke="currentColor"
43
+ >
44
+ <path
45
+ strokeLinecap="round"
46
+ strokeLinejoin="round"
47
+ strokeWidth={2}
48
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
49
+ />
50
+ </svg>
51
+ )}
52
+ </button>
53
+ <pre ref={preRef} {...props} />
54
+ </div>
55
+ );
56
+ }
@@ -17,14 +17,12 @@ export default function DocCard({ doc }: { doc: DocMeta }) {
17
17
  onKeyDown={(e) => {
18
18
  if (e.key === "Enter") router.push(href);
19
19
  }}
20
- className="dark:bg-navy-800/50 block cursor-pointer rounded-lg border border-slate-200 bg-white p-4 transition-all hover:border-violet-400/50 dark:border-[#a0a0a01c] dark:hover:border-violet-400/50"
20
+ className="dark:bg-navy-800/50 block cursor-pointer rounded-lg border border-slate-200 bg-white p-4 transition-all duration-200 hover:border-amber-300 hover:shadow-sm dark:border-[#a0a0a01c] dark:hover:border-amber-500/40"
21
21
  >
22
- <h3 className="mb-1 font-semibold text-slate-900 dark:text-slate-100">
23
- <Link href={href} className="hover:underline">
24
- {doc.frontmatter.title}
25
- </Link>
22
+ <h3 className="mb-2 font-semibold text-slate-900 dark:text-slate-100">
23
+ <Link href={href}>{doc.frontmatter.title}</Link>
26
24
  </h3>
27
- <p className="mb-2 line-clamp-2 text-sm text-slate-500 dark:text-slate-400">
25
+ <p className="mb-3 line-clamp-2 text-sm text-slate-500 dark:text-slate-400">
28
26
  {doc.excerpt}
29
27
  </p>
30
28
  <div className="flex items-center justify-between">
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import type { CategoryInfo } from "@/lib/types";
5
+ import Sidebar from "@/components/Sidebar";
6
+ import Footer from "@/components/layout/Footer";
7
+
8
+ export default function LayoutShell({
9
+ categories,
10
+ children,
11
+ }: {
12
+ categories: CategoryInfo[];
13
+ children: React.ReactNode;
14
+ }) {
15
+ const pathname = usePathname();
16
+ const isHome = pathname === "/";
17
+
18
+ return (
19
+ <>
20
+ <div className="flex">
21
+ {!isHome && (
22
+ <div data-pagefind-ignore>
23
+ <Sidebar categories={categories} />
24
+ </div>
25
+ )}
26
+ <div
27
+ className={`flex min-w-0 flex-1 justify-center ${!isHome ? "md:pl-64 lg:pl-[300px]" : ""}`}
28
+ >
29
+ <main
30
+ className={`min-h-screen w-full p-6 pb-24 md:p-8 md:pb-24 ${isHome ? "max-w-6xl" : "max-w-5xl"}`}
31
+ >
32
+ {children}
33
+ </main>
34
+ </div>
35
+ </div>
36
+ <Footer isHome={isHome} />
37
+ </>
38
+ );
39
+ }
@@ -2,6 +2,7 @@ import ReactMarkdown from "react-markdown";
2
2
  import remarkGfm from "remark-gfm";
3
3
  import rehypeHighlight from "rehype-highlight";
4
4
  import rehypeSlug from "rehype-slug";
5
+ import CodeBlock from "./CodeBlock";
5
6
 
6
7
  export default function MarkdownRenderer({ content }: { content: string }) {
7
8
  return (
@@ -9,6 +10,7 @@ export default function MarkdownRenderer({ content }: { content: string }) {
9
10
  <ReactMarkdown
10
11
  remarkPlugins={[remarkGfm]}
11
12
  rehypePlugins={[rehypeHighlight, rehypeSlug]}
13
+ components={{ pre: CodeBlock }}
12
14
  >
13
15
  {content}
14
16
  </ReactMarkdown>
@@ -46,7 +46,7 @@ export default function MobileNav({
46
46
  onClick={() => setOpen(false)}
47
47
  className={`flex items-center justify-between rounded-md px-3 py-1.5 text-sm capitalize ${
48
48
  isActive
49
- ? "font-medium text-violet-600 dark:text-violet-400"
49
+ ? "font-medium text-amber-700 dark:text-amber-400"
50
50
  : "text-slate-600 dark:text-slate-400"
51
51
  }`}
52
52
  >
@@ -25,7 +25,7 @@ export default function Sidebar({
25
25
  href={`/${cat.name}/`}
26
26
  className={`flex items-center justify-between rounded-full px-3 py-2 text-sm capitalize transition-colors duration-200 ${
27
27
  isActive
28
- ? "font-medium text-violet-600 dark:text-violet-400"
28
+ ? "font-medium text-amber-700 dark:text-amber-400"
29
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
30
  }`}
31
31
  >
@@ -35,7 +35,7 @@ export default function TableOfContents({ headings }: Props) {
35
35
  return (
36
36
  <aside className="sticky top-16 hidden h-[calc(100vh-4rem)] w-56 shrink-0 overflow-y-auto lg:block">
37
37
  <div className="p-4">
38
- <h4 className="mb-3 text-xs font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">
38
+ <h4 className="mb-3 border-b border-slate-600 pb-2 text-xs font-semibold tracking-wider text-slate-400 uppercase">
39
39
  On this page
40
40
  </h4>
41
41
  <ul className="space-y-1.5">
@@ -47,7 +47,7 @@ export default function TableOfContents({ headings }: Props) {
47
47
  h.level === 3 ? "pl-3" : ""
48
48
  } ${
49
49
  activeId === h.id
50
- ? "font-medium text-violet-600 dark:text-violet-400"
50
+ ? "font-medium text-amber-700 dark:text-amber-400"
51
51
  : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
52
52
  }`}
53
53
  >
@@ -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 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"
7
+ className="dark:bg-navy-950 inline-block cursor-pointer rounded-full bg-slate-200 px-2.5 py-0.5 text-xs text-slate-600 transition-colors duration-200 hover:bg-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200"
8
8
  >
9
9
  {tag}
10
10
  </Link>
@@ -1,8 +1,12 @@
1
1
  import Image from "next/image";
2
2
 
3
- export default function Footer() {
3
+ export default function Footer({ isHome = false }: { isHome?: boolean }) {
4
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]">
5
+ <footer
6
+ className={`dark:bg-navy-950 fixed right-0 bottom-0 z-40 border-t border-slate-200 bg-slate-50 dark:border-[#a0a0a01c] ${
7
+ isHome ? "left-0" : "left-0 md:left-64 lg:left-[300px]"
8
+ }`}
9
+ >
6
10
  <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
11
  <div className="flex items-center gap-2">
8
12
  <Image src="/logo.svg" alt="Coding Friend" width={20} height={20} />
@@ -12,7 +16,7 @@ export default function Footer() {
12
16
  href="https://github.com/dinhanhthi/coding-friend"
13
17
  target="_blank"
14
18
  rel="noopener noreferrer"
15
- className="text-violet-600 hover:text-violet-500 dark:text-violet-400 dark:hover:text-violet-300"
19
+ className="text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300"
16
20
  >
17
21
  Coding Friend
18
22
  </a>
@@ -21,7 +25,7 @@ export default function Footer() {
21
25
  href="https://dinhanhthi.com"
22
26
  target="_blank"
23
27
  rel="noopener noreferrer"
24
- className="text-violet-600 hover:text-violet-500 dark:text-violet-400 dark:hover:text-violet-300"
28
+ className="text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300"
25
29
  >
26
30
  Anh-Thi Dinh
27
31
  </a>