fix(theme): support full dynamic Light Mode and Dark Mode with CSS variables and storage sync

This commit is contained in:
asepharyana
2026-08-21 11:13:05 +07:00
parent 62116be88d
commit aedfb022f3
16 changed files with 464 additions and 348 deletions
+67 -67
View File
@@ -54,47 +54,47 @@ function CustomFieldBadges({
if (customEntries.length === 0) return null; if (customEntries.length === 0) return null;
return ( return (
<div className="flex flex-wrap items-center gap-2 mt-4 pt-3.5 border-t border-[#1f2022]"> <div className="flex flex-wrap items-center gap-2 mt-4 pt-3.5 border-t border-[var(--border-color)]">
{customEntries.map(([key, value]) => { {customEntries.map(([key, value]) => {
const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "); const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " ");
let colorClass = "bg-[#141517] border-[#23252a] text-[#d0d6e0]"; let colorClass = "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-secondary)]";
let icon = "🏷️"; let icon = "🏷️";
let displayValue: string; let displayValue: string;
if (typeof value === "number") { if (typeof value === "number") {
colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#5e6ad2] dark:text-[#7170ff]";
icon = "🎯"; icon = "🎯";
displayValue = `${value} pts`; displayValue = `${value} pts`;
} else if (typeof value === "boolean") { } else if (typeof value === "boolean") {
colorClass = value colorClass = value
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400" ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400"
: "bg-rose-500/10 border-rose-500/30 text-rose-400"; : "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400";
icon = value ? "✓" : "✕"; icon = value ? "✓" : "✕";
displayValue = value ? "Solved" : "Pending"; displayValue = value ? "Solved" : "Pending";
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#5e6ad2] dark:text-[#7170ff]";
icon = "🗂️"; icon = "🗂️";
displayValue = value.join(", "); displayValue = value.join(", ");
} else { } else {
const v = String(value).toLowerCase(); const v = String(value).toLowerCase();
if (/^(easy|simple|beginner)/i.test(v)) { if (/^(easy|simple|beginner)/i.test(v)) {
colorClass = "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"; colorClass = "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400";
icon = "🟢"; icon = "🟢";
displayValue = "Easy"; displayValue = "Easy";
} else if (/^(medium|intermediate)/i.test(v)) { } else if (/^(medium|intermediate)/i.test(v)) {
colorClass = "bg-amber-500/10 border-amber-500/30 text-amber-400"; colorClass = "bg-amber-500/10 border-amber-500/30 text-amber-600 dark:text-amber-400";
icon = "🟡"; icon = "🟡";
displayValue = "Medium"; displayValue = "Medium";
} else if (/^(hard|expert|advanced)/i.test(v)) { } else if (/^(hard|expert|advanced)/i.test(v)) {
colorClass = "bg-rose-500/10 border-rose-500/30 text-rose-400"; colorClass = "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400";
icon = "🔴"; icon = "🔴";
displayValue = "Hard"; displayValue = "Hard";
} else if (/^(pwn|web|crypto|misc|forensic|reverse|binary)/i.test(v)) { } else if (/^(pwn|web|crypto|misc|forensic|reverse|binary)/i.test(v)) {
colorClass = "bg-indigo-500/10 border-indigo-500/30 text-indigo-300"; colorClass = "bg-indigo-500/10 border-indigo-500/30 text-indigo-600 dark:text-indigo-300";
icon = "⚡"; icon = "⚡";
displayValue = v.toUpperCase(); displayValue = v.toUpperCase();
} else if (/ctf|def.?con|event/i.test(v)) { } else if (/ctf|def.?con|event/i.test(v)) {
colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#5e6ad2] dark:text-[#7170ff]";
icon = "🏆"; icon = "🏆";
displayValue = String(value); displayValue = String(value);
} else { } else {
@@ -108,7 +108,7 @@ function CustomFieldBadges({
className={`inline-flex items-center gap-1.5 px-2.5 py-1 border rounded-md text-xs font-mono transition-colors ${colorClass}`} className={`inline-flex items-center gap-1.5 px-2.5 py-1 border rounded-md text-xs font-mono transition-colors ${colorClass}`}
> >
<span className="text-[10px] opacity-70">{icon}</span> <span className="text-[10px] opacity-70">{icon}</span>
<span className="text-[#8a8f98] font-sans">{label}:</span> <span className="text-[var(--text-muted)] font-sans">{label}:</span>
<span className="font-medium">{displayValue}</span> <span className="font-medium">{displayValue}</span>
</span> </span>
); );
@@ -157,12 +157,12 @@ function FolderIndexPage({
return ( return (
<div> <div>
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-[#62666d] mb-6 flex-wrap"> <nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] mb-6 flex-wrap">
<Link href="/" className="hover:text-[#d0d6e0] transition-colors"> <Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
MCPedia MCPedia
</Link> </Link>
<span>/</span> <span>/</span>
<Link href={`/${section}`} className="hover:text-[#d0d6e0] transition-colors"> <Link href={`/${section}`} className="hover:text-[var(--text-primary)] transition-colors">
{sectionInfo.label} {sectionInfo.label}
</Link> </Link>
{slug.split("/").map((part, i) => { {slug.split("/").map((part, i) => {
@@ -172,7 +172,7 @@ function FolderIndexPage({
return ( return (
<span key={path} className="flex items-center gap-2"> <span key={path} className="flex items-center gap-2">
<span>/</span> <span>/</span>
<span className={isLast ? "text-[#8a8f98] font-medium" : "hover:text-[#d0d6e0]"}> <span className={isLast ? "text-[var(--text-primary)] font-semibold" : "hover:text-[var(--text-primary)]"}>
{label} {label}
</span> </span>
</span> </span>
@@ -181,22 +181,22 @@ function FolderIndexPage({
</nav> </nav>
{/* Header card */} {/* Header card */}
<div className="bg-[#0f1011] border border-[#1f2022] rounded-xl p-6 mb-8"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 mb-8 shadow-sm">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-[#141517] border border-[#23252a] flex items-center justify-center text-xl"> <div className="w-10 h-10 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-xl">
📁 📁
</div> </div>
<div> <div>
<h1 className="text-2xl sm:text-3xl font-semibold text-[#f7f8f8] tracking-tight"> <h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight">
{folderName} {folderName}
</h1> </h1>
<p className="text-xs text-[#8a8f98] mt-0.5"> <p className="text-xs text-[var(--text-muted)] mt-0.5">
Directory path: <code className="text-[#7170ff] font-mono">{section}/{slug}</code> Directory path: <code className="text-[#5e6ad2] dark:text-[#7170ff] font-mono">{section}/{slug}</code>
</p> </p>
</div> </div>
</div> </div>
<span className="text-xs font-mono text-[#8a8f98] bg-[#141517] border border-[#23252a] px-2.5 py-1 rounded-md"> <span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-2.5 py-1 rounded-md">
{immediateDocs.length + folders.length} item{folders.length + immediateDocs.length !== 1 ? "s" : ""} {immediateDocs.length + folders.length} item{folders.length + immediateDocs.length !== 1 ? "s" : ""}
</span> </span>
</div> </div>
@@ -205,23 +205,23 @@ function FolderIndexPage({
{/* Subfolders */} {/* Subfolders */}
{folders.length > 0 && ( {folders.length > 0 && (
<div className="mb-8"> <div className="mb-8">
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase mb-3 tracking-wider flex items-center gap-2"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 tracking-wider flex items-center gap-2">
<span>Subfolders</span> <span>Subfolders</span>
<span className="text-[10px] text-[#62666d]">({folders.length})</span> <span className="text-[10px] text-[var(--text-dim)]">({folders.length})</span>
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{folders.map(([folder, info]) => ( {folders.map(([folder, info]) => (
<Link <Link
key={folder} key={folder}
href={`/${section}/${slug}/${folder}`.replace(/\/+/g, "/")} href={`/${section}/${slug}/${folder}`.replace(/\/+/g, "/")}
className="group flex items-center gap-3 p-3.5 bg-[#0f1011] hover:bg-[#141517] border border-[#1f2022] hover:border-[#5e6ad2]/40 rounded-lg transition-all shadow-sm" className="group flex items-center gap-3 p-3.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg transition-all shadow-sm"
> >
<span className="text-2xl group-hover:scale-110 transition-transform">📁</span> <span className="text-2xl group-hover:scale-110 transition-transform">📁</span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-sm font-medium text-[#f7f8f8] group-hover:text-[#7170ff] transition-colors truncate"> <div className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[#5e6ad2] dark:group-hover:text-[#7170ff] transition-colors truncate">
{folder.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")} {folder.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
</div> </div>
<div className="text-xs text-[#62666d] mt-0.5 font-mono"> <div className="text-xs text-[var(--text-muted)] mt-0.5 font-mono">
{info.docCount} document{info.docCount !== 1 ? "s" : ""} {info.docCount} document{info.docCount !== 1 ? "s" : ""}
</div> </div>
</div> </div>
@@ -234,21 +234,21 @@ function FolderIndexPage({
{/* Documents */} {/* Documents */}
{immediateDocs.length > 0 && ( {immediateDocs.length > 0 && (
<div> <div>
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase mb-3 tracking-wider flex items-center gap-2"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 tracking-wider flex items-center gap-2">
<span>Documents</span> <span>Documents</span>
<span className="text-[10px] text-[#62666d]">({immediateDocs.length})</span> <span className="text-[10px] text-[var(--text-dim)]">({immediateDocs.length})</span>
</h2> </h2>
<div className="space-y-2"> <div className="space-y-2">
{immediateDocs.map((doc) => ( {immediateDocs.map((doc) => (
<Link <Link
key={doc.slug} key={doc.slug}
href={`/${doc.slug}`} href={`/${doc.slug}`}
className="group flex items-center justify-between gap-4 p-4 bg-[#0f1011] hover:bg-[#141517] border border-[#1f2022] hover:border-[#5e6ad2]/40 rounded-lg transition-all shadow-sm" className="group flex items-center justify-between gap-4 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg transition-all shadow-sm"
> >
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<span className="text-xl opacity-70 group-hover:opacity-100">📄</span> <span className="text-xl opacity-70 group-hover:opacity-100">📄</span>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-sm font-medium text-[#f7f8f8] group-hover:text-[#7170ff] transition-colors truncate"> <div className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[#5e6ad2] dark:group-hover:text-[#7170ff] transition-colors truncate">
{doc.title} {doc.title}
</div> </div>
{doc.tags.length > 0 && ( {doc.tags.length > 0 && (
@@ -256,7 +256,7 @@ function FolderIndexPage({
{doc.tags.slice(0, 4).map((t) => ( {doc.tags.slice(0, 4).map((t) => (
<span <span
key={t} key={t}
className="text-[11px] px-1.5 py-0.25 bg-[#141517] border border-[#23252a] rounded text-[#8a8f98]" className="text-[11px] px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]"
> >
#{t} #{t}
</span> </span>
@@ -265,7 +265,7 @@ function FolderIndexPage({
)} )}
</div> </div>
</div> </div>
<time className="text-xs text-[#62666d] shrink-0 font-mono"> <time className="text-xs text-[var(--text-dim)] shrink-0 font-mono">
{new Date(doc.updatedAt).toLocaleDateString(undefined, { {new Date(doc.updatedAt).toLocaleDateString(undefined, {
month: "short", month: "short",
day: "numeric", day: "numeric",
@@ -278,8 +278,8 @@ function FolderIndexPage({
)} )}
{folders.length === 0 && immediateDocs.length === 0 && ( {folders.length === 0 && immediateDocs.length === 0 && (
<div className="text-center py-12 border border-[#1f2022] rounded-xl bg-[#0f1011]"> <div className="text-center py-12 border border-[var(--border-color)] rounded-xl bg-[var(--bg-surface)]">
<p className="text-sm text-[#8a8f98]">No documents found in this directory.</p> <p className="text-sm text-[var(--text-muted)]">No documents found in this directory.</p>
</div> </div>
)} )}
</div> </div>
@@ -317,11 +317,11 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
<div> <div>
<Link <Link
href={`/${doc.slug}`} href={`/${doc.slug}`}
className="inline-flex items-center gap-1 text-xs text-[#8a8f98] hover:text-[#d0d6e0] mb-4 transition-colors" className="inline-flex items-center gap-1 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] mb-4 transition-colors"
> >
<span>← Back to document</span> <span>← Back to document</span>
</Link> </Link>
<h1 className="text-2xl font-semibold text-[#f7f8f8] mb-6"> <h1 className="text-2xl font-bold text-[var(--text-primary)] mb-6">
Edit: {doc.title} Edit: {doc.title}
</h1> </h1>
<DocForm <DocForm
@@ -356,8 +356,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
{/* Main Content Column */} {/* Main Content Column */}
<article className="flex-1 min-w-0"> <article className="flex-1 min-w-0">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-[#62666d] mb-4 flex-wrap"> <nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] mb-4 flex-wrap">
<Link href="/" className="hover:text-[#d0d6e0] transition-colors"> <Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
MCPedia MCPedia
</Link> </Link>
{doc.slug.split("/").map((part, i) => { {doc.slug.split("/").map((part, i) => {
@@ -371,7 +371,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
<span>/</span> <span>/</span>
<Link <Link
href={`/${crumbPath}`} href={`/${crumbPath}`}
className={isLast ? "text-[#8a8f98] font-medium" : "hover:text-[#d0d6e0] transition-colors"} className={isLast ? "text-[var(--text-primary)] font-semibold" : "hover:text-[var(--text-primary)] transition-colors"}
> >
{label} {label}
</Link> </Link>
@@ -381,24 +381,24 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</nav> </nav>
{/* Hero Metadata Card */} {/* Hero Metadata Card */}
<div className="bg-[#0f1011] border border-[#1f2022] rounded-xl p-6 mb-8 shadow-sm"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 mb-8 shadow-sm">
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4"> <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2 flex-wrap"> <div className="flex items-center gap-2 mb-2 flex-wrap">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono font-medium bg-[#5e6ad2]/15 text-[#7170ff] border border-[#5e6ad2]/30"> <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono font-medium bg-[#5e6ad2]/15 text-[#5e6ad2] dark:text-[#7170ff] border border-[#5e6ad2]/30">
<span>{sectionInfo.icon}</span> <span>{sectionInfo.icon}</span>
<span className="uppercase">{sectionInfo.label}</span> <span className="uppercase">{sectionInfo.label}</span>
</span> </span>
<span className="text-xs text-[#62666d]">·</span> <span className="text-xs text-[var(--text-dim)]">·</span>
<span className="text-xs text-[#8a8f98] capitalize"> <span className="text-xs text-[var(--text-muted)] capitalize">
{doc.type} {doc.type}
</span> </span>
<span className="text-xs text-[#62666d]">·</span> <span className="text-xs text-[var(--text-dim)]">·</span>
<span className="text-xs text-[#8a8f98] font-mono"> <span className="text-xs text-[var(--text-muted)] font-mono">
{readingTime} {readingTime}
</span> </span>
</div> </div>
<h1 className="text-2xl sm:text-3xl font-semibold text-[#f7f8f8] tracking-tight leading-tight"> <h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight leading-tight">
{doc.title} {doc.title}
</h1> </h1>
</div> </div>
@@ -408,14 +408,14 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</div> </div>
{/* Author + Date + Tags */} {/* Author + Date + Tags */}
<div className="flex flex-wrap items-center gap-3 text-xs text-[#8a8f98] pt-3 border-t border-[#1a1b1d]"> <div className="flex flex-wrap items-center gap-3 text-xs text-[var(--text-muted)] pt-3 border-t border-[var(--border-color)]">
{doc.author && ( {doc.author && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-4 h-4 rounded-full bg-[#5e6ad2] text-white flex items-center justify-center text-[10px] font-bold"> <div className="w-4 h-4 rounded-full bg-[#5e6ad2] text-white flex items-center justify-center text-[10px] font-bold">
{doc.author.charAt(0).toUpperCase()} {doc.author.charAt(0).toUpperCase()}
</div> </div>
<span className="text-[#d0d6e0]">{doc.author}</span> <span className="text-[var(--text-secondary)]">{doc.author}</span>
<span className="text-[#62666d]">·</span> <span className="text-[var(--text-dim)]">·</span>
</div> </div>
)} )}
<span> <span>
@@ -427,12 +427,12 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</span> </span>
{doc.tags.length > 0 && ( {doc.tags.length > 0 && (
<> <>
<span className="text-[#62666d]">·</span> <span className="text-[var(--text-dim)]">·</span>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{doc.tags.map((t) => ( {doc.tags.map((t) => (
<span <span
key={t} key={t}
className="px-2 py-0.5 bg-[#141517] border border-[#23252a] rounded text-[#8a8f98] text-[11px]" className="px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] text-[11px]"
> >
#{t} #{t}
</span> </span>
@@ -447,7 +447,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</div> </div>
{/* Mobile TOC (rendered inline on smaller screens) */} {/* Mobile TOC (rendered inline on smaller screens) */}
<div className="xl:hidden mb-6 p-4 bg-[#0f1011] border border-[#1f2022] rounded-lg"> <div className="xl:hidden mb-6 p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg shadow-sm">
<TOC source={doc.body} /> <TOC source={doc.body} />
</div> </div>
@@ -458,8 +458,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
{/* Related Documents */} {/* Related Documents */}
{related.length > 0 && ( {related.length > 0 && (
<aside className="mt-12 pt-8 border-t border-[#1f2022]"> <aside className="mt-12 pt-8 border-t border-[var(--border-color)]">
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase tracking-wider mb-4 flex items-center gap-2"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4 flex items-center gap-2">
<span>Related Knowledge</span> <span>Related Knowledge</span>
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -469,20 +469,20 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
<Link <Link
key={r.slug} key={r.slug}
href={`/${r.slug}`} href={`/${r.slug}`}
className="group block bg-[#0f1011] hover:bg-[#141517] border border-[#1f2022] hover:border-[#5e6ad2]/40 rounded-lg p-4 transition-all shadow-sm" className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg p-4 transition-all shadow-sm"
> >
<div className="flex items-center justify-between gap-2 mb-2"> <div className="flex items-center justify-between gap-2 mb-2">
<span className="text-xs text-[#7170ff] font-mono uppercase"> <span className="text-xs text-[#5e6ad2] dark:text-[#7170ff] font-mono uppercase">
{rInfo.icon} {r.section} {rInfo.icon} {r.section}
</span> </span>
<time className="text-[11px] text-[#62666d] font-mono"> <time className="text-[11px] text-[var(--text-dim)] font-mono">
{new Date(r.updatedAt).toLocaleDateString(undefined, { {new Date(r.updatedAt).toLocaleDateString(undefined, {
month: "short", month: "short",
day: "numeric", day: "numeric",
})} })}
</time> </time>
</div> </div>
<h3 className="text-sm font-medium text-[#f7f8f8] group-hover:text-[#7170ff] transition-colors line-clamp-1 mb-1"> <h3 className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[#5e6ad2] dark:group-hover:text-[#7170ff] transition-colors line-clamp-1 mb-1">
{r.title} {r.title}
</h3> </h3>
</Link> </Link>
@@ -494,9 +494,9 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
{/* Revision History */} {/* Revision History */}
{revisions.length > 0 && ( {revisions.length > 0 && (
<aside className="mt-10 pt-8 border-t border-[#1f2022]"> <aside className="mt-10 pt-8 border-t border-[var(--border-color)]">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase tracking-wider"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
Revision History ({revisions.length}) Revision History ({revisions.length})
</h2> </h2>
</div> </div>
@@ -504,16 +504,16 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
{revisions.map((rev) => ( {revisions.map((rev) => (
<div <div
key={rev.id} key={rev.id}
className="flex items-center justify-between gap-3 p-3 bg-[#0f1011] border border-[#1f2022] rounded-lg text-xs" className="flex items-center justify-between gap-3 p-3 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-xs shadow-sm"
> >
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className="font-mono text-[#7170ff] bg-[#5e6ad2]/10 px-1.5 py-0.5 rounded border border-[#5e6ad2]/30"> <span className="font-mono text-[#5e6ad2] dark:text-[#7170ff] bg-[#5e6ad2]/10 px-1.5 py-0.5 rounded border border-[#5e6ad2]/30 font-semibold">
v{rev.revisionNo} v{rev.revisionNo}
</span> </span>
<span className="text-[#d0d6e0] truncate"> <span className="text-[var(--text-primary)] truncate">
{rev.reason || "Updated document"} {rev.reason || "Updated document"}
</span> </span>
<span className="text-[#62666d] hidden sm:inline"> <span className="text-[var(--text-dim)] hidden sm:inline">
· {new Date(rev.createdAt).toLocaleString()} · {new Date(rev.createdAt).toLocaleString()}
</span> </span>
</div> </div>
@@ -522,7 +522,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
<input type="hidden" name="id" value={rev.id} /> <input type="hidden" name="id" value={rev.id} />
<button <button
type="submit" type="submit"
className="text-xs text-[#d0d6e0] hover:text-white bg-[#141517] hover:bg-[#23252a] px-2.5 py-1 rounded border border-[#23252a] hover:border-[#5e6ad2] transition-colors" className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] hover:border-[var(--brand)] transition-colors font-medium shadow-sm"
> >
Restore Restore
</button> </button>
@@ -536,7 +536,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</article> </article>
{/* Desktop Sticky Table of Contents Column */} {/* Desktop Sticky Table of Contents Column */}
<aside className="hidden xl:block w-64 shrink-0 sticky top-20 max-h-[calc(100vh-6rem)] overflow-y-auto pl-6 border-l border-[#1f2022]"> <aside className="hidden xl:block w-64 shrink-0 sticky top-20 max-h-[calc(100vh-6rem)] overflow-y-auto pl-6 border-l border-[var(--border-color)]">
<TOC source={doc.body} /> <TOC source={doc.body} />
</aside> </aside>
</div> </div>
+25 -25
View File
@@ -84,10 +84,10 @@ function renderTree(nodes: TreeNode[], pathname: string) {
href={`/${node.slug}`} href={`/${node.slug}`}
className={`flex items-center justify-between gap-2 text-xs py-1.5 px-2.5 rounded-lg transition-all ${ className={`flex items-center justify-between gap-2 text-xs py-1.5 px-2.5 rounded-lg transition-all ${
isActive isActive
? "text-[#7170ff] bg-[#5e6ad2]/15 font-medium border border-[#5e6ad2]/30" ? "text-[#5e6ad2] dark:text-[#7170ff] bg-[#5e6ad2]/15 font-semibold border border-[#5e6ad2]/30"
: hasActiveChild : hasActiveChild
? "text-[#d0d6e0] bg-[#141517]" ? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#141517]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`} }`}
> >
<span className="flex items-center gap-2 truncate"> <span className="flex items-center gap-2 truncate">
@@ -95,13 +95,13 @@ function renderTree(nodes: TreeNode[], pathname: string) {
<span className="truncate">{node.doc?.title || node.name}</span> <span className="truncate">{node.doc?.title || node.name}</span>
</span> </span>
{isFolder && ( {isFolder && (
<span className="text-[10px] text-[#62666d] bg-[#191a1b] px-1.5 py-0.5 rounded font-mono"> <span className="text-[10px] text-[var(--text-dim)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-1.5 py-0.5 rounded font-mono">
{node.docCount} docs {node.docCount} docs
</span> </span>
)} )}
</Link> </Link>
{node.children.length > 0 && ( {node.children.length > 0 && (
<div className="ml-4 border-l border-[#1f2022] pl-2 mt-1"> <div className="ml-4 border-l border-[var(--border-color)] pl-2 mt-1">
{renderTree(node.children, pathname)} {renderTree(node.children, pathname)}
</div> </div>
)} )}
@@ -123,8 +123,8 @@ export default async function SectionIndexPage({
if (!sectionInfo) { if (!sectionInfo) {
return ( return (
<div className="py-12 text-center"> <div className="py-12 text-center">
<h1 className="text-2xl font-semibold text-[#f7f8f8]">Unknown section</h1> <h1 className="text-2xl font-semibold text-[var(--text-primary)]">Unknown section</h1>
<p className="text-sm text-[#8a8f98] mt-2">Section &quot;{section}&quot; does not exist.</p> <p className="text-sm text-[var(--text-muted)] mt-2">Section &quot;{section}&quot; does not exist.</p>
</div> </div>
); );
} }
@@ -136,33 +136,33 @@ export default async function SectionIndexPage({
return ( return (
<div className="space-y-10"> <div className="space-y-10">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-[#62666d]"> <nav className="flex items-center gap-2 text-xs text-[var(--text-dim)]">
<Link href="/" className="hover:text-[#d0d6e0] transition-colors"> <Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
MCPedia MCPedia
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-[#8a8f98] font-medium">{sectionInfo.label}</span> <span className="text-[var(--text-muted)] font-medium">{sectionInfo.label}</span>
</nav> </nav>
{/* Hero Card */} {/* Hero Card */}
<div className="bg-[#0f1011] border border-[#1f2022] rounded-xl p-6 shadow-sm"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 shadow-sm">
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-3"> <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-[#141517] border border-[#23252a] flex items-center justify-center text-2xl shadow-inner"> <div className="w-12 h-12 rounded-xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-2xl shadow-inner">
{sectionInfo.icon} {sectionInfo.icon}
</div> </div>
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold text-[#f7f8f8] tracking-tight"> <h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight">
{sectionInfo.label} {sectionInfo.label}
</h1> </h1>
<p className="text-xs text-[#8a8f98] mt-0.5 font-mono uppercase"> <p className="text-xs text-[var(--text-muted)] mt-0.5 font-mono uppercase">
Section: {section} Section: {section}
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-mono text-[#7170ff] bg-[#5e6ad2]/10 border border-[#5e6ad2]/25 px-2.5 py-1 rounded-md"> <span className="text-xs font-mono text-[#5e6ad2] dark:text-[#7170ff] bg-[#5e6ad2]/10 border border-[#5e6ad2]/25 px-2.5 py-1 rounded-md">
{sectionDocs.length} document{sectionDocs.length !== 1 ? "s" : ""} {sectionDocs.length} document{sectionDocs.length !== 1 ? "s" : ""}
</span> </span>
<Link <Link
@@ -174,15 +174,15 @@ export default async function SectionIndexPage({
</div> </div>
</div> </div>
<p className="text-sm text-[#8a8f98] leading-relaxed max-w-2xl mt-2"> <p className="text-sm text-[var(--text-muted)] leading-relaxed max-w-2xl mt-2">
{sectionInfo.desc} {sectionInfo.desc}
</p> </p>
</div> </div>
{/* Hierarchical Folder Directory */} {/* Hierarchical Folder Directory */}
{tree.length > 0 && ( {tree.length > 0 && (
<section className="bg-[#0c0d0e] border border-[#1f2022] rounded-xl p-6"> <section className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 shadow-sm">
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase tracking-wider mb-4 flex items-center gap-2"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4 flex items-center gap-2">
<span>Folder Structure</span> <span>Folder Structure</span>
</h2> </h2>
{renderTree(tree, section)} {renderTree(tree, section)}
@@ -192,7 +192,7 @@ export default async function SectionIndexPage({
{/* Document List */} {/* Document List */}
{sectionDocs.length > 0 ? ( {sectionDocs.length > 0 ? (
<section> <section>
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase tracking-wider mb-4"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4">
All Documents in {sectionInfo.label} All Documents in {sectionInfo.label}
</h2> </h2>
<div className="space-y-2"> <div className="space-y-2">
@@ -205,12 +205,12 @@ export default async function SectionIndexPage({
<Link <Link
key={d.slug} key={d.slug}
href={`/${d.slug}`} href={`/${d.slug}`}
className="group flex items-center justify-between gap-4 p-4 bg-[#0f1011] hover:bg-[#141517] border border-[#1f2022] hover:border-[#5e6ad2]/40 rounded-xl transition-all shadow-sm" className="group flex items-center justify-between gap-4 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl transition-all shadow-sm"
> >
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<span className="text-lg opacity-70 group-hover:opacity-100">📄</span> <span className="text-lg opacity-70 group-hover:opacity-100">📄</span>
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm font-medium text-[#f7f8f8] group-hover:text-[#7170ff] transition-colors truncate"> <h3 className="text-sm font-medium text-[var(--text-primary)] group-hover:text-[#5e6ad2] dark:group-hover:text-[#7170ff] transition-colors truncate">
{d.title} {d.title}
</h3> </h3>
{d.tags.length > 0 && ( {d.tags.length > 0 && (
@@ -218,7 +218,7 @@ export default async function SectionIndexPage({
{d.tags.slice(0, 4).map((t) => ( {d.tags.slice(0, 4).map((t) => (
<span <span
key={t} key={t}
className="text-[10px] px-1.5 py-0.25 bg-[#141517] border border-[#23252a] rounded text-[#8a8f98]" className="text-[10px] px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]"
> >
#{t} #{t}
</span> </span>
@@ -227,7 +227,7 @@ export default async function SectionIndexPage({
)} )}
</div> </div>
</div> </div>
<time className="text-xs text-[#62666d] font-mono shrink-0"> <time className="text-xs text-[var(--text-dim)] font-mono shrink-0">
{new Date(d.updatedAt).toLocaleDateString(undefined, { {new Date(d.updatedAt).toLocaleDateString(undefined, {
month: "short", month: "short",
day: "numeric", day: "numeric",
@@ -239,8 +239,8 @@ export default async function SectionIndexPage({
</div> </div>
</section> </section>
) : ( ) : (
<div className="text-center py-12 border border-[#1f2022] rounded-xl bg-[#0f1011]"> <div className="text-center py-12 border border-[var(--border-color)] rounded-xl bg-[var(--bg-surface)]">
<p className="text-sm text-[#8a8f98]">No documents in this section yet.</p> <p className="text-sm text-[var(--text-muted)]">No documents in this section yet.</p>
</div> </div>
)} )}
</div> </div>
+24 -24
View File
@@ -91,13 +91,13 @@ export default function CommandMenu() {
return ( return (
<button <button
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="hidden md:flex items-center gap-2 px-3 py-1.5 bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] hover:border-[#383b42] text-xs text-[#8a8f98] hover:text-[#d0d6e0] rounded-lg transition-all shadow-inner" className="hidden md:flex items-center gap-2 px-3 py-1.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-sm"
> >
<svg className="w-3.5 h-3.5 text-[#62666d]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5 text-[var(--text-dim)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
<span>Search documentation...</span> <span>Search docs...</span>
<kbd className="ml-2 font-mono bg-[#08090a] px-1.5 py-0.5 rounded border border-[#1f2022] text-[10px] text-[#62666d]"> <kbd className="ml-2 font-mono bg-[var(--bg-elevated)] px-1.5 py-0.5 rounded border border-[var(--border-color)] text-[10px] text-[var(--text-dim)]">
⌘K ⌘K
</kbd> </kbd>
</button> </button>
@@ -108,15 +108,15 @@ export default function CommandMenu() {
<div className="fixed inset-0 z-50 flex items-start justify-center pt-20 px-4"> <div className="fixed inset-0 z-50 flex items-start justify-center pt-20 px-4">
{/* Backdrop */} {/* Backdrop */}
<div <div
className="fixed inset-0 bg-black/70 backdrop-blur-sm transition-opacity" className="fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm transition-opacity"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
/> />
{/* Modal */} {/* Modal */}
<div className="relative w-full max-w-2xl bg-[#0f1011] border border-[#27292d] rounded-xl shadow-2xl overflow-hidden animate-fade-in"> <div className="relative w-full max-w-2xl bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-2xl overflow-hidden animate-fade-in">
{/* Search input bar */} {/* Search input bar */}
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-[#1f2022] bg-[#141517]"> <div className="flex items-center gap-3 px-4 py-3.5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
<svg className="w-5 h-5 text-[#7170ff] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5 text-[#5e6ad2] dark:text-[#7170ff] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
<input <input
@@ -126,14 +126,14 @@ export default function CommandMenu() {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyNavigation} onKeyDown={handleKeyNavigation}
placeholder="Type to search (titles, keywords, concepts)..." placeholder="Type to search (titles, keywords, concepts)..."
className="w-full bg-transparent text-[#f7f8f8] placeholder-[#62666d] text-sm focus:outline-none" className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm focus:outline-none"
/> />
{loading && ( {loading && (
<div className="w-4 h-4 border-2 border-[#5e6ad2] border-t-transparent rounded-full animate-spin shrink-0" /> <div className="w-4 h-4 border-2 border-[#5e6ad2] border-t-transparent rounded-full animate-spin shrink-0" />
)} )}
<button <button
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="text-xs text-[#62666d] hover:text-[#d0d6e0] bg-[#1b1d20] px-1.5 py-0.5 rounded border border-[#27292d]" className="text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)]"
> >
ESC ESC
</button> </button>
@@ -143,14 +143,14 @@ export default function CommandMenu() {
<div className="max-h-96 overflow-y-auto p-2"> <div className="max-h-96 overflow-y-auto p-2">
{query.trim() === "" ? ( {query.trim() === "" ? (
<div className="px-3 py-6 text-center"> <div className="px-3 py-6 text-center">
<p className="text-xs text-[#8a8f98]">Search full-text, semantic embeddings, and CTF writeups.</p> <p className="text-xs text-[var(--text-muted)]">Search full-text, semantic embeddings, and CTF writeups.</p>
<div className="flex justify-center gap-2 mt-3 text-[11px] text-[#62666d]"> <div className="flex justify-center gap-2 mt-3 text-[11px] text-[var(--text-dim)]">
<span>Navigation: <kbd className="bg-[#141517] px-1 py-0.5 rounded border border-[#1f2022]">↑</kbd> <kbd className="bg-[#141517] px-1 py-0.5 rounded border border-[#1f2022]">↓</kbd></span> <span>Navigation: <kbd className="bg-[var(--bg-elevated)] px-1 py-0.5 rounded border border-[var(--border-color)]">↑</kbd> <kbd className="bg-[var(--bg-elevated)] px-1 py-0.5 rounded border border-[var(--border-color)]">↓</kbd></span>
<span>Select: <kbd className="bg-[#141517] px-1 py-0.5 rounded border border-[#1f2022]">↵</kbd></span> <span>Select: <kbd className="bg-[var(--bg-elevated)] px-1 py-0.5 rounded border border-[var(--border-color)]">↵</kbd></span>
</div> </div>
</div> </div>
) : results.length === 0 && !loading ? ( ) : results.length === 0 && !loading ? (
<div className="px-4 py-8 text-center text-xs text-[#8a8f98]"> <div className="px-4 py-8 text-center text-xs text-[var(--text-muted)]">
No documents matching &quot;{query}&quot; No documents matching &quot;{query}&quot;
</div> </div>
) : ( ) : (
@@ -164,27 +164,27 @@ export default function CommandMenu() {
onMouseEnter={() => setSelectedIndex(index)} onMouseEnter={() => setSelectedIndex(index)}
className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${ className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${
isSelected isSelected
? "bg-[#5e6ad2]/15 border border-[#5e6ad2]/40 text-[#f7f8f8]" ? "bg-[#5e6ad2]/15 border border-[#5e6ad2]/40 text-[var(--text-primary)]"
: "hover:bg-[#141517] text-[#d0d6e0] border border-transparent" : "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent"
}`} }`}
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs px-1.5 py-0.25 bg-[#191a1b] border border-[#23252a] rounded text-[#7170ff] font-mono"> <span className="text-xs px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[#5e6ad2] dark:text-[#7170ff] font-mono">
{hit.section} {hit.section}
</span> </span>
<span className={`text-sm font-medium ${isSelected ? "text-[#7170ff]" : "text-[#f7f8f8]"} truncate`}> <span className={`text-sm font-medium ${isSelected ? "text-[#5e6ad2] dark:text-[#7170ff]" : "text-[var(--text-primary)]"} truncate`}>
{hit.title} {hit.title}
</span> </span>
</div> </div>
{hit.snippet && ( {hit.snippet && (
<p <p
className="text-xs text-[#8a8f98] mt-1 line-clamp-1 [&>mark]:bg-[#5e6ad2]/30 [&>mark]:text-[#f7f8f8] [&>mark]:rounded [&>mark]:px-0.5" className="text-xs text-[var(--text-muted)] mt-1 line-clamp-1 [&>mark]:bg-[#5e6ad2]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:rounded [&>mark]:px-0.5"
dangerouslySetInnerHTML={{ __html: hit.snippet }} dangerouslySetInnerHTML={{ __html: hit.snippet }}
/> />
)} )}
</div> </div>
<span className="text-[11px] text-[#62666d] shrink-0 self-center"> <span className="text-[11px] text-[var(--text-dim)] shrink-0 self-center">
{hit.slug} {hit.slug}
</span> </span>
</li> </li>
@@ -195,12 +195,12 @@ export default function CommandMenu() {
</div> </div>
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-between px-4 py-2.5 border-t border-[#1f2022] bg-[#0c0d0e] text-[11px] text-[#62666d]"> <div className="flex items-center justify-between px-4 py-2.5 border-t border-[var(--border-color)] bg-[var(--bg-elevated)] text-[11px] text-[var(--text-dim)]">
<span>Tip: Press <kbd className="bg-[#141517] px-1 py-0.5 rounded border border-[#1f2022]">Hybrid</kbd> search is enabled by default</span> <span>Tip: Press <kbd className="bg-[var(--bg-surface)] px-1 py-0.5 rounded border border-[var(--border-color)]">Hybrid</kbd> search is enabled</span>
<Link <Link
href={`/search?q=${encodeURIComponent(query)}`} href={`/search?q=${encodeURIComponent(query)}`}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="hover:text-[#7170ff] transition-colors" className="hover:text-[#5e6ad2] dark:hover:text-[#7170ff] transition-colors"
> >
Open full search page → Open full search page →
</Link> </Link>
+8 -8
View File
@@ -32,14 +32,14 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
onClick={handleCopyMarkdown} onClick={handleCopyMarkdown}
type="button" type="button"
title="Copy raw markdown content" title="Copy raw markdown content"
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] hover:border-[#383b42] text-[#8a8f98] hover:text-[#f7f8f8] rounded-md transition-all" className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-sm"
> >
{copiedMd ? ( {copiedMd ? (
<> <>
<svg className="w-3.5 h-3.5 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span className="text-emerald-400">Copied MD</span> <span className="text-emerald-500 font-medium">Copied MD</span>
</> </>
) : ( ) : (
<> <>
@@ -56,14 +56,14 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
onClick={handleCopyLink} onClick={handleCopyLink}
type="button" type="button"
title="Copy link to document" title="Copy link to document"
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] hover:border-[#383b42] text-[#8a8f98] hover:text-[#f7f8f8] rounded-md transition-all" className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-sm"
> >
{copiedLink ? ( {copiedLink ? (
<> <>
<svg className="w-3.5 h-3.5 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span className="text-emerald-400">Copied</span> <span className="text-emerald-500 font-medium">Copied Link</span>
</> </>
) : ( ) : (
<> <>
@@ -78,12 +78,12 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
{/* Edit Link */} {/* Edit Link */}
<Link <Link
href={canEdit ? `/${slug}?edit=1` : `/login?redirect=/${slug}`} href={canEdit ? `/${slug}?edit=1` : `/login?redirect=/${slug}`}
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[#5e6ad2]/15 hover:bg-[#5e6ad2]/25 border border-[#5e6ad2]/30 hover:border-[#5e6ad2]/60 text-[#7170ff] hover:text-white rounded-md transition-all" className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[#5e6ad2]/10 hover:bg-[#5e6ad2]/20 border border-[#5e6ad2]/30 hover:border-[#5e6ad2]/60 text-[#5e6ad2] dark:text-[#7170ff] rounded-md transition-all font-medium"
> >
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg> </svg>
<span>{canEdit ? "Edit" : "Edit"}</span> <span>Edit</span>
</Link> </Link>
</div> </div>
); );
+37 -37
View File
@@ -82,7 +82,7 @@ export default function DocForm({
const availableFolders = existingFolders?.[section] ?? []; const availableFolders = existingFolders?.[section] ?? [];
const baseInputCls = const baseInputCls =
"w-full px-3.5 py-2 bg-[#0f1011] border border-[#23252a] hover:border-[#383b42] rounded-lg text-sm text-[#f7f8f8] placeholder-[#62666d] focus:outline-none focus:border-[#5e6ad2] focus:ring-1 focus:ring-[#5e6ad2] transition-colors"; "w-full px-3.5 py-2 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-1 focus:ring-[#5e6ad2] transition-colors";
function addCustomField(key = "", value = "") { function addCustomField(key = "", value = "") {
setCustomFields([...customFields, { key, value }]); setCustomFields([...customFields, { key, value }]);
@@ -184,14 +184,14 @@ export default function DocForm({
return ( return (
<form onSubmit={handleSubmit} className="space-y-6 max-w-4xl"> <form onSubmit={handleSubmit} className="space-y-6 max-w-4xl">
{error && ( {error && (
<div className="p-3.5 rounded-lg border border-rose-500/30 bg-rose-500/10 text-rose-400 text-sm"> <div className="p-3.5 rounded-lg border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-sm">
{error} {error}
</div> </div>
)} )}
{/* Title */} {/* Title */}
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Document Title * Document Title *
</label> </label>
<input <input
@@ -207,7 +207,7 @@ export default function DocForm({
{/* Section + Type + Status */} {/* Section + Type + Status */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Section Section
</label> </label>
<select <select
@@ -224,7 +224,7 @@ export default function DocForm({
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Type Type
</label> </label>
<select <select
@@ -240,7 +240,7 @@ export default function DocForm({
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Status Status
</label> </label>
<select <select
@@ -257,7 +257,7 @@ export default function DocForm({
{/* Folder + Slug */} {/* Folder + Slug */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Parent Folder Parent Folder
</label> </label>
<select <select
@@ -275,7 +275,7 @@ export default function DocForm({
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
{isEdit ? "Full Slug" : "Slug (File name) *"} {isEdit ? "Full Slug" : "Slug (File name) *"}
</label> </label>
{isEdit ? ( {isEdit ? (
@@ -283,7 +283,7 @@ export default function DocForm({
type="text" type="text"
value={slug ?? ""} value={slug ?? ""}
readOnly readOnly
className="w-full px-3.5 py-2 bg-[#141517] border border-[#23252a] rounded-lg text-sm text-[#8a8f98] font-mono" className="w-full px-3.5 py-2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg text-sm text-[var(--text-muted)] font-mono"
/> />
) : ( ) : (
<input <input
@@ -295,8 +295,8 @@ export default function DocForm({
required required
/> />
)} )}
<p className="text-[11px] text-[#62666d] mt-1 font-mono"> <p className="text-[11px] text-[var(--text-dim)] mt-1 font-mono">
Path: <span className="text-[#7170ff]">{effectiveSlug || section}</span> Path: <span className="text-[#5e6ad2] dark:text-[#7170ff]">{effectiveSlug || section}</span>
</p> </p>
</div> </div>
</div> </div>
@@ -304,7 +304,7 @@ export default function DocForm({
{/* Tags + Author */} {/* Tags + Author */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Tags (comma separated) Tags (comma separated)
</label> </label>
<input <input
@@ -316,7 +316,7 @@ export default function DocForm({
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Author Author
</label> </label>
<input <input
@@ -330,25 +330,25 @@ export default function DocForm({
</div> </div>
{/* Custom Metadata Fields */} {/* Custom Metadata Fields */}
<div className="p-4 bg-[#0c0d0e] border border-[#1f2022] rounded-xl space-y-3"> <div className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-[#8a8f98]"> <h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)]">
Dynamic Custom Metadata Dynamic Custom Metadata
</h3> </h3>
<p className="text-[11px] text-[#62666d] mt-0.5"> <p className="text-[11px] text-[var(--text-dim)] mt-0.5">
Add custom key-values (event, difficulty, points, etc.) rendered as styled badges. Add custom key-values (event, difficulty, points, etc.) rendered as styled badges.
</p> </p>
</div> </div>
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[11px] text-[#62666d]">Presets:</span> <span className="text-[11px] text-[var(--text-dim)]">Presets:</span>
{PRESET_FIELDS.map((preset) => ( {PRESET_FIELDS.map((preset) => (
<button <button
key={preset.key} key={preset.key}
type="button" type="button"
onClick={() => addCustomField(preset.key, preset.value)} onClick={() => addCustomField(preset.key, preset.value)}
className="px-2 py-0.5 text-[11px] bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] text-[#8a8f98] hover:text-[#d0d6e0] rounded transition-colors" className="px-2 py-0.5 text-[11px] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded transition-colors"
> >
+{preset.key} +{preset.key}
</button> </button>
@@ -375,7 +375,7 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => removeCustomField(i)} onClick={() => removeCustomField(i)}
className="p-2 text-[#fca5a5] hover:text-rose-400 rounded hover:bg-[#191a1b] transition-colors" className="p-2 text-rose-500 hover:text-rose-600 rounded hover:bg-[var(--bg-elevated)] transition-colors"
title="Remove field" title="Remove field"
> >
✕ ✕
@@ -386,16 +386,16 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => addCustomField()} onClick={() => addCustomField()}
className="text-xs text-[#7170ff] hover:text-[#828fff] px-3 py-1.5 rounded-lg border border-[#5e6ad2]/30 hover:border-[#5e6ad2]/60 bg-[#5e6ad2]/5 transition-colors" className="text-xs text-[#5e6ad2] dark:text-[#7170ff] hover:underline px-3 py-1.5 rounded-lg border border-[#5e6ad2]/30 hover:border-[#5e6ad2]/60 bg-[#5e6ad2]/5 transition-colors font-medium"
> >
+ Add Custom Field + Add Custom Field
</button> </button>
</div> </div>
{/* Body with Write / Preview Tabs */} {/* Body with Write / Preview Tabs */}
<div className="border border-[#1f2022] rounded-xl overflow-hidden bg-[#0c0d0e]"> <div className="border border-[var(--border-color)] rounded-xl overflow-hidden bg-[var(--bg-surface)] shadow-sm">
{/* Editor Toolbar */} {/* Editor Toolbar */}
<div className="flex items-center justify-between px-3 py-2 bg-[#141517] border-b border-[#1f2022] flex-wrap gap-2"> <div className="flex items-center justify-between px-3 py-2 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2">
{/* Tabs */} {/* Tabs */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
@@ -403,8 +403,8 @@ export default function DocForm({
onClick={() => setActiveTab("write")} onClick={() => setActiveTab("write")}
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${ className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${
activeTab === "write" activeTab === "write"
? "bg-[#5e6ad2] text-white" ? "bg-[#5e6ad2] text-white shadow-sm"
: "text-[#8a8f98] hover:text-[#d0d6e0]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
> >
Write Write
@@ -414,8 +414,8 @@ export default function DocForm({
onClick={() => setActiveTab("preview")} onClick={() => setActiveTab("preview")}
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${ className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${
activeTab === "preview" activeTab === "preview"
? "bg-[#5e6ad2] text-white" ? "bg-[#5e6ad2] text-white shadow-sm"
: "text-[#8a8f98] hover:text-[#d0d6e0]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
> >
Live Preview Live Preview
@@ -424,11 +424,11 @@ export default function DocForm({
{/* Markdown Action Shortcuts */} {/* Markdown Action Shortcuts */}
{activeTab === "write" && ( {activeTab === "write" && (
<div className="flex items-center gap-1 text-xs text-[#8a8f98]"> <div className="flex items-center gap-1 text-xs text-[var(--text-muted)]">
<button <button
type="button" type="button"
onClick={() => insertMarkdown("**", "**")} onClick={() => insertMarkdown("**", "**")}
className="px-2 py-0.5 hover:bg-[#23252a] rounded text-[#d0d6e0]" className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
title="Bold" title="Bold"
> >
<b>B</b> <b>B</b>
@@ -436,7 +436,7 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => insertMarkdown("*", "*")} onClick={() => insertMarkdown("*", "*")}
className="px-2 py-0.5 hover:bg-[#23252a] rounded text-[#d0d6e0]" className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
title="Italic" title="Italic"
> >
<i>I</i> <i>I</i>
@@ -444,7 +444,7 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => insertMarkdown("## ")} onClick={() => insertMarkdown("## ")}
className="px-2 py-0.5 hover:bg-[#23252a] rounded text-[#d0d6e0]" className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
title="Heading 2" title="Heading 2"
> >
H2 H2
@@ -452,7 +452,7 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => insertMarkdown("```\n", "\n```")} onClick={() => insertMarkdown("```\n", "\n```")}
className="px-2 py-0.5 hover:bg-[#23252a] rounded text-[#d0d6e0] font-mono" className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] font-mono"
title="Code Block" title="Code Block"
> >
{"</>"} {"</>"}
@@ -460,7 +460,7 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => insertMarkdown("[", "](https://)")} onClick={() => insertMarkdown("[", "](https://)")}
className="px-2 py-0.5 hover:bg-[#23252a] rounded text-[#d0d6e0]" className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
title="Link" title="Link"
> >
Link Link
@@ -468,7 +468,7 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => insertMarkdown("> [!NOTE]\n> ")} onClick={() => insertMarkdown("> [!NOTE]\n> ")}
className="px-2 py-0.5 hover:bg-[#23252a] rounded text-[#d0d6e0]" className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
title="Note Callout" title="Note Callout"
> >
Note Note
@@ -483,16 +483,16 @@ export default function DocForm({
ref={textareaRef} ref={textareaRef}
value={body} value={body}
onChange={(e) => setBody(e.target.value)} onChange={(e) => setBody(e.target.value)}
className="w-full h-96 p-4 bg-transparent text-[#f7f8f8] placeholder-[#62666d] font-mono text-sm focus:outline-none resize-y leading-relaxed" className="w-full h-96 p-4 bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] font-mono text-sm focus:outline-none resize-y leading-relaxed"
placeholder="# Write your markdown document here..." placeholder="# Write your markdown document here..."
required required
/> />
) : ( ) : (
<div className="p-6 h-96 overflow-y-auto bg-[#08090a]"> <div className="p-6 h-96 overflow-y-auto bg-[var(--bg-app)]">
{body.trim() ? ( {body.trim() ? (
<Markdown source={body} /> <Markdown source={body} />
) : ( ) : (
<p className="text-xs text-[#62666d] italic">Nothing to preview yet.</p> <p className="text-xs text-[var(--text-dim)] italic">Nothing to preview yet.</p>
)} )}
</div> </div>
)} )}
@@ -513,7 +513,7 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => router.back()} onClick={() => router.back()}
className="px-4 py-2.5 border border-[#23252a] hover:border-[#383b42] text-[#8a8f98] hover:text-[#d0d6e0] rounded-lg text-sm transition-colors" className="px-4 py-2.5 border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] rounded-lg text-sm transition-colors shadow-sm"
> >
Cancel Cancel
</button> </button>
+16 -16
View File
@@ -12,7 +12,7 @@ export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return ( return (
<header className="sticky top-0 z-30 border-b border-[#1f2022] glass-nav"> <header className="sticky top-0 z-30 border-b border-[var(--border-color)] glass-nav">
<div className="mx-auto max-w-7xl px-4 sm:px-6 h-14 flex items-center justify-between gap-4"> <div className="mx-auto max-w-7xl px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
{/* Brand */} {/* Brand */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -20,11 +20,11 @@ export default function Header() {
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[#5e6ad2] to-[#7170ff] flex items-center justify-center text-white font-bold text-xs shadow-md shadow-[#5e6ad2]/20 group-hover:scale-105 transition-transform"> <div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[#5e6ad2] to-[#7170ff] flex items-center justify-center text-white font-bold text-xs shadow-md shadow-[#5e6ad2]/20 group-hover:scale-105 transition-transform">
M M
</div> </div>
<span className="font-semibold text-[#f7f8f8] text-base tracking-tight group-hover:text-white transition-colors"> <span className="font-semibold text-[var(--text-primary)] text-base tracking-tight transition-colors">
MCPedia MCPedia
</span> </span>
</Link> </Link>
<span className="hidden sm:inline-block text-[10px] uppercase font-mono px-1.5 py-0.5 rounded bg-[#141517] border border-[#23252a] text-[#8a8f98]"> <span className="hidden sm:inline-block text-[10px] uppercase font-mono px-1.5 py-0.5 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)]">
v0.1.0 v0.1.0
</span> </span>
</div> </div>
@@ -39,11 +39,11 @@ export default function Header() {
href={`/${s.id}`} href={`/${s.id}`}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${ className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
isActive isActive
? "text-[#f7f8f8] bg-[#141517] border border-[#23252a]" ? "text-[var(--text-primary)] bg-[var(--bg-elevated)] border border-[var(--border-color)]"
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#141517]/50" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface-hover)]"
}`} }`}
> >
<span className="mr-1.5 opacity-70">{s.icon}</span> <span className="mr-1.5 opacity-80">{s.icon}</span>
{s.label} {s.label}
</Link> </Link>
); );
@@ -56,7 +56,7 @@ export default function Header() {
<Link <Link
href="/search" href="/search"
className="md:hidden p-1.5 text-[#8a8f98] hover:text-[#f7f8f8] rounded-lg hover:bg-[#141517] transition-colors" className="md:hidden p-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-elevated)] transition-colors"
title="Search" title="Search"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -66,7 +66,7 @@ export default function Header() {
<Link <Link
href="/create" href="/create"
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1 bg-[#5e6ad2] hover:bg-[#7170ff] text-white text-xs font-medium rounded-lg transition-all shadow-sm hover:shadow-[#5e6ad2]/20" className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1 bg-[#5e6ad2] hover:bg-[#7170ff] text-white text-xs font-medium rounded-lg transition-all shadow-sm"
> >
<span>+</span> <span>+</span>
<span>New Doc</span> <span>New Doc</span>
@@ -76,21 +76,21 @@ export default function Header() {
href="/login" href="/login"
className={`px-2.5 py-1 text-xs rounded-lg transition-colors ${ className={`px-2.5 py-1 text-xs rounded-lg transition-colors ${
pathname === "/login" pathname === "/login"
? "text-[#f7f8f8] bg-[#141517]" ? "text-[var(--text-primary)] bg-[var(--bg-elevated)] font-medium"
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#141517]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`} }`}
> >
Login Login
</Link> </Link>
<div className="border-l border-[#1f2022] pl-2.5 flex items-center"> <div className="border-l border-[var(--border-color)] pl-2.5 flex items-center">
<ThemeToggle /> <ThemeToggle />
</div> </div>
{/* Mobile menu toggle */} {/* Mobile menu toggle */}
<button <button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-1 text-[#8a8f98] hover:text-[#f7f8f8]" className="md:hidden p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)]"
aria-label="Toggle navigation" aria-label="Toggle navigation"
> >
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -106,23 +106,23 @@ export default function Header() {
{/* Mobile Drawer */} {/* Mobile Drawer */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="md:hidden border-t border-[#1f2022] bg-[#0c0d0e] px-4 py-3 space-y-1 animate-fade-in"> <div className="md:hidden border-t border-[var(--border-color)] bg-[var(--bg-surface)] px-4 py-3 space-y-1 animate-fade-in shadow-lg">
{SECTIONS.map((s) => ( {SECTIONS.map((s) => (
<Link <Link
key={s.id} key={s.id}
href={`/${s.id}`} href={`/${s.id}`}
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-[#d0d6e0] hover:bg-[#141517] hover:text-[#f7f8f8]" className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] hover:text-[var(--text-primary)]"
> >
<span>{s.icon}</span> <span>{s.icon}</span>
<span>{s.label}</span> <span>{s.label}</span>
</Link> </Link>
))} ))}
<div className="pt-2 border-t border-[#1f2022] flex gap-2"> <div className="pt-2 border-t border-[var(--border-color)] flex gap-2">
<Link <Link
href="/search" href="/search"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
className="flex-1 text-center py-2 text-xs bg-[#141517] text-[#d0d6e0] rounded-lg" className="flex-1 text-center py-2 text-xs bg-[var(--bg-elevated)] text-[var(--text-secondary)] rounded-lg font-medium border border-[var(--border-color)]"
> >
Search Search
</Link> </Link>
+9 -9
View File
@@ -60,9 +60,9 @@ export default function McpConfigSnippet() {
} }
return ( return (
<div className="bg-[#0c0d0e] border border-[#1f2022] rounded-xl overflow-hidden shadow-lg"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-md">
{/* Header bar with tabs */} {/* Header bar with tabs */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[#141517] border-b border-[#1f2022] flex-wrap gap-2"> <div className="flex items-center justify-between px-4 py-2.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{CONFIGS.map((c) => ( {CONFIGS.map((c) => (
<button <button
@@ -71,7 +71,7 @@ export default function McpConfigSnippet() {
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${ className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${
activeTab === c.id activeTab === c.id
? "bg-[#5e6ad2] text-white shadow-sm" ? "bg-[#5e6ad2] text-white shadow-sm"
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#1b1d20]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
}`} }`}
> >
{c.label} {c.label}
@@ -82,14 +82,14 @@ export default function McpConfigSnippet() {
<button <button
onClick={handleCopy} onClick={handleCopy}
type="button" type="button"
className="flex items-center gap-1.5 text-xs text-[#8a8f98] hover:text-white bg-[#1a1b1d] hover:bg-[#23252a] px-2.5 py-1 rounded border border-[#27292d] transition-all" className="flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] transition-all shadow-sm"
> >
{copied ? ( {copied ? (
<> <>
<svg className="w-3.5 h-3.5 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span className="text-emerald-400 font-medium">Copied!</span> <span className="text-emerald-500 font-medium">Copied!</span>
</> </>
) : ( ) : (
<> <>
@@ -103,12 +103,12 @@ export default function McpConfigSnippet() {
</div> </div>
{/* Code preview */} {/* Code preview */}
<pre className="p-4 text-xs font-mono text-[#d0d6e0] overflow-x-auto leading-relaxed"> <pre className="p-4 text-xs font-mono text-[var(--pre-text)] bg-[var(--code-block-bg)] overflow-x-auto leading-relaxed">
<code>{current.content}</code> <code>{current.content}</code>
</pre> </pre>
<div className="px-4 py-2 bg-[#090a0b] border-t border-[#1a1b1d] text-[11px] text-[#62666d]"> <div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)]">
Config location: <span className="font-mono text-[#8a8f98]">{current.filename}</span> Config location: <span className="font-mono text-[var(--text-muted)]">{current.filename}</span>
</div> </div>
</div> </div>
); );
+16 -16
View File
@@ -102,14 +102,14 @@ function TreeNodeItem({
const isCollapsed = collapsedMap[node.slug] ?? false; const isCollapsed = collapsedMap[node.slug] ?? false;
return ( return (
<li className={`select-none ${depth > 0 ? "ml-3 pl-2 border-l border-[#1f2022]" : ""}`}> <li className={`select-none ${depth > 0 ? "ml-3 pl-2 border-l border-[var(--border-color)]" : ""}`}>
<div <div
className={`group flex items-center justify-between gap-1.5 text-xs py-1.5 px-2 rounded-md transition-all ${ className={`group flex items-center justify-between gap-1.5 text-xs py-1.5 px-2 rounded-md transition-all ${
isActive isActive
? "text-[#7170ff] bg-[#5e6ad2]/15 font-medium border border-[#5e6ad2]/30 shadow-sm" ? "text-[#5e6ad2] dark:text-[#7170ff] bg-[#5e6ad2]/15 font-semibold border border-[#5e6ad2]/30 shadow-sm"
: isParentOfActive : isParentOfActive
? "text-[#d0d6e0] bg-[#141517]/80" ? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#141517]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`} }`}
> >
<Link <Link
@@ -125,7 +125,7 @@ function TreeNodeItem({
{isFolder && ( {isFolder && (
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<span className="text-[10px] text-[#62666d] bg-[#191a1b] px-1 py-0.2 rounded font-mono"> <span className="text-[10px] text-[var(--text-muted)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-1 py-0.2 rounded font-mono">
{node.docCount} {node.docCount}
</span> </span>
<button <button
@@ -135,7 +135,7 @@ function TreeNodeItem({
e.stopPropagation(); e.stopPropagation();
toggleCollapse(node.slug); toggleCollapse(node.slug);
}} }}
className="p-0.5 text-[#62666d] hover:text-[#d0d6e0] rounded hover:bg-[#23252a] transition-transform" className="p-0.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded hover:bg-[var(--bg-elevated-hover)] transition-transform"
aria-label={isCollapsed ? "Expand folder" : "Collapse folder"} aria-label={isCollapsed ? "Expand folder" : "Collapse folder"}
> >
<svg <svg
@@ -210,10 +210,10 @@ export default function Sidebar() {
value={filterText} value={filterText}
onChange={(e) => setFilterText(e.target.value)} onChange={(e) => setFilterText(e.target.value)}
placeholder="Filter docs..." placeholder="Filter docs..."
className="w-full pl-7 pr-3 py-1.5 bg-[#141517] border border-[#23252a] hover:border-[#383b42] rounded-md text-xs text-[#f7f8f8] placeholder-[#62666d] focus:outline-none focus:border-[#5e6ad2] transition-colors" className="w-full pl-7 pr-3 py-1.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-md text-xs text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] transition-colors"
/> />
<svg <svg
className="w-3.5 h-3.5 text-[#62666d] absolute left-2 top-2.5" className="w-3.5 h-3.5 text-[var(--text-muted)] absolute left-2 top-2.5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -223,7 +223,7 @@ export default function Sidebar() {
{filterText && ( {filterText && (
<button <button
onClick={() => setFilterText("")} onClick={() => setFilterText("")}
className="absolute right-2 top-2 text-xs text-[#62666d] hover:text-[#d0d6e0]" className="absolute right-2 top-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)]"
> >
✕ ✕
</button> </button>
@@ -232,7 +232,7 @@ export default function Sidebar() {
</div> </div>
{/* Summary count */} {/* Summary count */}
<div className="flex items-center justify-between px-1 mb-3 text-[11px] text-[#62666d]"> <div className="flex items-center justify-between px-1 mb-3 text-[11px] text-[var(--text-dim)]">
<span className="uppercase tracking-wider font-mono">Documentation</span> <span className="uppercase tracking-wider font-mono">Documentation</span>
<span>{filteredDocs.length} items</span> <span>{filteredDocs.length} items</span>
</div> </div>
@@ -247,20 +247,20 @@ export default function Sidebar() {
const isSectionActive = pathname === `/${id}` || pathname.startsWith(`/${id}/`); const isSectionActive = pathname === `/${id}` || pathname.startsWith(`/${id}/`);
return ( return (
<div key={id} className="border-b border-[#141516] pb-3 last:border-0"> <div key={id} className="border-b border-[var(--border-color)] pb-3 last:border-0">
<Link <Link
href={`/${id}`} href={`/${id}`}
className={`flex items-center justify-between gap-1.5 px-1 py-1 mb-1 rounded text-xs font-semibold uppercase tracking-wider transition-colors ${ className={`flex items-center justify-between gap-1.5 px-1 py-1 mb-1 rounded text-xs font-semibold uppercase tracking-wider transition-colors ${
isSectionActive isSectionActive
? "text-[#7170ff]" ? "text-[#5e6ad2] dark:text-[#7170ff]"
: "text-[#8a8f98] hover:text-[#d0d6e0]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span>{icon}</span> <span>{icon}</span>
<span>{label}</span> <span>{label}</span>
</div> </div>
<span className="text-[10px] text-[#62666d] font-normal font-mono"> <span className="text-[10px] text-[var(--text-dim)] font-normal font-mono">
{sectionDocs.length} {sectionDocs.length}
</span> </span>
</Link> </Link>
@@ -283,10 +283,10 @@ export default function Sidebar() {
</div> </div>
{/* Bottom quick links */} {/* Bottom quick links */}
<div className="pt-3 border-t border-[#1f2022] mt-auto"> <div className="pt-3 border-t border-[var(--border-color)] mt-auto">
<Link <Link
href="/create" href="/create"
className="flex items-center justify-center gap-1.5 w-full py-1.5 text-xs bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] text-[#d0d6e0] hover:text-white rounded-md transition-colors font-medium" className="flex items-center justify-center gap-1.5 w-full py-1.5 text-xs bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-md transition-colors font-medium"
> >
<span>+ Create New Document</span> <span>+ Create New Document</span>
</Link> </Link>
+6 -6
View File
@@ -60,16 +60,16 @@ export default function TOC({ source }: { source: string }) {
return ( return (
<nav className="text-xs"> <nav className="text-xs">
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-[#1f2022]"> <div className="flex items-center gap-2 mb-3 pb-2 border-b border-[var(--border-color)]">
<svg className="w-3.5 h-3.5 text-[#7170ff]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5 text-[#5e6ad2] dark:text-[#7170ff]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" />
</svg> </svg>
<span className="font-semibold uppercase tracking-wider text-[#8a8f98] text-[11px]"> <span className="font-semibold uppercase tracking-wider text-[var(--text-muted)] text-[11px]">
On this page On this page
</span> </span>
</div> </div>
<ul className="space-y-1.5 border-l border-[#1f2022] pl-2.5"> <ul className="space-y-1.5 border-l border-[var(--border-color)] pl-2.5">
{toc.map((entry) => { {toc.map((entry) => {
const isActive = activeId === entry.id; const isActive = activeId === entry.id;
return ( return (
@@ -92,8 +92,8 @@ export default function TOC({ source }: { source: string }) {
}} }}
className={`block py-0.5 transition-colors line-clamp-1 ${ className={`block py-0.5 transition-colors line-clamp-1 ${
isActive isActive
? "text-[#7170ff] font-medium translate-x-0.5" ? "text-[#5e6ad2] dark:text-[#7170ff] font-semibold translate-x-0.5"
: "text-[#8a8f98] hover:text-[#d0d6e0]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
title={entry.text} title={entry.text}
> >
+36 -10
View File
@@ -4,33 +4,59 @@ import { useEffect, useState } from "react";
const STORAGE_KEY = "mcpedia-theme"; const STORAGE_KEY = "mcpedia-theme";
/** Dark mode toggle — persisted in localStorage, defaults to system preference. */
export default function ThemeToggle() { export default function ThemeToggle() {
const [dark, setDark] = useState(false); const [mounted, setMounted] = useState(false);
const [isDark, setIsDark] = useState(true);
useEffect(() => { useEffect(() => {
setMounted(true);
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STORAGE_KEY);
const initial = const darkActive =
stored === "dark" || stored === "dark" ||
(stored === null && (stored === null &&
window.matchMedia("(prefers-color-scheme: dark)").matches); window.matchMedia("(prefers-color-scheme: dark)").matches);
setDark(initial); setIsDark(darkActive);
if (darkActive) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}, []); }, []);
function toggle() { function toggle() {
const next = !dark; const next = !isDark;
setDark(next); setIsDark(next);
localStorage.setItem(STORAGE_KEY, next ? "dark" : "light"); localStorage.setItem(STORAGE_KEY, next ? "dark" : "light");
document.documentElement.classList.toggle("dark", next); if (next) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
if (!mounted) {
return (
<div className="w-8 h-8 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] opacity-50" />
);
} }
return ( return (
<button <button
onClick={toggle} onClick={toggle}
aria-label="Toggle dark mode" type="button"
className="text-[#d0d6e0] hover:text-[#f7f8f8] p-1 rounded transition-colors" aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
className="p-1.5 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)] border border-transparent hover:border-[var(--border-color)] transition-all flex items-center justify-center"
> >
{dark ? "☀️" : "🌙"} {isDark ? (
<svg className="w-4 h-4 text-amber-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
</svg>
) : (
<svg className="w-4 h-4 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
</svg>
)}
</button> </button>
); );
} }
+7 -7
View File
@@ -19,23 +19,23 @@ export default async function CreatePage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-[#62666d]"> <nav className="flex items-center gap-2 text-xs text-[var(--text-dim)]">
<Link href="/" className="hover:text-[#d0d6e0] transition-colors"> <Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
MCPedia MCPedia
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-[#8a8f98] font-medium">Create Document</span> <span className="text-[var(--text-muted)] font-medium">Create Document</span>
</nav> </nav>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between pb-4 border-b border-[#1f2022]"> <div className="flex items-center justify-between pb-4 border-b border-[var(--border-color)]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-[#5e6ad2]/15 border border-[#5e6ad2]/30 flex items-center justify-center text-xl text-[#7170ff]"> <div className="w-10 h-10 rounded-lg bg-[#5e6ad2]/15 border border-[#5e6ad2]/30 flex items-center justify-center text-xl text-[#5e6ad2] dark:text-[#7170ff]">
✏️ ✏️
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-[#f7f8f8] tracking-tight">Create Document</h1> <h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">Create Document</h1>
<p className="text-xs text-[#8a8f98] mt-0.5"> <p className="text-xs text-[var(--text-muted)] mt-0.5">
Writes markdown file to disk, records revision, and computes 2048-dim vector embeddings. Writes markdown file to disk, records revision, and computes 2048-dim vector embeddings.
</p> </p>
</div> </div>
+113 -42
View File
@@ -1,43 +1,102 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
/* Modern Linear / Vercel style dark-mode-first tokens */ /* Light mode defaults (:root) */
@custom-variant dark (&:where(.dark, .dark *)); :root {
color-scheme: light;
--bg-app: #f8fafc;
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--bg-elevated: #f1f5f9;
--bg-elevated-hover: #e2e8f0;
--border-color: #e2e8f0;
--border-subtle: #edf2f7;
--border-card: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #334155;
--text-muted: #64748b;
--text-dim: #94a3b8;
--brand: #5e6ad2;
--accent: #4f46e5;
--accent-hover: #4338ca;
--nav-bg: rgba(255, 255, 255, 0.88);
--code-bg: #f1f5f9;
--code-block-bg: #0f172a;
--pre-text: #f8fafc;
--table-zebra: rgba(241, 245, 249, 0.6);
--scrollbar-thumb: #cbd5e1;
--scrollbar-thumb-hover: #94a3b8;
--shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05);
}
/* Dark mode overrides (html.dark) */
html.dark {
color-scheme: dark;
--bg-app: #08090a;
--bg-surface: #0f1011;
--bg-surface-hover: #131415;
--bg-elevated: #141517;
--bg-elevated-hover: #1b1d20;
--border-color: #1f2022;
--border-subtle: rgba(255, 255, 255, 0.06);
--border-card: #1f2022;
--text-primary: #f7f8f8;
--text-secondary: #d0d6e0;
--text-muted: #8a8f98;
--text-dim: #62666d;
--brand: #5e6ad2;
--accent: #7170ff;
--accent-hover: #828fff;
--nav-bg: rgba(8, 9, 10, 0.85);
--code-bg: #141517;
--code-block-bg: #0c0d0e;
--pre-text: #e2e4e7;
--table-zebra: rgba(255, 255, 255, 0.02);
--scrollbar-thumb: #23252a;
--scrollbar-thumb-hover: #383b42;
--shadow-card: none;
}
@theme inline { @theme inline {
--font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace; --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
--color-background: #08090a; --color-background: var(--bg-app);
--color-surface: #0f1011; --color-surface: var(--bg-surface);
--color-elevated: #141517; --color-surface-hover: var(--bg-surface-hover);
--color-elevated-hover: #1b1d20; --color-elevated: var(--bg-elevated);
--color-border: #1f2022; --color-elevated-hover: var(--bg-elevated-hover);
--color-border-subtle: rgba(255, 255, 255, 0.06); --color-border: var(--border-color);
--color-border-standard: rgba(255, 255, 255, 0.1); --color-border-subtle: var(--border-subtle);
--color-text-primary: #f7f8f8; --color-text-primary: var(--text-primary);
--color-text-secondary: #d0d6e0; --color-text-secondary: var(--text-secondary);
--color-text-tertiary: #8a8f98; --color-text-tertiary: var(--text-muted);
--color-text-quaternary: #62666d; --color-text-quaternary: var(--text-dim);
--color-brand: #5e6ad2; --color-brand: var(--brand);
--color-accent: #7170ff; --color-accent: var(--accent);
--color-accent-hover: #828fff; --color-accent-hover: var(--accent-hover);
} }
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
color-scheme: dark;
} }
body { body {
background: var(--color-background); background-color: var(--bg-app);
color: var(--color-text-secondary); color: var(--text-secondary);
font-family: var(--font-sans); font-family: var(--font-sans);
font-feature-settings: "cv01", "ss03"; font-feature-settings: "cv01", "ss03";
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
transition: background-color 0.2s ease, color 0.2s ease;
} }
/* Custom scrollbar */ /* Custom scrollbar */
@@ -51,29 +110,41 @@ body {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #23252a; background: var(--scrollbar-thumb);
border-radius: 9999px; border-radius: 9999px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #383b42; background: var(--scrollbar-thumb-hover);
} }
/* Glassmorphism helpers */ /* Glassmorphism helpers */
.glass-panel { .glass-panel {
background: rgba(15, 16, 17, 0.75); background: var(--bg-surface);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid var(--border-color);
} }
.glass-nav { .glass-nav {
background: rgba(8, 9, 10, 0.85); background: var(--nav-bg);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
} }
/* Prose markdown typography */ /* Prose markdown typography for both Light and Dark modes */
.prose {
color: var(--text-secondary) !important;
}
.prose h1, .prose h2, .prose h3, .prose h4, .prose strong {
color: var(--text-primary) !important;
}
.prose a {
color: var(--accent) !important;
}
.prose table { .prose table {
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
@@ -82,19 +153,19 @@ body {
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
border: 1px solid #1f2022; border: 1px solid var(--border-color);
} }
.prose th, .prose th,
.prose td { .prose td {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
text-align: left; text-align: left;
border-bottom: 1px solid #1f2022; border-bottom: 1px solid var(--border-color);
} }
.prose th { .prose th {
background: #141517; background: var(--bg-elevated);
color: #f7f8f8; color: var(--text-primary);
font-weight: 600; font-weight: 600;
font-size: 0.8125rem; font-size: 0.8125rem;
text-transform: uppercase; text-transform: uppercase;
@@ -102,20 +173,20 @@ body {
} }
.prose td { .prose td {
color: #d0d6e0; color: var(--text-secondary);
} }
.prose tbody tr:last-child td { .prose tbody tr:last-child td {
border-bottom: none; border-bottom: none;
} }
.prose tbody tr:hover { .prose tbody tr:nth-child(even) {
background: rgba(255, 255, 255, 0.02); background: var(--table-zebra);
} }
.prose pre { .prose pre {
background: #0d0e0f !important; background: var(--code-block-bg) !important;
border: 1px solid #1f2022; border: 1px solid var(--border-color);
border-radius: 0.625rem; border-radius: 0.625rem;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
overflow-x: auto; overflow-x: auto;
@@ -125,15 +196,15 @@ body {
.prose code { .prose code {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.85em; font-size: 0.85em;
color: #e2e4e7;
} }
.prose :not(pre) > code { .prose :not(pre) > code {
background: #141517; background: var(--code-bg);
border: 1px solid rgba(255, 255, 255, 0.08); color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 0.15rem 0.35rem; padding: 0.15rem 0.35rem;
border-radius: 0.25rem; border-radius: 0.25rem;
font-weight: 400; font-weight: 500;
} }
.prose :not(pre) > code::before, .prose :not(pre) > code::before,
@@ -142,17 +213,17 @@ body {
} }
.prose blockquote { .prose blockquote {
border-left: 3px solid #5e6ad2; border-left: 3px solid var(--brand);
background: rgba(94, 106, 210, 0.05); background: rgba(94, 106, 210, 0.08);
border-radius: 0 0.375rem 0.375rem 0; border-radius: 0 0.375rem 0.375rem 0;
margin: 1.5em 0; margin: 1.5em 0;
padding: 0.75em 1.25em; padding: 0.75em 1.25em;
font-style: normal; font-style: normal;
color: #a0a4a8; color: var(--text-muted);
} }
.prose hr { .prose hr {
border-color: #1f2022; border-color: var(--border-color);
margin: 2.5rem 0; margin: 2.5rem 0;
} }
+22 -3
View File
@@ -15,8 +15,27 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" className="h-full scroll-smooth"> <html lang="en" className="h-full scroll-smooth" suppressHydrationWarning>
<body className="min-h-screen bg-[#08090a] text-[#e2e4e7] font-sans antialiased selection:bg-[#5e6ad2]/30 selection:text-white flex flex-col"> <head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var stored = localStorage.getItem('mcpedia-theme');
var isDark = stored === 'dark' || (stored === null && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch(e) {}
})();
`,
}}
/>
</head>
<body className="min-h-screen bg-[var(--bg-app)] text-[var(--text-secondary)] font-sans antialiased selection:bg-[#5e6ad2]/30 selection:text-white flex flex-col transition-colors duration-150">
<div hidden> <div hidden>
<div id="a11y-live-region" aria-live="polite" aria-atomic="true" /> <div id="a11y-live-region" aria-live="polite" aria-atomic="true" />
</div> </div>
@@ -25,7 +44,7 @@ export default function RootLayout({
<div className="mx-auto max-w-7xl w-full flex-1 flex flex-col"> <div className="mx-auto max-w-7xl w-full flex-1 flex flex-col">
<div className="flex flex-1"> <div className="flex flex-1">
<aside className="hidden lg:block w-72 shrink-0 border-r border-[#1f2022] sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto"> <aside className="hidden lg:block w-72 shrink-0 border-r border-[var(--border-color)] sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto bg-[var(--bg-app)]">
<Sidebar /> <Sidebar />
</aside> </aside>
<main className="flex-1 min-w-0"> <main className="flex-1 min-w-0">
+10 -10
View File
@@ -38,7 +38,7 @@ export default function LoginPage() {
return ( return (
<div className="flex min-h-[65vh] items-center justify-center px-4"> <div className="flex min-h-[65vh] items-center justify-center px-4">
<div className="w-full max-w-md bg-[#0f1011] border border-[#1f2022] rounded-2xl p-6 sm:p-8 shadow-2xl relative overflow-hidden"> <div className="w-full max-w-md bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-8 shadow-2xl relative overflow-hidden">
{/* Top decorative glow */} {/* Top decorative glow */}
<div className="absolute -top-12 left-1/2 -translate-x-1/2 w-48 h-24 bg-[#5e6ad2]/20 blur-2xl pointer-events-none rounded-full" /> <div className="absolute -top-12 left-1/2 -translate-x-1/2 w-48 h-24 bg-[#5e6ad2]/20 blur-2xl pointer-events-none rounded-full" />
@@ -46,21 +46,21 @@ export default function LoginPage() {
<div className="w-12 h-12 rounded-xl bg-[#5e6ad2] text-white font-bold text-xl flex items-center justify-center mx-auto mb-3 shadow-lg shadow-[#5e6ad2]/25"> <div className="w-12 h-12 rounded-xl bg-[#5e6ad2] text-white font-bold text-xl flex items-center justify-center mx-auto mb-3 shadow-lg shadow-[#5e6ad2]/25">
M M
</div> </div>
<h1 className="text-2xl font-bold text-[#f7f8f8] tracking-tight">Admin Authentication</h1> <h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">Admin Authentication</h1>
<p className="text-xs text-[#8a8f98] mt-1.5"> <p className="text-xs text-[var(--text-muted)] mt-1.5">
Enter your admin password to create, edit, or delete documents. Enter your admin password to create, edit, or delete documents.
</p> </p>
</div> </div>
{error && ( {error && (
<div className="p-3.5 mb-6 rounded-lg border border-rose-500/30 bg-rose-500/10 text-rose-400 text-xs"> <div className="p-3.5 mb-6 rounded-lg border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-xs">
{error} {error}
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[#8a8f98] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Admin Password Admin Password
</label> </label>
<div className="relative"> <div className="relative">
@@ -69,14 +69,14 @@ export default function LoginPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password..." placeholder="Enter password..."
className="w-full px-3.5 py-2.5 bg-[#141517] border border-[#23252a] hover:border-[#383b42] rounded-lg text-sm text-[#f7f8f8] placeholder-[#62666d] focus:outline-none focus:border-[#5e6ad2] focus:ring-1 focus:ring-[#5e6ad2] transition-colors" className="w-full px-3.5 py-2.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-1 focus:ring-[#5e6ad2] transition-colors"
required required
autoFocus autoFocus
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-2.5 text-xs text-[#62666d] hover:text-[#d0d6e0]" className="absolute right-3 top-2.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)]"
> >
{showPassword ? "Hide" : "Show"} {showPassword ? "Hide" : "Show"}
</button> </button>
@@ -95,14 +95,14 @@ export default function LoginPage() {
</button> </button>
</form> </form>
<div className="mt-8 pt-6 border-t border-[#1a1b1d] flex items-center justify-between text-xs text-[#8a8f98]"> <div className="mt-8 pt-6 border-t border-[var(--border-color)] flex items-center justify-between text-xs text-[var(--text-muted)]">
<Link <Link
href="/" href="/"
className="hover:text-[#d0d6e0] transition-colors" className="hover:text-[var(--text-primary)] transition-colors"
> >
← Back to Home ← Back to Home
</Link> </Link>
<span className="text-[11px] text-[#62666d]">HTTP-only Secure Cookie</span> <span className="text-[11px] text-[var(--text-dim)]">HTTP-only Secure Cookie</span>
</div> </div>
</div> </div>
</div> </div>
+39 -39
View File
@@ -62,7 +62,7 @@ function buildFolderTree(docs: { slug: string; title: string }[], section: strin
const aFolder = a.children.length > 0 ? 0 : 1; const aFolder = a.children.length > 0 ? 0 : 1;
const bFolder = b.children.length > 0 ? 0 : 1; const bFolder = b.children.length > 0 ? 0 : 1;
if (aFolder !== bFolder) return aFolder - bFolder; if (aFolder !== bFolder) return aFolder - bFolder;
return a.title.localeCompare(b.title); return (a.title || a.name).localeCompare(b.title || b.name);
}); });
} }
@@ -86,16 +86,16 @@ function renderTree(nodes: TreeNode[], pathname: string) {
href={`/${node.slug}`} href={`/${node.slug}`}
className={`flex items-center gap-1.5 text-xs py-1 px-1.5 rounded transition-all ${ className={`flex items-center gap-1.5 text-xs py-1 px-1.5 rounded transition-all ${
pathname === `/${node.slug}` pathname === `/${node.slug}`
? "text-[#7170ff] bg-[#5e6ad2]/15 font-medium" ? "text-[#5e6ad2] dark:text-[#7170ff] bg-[#5e6ad2]/15 font-medium"
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#141517]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`} }`}
title={node.title} title={node.title}
> >
<span className="opacity-70">{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span> <span className="opacity-80">{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span>
<span className="truncate">{node.title || node.name}</span> <span className="truncate">{node.title || node.name}</span>
</Link> </Link>
{node.children.length > 0 && ( {node.children.length > 0 && (
<div className="ml-3.5 border-l border-[#1f2022] pl-2 mt-0.5"> <div className="ml-3.5 border-l border-[var(--border-color)] pl-2 mt-0.5">
{renderTree(node.children, pathname)} {renderTree(node.children, pathname)}
</div> </div>
)} )}
@@ -124,7 +124,7 @@ function SectionTree({ section, docs, pathname }: SectionTreeProps) {
{tree.length > 0 ? ( {tree.length > 0 ? (
renderTree(tree, pathname) renderTree(tree, pathname)
) : ( ) : (
<p className="text-xs text-[#62666d] pl-1">No documents</p> <p className="text-xs text-[var(--text-dim)] pl-1">No documents</p>
)} )}
</div> </div>
); );
@@ -144,19 +144,19 @@ export default async function HomePage() {
<div className="space-y-16"> <div className="space-y-16">
{/* Hero Section */} {/* Hero Section */}
<section className="relative pt-4 pb-2"> <section className="relative pt-4 pb-2">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#5e6ad2]/10 border border-[#5e6ad2]/25 text-[#7170ff] text-xs font-medium mb-6"> <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#5e6ad2]/10 border border-[#5e6ad2]/25 text-[#5e6ad2] dark:text-[#7170ff] text-xs font-medium mb-6">
<span className="w-2 h-2 rounded-full bg-[#5e6ad2] animate-pulse" /> <span className="w-2 h-2 rounded-full bg-[#5e6ad2] animate-pulse" />
<span>Model Context Protocol + Unified Knowledge Base</span> <span>Model Context Protocol + Unified Knowledge Base</span>
</div> </div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-[#f7f8f8] tracking-tight mb-5 leading-tight"> <h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-[var(--text-primary)] tracking-tight mb-5 leading-tight">
Knowledge Base for <br /> Knowledge Base for <br />
<span className="bg-gradient-to-r from-[#7170ff] via-[#828fff] to-white bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-[#5e6ad2] via-[#7170ff] to-[#4338ca] dark:to-white bg-clip-text text-transparent">
Humans and AI Agents Humans and AI Agents
</span> </span>
</h1> </h1>
<p className="text-base sm:text-lg text-[#8a8f98] max-w-2xl leading-relaxed mb-8"> <p className="text-base sm:text-lg text-[var(--text-muted)] max-w-2xl leading-relaxed mb-8">
A high-performance, content-first documentation system. Browse hierarchical notes and CTF writeups in your browser, or connect AI coding assistants directly via native MCP tools. A high-performance, content-first documentation system. Browse hierarchical notes and CTF writeups in your browser, or connect AI coding assistants directly via native MCP tools.
</p> </p>
@@ -171,38 +171,38 @@ export default async function HomePage() {
</Link> </Link>
<Link <Link
href="/search" href="/search"
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] hover:border-[#383b42] text-[#d0d6e0] hover:text-white rounded-lg font-medium text-sm transition-all" className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg font-medium text-sm transition-all shadow-sm"
> >
<svg className="w-4 h-4 text-[#8a8f98]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4 text-[var(--text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
<span>Hybrid Search</span> <span>Hybrid Search</span>
</Link> </Link>
<Link <Link
href="/create" href="/create"
className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] text-[#8a8f98] hover:text-[#d0d6e0] rounded-lg text-sm transition-colors" className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg text-sm transition-colors shadow-sm"
> >
<span>+ Create Document</span> <span>+ Create Document</span>
</Link> </Link>
</div> </div>
{/* Stats bar */} {/* Stats bar */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 p-4 bg-[#0c0d0e] border border-[#1f2022] rounded-xl max-w-3xl"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl max-w-3xl shadow-sm">
<div> <div>
<div className="text-xl font-semibold text-[#f7f8f8]">{all.length}</div> <div className="text-xl font-bold text-[var(--text-primary)]">{all.length}</div>
<div className="text-xs text-[#62666d]">Documents indexed</div> <div className="text-xs text-[var(--text-muted)]">Documents indexed</div>
</div> </div>
<div> <div>
<div className="text-xl font-semibold text-[#f7f8f8]">{SECTIONS.length}</div> <div className="text-xl font-bold text-[var(--text-primary)]">{SECTIONS.length}</div>
<div className="text-xs text-[#62666d]">Core sections</div> <div className="text-xs text-[var(--text-muted)]">Core sections</div>
</div> </div>
<div> <div>
<div className="text-xl font-semibold text-[#7170ff]">RRF Hybrid</div> <div className="text-xl font-bold text-[#5e6ad2] dark:text-[#7170ff]">RRF Hybrid</div>
<div className="text-xs text-[#62666d]">FTS + 2048-dim vectors</div> <div className="text-xs text-[var(--text-muted)]">FTS + 2048-dim vectors</div>
</div> </div>
<div> <div>
<div className="text-xl font-semibold text-emerald-400">10 MCP Tools</div> <div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">10 MCP Tools</div>
<div className="text-xs text-[#62666d]">Streamable HTTP :4021</div> <div className="text-xs text-[var(--text-muted)]">Streamable HTTP :4021</div>
</div> </div>
</div> </div>
</section> </section>
@@ -210,7 +210,7 @@ export default async function HomePage() {
{/* Browse by Section */} {/* Browse by Section */}
<section> <section>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase tracking-wider"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
Browse by Section Browse by Section
</h2> </h2>
</div> </div>
@@ -221,28 +221,28 @@ export default async function HomePage() {
return ( return (
<div <div
key={s.id} key={s.id}
className="group flex flex-col justify-between bg-[#0f1011] hover:bg-[#131415] border border-[#1f2022] hover:border-[#5e6ad2]/40 rounded-xl p-5 transition-all shadow-sm" className="group flex flex-col justify-between bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl p-5 transition-all shadow-sm"
> >
<div> <div>
<div className="flex items-center justify-between gap-2 mb-3"> <div className="flex items-center justify-between gap-2 mb-3">
<span className="text-2xl">{s.icon}</span> <span className="text-2xl">{s.icon}</span>
<Link <Link
href={`/${s.id}`} href={`/${s.id}`}
className="text-xs font-mono text-[#7170ff] hover:underline" className="text-xs font-mono text-[#5e6ad2] dark:text-[#7170ff] hover:underline"
> >
{sectionDocs.length} docs → {sectionDocs.length} docs →
</Link> </Link>
</div> </div>
<h3 className="font-semibold text-base text-[#f7f8f8] group-hover:text-[#7170ff] transition-colors mb-1.5"> <h3 className="font-semibold text-base text-[var(--text-primary)] group-hover:text-[#5e6ad2] dark:group-hover:text-[#7170ff] transition-colors mb-1.5">
{s.label} {s.label}
</h3> </h3>
<p className="text-xs text-[#8a8f98] mb-4 line-clamp-2 leading-relaxed"> <p className="text-xs text-[var(--text-muted)] mb-4 line-clamp-2 leading-relaxed">
{s.desc} {s.desc}
</p> </p>
</div> </div>
{/* Inline folder tree */} {/* Inline folder tree */}
<div className="pt-3 border-t border-[#1a1b1d]"> <div className="pt-3 border-t border-[var(--border-color)]">
<SectionTree <SectionTree
section={s.id} section={s.id}
docs={all.map((d) => ({ slug: d.slug, title: d.title }))} docs={all.map((d) => ({ slug: d.slug, title: d.title }))}
@@ -259,12 +259,12 @@ export default async function HomePage() {
{recent.length > 0 && ( {recent.length > 0 && (
<section> <section>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xs font-semibold text-[#8a8f98] uppercase tracking-wider"> <h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
Recently Updated Knowledge Recently Updated Knowledge
</h2> </h2>
<Link <Link
href="/docs" href="/docs"
className="text-xs text-[#7170ff] hover:underline" className="text-xs text-[#5e6ad2] dark:text-[#7170ff] hover:underline font-medium"
> >
View all ({all.length}) → View all ({all.length}) →
</Link> </Link>
@@ -277,20 +277,20 @@ export default async function HomePage() {
<Link <Link
key={d.slug} key={d.slug}
href={`/${d.slug}`} href={`/${d.slug}`}
className="group block bg-[#0f1011] hover:bg-[#131415] border border-[#1f2022] hover:border-[#5e6ad2]/40 rounded-xl p-4 transition-all shadow-sm" className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl p-4 transition-all shadow-sm"
> >
<div className="flex items-center justify-between gap-2 mb-2"> <div className="flex items-center justify-between gap-2 mb-2">
<span className="text-xs font-mono text-[#7170ff] uppercase"> <span className="text-xs font-mono text-[#5e6ad2] dark:text-[#7170ff] uppercase">
{sInfo?.icon} {d.section} {sInfo?.icon} {d.section}
</span> </span>
<time className="text-[11px] text-[#62666d] font-mono"> <time className="text-[11px] text-[var(--text-dim)] font-mono">
{new Date(d.updatedAt).toLocaleDateString(undefined, { {new Date(d.updatedAt).toLocaleDateString(undefined, {
month: "short", month: "short",
day: "numeric", day: "numeric",
})} })}
</time> </time>
</div> </div>
<h3 className="font-medium text-sm text-[#f7f8f8] group-hover:text-[#7170ff] transition-colors line-clamp-1 mb-2"> <h3 className="font-medium text-sm text-[var(--text-primary)] group-hover:text-[#5e6ad2] dark:group-hover:text-[#7170ff] transition-colors line-clamp-1 mb-2">
{d.title} {d.title}
</h3> </h3>
{d.tags.length > 0 && ( {d.tags.length > 0 && (
@@ -298,7 +298,7 @@ export default async function HomePage() {
{d.tags.slice(0, 3).map((t) => ( {d.tags.slice(0, 3).map((t) => (
<span <span
key={t} key={t}
className="text-[10px] px-1.5 py-0.25 bg-[#141517] border border-[#23252a] rounded text-[#8a8f98]" className="text-[10px] px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]"
> >
#{t} #{t}
</span> </span>
@@ -313,15 +313,15 @@ export default async function HomePage() {
)} )}
{/* MCP Agent Integration Showcase */} {/* MCP Agent Integration Showcase */}
<section className="border-t border-[#1f2022] pt-12 pb-6"> <section className="border-t border-[var(--border-color)] pt-12 pb-6">
<div className="mb-6"> <div className="mb-6">
<div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded bg-[#141517] border border-[#23252a] text-[#8a8f98] text-xs font-mono mb-2"> <div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] text-xs font-mono mb-2">
<span>⚡ Native Model Context Protocol (MCP)</span> <span>⚡ Native Model Context Protocol (MCP)</span>
</div> </div>
<h2 className="text-2xl font-bold text-[#f7f8f8] tracking-tight mb-2"> <h2 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight mb-2">
Connect AI Coding Assistants Connect AI Coding Assistants
</h2> </h2>
<p className="text-sm text-[#8a8f98] max-w-xl"> <p className="text-sm text-[var(--text-muted)] max-w-xl">
Give Claude Desktop, Cursor, Antigravity, or Zed direct access to your knowledge base via semantic search, read resources, and document tools. Give Claude Desktop, Cursor, Antigravity, or Zed direct access to your knowledge base via semantic search, read resources, and document tools.
</p> </p>
</div> </div>
+29 -29
View File
@@ -74,16 +74,16 @@ function SearchContent() {
<div className="max-w-4xl space-y-8"> <div className="max-w-4xl space-y-8">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-3xl font-bold text-[#f7f8f8] tracking-tight mb-2"> <h1 className="text-3xl font-bold text-[var(--text-primary)] tracking-tight mb-2">
Search Knowledge Base Search Knowledge Base
</h1> </h1>
<p className="text-sm text-[#8a8f98]"> <p className="text-sm text-[var(--text-muted)]">
Find answers across all documentation, CTF writeups, research notes, and semantic vector chunks. Find answers across all documentation, CTF writeups, research notes, and semantic vector chunks.
</p> </p>
</div> </div>
{/* Search Box & Controls */} {/* Search Box & Controls */}
<div className="bg-[#0f1011] border border-[#1f2022] rounded-xl p-4 sm:p-5 shadow-sm space-y-4"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-4 sm:p-5 shadow-sm space-y-4">
{/* Search Input */} {/* Search Input */}
<div className="relative"> <div className="relative">
<input <input
@@ -91,11 +91,11 @@ function SearchContent() {
value={query} value={query}
onChange={handleChange} onChange={handleChange}
placeholder="Search by keywords, challenge names, tools, concepts..." placeholder="Search by keywords, challenge names, tools, concepts..."
className="w-full pl-11 pr-10 py-3 bg-[#141517] border border-[#23252a] hover:border-[#383b42] rounded-lg text-[#f7f8f8] placeholder-[#62666d] focus:outline-none focus:border-[#5e6ad2] focus:ring-2 focus:ring-[#5e6ad2]/20 text-base transition-all" className="w-full pl-11 pr-10 py-3 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[#5e6ad2]/20 text-base transition-all"
autoFocus autoFocus
/> />
<svg <svg
className="w-5 h-5 text-[#7170ff] absolute left-3.5 top-3.5" className="w-5 h-5 text-[#5e6ad2] dark:text-[#7170ff] absolute left-3.5 top-3.5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -108,7 +108,7 @@ function SearchContent() {
setQuery(""); setQuery("");
setResults([]); setResults([]);
}} }}
className="absolute right-3.5 top-3.5 text-xs text-[#62666d] hover:text-[#d0d6e0] p-0.5" className="absolute right-3.5 top-3.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] p-0.5"
> >
✕ ✕
</button> </button>
@@ -116,10 +116,10 @@ function SearchContent() {
</div> </div>
{/* Search Mode & Section Filters */} {/* Search Mode & Section Filters */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2 border-t border-[#1a1b1d] text-xs"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2 border-t border-[var(--border-color)] text-xs">
{/* Mode Selector */} {/* Mode Selector */}
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[#62666d] mr-1">Algorithm:</span> <span className="text-[var(--text-muted)] mr-1">Algorithm:</span>
{[ {[
{ id: "hybrid", label: "⚡ Hybrid (RRF)", desc: "FTS + Vector Fusion" }, { id: "hybrid", label: "⚡ Hybrid (RRF)", desc: "FTS + Vector Fusion" },
{ id: "keyword", label: "🔍 Keyword", desc: "PostgreSQL FTS" }, { id: "keyword", label: "🔍 Keyword", desc: "PostgreSQL FTS" },
@@ -133,7 +133,7 @@ function SearchContent() {
className={`px-3 py-1 rounded-md border font-medium transition-all ${ className={`px-3 py-1 rounded-md border font-medium transition-all ${
mode === m.id mode === m.id
? "bg-[#5e6ad2] border-[#5e6ad2] text-white shadow-sm" ? "bg-[#5e6ad2] border-[#5e6ad2] text-white shadow-sm"
: "bg-[#141517] border-[#23252a] text-[#8a8f98] hover:text-[#d0d6e0]" : "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
> >
{m.label} {m.label}
@@ -148,8 +148,8 @@ function SearchContent() {
onClick={() => setSelectedSection("all")} onClick={() => setSelectedSection("all")}
className={`px-2.5 py-1 rounded-md text-xs transition-colors ${ className={`px-2.5 py-1 rounded-md text-xs transition-colors ${
selectedSection === "all" selectedSection === "all"
? "bg-[#23252a] text-white" ? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-medium"
: "text-[#62666d] hover:text-[#8a8f98]" : "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
}`} }`}
> >
All Sections All Sections
@@ -161,8 +161,8 @@ function SearchContent() {
onClick={() => setSelectedSection(s.id)} onClick={() => setSelectedSection(s.id)}
className={`px-2.5 py-1 rounded-md text-xs transition-colors ${ className={`px-2.5 py-1 rounded-md text-xs transition-colors ${
selectedSection === s.id selectedSection === s.id
? "bg-[#23252a] text-white" ? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-medium"
: "text-[#62666d] hover:text-[#8a8f98]" : "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
}`} }`}
> >
{s.label} {s.label}
@@ -174,7 +174,7 @@ function SearchContent() {
{/* Loading State */} {/* Loading State */}
{loading && ( {loading && (
<div className="flex items-center gap-3 py-8 text-[#8a8f98] text-sm"> <div className="flex items-center gap-3 py-8 text-[var(--text-muted)] text-sm">
<div className="w-5 h-5 border-2 border-[#5e6ad2] border-t-transparent rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-[#5e6ad2] border-t-transparent rounded-full animate-spin" />
<span>Searching knowledge base...</span> <span>Searching knowledge base...</span>
</div> </div>
@@ -182,10 +182,10 @@ function SearchContent() {
{/* Empty State */} {/* Empty State */}
{!loading && query && filteredResults.length === 0 && ( {!loading && query && filteredResults.length === 0 && (
<div className="text-center py-16 bg-[#0f1011] border border-[#1f2022] rounded-xl"> <div className="text-center py-16 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-sm">
<div className="text-3xl mb-3">🔍</div> <div className="text-3xl mb-3">🔍</div>
<h3 className="text-base font-medium text-[#f7f8f8] mb-1">No matching documents</h3> <h3 className="text-base font-semibold text-[var(--text-primary)] mb-1">No matching documents</h3>
<p className="text-xs text-[#8a8f98] max-w-sm mx-auto"> <p className="text-xs text-[var(--text-muted)] max-w-sm mx-auto">
Try switching algorithm mode to <strong>Hybrid</strong> or <strong>Semantic</strong>, or search with different keywords. Try switching algorithm mode to <strong>Hybrid</strong> or <strong>Semantic</strong>, or search with different keywords.
</p> </p>
</div> </div>
@@ -193,8 +193,8 @@ function SearchContent() {
{/* Initial Hint */} {/* Initial Hint */}
{!loading && !query && ( {!loading && !query && (
<div className="p-8 text-center bg-[#0c0d0e] border border-[#1f2022] rounded-xl space-y-4"> <div className="p-8 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-4 shadow-sm">
<p className="text-xs text-[#8a8f98]">Popular search queries:</p> <p className="text-xs text-[var(--text-muted)]">Popular search queries:</p>
<div className="flex justify-center flex-wrap gap-2 text-xs"> <div className="flex justify-center flex-wrap gap-2 text-xs">
{["stack alignment", "ret2win", "bullmq worker", "streamable http", "full-text search", "caddy reverse proxy"].map((term) => ( {["stack alignment", "ret2win", "bullmq worker", "streamable http", "full-text search", "caddy reverse proxy"].map((term) => (
<button <button
@@ -203,7 +203,7 @@ function SearchContent() {
setQuery(term); setQuery(term);
handleSearch(term, mode); handleSearch(term, mode);
}} }}
className="px-3 py-1 bg-[#141517] hover:bg-[#1b1d20] border border-[#23252a] hover:border-[#5e6ad2]/40 rounded-md text-[#d0d6e0] transition-colors" className="px-3 py-1 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
> >
{term} {term}
</button> </button>
@@ -215,7 +215,7 @@ function SearchContent() {
{/* Results List */} {/* Results List */}
{!loading && filteredResults.length > 0 && ( {!loading && filteredResults.length > 0 && (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between text-xs text-[#62666d]"> <div className="flex items-center justify-between text-xs text-[var(--text-dim)]">
<span>Found {filteredResults.length} result{filteredResults.length !== 1 ? "s" : ""}</span> <span>Found {filteredResults.length} result{filteredResults.length !== 1 ? "s" : ""}</span>
<span className="font-mono">Ranked by {mode} score</span> <span className="font-mono">Ranked by {mode} score</span>
</div> </div>
@@ -225,29 +225,29 @@ function SearchContent() {
<Link <Link
key={r.slug} key={r.slug}
href={`/${r.slug}`} href={`/${r.slug}`}
className="group block bg-[#0f1011] hover:bg-[#131415] border border-[#1f2022] hover:border-[#5e6ad2]/40 rounded-xl p-4 sm:p-5 transition-all shadow-sm" className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl p-4 sm:p-5 transition-all shadow-sm"
> >
<div className="flex items-center justify-between gap-3 mb-2"> <div className="flex items-center justify-between gap-3 mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-mono px-2 py-0.5 rounded bg-[#5e6ad2]/10 border border-[#5e6ad2]/30 text-[#7170ff] uppercase"> <span className="text-xs font-mono px-2 py-0.5 rounded bg-[#5e6ad2]/10 border border-[#5e6ad2]/30 text-[#5e6ad2] dark:text-[#7170ff] uppercase">
{r.section} {r.section}
</span> </span>
<span className="text-xs text-[#62666d] font-mono truncate"> <span className="text-xs text-[var(--text-dim)] font-mono truncate">
/{r.slug} /{r.slug}
</span> </span>
</div> </div>
<span className="text-xs font-mono text-[#8a8f98] bg-[#141517] px-2 py-0.5 rounded"> <span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-2 py-0.5 rounded">
Score: {r.score.toFixed(3)} Score: {r.score.toFixed(3)}
</span> </span>
</div> </div>
<h2 className="text-base font-semibold text-[#f7f8f8] group-hover:text-[#7170ff] transition-colors mb-2"> <h2 className="text-base font-semibold text-[var(--text-primary)] group-hover:text-[#5e6ad2] dark:group-hover:text-[#7170ff] transition-colors mb-2">
{r.title} {r.title}
</h2> </h2>
{r.snippet && ( {r.snippet && (
<p <p
className="text-xs text-[#8a8f98] leading-relaxed line-clamp-2 [&>mark]:bg-[#5e6ad2]/30 [&>mark]:text-white [&>mark]:px-1 [&>mark]:rounded [&>mark]:font-medium" className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[#5e6ad2]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:px-1 [&>mark]:rounded [&>mark]:font-medium"
dangerouslySetInnerHTML={{ __html: r.snippet }} dangerouslySetInnerHTML={{ __html: r.snippet }}
/> />
)} )}
@@ -265,8 +265,8 @@ export default function SearchPage() {
<Suspense <Suspense
fallback={ fallback={
<div className="max-w-4xl py-12"> <div className="max-w-4xl py-12">
<h1 className="text-2xl font-bold text-[#f7f8f8] mb-4">Search</h1> <h1 className="text-2xl font-bold text-[var(--text-primary)] mb-4">Search</h1>
<p className="text-sm text-[#8a8f98]">Loading search module...</p> <p className="text-sm text-[var(--text-muted)]">Loading search module...</p>
</div> </div>
} }
> >