coding-friend-cli 1.5.0 → 1.5.2

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.
@@ -29,8 +29,10 @@ import { dirname, join } from "path";
29
29
  import { fileURLToPath } from "url";
30
30
  var __dirname = dirname(fileURLToPath(import.meta.url));
31
31
  function getLibPath(name) {
32
- const libDir = join(__dirname, "..", "..", "lib", name);
33
- if (existsSync(libDir)) return libDir;
32
+ const bundled = join(__dirname, "..", "lib", name);
33
+ if (existsSync(bundled)) return bundled;
34
+ const dev = join(__dirname, "..", "..", "lib", name);
35
+ if (existsSync(dev)) return dev;
34
36
  throw new Error(
35
37
  `Could not find lib/${name}. Ensure it exists in the CLI package.`
36
38
  );
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLibPath,
3
3
  resolveDocsDir
4
- } from "./chunk-X645ACZJ.js";
4
+ } from "./chunk-WK5YYHXM.js";
5
5
  import "./chunk-JWAJ4XPK.js";
6
6
  import {
7
7
  run,
package/dist/index.js CHANGED
@@ -26,11 +26,11 @@ program.command("init").description("Initialize coding-friend in current project
26
26
  await initCommand();
27
27
  });
28
28
  program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
29
- const { hostCommand } = await import("./host-VR5POAVU.js");
29
+ const { hostCommand } = await import("./host-SQEDE3NN.js");
30
30
  await hostCommand(path, opts);
31
31
  });
32
32
  program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
33
- const { mcpCommand } = await import("./mcp-JCQUGUPJ.js");
33
+ const { mcpCommand } = await import("./mcp-QRPBL4ML.js");
34
34
  await mcpCommand(path);
35
35
  });
36
36
  program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLibPath,
3
3
  resolveDocsDir
4
- } from "./chunk-X645ACZJ.js";
4
+ } from "./chunk-WK5YYHXM.js";
5
5
  import "./chunk-JWAJ4XPK.js";
6
6
  import {
7
7
  run
@@ -1,5 +1,10 @@
1
1
  # Changelog (Learn Host)
2
2
 
3
+ ## v0.2.0 (2026-03-03)
4
+
5
+ - Add dedicated tag pages for filtering docs by tag ([#06f5847](https://github.com/dinhanhthi/coding-friend/commit/06f5847))
6
+ - Style improvements across website and learn-host ([#0029522](https://github.com/dinhanhthi/coding-friend/commit/0029522))
7
+
3
8
  ## v0.1.0 (2026-03-03)
4
9
 
5
10
  - Add copy button to code blocks ([#ac47c74](https://github.com/dinhanhthi/coding-friend/commit/ac47c74))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-learn-host",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p 3333",
@@ -51,7 +51,7 @@ export default async function DocPage({
51
51
  {doc.frontmatter.tags.length > 0 && (
52
52
  <div className="mt-3 flex flex-wrap gap-1.5">
53
53
  {doc.frontmatter.tags.map((tag) => (
54
- <TagBadge key={tag} tag={tag} />
54
+ <TagBadge key={tag} tag={tag} size="sm" />
55
55
  ))}
56
56
  </div>
57
57
  )}
@@ -21,7 +21,7 @@ export default async function CategoryPage({
21
21
  <div>
22
22
  <Breadcrumbs crumbs={[{ label: displayName }]} />
23
23
  <h1 className="mb-1 text-2xl font-bold capitalize">{displayName}</h1>
24
- <p className="mb-6 text-slate-500 dark:text-slate-400">
24
+ <p className="mb-6 pl-0.5 text-slate-500 dark:text-slate-400">
25
25
  {docs.length} {docs.length === 1 ? "doc" : "docs"}
26
26
  </p>
27
27
 
@@ -33,7 +33,7 @@ export default function RootLayout({
33
33
  rel="stylesheet"
34
34
  />
35
35
  </head>
36
- <body className="dark:bg-navy-900 bg-white text-slate-900 antialiased dark:text-slate-50">
36
+ <body className="dark:bg-navy-950 bg-white text-slate-900 antialiased dark:text-slate-50">
37
37
  <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
38
38
  <Header categories={categories} />
39
39
  <div data-pagefind-ignore className="md:hidden">
@@ -47,7 +47,7 @@ export default function HomePage() {
47
47
  <Link
48
48
  key={cat.name}
49
49
  href={`/${cat.name}/`}
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"
50
+ className="group dark:bg-navy-900/80 dark:hover:bg-navy-800/60 hover:border-navy-400 relative cursor-pointer overflow-hidden rounded-xl border border-slate-200 bg-slate-50 p-4 transition-all duration-200 hover:-translate-y-0.5 dark:border-[#a0a0a01c]"
51
51
  >
52
52
  <div className="mb-1 font-medium text-slate-900 capitalize dark:text-slate-100">
53
53
  {cat.name.replace(/[_-]/g, " ")}
@@ -55,7 +55,7 @@ export default function HomePage() {
55
55
  <div className="text-sm text-slate-500 dark:text-slate-400">
56
56
  {cat.docCount} {cat.docCount === 1 ? "doc" : "docs"}
57
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">
58
+ <div className="absolute right-3 bottom-3 flex h-7 w-7 items-center justify-center rounded-full bg-slate-200 text-xs font-semibold text-slate-500 dark:bg-slate-600/50 dark:text-slate-400">
59
59
  {cat.docCount}
60
60
  </div>
61
61
  </Link>
@@ -0,0 +1,39 @@
1
+ import { getAllTags, getDocsByTag } from "@/lib/docs";
2
+ import DocCard from "@/components/DocCard";
3
+ import Breadcrumbs from "@/components/Breadcrumbs";
4
+ import { notFound } from "next/navigation";
5
+
6
+ export async function generateStaticParams() {
7
+ return getAllTags().map((t) => ({ tag: t.tag }));
8
+ }
9
+
10
+ export const dynamicParams = true;
11
+
12
+ export default async function TagPage({
13
+ params,
14
+ }: {
15
+ params: Promise<{ tag: string }>;
16
+ }) {
17
+ const { tag } = await params;
18
+ const docs = getDocsByTag(tag);
19
+ if (docs.length === 0) notFound();
20
+
21
+ return (
22
+ <div>
23
+ <Breadcrumbs crumbs={[{ label: `# ${tag}` }]} />
24
+ <h1 className="mb-1 flex items-center gap-2 text-2xl font-bold">
25
+ <span className="text-slate-400 dark:text-slate-500">#</span>
26
+ {tag}
27
+ </h1>
28
+ <p className="mb-6 pl-0.5 text-slate-500 dark:text-slate-400">
29
+ {docs.length} {docs.length === 1 ? "doc" : "docs"}
30
+ </p>
31
+
32
+ <div className="grid gap-3">
33
+ {docs.map((doc) => (
34
+ <DocCard key={`${doc.category}/${doc.slug}`} doc={doc} />
35
+ ))}
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -2,7 +2,6 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { useRouter } from "next/navigation";
5
- import TagBadge from "./TagBadge";
6
5
  import type { DocMeta } from "@/lib/types";
7
6
 
8
7
  export default function DocCard({ doc }: { doc: DocMeta }) {
@@ -17,21 +16,25 @@ export default function DocCard({ doc }: { doc: DocMeta }) {
17
16
  onKeyDown={(e) => {
18
17
  if (e.key === "Enter") router.push(href);
19
18
  }}
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"
19
+ className="hover:border-navy-400 dark:bg-navy-900/80 dark:hover:bg-navy-800/60 block cursor-pointer rounded-xl border border-slate-300 p-4 transition-all duration-200 hover:-translate-y-0.5 dark:border-[#a0a0a01c]"
21
20
  >
22
- <h3 className="mb-2 font-semibold text-slate-900 dark:text-slate-100">
21
+ <h3 className="mb-2 font-medium text-slate-900 dark:text-slate-100">
23
22
  <Link href={href}>{doc.frontmatter.title}</Link>
24
23
  </h3>
25
- <p className="mb-3 line-clamp-2 text-sm text-slate-500 dark:text-slate-400">
24
+ <p className="mb-3 line-clamp-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
26
25
  {doc.excerpt}
27
26
  </p>
28
27
  <div className="flex items-center justify-between">
29
- <div
30
- className="flex flex-wrap items-center gap-2"
31
- onClick={(e) => e.stopPropagation()}
32
- >
28
+ <div className="flex flex-wrap items-center gap-2">
33
29
  {doc.frontmatter.tags.slice(0, 3).map((tag) => (
34
- <TagBadge key={tag} tag={tag} />
30
+ <Link
31
+ key={tag}
32
+ href={`/tag/${encodeURIComponent(tag)}/`}
33
+ onClick={(e) => e.stopPropagation()}
34
+ className="inline-flex items-center justify-center rounded-full border border-slate-200 bg-slate-100 px-2.5 py-0.5 text-xs text-slate-600 dark:border-slate-600 dark:bg-slate-600/50 dark:text-slate-300"
35
+ >
36
+ {tag}
37
+ </Link>
35
38
  ))}
36
39
  {doc.frontmatter.tags.length > 3 && (
37
40
  <span className="text-xs text-slate-400">
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useRef, useCallback } from "react";
3
+ import { DialogDescription, DialogTitle } from "@radix-ui/react-dialog";
4
+ import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
4
5
  import { Command } from "cmdk";
5
6
  import { useRouter } from "next/navigation";
6
- import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
7
- import { DialogTitle, DialogDescription } from "@radix-ui/react-dialog";
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
8
 
9
9
  interface PagefindResult {
10
10
  id: string;
@@ -41,7 +41,7 @@ function normalizePagefindUrl(url: string): string {
41
41
  function ExcerptMarkup({ html }: { html: string }) {
42
42
  return (
43
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"
44
+ className="mt-0.5 line-clamp-2 text-sm text-slate-500 dark:text-slate-400 [&_mark]:bg-transparent [&_mark]:text-yellow-600!"
45
45
  dangerouslySetInnerHTML={{ __html: html }}
46
46
  />
47
47
  );
@@ -162,9 +162,6 @@ export default function PagefindSearch() {
162
162
  d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
163
163
  />
164
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">&#8984;</span>K
167
- </kbd>
168
165
  </button>
169
166
 
170
167
  {/* cmdk dialog */}
@@ -12,9 +12,9 @@ export default function Sidebar({
12
12
  const pathname = usePathname();
13
13
 
14
14
  return (
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]">
15
+ <aside className="dark:bg-navy-900/80 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
16
  <nav
17
- className="scrollbar-none flex-1 space-y-1 overflow-y-auto p-4"
17
+ className="scrollbar-none flex-1 space-y-1 overflow-y-auto px-2 py-4"
18
18
  style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
19
19
  >
20
20
  {categories.map((cat) => {
@@ -23,14 +23,14 @@ export default function Sidebar({
23
23
  <Link
24
24
  key={cat.name}
25
25
  href={`/${cat.name}/`}
26
- className={`flex items-center justify-between rounded-full px-3 py-2 text-sm capitalize transition-colors duration-200 ${
26
+ className={`flex items-center justify-between rounded-full py-1.5 pr-2 pl-4 text-sm capitalize transition-colors duration-200 ${
27
27
  isActive
28
28
  ? "font-medium text-amber-700 dark:text-amber-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"
29
+ : "dark:hover:bg-navy-800/70 text-slate-600 hover:bg-slate-200/50 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
30
30
  }`}
31
31
  >
32
32
  <span>{cat.name.replace(/[_-]/g, " ")}</span>
33
- <span className="dark:bg-navy-800 rounded-full border border-slate-200 bg-slate-200 px-1.5 py-0.5 text-xs text-slate-500 dark:border-[#a0a0a01c] dark:text-slate-400">
33
+ <span className="flex h-7 w-7 scale-85 items-center justify-center rounded-full border border-slate-200 bg-slate-200/60 text-xs font-semibold text-slate-400 dark:border-slate-600/50 dark:bg-slate-600/40 dark:text-slate-400/80">
34
34
  {cat.docCount}
35
35
  </span>
36
36
  </Link>
@@ -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 border-b border-slate-600 pb-2 text-xs font-semibold tracking-wider text-slate-400 uppercase">
38
+ <h4 className="mb-3 border-b border-slate-300 pb-2 text-xs font-medium tracking-wider text-slate-500 uppercase dark:border-slate-600">
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-amber-700 dark:text-amber-400"
50
+ ? "text-slate-900 dark:text-white"
51
51
  : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
52
52
  }`}
53
53
  >
@@ -1,10 +1,18 @@
1
1
  import Link from "next/link";
2
2
 
3
- export default function TagBadge({ tag }: { tag: string }) {
3
+ export default function TagBadge({
4
+ tag,
5
+ size = "md",
6
+ }: {
7
+ tag: string;
8
+ size?: "sm" | "md";
9
+ }) {
10
+ const sizeClass = size === "sm" ? "text-xs" : "text-sm";
11
+ const paddingClass = size === "sm" ? "px-2 py-0.5" : "px-2.5 py-0.5";
4
12
  return (
5
13
  <Link
6
- href={`/search/?q=${encodeURIComponent(tag)}`}
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"
14
+ href={`/tag/${encodeURIComponent(tag)}/`}
15
+ className={`hover:border-navy-400 dark:bg-navy-900/80 dark:hover:bg-navy-800/80 inline-block cursor-pointer rounded-full border border-slate-200 bg-slate-50 ${paddingClass} ${sizeClass} text-slate-700 duration-200 hover:-translate-y-0.5 hover:text-slate-800 dark:border-[#a0a0a01c] dark:text-slate-400 dark:hover:text-slate-200`}
8
16
  >
9
17
  {tag}
10
18
  </Link>
@@ -20,7 +20,7 @@ export default function ThemeToggle() {
20
20
  return (
21
21
  <button
22
22
  onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
23
- className="dark:hover:bg-navy-800 cursor-pointer rounded-lg p-2 transition-colors hover:bg-slate-100"
23
+ className="cursor-pointer rounded-lg p-2 text-slate-500 transition-colors duration-200 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
24
24
  aria-label="Toggle theme"
25
25
  >
26
26
  {theme === "dark" ? (
@@ -7,7 +7,7 @@ export default function Footer({ isHome = false }: { isHome?: boolean }) {
7
7
  isHome ? "left-0" : "left-0 md:left-64 lg:left-[300px]"
8
8
  }`}
9
9
  >
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">
10
+ <div className="flex flex-row flex-wrap items-center gap-1 px-6 py-3 text-center text-xs text-slate-600 dark:text-slate-400">
11
11
  <div className="flex items-center gap-2">
12
12
  <Image src="/logo.svg" alt="Coding Friend" width={20} height={20} />
13
13
  <span>
@@ -16,7 +16,7 @@ export default function Footer({ isHome = false }: { isHome?: boolean }) {
16
16
  href="https://github.com/dinhanhthi/coding-friend"
17
17
  target="_blank"
18
18
  rel="noopener noreferrer"
19
- className="text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300"
19
+ className="text-sky-600 hover:text-orange-500 dark:text-sky-300 dark:hover:text-orange-400"
20
20
  >
21
21
  Coding Friend
22
22
  </a>
@@ -25,7 +25,7 @@ export default function Footer({ isHome = false }: { isHome?: boolean }) {
25
25
  href="https://dinhanhthi.com"
26
26
  target="_blank"
27
27
  rel="noopener noreferrer"
28
- className="text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300"
28
+ className="text-sky-600 hover:text-orange-500 dark:text-sky-300 dark:hover:text-orange-400"
29
29
  >
30
30
  Anh-Thi Dinh
31
31
  </a>
@@ -22,7 +22,7 @@ export default function Header({ categories }: { categories: CategoryInfo[] }) {
22
22
  </Link>
23
23
 
24
24
  {/* Right */}
25
- <div className="flex items-center gap-1 md:gap-3">
25
+ <div className="flex items-center gap-1">
26
26
  {/* Search */}
27
27
  <PagefindSearch />
28
28
 
@@ -199,6 +199,10 @@ export function extractHeadings(content: string): TocItem[] {
199
199
  return headings;
200
200
  }
201
201
 
202
+ export function getDocsByTag(tag: string): DocMeta[] {
203
+ return getAllDocs().filter((d) => d.frontmatter.tags.includes(tag));
204
+ }
205
+
202
206
  export function getAllTags(): { tag: string; count: number }[] {
203
207
  const tagMap = new Map<string, number>();
204
208
  for (const doc of getAllDocs()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {