feat(web): overhaul frontend with cyber-intelligence design and interactive topology
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
This commit is contained in:
@@ -11,6 +11,24 @@ import PdfExportView from "@/components/PdfExportView";
|
|||||||
import { classifyPath, extractFoldersForSection } from "@mcpedia/core";
|
import { classifyPath, extractFoldersForSection } from "@mcpedia/core";
|
||||||
import { getSectionMeta } from "@mcpedia/config";
|
import { getSectionMeta } from "@mcpedia/config";
|
||||||
import type { DocumentMeta } from "@mcpedia/core";
|
import type { DocumentMeta } from "@mcpedia/core";
|
||||||
|
import {
|
||||||
|
Folder,
|
||||||
|
FileText,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
Tag,
|
||||||
|
History,
|
||||||
|
FileDown,
|
||||||
|
ArrowLeft,
|
||||||
|
BookOpen,
|
||||||
|
Sparkles,
|
||||||
|
ShieldCheck,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Target,
|
||||||
|
Trophy,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -45,48 +63,45 @@ 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-[var(--border-color)]">
|
<div className="flex flex-wrap items-center gap-2 mt-4 pt-4 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-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-secondary)]";
|
let colorClass = "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-secondary)]";
|
||||||
let icon = "🏷️";
|
|
||||||
let displayValue: string;
|
let displayValue: string;
|
||||||
|
let IconComponent = Tag;
|
||||||
|
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
|
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
|
||||||
icon = "🎯";
|
IconComponent = Target;
|
||||||
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-600 dark: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-600 dark:text-rose-400";
|
: "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400";
|
||||||
icon = value ? "✓" : "✕";
|
IconComponent = value ? CheckCircle2 : XCircle;
|
||||||
displayValue = value ? "Solved" : "Pending";
|
displayValue = value ? "Solved" : "Pending";
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
|
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
|
||||||
icon = "🗂️";
|
IconComponent = Folder;
|
||||||
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-600 dark:text-emerald-400";
|
colorClass = "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400";
|
||||||
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-600 dark:text-amber-400";
|
colorClass = "bg-amber-500/10 border-amber-500/30 text-amber-600 dark:text-amber-400";
|
||||||
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-600 dark:text-rose-400";
|
colorClass = "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400";
|
||||||
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-600 dark:text-indigo-300";
|
colorClass = "bg-indigo-500/10 border-indigo-500/30 text-indigo-600 dark:text-indigo-300";
|
||||||
icon = "⚡";
|
IconComponent = Zap;
|
||||||
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-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
|
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
|
||||||
icon = "🏆";
|
IconComponent = Trophy;
|
||||||
displayValue = String(value);
|
displayValue = String(value);
|
||||||
} else {
|
} else {
|
||||||
displayValue = String(value);
|
displayValue = String(value);
|
||||||
@@ -96,11 +111,11 @@ function CustomFieldBadges({
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={key}
|
key={key}
|
||||||
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-3 py-1 border rounded-lg text-xs font-mono transition-colors ${colorClass}`}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] opacity-70">{icon}</span>
|
<IconComponent className="w-3.5 h-3.5 opacity-80" />
|
||||||
<span className="text-[var(--text-muted)] font-sans">{label}:</span>
|
<span className="text-[var(--text-muted)] font-sans">{label}:</span>
|
||||||
<span className="font-medium">{displayValue}</span>
|
<span className="font-semibold">{displayValue}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -146,9 +161,9 @@ function FolderIndexPage({
|
|||||||
const folderName = slug.split("/").pop()!.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
const folderName = slug.split("/").pop()!.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-8">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb Navigation */}
|
||||||
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] mb-6 flex-wrap">
|
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] flex-wrap">
|
||||||
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
||||||
MCPedia
|
MCPedia
|
||||||
</Link>
|
</Link>
|
||||||
@@ -171,58 +186,61 @@ function FolderIndexPage({
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Header card */}
|
{/* Header Card */}
|
||||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 mb-8 shadow-sm">
|
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-7 shadow-sm">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3.5">
|
||||||
<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 className="w-12 h-12 rounded-2xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-[var(--brand)] dark:text-[var(--accent)] shadow-inner">
|
||||||
📁
|
<Folder className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight">
|
<h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
|
||||||
{folderName}
|
{folderName}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-0.5">
|
<p className="text-xs text-[var(--text-muted)] mt-1 font-mono">
|
||||||
Directory path: <code className="text-[var(--brand)] dark:text-[var(--accent)] font-mono">{section}/{slug}</code>
|
Directory: <code className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">{section}/{slug}</code>
|
||||||
</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-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] 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-3 py-1.5 rounded-lg">
|
||||||
{immediateDocs.length + folders.length} item{folders.length + immediateDocs.length !== 1 ? "s" : ""}
|
{immediateDocs.length + folders.length} item{folders.length + immediateDocs.length !== 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href={`/${section}/${slug}/export`}
|
href={`/${section}/${slug}/export`}
|
||||||
className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3 py-1.5 rounded-lg font-medium transition-all shadow-xs"
|
className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3.5 py-1.5 rounded-lg font-semibold transition-all shadow-md shadow-[var(--brand)]/20"
|
||||||
title={`Export all ${folderName} documents as a single PDF report`}
|
title={`Export all ${folderName} documents as a single PDF report`}
|
||||||
>
|
>
|
||||||
<span>📄</span>
|
<FileDown className="w-3.5 h-3.5" />
|
||||||
<span>Export PDF Report</span>
|
<span>Export PDF Report</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subfolders */}
|
{/* Subfolders Grid */}
|
||||||
{folders.length > 0 && (
|
{folders.length > 0 && (
|
||||||
<div className="mb-8">
|
<div>
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 tracking-wider flex items-center gap-2">
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase mb-3.5 tracking-wider font-mono flex items-center gap-2">
|
||||||
<span>Subfolders</span>
|
<Folder className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
<span className="text-[10px] text-[var(--text-dim)]">({folders.length})</span>
|
<span>Subfolders ({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-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg transition-all shadow-sm"
|
className="group flex items-center gap-3.5 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<span className="text-2xl group-hover:scale-110 transition-transform">📁</span>
|
<div className="p-2 rounded-xl bg-[var(--bg-elevated)] text-[var(--brand)] dark:text-[var(--accent)]">
|
||||||
|
<Folder className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
|
<div className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] 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-[var(--text-muted)] mt-0.5 font-mono">
|
<div className="text-xs text-[var(--text-muted)] font-mono mt-0.5">
|
||||||
{info.docCount} document{info.docCount !== 1 ? "s" : ""}
|
{info.docCount} document{info.docCount !== 1 ? "s" : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,32 +250,34 @@ function FolderIndexPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Documents */}
|
{/* Documents List */}
|
||||||
{immediateDocs.length > 0 && (
|
{immediateDocs.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 tracking-wider flex items-center gap-2">
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase mb-3.5 tracking-wider font-mono flex items-center gap-2">
|
||||||
<span>Documents</span>
|
<FileText className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
<span className="text-[10px] text-[var(--text-dim)]">({immediateDocs.length})</span>
|
<span>Documents ({immediateDocs.length})</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
{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-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg transition-all shadow-sm"
|
className="group flex items-center justify-between gap-4 p-4 sm:p-5 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3.5 min-w-0">
|
||||||
<span className="text-xl opacity-70 group-hover:opacity-100">📄</span>
|
<div className="p-2 rounded-xl bg-[var(--bg-elevated)] text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
|
<div className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</div>
|
</div>
|
||||||
{doc.tags.length > 0 && (
|
{doc.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||||
{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-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]"
|
className="text-[10px] px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -266,11 +286,15 @@ function FolderIndexPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<time className="text-xs text-[var(--text-dim)] shrink-0 font-mono">
|
|
||||||
|
<time className="text-xs text-[var(--text-dim)] shrink-0 font-mono flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>
|
||||||
{new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
{new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
})}
|
})}
|
||||||
|
</span>
|
||||||
</time>
|
</time>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -279,7 +303,7 @@ function FolderIndexPage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{folders.length === 0 && immediateDocs.length === 0 && (
|
{folders.length === 0 && immediateDocs.length === 0 && (
|
||||||
<div className="text-center py-12 border border-[var(--border-color)] rounded-xl bg-[var(--bg-surface)]">
|
<div className="text-center py-16 border border-[var(--border-color)] rounded-2xl bg-[var(--bg-surface)]">
|
||||||
<p className="text-sm text-[var(--text-muted)]">No documents found in this directory.</p>
|
<p className="text-sm text-[var(--text-muted)]">No documents found in this directory.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -331,14 +355,15 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
if (edit === "1" && canEdit) {
|
if (edit === "1" && canEdit) {
|
||||||
const existingFolders = extractFoldersForSection(docSlugs, doc.section);
|
const existingFolders = extractFoldersForSection(docSlugs, doc.section);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-6">
|
||||||
<Link
|
<Link
|
||||||
href={`/${doc.slug}`}
|
href={`/${doc.slug}`}
|
||||||
className="inline-flex items-center gap-1 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] mb-4 transition-colors"
|
className="inline-flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
>
|
>
|
||||||
<span>← Back to document</span>
|
<ArrowLeft className="w-3.5 h-3.5" />
|
||||||
|
<span>Back to document</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-[var(--text-primary)] mb-6">
|
<h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
|
||||||
Edit: {doc.title}
|
Edit: {doc.title}
|
||||||
</h1>
|
</h1>
|
||||||
<DocForm
|
<DocForm
|
||||||
@@ -373,7 +398,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
<div className="flex items-start gap-8">
|
<div className="flex items-start gap-8">
|
||||||
{/* Main Content Column */}
|
{/* Main Content Column */}
|
||||||
<article className="flex-1 min-w-0">
|
<article className="flex-1 min-w-0">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumbs Navigation */}
|
||||||
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] 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-[var(--text-primary)] transition-colors">
|
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
||||||
MCPedia
|
MCPedia
|
||||||
@@ -399,43 +424,47 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Hero Metadata Card */}
|
{/* Hero Metadata Card */}
|
||||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 mb-8 shadow-sm">
|
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-7 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-5 mb-5">
|
||||||
<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.5 flex-wrap">
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono font-medium bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30">
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-mono font-bold bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30 uppercase">
|
||||||
<span>{sectionInfo.icon}</span>
|
<span>{sectionInfo.icon}</span>
|
||||||
<span className="uppercase">{sectionInfo.label}</span>
|
<span>{sectionInfo.label}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-[var(--text-dim)]">·</span>
|
<span className="text-xs text-[var(--text-dim)]">·</span>
|
||||||
<span className="text-xs text-[var(--text-muted)]">
|
<span className="text-xs text-[var(--text-muted)] font-medium">
|
||||||
{doc.type.charAt(0).toUpperCase() + doc.type.slice(1)}
|
{doc.type.charAt(0).toUpperCase() + doc.type.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-[var(--text-dim)]">·</span>
|
<span className="text-xs text-[var(--text-dim)]">·</span>
|
||||||
<span className="text-xs text-[var(--text-muted)] font-mono">
|
<span className="text-xs text-[var(--text-muted)] font-mono flex items-center gap-1">
|
||||||
{readingTime}
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>{readingTime}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight leading-tight">
|
|
||||||
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] tracking-tight leading-tight">
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions Deck */}
|
||||||
<DocActions slug={doc.slug} body={doc.body} canEdit={canEdit} />
|
<DocActions slug={doc.slug} body={doc.body} canEdit={canEdit} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Author + Date + Tags */}
|
{/* Author, Date, and Tags */}
|
||||||
<div className="flex flex-wrap items-center gap-3 text-xs text-[var(--text-muted)] pt-3 border-t border-[var(--border-color)]">
|
<div className="flex flex-wrap items-center gap-3 text-xs text-[var(--text-muted)] pt-4 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-[var(--brand)] text-white flex items-center justify-center text-[10px] font-bold">
|
<div className="w-4 h-4 rounded-full bg-[var(--brand)] 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-[var(--text-secondary)]">{doc.author}</span>
|
<span className="text-[var(--text-secondary)] font-medium">{doc.author}</span>
|
||||||
<span className="text-[var(--text-dim)]">·</span>
|
<span className="text-[var(--text-dim)]">·</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3 text-[var(--text-dim)]" />
|
||||||
<span>
|
<span>
|
||||||
Updated {new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
Updated {new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
||||||
month: "long",
|
month: "long",
|
||||||
@@ -443,6 +472,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
{doc.tags.length > 0 && (
|
{doc.tags.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<span className="text-[var(--text-dim)]">·</span>
|
<span className="text-[var(--text-dim)]">·</span>
|
||||||
@@ -450,7 +481,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
{doc.tags.map((t) => (
|
{doc.tags.map((t) => (
|
||||||
<span
|
<span
|
||||||
key={t}
|
key={t}
|
||||||
className="px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] text-[11px]"
|
className="px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] text-[11px] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -465,32 +496,35 @@ 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-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg shadow-sm">
|
<div className="xl:hidden mb-6 p-5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-xs">
|
||||||
<TOC source={doc.body} />
|
<TOC source={doc.body} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Markdown content */}
|
{/* Markdown Content */}
|
||||||
<div className="mb-14">
|
<div className="mb-14">
|
||||||
<Markdown source={doc.body} />
|
<Markdown source={doc.body} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Related Documents */}
|
{/* Related Documents Grid */}
|
||||||
{related.length > 0 && (
|
{related.length > 0 && (
|
||||||
<aside className="mt-12 pt-8 border-t border-[var(--border-color)]">
|
<aside className="mt-14 pt-8 border-t border-[var(--border-color)]">
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4 flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<span>Related Knowledge</span>
|
<Sparkles className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Related Knowledge & Writeups
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
|
||||||
{related.map((r) => {
|
{related.map((r) => {
|
||||||
const rInfo = getSectionInfo(r.section);
|
const rInfo = getSectionInfo(r.section);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={r.slug}
|
key={r.slug}
|
||||||
href={`/${r.slug}`}
|
href={`/${r.slug}`}
|
||||||
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"
|
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<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-[var(--brand)] dark:text-[var(--accent)] font-mono uppercase">
|
<span className="text-xs text-[var(--brand)] dark:text-[var(--accent)] font-mono uppercase font-semibold">
|
||||||
{rInfo.icon} {r.section}
|
{rInfo.icon} {r.section}
|
||||||
</span>
|
</span>
|
||||||
<time className="text-[11px] text-[var(--text-dim)] font-mono">
|
<time className="text-[11px] text-[var(--text-dim)] font-mono">
|
||||||
@@ -500,7 +534,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-1">
|
<h3 className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-1">
|
||||||
{r.title}
|
{r.title}
|
||||||
</h3>
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -510,28 +544,31 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Revision History */}
|
{/* Revision History Timeline */}
|
||||||
{revisions.length > 0 && (
|
{revisions.length > 0 && (
|
||||||
<aside className="mt-10 pt-8 border-t border-[var(--border-color)]">
|
<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-[var(--text-muted)] uppercase tracking-wider">
|
<div className="flex items-center gap-2">
|
||||||
|
<History className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
Revision History ({revisions.length})
|
Revision History ({revisions.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{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-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-xs shadow-sm"
|
className="flex items-center justify-between gap-3 p-3.5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl text-xs shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
<span className="font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 px-1.5 py-0.5 rounded border border-[var(--brand)]/30 font-semibold">
|
<span className="font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 px-2 py-0.5 rounded-md border border-[var(--brand)]/30 font-bold">
|
||||||
v{rev.revisionNo}
|
v{rev.revisionNo}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[var(--text-primary)] truncate">
|
<span className="text-[var(--text-primary)] font-medium truncate">
|
||||||
{rev.reason || "Updated document"}
|
{rev.reason || "Updated document in PostgreSQL"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[var(--text-dim)] hidden sm:inline">
|
<span className="text-[var(--text-dim)] hidden sm:inline font-mono">
|
||||||
· {new Date(rev.createdAt).toLocaleString()}
|
· {new Date(rev.createdAt).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -540,7 +577,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-[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"
|
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] px-3 py-1 rounded-lg border border-[var(--border-color)] hover:border-[var(--brand)] transition-colors font-semibold shadow-xs cursor-pointer"
|
||||||
>
|
>
|
||||||
Restore
|
Restore
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { listDocuments } from "@mcpedia/core";
|
import { listDocuments } from "@mcpedia/core";
|
||||||
import { getSectionMeta } from "@mcpedia/config";
|
import { getSectionMeta } from "@mcpedia/config";
|
||||||
|
import {
|
||||||
|
Folder,
|
||||||
|
FileText,
|
||||||
|
ChevronRight,
|
||||||
|
FileDown,
|
||||||
|
Plus,
|
||||||
|
Compass,
|
||||||
|
ArrowLeft,
|
||||||
|
Clock,
|
||||||
|
Tag,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -81,26 +92,28 @@ function renderTree(nodes: TreeNode[], pathname: string) {
|
|||||||
<li key={node.slug}>
|
<li key={node.slug}>
|
||||||
<Link
|
<Link
|
||||||
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-2 px-3 rounded-xl transition-all ${
|
||||||
isActive
|
isActive
|
||||||
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30"
|
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30 shadow-xs"
|
||||||
: hasActiveChild
|
: hasActiveChild
|
||||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
|
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
: "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.5 truncate">
|
||||||
<span>{isFolder ? "📁" : "📄"}</span>
|
<span className="text-[var(--text-dim)]">
|
||||||
<span className="truncate">{node.doc?.title || node.name}</span>
|
{isFolder ? <Folder className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" /> : <FileText className="w-4 h-4" />}
|
||||||
|
</span>
|
||||||
|
<span className="truncate font-medium">{node.doc?.title || node.name}</span>
|
||||||
</span>
|
</span>
|
||||||
{isFolder && (
|
{isFolder && (
|
||||||
<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">
|
<span className="text-[10px] text-[var(--text-dim)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-2 py-0.5 rounded-md 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-[var(--border-color)] pl-2 mt-1">
|
<div className="ml-4 border-l border-[var(--border-color)] pl-3 mt-1 space-y-1">
|
||||||
{renderTree(node.children, pathname)}
|
{renderTree(node.children, pathname)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -125,66 +138,74 @@ export default async function SectionIndexPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb Navigation */}
|
||||||
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)]">
|
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)]">
|
||||||
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors flex items-center gap-1">
|
||||||
MCPedia
|
<span>MCPedia</span>
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-[var(--text-muted)] font-medium">{sectionInfo.label}</span>
|
<span className="text-[var(--text-muted)] font-semibold">{sectionInfo.label}</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Hero Card */}
|
{/* Hero Card */}
|
||||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 shadow-sm">
|
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-7 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-5 mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-4">
|
||||||
<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">
|
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-3xl shadow-inner">
|
||||||
{sectionInfo.icon}
|
{sectionInfo.icon}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight">
|
<div className="flex items-center gap-2">
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
|
||||||
{sectionInfo.label}
|
{sectionInfo.label}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-0.5 font-mono uppercase">
|
</div>
|
||||||
Section: {section}
|
<p className="text-xs text-[var(--text-muted)] font-mono uppercase tracking-wider mt-1">
|
||||||
|
Section Domain: <span className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">{section}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
||||||
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 border border-[var(--brand)]/25 px-2.5 py-1 rounded-md">
|
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 border border-[var(--brand)]/25 px-3 py-1.5 rounded-lg font-semibold">
|
||||||
{sectionDocs.length} document{sectionDocs.length !== 1 ? "s" : ""}
|
{sectionDocs.length} document{sectionDocs.length !== 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{sectionDocs.length > 0 && (
|
{sectionDocs.length > 0 && (
|
||||||
<Link
|
<Link
|
||||||
href={`/${section}/export`}
|
href={`/${section}/export`}
|
||||||
className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)]/10 hover:bg-[var(--brand)]/20 border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 text-[var(--brand)] dark:text-[var(--accent)] px-3 py-1 rounded-md font-medium transition-all shadow-xs"
|
className="inline-flex items-center gap-1.5 text-xs bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] px-3 py-1.5 rounded-lg font-semibold transition-all shadow-xs"
|
||||||
title={`Export all ${sectionInfo.label} documents to PDF`}
|
title={`Export all ${sectionInfo.label} documents to publication-grade PDF`}
|
||||||
>
|
>
|
||||||
<span>📄</span>
|
<FileDown className="w-3.5 h-3.5" />
|
||||||
<span>Export PDF</span>
|
<span>Export PDF</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/create"
|
href="/create"
|
||||||
className="inline-flex items-center gap-1 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3 py-1 rounded-md font-medium transition-all"
|
className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3.5 py-1.5 rounded-lg font-semibold transition-all shadow-md shadow-[var(--brand)]/20"
|
||||||
>
|
>
|
||||||
<span>+ Create</span>
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
<span>Create Doc</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-[var(--text-muted)] leading-relaxed max-w-2xl mt-2">
|
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed max-w-2xl mt-3">
|
||||||
{sectionInfo.desc}
|
{sectionInfo.desc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hierarchical Folder Directory */}
|
{/* Hierarchical Folder Directory */}
|
||||||
{tree.length > 0 && (
|
{tree.length > 0 && (
|
||||||
<section className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 shadow-sm">
|
<section className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 shadow-sm">
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4 flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-4 pb-2 border-b border-[var(--border-color)]">
|
||||||
<span>Folder Structure</span>
|
<Folder className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Folder & Chapter Directory
|
||||||
</h2>
|
</h2>
|
||||||
|
</div>
|
||||||
{renderTree(tree, section)}
|
{renderTree(tree, section)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@@ -192,10 +213,14 @@ export default async function SectionIndexPage({
|
|||||||
{/* Document List */}
|
{/* Document List */}
|
||||||
{sectionDocs.length > 0 ? (
|
{sectionDocs.length > 0 ? (
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
All Documents in {sectionInfo.label}
|
<FileText className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
All Knowledge in {sectionInfo.label}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2.5">
|
||||||
{[...sectionDocs]
|
{[...sectionDocs]
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
@@ -205,20 +230,22 @@ 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-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl transition-all shadow-sm"
|
className="group flex items-center justify-between gap-4 p-4 sm:p-5 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3.5 min-w-0">
|
||||||
<span className="text-lg opacity-70 group-hover:opacity-100">📄</span>
|
<div className="p-2 rounded-xl bg-[var(--bg-elevated)] text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors shrink-0">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="text-sm font-medium text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
|
<h3 className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
|
||||||
{d.title}
|
{d.title}
|
||||||
</h3>
|
</h3>
|
||||||
{d.tags.length > 0 && (
|
{d.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-1.5">
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||||
{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-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]"
|
className="text-[10px] px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -227,19 +254,23 @@ export default async function SectionIndexPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0">
|
|
||||||
|
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0 flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>
|
||||||
{new Date(d.updatedAt).toLocaleDateString(undefined, {
|
{new Date(d.updatedAt).toLocaleDateString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})}
|
})}
|
||||||
|
</span>
|
||||||
</time>
|
</time>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 border border-[var(--border-color)] rounded-xl bg-[var(--bg-surface)]">
|
<div className="text-center py-16 border border-[var(--border-color)] rounded-2xl bg-[var(--bg-surface)]">
|
||||||
<p className="text-sm text-[var(--text-muted)]">No documents in this section yet.</p>
|
<p className="text-sm text-[var(--text-muted)]">No documents in this section yet.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,21 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
FileText,
|
||||||
|
Plus,
|
||||||
|
Compass,
|
||||||
|
Sparkles,
|
||||||
|
ArrowRight,
|
||||||
|
BookOpen,
|
||||||
|
Flag,
|
||||||
|
Layers,
|
||||||
|
SunMoon,
|
||||||
|
Command,
|
||||||
|
X,
|
||||||
|
Flame,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface Hit {
|
interface Hit {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -12,6 +27,15 @@ interface Hit {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface QuickAction {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
icon: any;
|
||||||
|
href?: string;
|
||||||
|
action?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CommandMenu() {
|
export default function CommandMenu() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@@ -21,10 +45,48 @@ export default function CommandMenu() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const quickActions: QuickAction[] = [
|
||||||
|
{
|
||||||
|
id: "create-doc",
|
||||||
|
title: "Create New Document",
|
||||||
|
subtitle: "Write technical doc, writeup, or research note",
|
||||||
|
icon: Plus,
|
||||||
|
href: "/create",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hybrid-search",
|
||||||
|
title: "Multi-Modal Hybrid Search",
|
||||||
|
subtitle: "Full-text + Semantic vector search console",
|
||||||
|
icon: Search,
|
||||||
|
href: "/search",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browse-docs",
|
||||||
|
title: "Documentation Hub",
|
||||||
|
subtitle: "System setup, API references, protocol specs",
|
||||||
|
icon: BookOpen,
|
||||||
|
href: "/docs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browse-writeups",
|
||||||
|
title: "CTF & Challenge Writeups",
|
||||||
|
subtitle: "Pwn, Reverse, Web, Crypto writeups and solutions",
|
||||||
|
icon: Flag,
|
||||||
|
href: "/writeups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browse-research",
|
||||||
|
title: "Deep-Dive Research",
|
||||||
|
subtitle: "Architecture notes, benchmarks, experiments",
|
||||||
|
icon: Sparkles,
|
||||||
|
href: "/research",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Global shortcut ⌘K or /
|
// Global shortcut ⌘K or /
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setOpen((prev) => !prev);
|
setOpen((prev) => !prev);
|
||||||
} else if (e.key === "/" && !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName)) {
|
} else if (e.key === "/" && !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName)) {
|
||||||
@@ -56,7 +118,7 @@ export default function CommandMenu() {
|
|||||||
const timer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=8`);
|
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=10`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setResults(data.results || []);
|
setResults(data.results || []);
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
@@ -65,25 +127,31 @@ export default function CommandMenu() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 120);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
function handleNavigate(slug: string) {
|
function handleNavigate(href: string) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
router.push(`/${slug}`);
|
router.push(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalItems = query.trim() ? results.length : quickActions.length;
|
||||||
|
|
||||||
function handleKeyNavigation(e: React.KeyboardEvent) {
|
function handleKeyNavigation(e: React.KeyboardEvent) {
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSelectedIndex((prev) => (results.length > 0 ? (prev + 1) % results.length : 0));
|
setSelectedIndex((prev) => (totalItems > 0 ? (prev + 1) % totalItems : 0));
|
||||||
} else if (e.key === "ArrowUp") {
|
} else if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSelectedIndex((prev) => (results.length > 0 ? (prev - 1 + results.length) % results.length : 0));
|
setSelectedIndex((prev) => (totalItems > 0 ? (prev - 1 + totalItems) % totalItems : 0));
|
||||||
} else if (e.key === "Enter" && results[selectedIndex]) {
|
} else if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleNavigate(results[selectedIndex].slug);
|
if (query.trim() && results[selectedIndex]) {
|
||||||
|
handleNavigate(`/${results[selectedIndex].slug}`);
|
||||||
|
} else if (!query.trim() && quickActions[selectedIndex]?.href) {
|
||||||
|
handleNavigate(quickActions[selectedIndex].href!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +159,12 @@ 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-[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"
|
type="button"
|
||||||
|
className="hidden md:flex items-center gap-2.5 px-3 py-1.5 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] 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-xs cursor-pointer group"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5 text-[var(--text-dim)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Search className="w-3.5 h-3.5 text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<span className="font-sans">Search knowledge...</span>
|
||||||
</svg>
|
<kbd className="ml-2 font-mono bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)] text-[10px] text-[var(--text-dim)] shadow-xs">
|
||||||
<span>Search docs...</span>
|
|
||||||
<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>
|
||||||
@@ -105,87 +172,118 @@ export default function CommandMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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-16 sm:pt-24 px-4">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm transition-opacity"
|
className="fixed inset-0 bg-slate-950/60 dark:bg-black/80 backdrop-blur-md transition-opacity"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Omnisearch Modal */}
|
||||||
<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">
|
<div className="relative w-full max-w-2xl bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-2xl overflow-hidden animate-fade-in ring-1 ring-white/10">
|
||||||
{/* Search input bar */}
|
{/* Search input bar */}
|
||||||
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
|
<div className="flex items-center gap-3 px-4 sm:px-5 py-4 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]/70">
|
||||||
<svg className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Search className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] shrink-0" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
onKeyDown={handleKeyNavigation}
|
onKeyDown={handleKeyNavigation}
|
||||||
placeholder="Type to search (titles, keywords, concepts)..."
|
placeholder="Search documents, writeups, concepts, or commands..."
|
||||||
className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm focus:outline-none"
|
className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm sm:text-base focus:outline-none"
|
||||||
/>
|
/>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="w-4 h-4 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin shrink-0" />
|
<div className="w-4 h-4 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin shrink-0" />
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
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)]"
|
type="button"
|
||||||
|
className="text-[11px] font-mono text-[var(--text-dim)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] px-2 py-0.5 rounded border border-[var(--border-color)]"
|
||||||
>
|
>
|
||||||
ESC
|
ESC
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results / Suggestions */}
|
{/* Results / Quick Actions List */}
|
||||||
<div className="max-h-96 overflow-y-auto p-2">
|
<div className="max-h-[22rem] overflow-y-auto p-2">
|
||||||
{query.trim() === "" ? (
|
{query.trim() === "" ? (
|
||||||
<div className="px-3 py-6 text-center">
|
<div className="p-2 space-y-1">
|
||||||
<p className="text-xs text-[var(--text-muted)]">Search full-text, semantic embeddings, and CTF writeups.</p>
|
<div className="px-3 py-1.5 text-[10px] font-mono uppercase tracking-wider text-[var(--text-dim)]">
|
||||||
<div className="flex justify-center gap-2 mt-3 text-[11px] text-[var(--text-dim)]">
|
Quick Shortcuts & Actions
|
||||||
<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-[var(--bg-elevated)] px-1 py-0.5 rounded border border-[var(--border-color)]">↵</kbd></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{quickActions.map((action, idx) => {
|
||||||
|
const Icon = action.icon;
|
||||||
|
const isSelected = idx === selectedIndex;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={action.id}
|
||||||
|
onClick={() => action.href && handleNavigate(action.href)}
|
||||||
|
onMouseEnter={() => setSelectedIndex(idx)}
|
||||||
|
className={`flex items-center justify-between gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-all ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)] shadow-xs"
|
||||||
|
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs sm:text-sm font-semibold text-[var(--text-primary)]">
|
||||||
|
{action.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[var(--text-muted)]">
|
||||||
|
{action.subtitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className={`w-3.5 h-3.5 text-[var(--text-dim)] ${isSelected ? "text-[var(--brand)] translate-x-0.5" : ""} transition-all`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : results.length === 0 && !loading ? (
|
) : results.length === 0 && !loading ? (
|
||||||
<div className="px-4 py-8 text-center text-xs text-[var(--text-muted)]">
|
<div className="px-4 py-12 text-center text-xs text-[var(--text-muted)] space-y-2">
|
||||||
No documents matching "{query}"
|
<p>No knowledge base items matched "{query}"</p>
|
||||||
|
<p className="text-[11px] text-[var(--text-dim)]">
|
||||||
|
Try searching with partial words or check semantic search
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1 p-1">
|
||||||
{results.map((hit, index) => {
|
{results.map((hit, index) => {
|
||||||
const isSelected = index === selectedIndex;
|
const isSelected = index === selectedIndex;
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={hit.slug}
|
key={hit.slug}
|
||||||
onClick={() => handleNavigate(hit.slug)}
|
onClick={() => handleNavigate(`/${hit.slug}`)}
|
||||||
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-xl cursor-pointer transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)]"
|
? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)] shadow-xs"
|
||||||
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] 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 mb-0.5">
|
||||||
<span className="text-xs px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--brand)] dark:text-[var(--accent)] font-mono">
|
<span className="text-[10px] font-mono px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--brand)] dark:text-[var(--accent)] uppercase font-semibold">
|
||||||
{hit.section}
|
{hit.section}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-sm font-medium ${isSelected ? "text-[var(--brand)] dark:text-[var(--accent)]" : "text-[var(--text-primary)]"} truncate`}>
|
<span className={`text-xs sm:text-sm font-semibold ${isSelected ? "text-[var(--brand)] dark:text-[var(--accent)]" : "text-[var(--text-primary)]"} truncate`}>
|
||||||
{hit.title}
|
{hit.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{hit.snippet && (
|
{hit.snippet && (
|
||||||
<p
|
<p
|
||||||
className="text-xs text-[var(--text-muted)] mt-1 line-clamp-1 [&>mark]:bg-[var(--brand)]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:rounded [&>mark]:px-0.5"
|
className="text-xs text-[var(--text-muted)] line-clamp-1 mt-1 [&>mark]:bg-[var(--brand)]/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-[var(--text-dim)] shrink-0 self-center">
|
<span className="text-[10px] font-mono text-[var(--text-dim)] shrink-0 self-center">
|
||||||
{hit.slug}
|
/{hit.slug}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -195,14 +293,18 @@ export default function CommandMenu() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<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)]">
|
<div className="flex items-center justify-between px-4 py-2.5 border-t border-[var(--border-color)] bg-[var(--bg-elevated)]/60 text-[11px] text-[var(--text-dim)]">
|
||||||
<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>
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Navigate: <kbd className="bg-[var(--bg-surface)] px-1 py-0.5 rounded border border-[var(--border-color)]">↑↓</kbd></span>
|
||||||
|
<span>Select: <kbd className="bg-[var(--bg-surface)] px-1 py-0.5 rounded border border-[var(--border-color)]">↵</kbd></span>
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/search?q=${encodeURIComponent(query)}`}
|
href={`/search?q=${encodeURIComponent(query)}`}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] transition-colors"
|
className="text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-medium flex items-center gap-1"
|
||||||
>
|
>
|
||||||
Open full search page →
|
<span>Full Search Console</span>
|
||||||
|
<ArrowRight className="w-3 h-3" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
Share2,
|
||||||
|
FileDown,
|
||||||
|
Edit3,
|
||||||
|
ExternalLink,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface DocActionsProps {
|
interface DocActionsProps {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -27,49 +35,41 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5 shrink-0 flex-wrap">
|
<div className="flex items-center gap-1.5 shrink-0 flex-wrap">
|
||||||
{/* Copy Markdown */}
|
{/* Copy Raw Markdown */}
|
||||||
<button
|
<button
|
||||||
onClick={handleCopyMarkdown}
|
onClick={handleCopyMarkdown}
|
||||||
type="button"
|
type="button"
|
||||||
title="Copy raw markdown content"
|
title="Copy raw markdown content to clipboard"
|
||||||
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"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 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-lg transition-all shadow-xs cursor-pointer"
|
||||||
>
|
>
|
||||||
{copiedMd ? (
|
{copiedMd ? (
|
||||||
<>
|
<>
|
||||||
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-emerald-500 font-medium">Copied MD</span>
|
<span className="text-emerald-500 font-medium">Copied MD</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Copy className="w-3.5 h-3.5" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
|
|
||||||
</svg>
|
|
||||||
<span>Raw MD</span>
|
<span>Raw MD</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Copy Link */}
|
{/* Copy Share Link */}
|
||||||
<button
|
<button
|
||||||
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-[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"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 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-lg transition-all shadow-xs cursor-pointer"
|
||||||
>
|
>
|
||||||
{copiedLink ? (
|
{copiedLink ? (
|
||||||
<>
|
<>
|
||||||
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-emerald-500 font-medium">Copied Link</span>
|
<span className="text-emerald-500 font-medium">Copied Link</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Share2 className="w-3.5 h-3.5" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
||||||
</svg>
|
|
||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -78,23 +78,19 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
|
|||||||
{/* Export PDF */}
|
{/* Export PDF */}
|
||||||
<Link
|
<Link
|
||||||
href={`/${slug}/export`}
|
href={`/${slug}/export`}
|
||||||
title="Export document or parent folder to PDF"
|
title="Export document or parent folder to publication-grade PDF"
|
||||||
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"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 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-lg transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<FileDown className="w-3.5 h-3.5" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
|
<span>Export PDF</span>
|
||||||
</svg>
|
|
||||||
<span>PDF</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* 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-[var(--brand)]/10 hover:bg-[var(--brand)]/20 border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 text-[var(--brand)] dark:text-[var(--accent)] rounded-md transition-all font-medium"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-[var(--brand)]/10 hover:bg-[var(--brand)]/20 border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 text-[var(--brand)] dark:text-[var(--accent)] rounded-lg transition-all font-medium shadow-xs"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Edit3 className="w-3.5 h-3.5" />
|
||||||
<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>
|
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,27 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Markdown from "./Markdown";
|
import Markdown from "./Markdown";
|
||||||
|
import {
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Heading2,
|
||||||
|
Heading3,
|
||||||
|
Code,
|
||||||
|
Link as LinkIcon,
|
||||||
|
Table as TableIcon,
|
||||||
|
Info,
|
||||||
|
Lightbulb,
|
||||||
|
AlertTriangle,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Check,
|
||||||
|
Eye,
|
||||||
|
Edit3,
|
||||||
|
FileCode,
|
||||||
|
Tag,
|
||||||
|
Folder,
|
||||||
|
Sliders,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface DocFormProps {
|
interface DocFormProps {
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
@@ -22,8 +43,26 @@ interface DocFormProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SECTION_LIST = ["docs", "writeups", "research", "notes", "guides", "tutorials", "ctf", "api", "projects"];
|
const DEFAULT_SECTION_LIST = [
|
||||||
const DEFAULT_TYPE_LIST = ["documentation", "writeup", "research", "note", "guide", "tutorial", "spec"];
|
"docs",
|
||||||
|
"writeups",
|
||||||
|
"research",
|
||||||
|
"notes",
|
||||||
|
"guides",
|
||||||
|
"tutorials",
|
||||||
|
"ctf",
|
||||||
|
"api",
|
||||||
|
"projects",
|
||||||
|
];
|
||||||
|
const DEFAULT_TYPE_LIST = [
|
||||||
|
"documentation",
|
||||||
|
"writeup",
|
||||||
|
"research",
|
||||||
|
"note",
|
||||||
|
"guide",
|
||||||
|
"tutorial",
|
||||||
|
"spec",
|
||||||
|
];
|
||||||
|
|
||||||
const PRESET_FIELDS = [
|
const PRESET_FIELDS = [
|
||||||
{ key: "difficulty", value: "medium" },
|
{ key: "difficulty", value: "medium" },
|
||||||
@@ -88,7 +127,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-[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-[var(--brand)] transition-colors";
|
"w-full px-3.5 py-2.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[var(--brand)]/20 transition-all";
|
||||||
|
|
||||||
function addCustomField(key = "", value = "") {
|
function addCustomField(key = "", value = "") {
|
||||||
setCustomFields([...customFields, { key, value }]);
|
setCustomFields([...customFields, { key, value }]);
|
||||||
@@ -128,7 +167,7 @@ export default function DocForm({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (!effectiveSlug || !title || !body) {
|
if (!effectiveSlug || !title || !body) {
|
||||||
setError("Slug, title, and body are required.");
|
setError("Slug, title, and markdown body are required.");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -175,7 +214,7 @@ export default function DocForm({
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
throw new Error(data.error ?? "Save failed");
|
throw new Error(data.error ?? "Failed to save document. Verify ADMIN_PASSWORD or secret.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
@@ -190,39 +229,38 @@ 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-500 dark:text-rose-400 text-sm">
|
<div className="p-4 rounded-xl border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-xs sm:text-sm font-medium animate-fade-in">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Title */}
|
{/* Document Title */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Document Title *
|
Document Title *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="e.g. Database Architecture Overview"
|
placeholder="e.g. Exploiting Stack Alignment Issues in x86_64"
|
||||||
className={`${baseInputCls} text-base`}
|
className={`${baseInputCls} text-base sm:text-lg font-semibold`}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Section
|
Section
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
list="section-list"
|
list="section-list"
|
||||||
value={section}
|
value={section}
|
||||||
onChange={(e) => setSection(e.target.value)}
|
onChange={(e) => setSection(e.target.value)}
|
||||||
placeholder="e.g. docs, writeups, ctf, guides"
|
placeholder="e.g. docs, writeups, research"
|
||||||
className={baseInputCls}
|
className={baseInputCls}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -234,18 +272,17 @@ export default function DocForm({
|
|||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Type
|
Document Type
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
list="type-list"
|
list="type-list"
|
||||||
value={type}
|
value={type}
|
||||||
onChange={(e) => setType(e.target.value)}
|
onChange={(e) => setType(e.target.value)}
|
||||||
placeholder="e.g. documentation, writeup, note"
|
placeholder="e.g. writeup, documentation, note"
|
||||||
className={baseInputCls}
|
className={baseInputCls}
|
||||||
/>
|
/>
|
||||||
<datalist id="type-list">
|
<datalist id="type-list">
|
||||||
@@ -256,9 +293,9 @@ export default function DocForm({
|
|||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Status
|
Status
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@@ -267,7 +304,7 @@ export default function DocForm({
|
|||||||
className={baseInputCls}
|
className={baseInputCls}
|
||||||
>
|
>
|
||||||
<option value="published">Published</option>
|
<option value="published">Published</option>
|
||||||
<option value="draft">Draft</option>
|
<option value="draft">Draft (Unindexed)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,8 +312,8 @@ 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-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Parent Folder
|
Parent Folder / Chapter
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={parentFolder}
|
value={parentFolder}
|
||||||
@@ -292,16 +329,17 @@ export default function DocForm({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
{isEdit ? "Full Slug" : "Slug (File name) *"}
|
{isEdit ? "Document Path" : "Slug (File name) *"}
|
||||||
</label>
|
</label>
|
||||||
{isEdit ? (
|
{isEdit ? (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={slug ?? ""}
|
value={slug ?? ""}
|
||||||
readOnly
|
readOnly
|
||||||
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"
|
className="w-full px-3.5 py-2.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-xl text-sm text-[var(--text-muted)] font-mono"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
@@ -309,12 +347,12 @@ export default function DocForm({
|
|||||||
value={slugInput}
|
value={slugInput}
|
||||||
onChange={(e) => setSlugInput(e.target.value)}
|
onChange={(e) => setSlugInput(e.target.value)}
|
||||||
className={`${baseInputCls} font-mono`}
|
className={`${baseInputCls} font-mono`}
|
||||||
placeholder="my-new-post"
|
placeholder="ret2win-guide"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-[11px] text-[var(--text-dim)] mt-1 font-mono">
|
<p className="text-[11px] text-[var(--text-dim)] mt-1.5 font-mono">
|
||||||
Path: <span className="text-[var(--brand)] dark:text-[var(--accent)]">{effectiveSlug || section}</span>
|
Full Slug: <span className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">{effectiveSlug || section}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +360,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-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Tags (comma separated)
|
Tags (comma separated)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -330,11 +368,11 @@ export default function DocForm({
|
|||||||
value={tags}
|
value={tags}
|
||||||
onChange={(e) => setTags(e.target.value)}
|
onChange={(e) => setTags(e.target.value)}
|
||||||
className={baseInputCls}
|
className={baseInputCls}
|
||||||
placeholder="ctf, pwn, reverse, tutorial"
|
placeholder="pwn, x86_64, rop, ctf"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Author
|
Author
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -342,31 +380,32 @@ export default function DocForm({
|
|||||||
value={author}
|
value={author}
|
||||||
onChange={(e) => setAuthor(e.target.value)}
|
onChange={(e) => setAuthor(e.target.value)}
|
||||||
className={baseInputCls}
|
className={baseInputCls}
|
||||||
placeholder="asep"
|
placeholder="e.g. asepharyana"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Metadata Fields */}
|
{/* Custom Metadata Fields */}
|
||||||
<div className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3 shadow-sm">
|
<div className="p-4 sm:p-5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl space-y-4 shadow-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)]">
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] font-mono flex items-center gap-1.5">
|
||||||
Dynamic Custom Metadata
|
<Sliders className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
<span>Dynamic Technical Metadata</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] text-[var(--text-dim)] 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.
|
Custom fields (points, difficulty, event, solved) will render as styled badges in the document hero.
|
||||||
</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-[var(--text-dim)]">Presets:</span>
|
<span className="text-[10px] font-mono text-[var(--text-dim)] uppercase">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-[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"
|
className="px-2 py-0.5 text-[11px] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-colors font-mono cursor-pointer"
|
||||||
>
|
>
|
||||||
+{preset.key}
|
+{preset.key}
|
||||||
</button>
|
</button>
|
||||||
@@ -393,10 +432,10 @@ export default function DocForm({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeCustomField(i)}
|
onClick={() => removeCustomField(i)}
|
||||||
className="p-2 text-rose-500 hover:text-rose-600 rounded hover:bg-[var(--bg-elevated)] transition-colors"
|
className="p-2 text-rose-500 hover:text-rose-600 rounded-lg hover:bg-[var(--bg-elevated)] transition-colors cursor-pointer"
|
||||||
title="Remove field"
|
title="Remove field"
|
||||||
>
|
>
|
||||||
✕
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -404,105 +443,124 @@ export default function DocForm({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => addCustomField()}
|
onClick={() => addCustomField()}
|
||||||
className="text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline px-3 py-1.5 rounded-lg border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 bg-[var(--brand)]/5 transition-colors font-medium"
|
className="inline-flex items-center gap-1.5 text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline px-3 py-1.5 rounded-lg border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 bg-[var(--brand)]/5 transition-colors font-medium cursor-pointer"
|
||||||
>
|
>
|
||||||
+ Add Custom Field
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
<span>Add Custom Field</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body with Write / Preview Tabs */}
|
{/* Markdown Body Editor */}
|
||||||
<div className="border border-[var(--border-color)] rounded-xl overflow-hidden bg-[var(--bg-surface)] shadow-sm">
|
<div className="border border-[var(--border-color)] rounded-2xl overflow-hidden bg-[var(--bg-surface)] shadow-sm">
|
||||||
{/* Editor Toolbar */}
|
{/* Editor Toolbar */}
|
||||||
<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">
|
<div className="flex items-center justify-between px-3 py-2 bg-[var(--bg-elevated)]/60 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
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab("write")}
|
onClick={() => setActiveTab("write")}
|
||||||
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-semibold transition-all cursor-pointer ${
|
||||||
activeTab === "write"
|
activeTab === "write"
|
||||||
? "bg-[var(--brand)] text-white shadow-sm"
|
? "bg-[var(--brand)] text-white shadow-xs"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Write
|
<Edit3 className="w-3.5 h-3.5" />
|
||||||
|
<span>Write</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab("preview")}
|
onClick={() => setActiveTab("preview")}
|
||||||
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-semibold transition-all cursor-pointer ${
|
||||||
activeTab === "preview"
|
activeTab === "preview"
|
||||||
? "bg-[var(--brand)] text-white shadow-sm"
|
? "bg-[var(--brand)] text-white shadow-xs"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Live Preview
|
<Eye className="w-3.5 h-3.5" />
|
||||||
|
<span>Live Preview</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Markdown Action Shortcuts */}
|
{/* Quick Markdown Formatter Shortcuts */}
|
||||||
{activeTab === "write" && (
|
{activeTab === "write" && (
|
||||||
<div className="flex items-center gap-1 text-xs text-[var(--text-muted)]">
|
<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-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||||
title="Bold"
|
title="Bold (**text**)"
|
||||||
>
|
>
|
||||||
<b>B</b>
|
<Bold className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => insertMarkdown("*", "*")}
|
onClick={() => insertMarkdown("*", "*")}
|
||||||
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||||
title="Italic"
|
title="Italic (*text*)"
|
||||||
>
|
>
|
||||||
<i>I</i>
|
<Italic className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => insertMarkdown("## ")}
|
onClick={() => insertMarkdown("## ")}
|
||||||
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||||
title="Heading 2"
|
title="Heading 2"
|
||||||
>
|
>
|
||||||
H2
|
<Heading2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => insertMarkdown("### ")}
|
||||||
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||||
|
title="Heading 3"
|
||||||
|
>
|
||||||
|
<Heading3 className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => insertMarkdown("```\n", "\n```")}
|
onClick={() => insertMarkdown("```\n", "\n```")}
|
||||||
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] font-mono"
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||||
title="Code Block"
|
title="Code Block"
|
||||||
>
|
>
|
||||||
{"</>"}
|
<Code className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => insertMarkdown("[", "](https://)")}
|
onClick={() => insertMarkdown("[", "](https://)")}
|
||||||
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||||
title="Link"
|
title="Link"
|
||||||
>
|
>
|
||||||
Link
|
<LinkIcon className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => insertMarkdown("> [!NOTE]\n> ")}
|
onClick={() => insertMarkdown("> [!NOTE]\n> ")}
|
||||||
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)] text-indigo-500"
|
||||||
title="Note Callout"
|
title="Note Callout"
|
||||||
>
|
>
|
||||||
Note
|
<Info className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => insertMarkdown("> [!TIP]\n> ")}
|
||||||
|
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)] text-emerald-500"
|
||||||
|
title="Tip Callout"
|
||||||
|
>
|
||||||
|
<Lightbulb className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* Tab Content */}
|
||||||
{activeTab === "write" ? (
|
{activeTab === "write" ? (
|
||||||
<textarea
|
<textarea
|
||||||
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-[var(--text-primary)] placeholder-[var(--text-dim)] font-mono text-sm focus:outline-none resize-y leading-relaxed"
|
className="w-full h-96 p-4 sm:p-5 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 technical documentation or writeup here in Markdown..."
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -510,28 +568,29 @@ export default function DocForm({
|
|||||||
{body.trim() ? (
|
{body.trim() ? (
|
||||||
<Markdown source={body} />
|
<Markdown source={body} />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-[var(--text-dim)] italic">Nothing to preview yet.</p>
|
<p className="text-xs text-[var(--text-dim)] italic">No content to preview yet.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Submit Action Buttons */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-6 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg font-medium text-sm transition-all shadow-md shadow-[var(--brand)]/20 disabled:opacity-50 flex items-center gap-2"
|
className="px-6 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-xl font-semibold text-sm transition-all shadow-md shadow-[var(--brand)]/20 hover:shadow-[var(--brand)]/35 disabled:opacity-50 flex items-center gap-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
)}
|
)}
|
||||||
<span>{loading ? "Saving document..." : mode === "create" ? "Create Document" : "Save Changes"}</span>
|
<span>{loading ? "Persisting Document..." : mode === "create" ? "Publish to PostgreSQL" : "Save Changes"}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
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"
|
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-xl text-sm transition-colors shadow-xs cursor-pointer"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ import { usePathname } from "next/navigation";
|
|||||||
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
|
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
|
||||||
import ThemeToggle from "@/components/ThemeToggle";
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
import CommandMenu from "@/components/CommandMenu";
|
import CommandMenu from "@/components/CommandMenu";
|
||||||
|
import {
|
||||||
|
Sparkles,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
|
Lock,
|
||||||
|
Layers,
|
||||||
|
FileText,
|
||||||
|
Activity,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -24,126 +36,143 @@ export default function Header() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-30 border-b border-[var(--border-color)] glass-nav">
|
<header className="sticky top-0 z-40 border-b border-[var(--border-color)] glass-nav transition-all">
|
||||||
<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-15 flex items-center justify-between gap-3 sm:gap-4">
|
||||||
{/* Brand */}
|
{/* Left: Brand Identity */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/" className="flex items-center gap-2 group">
|
<Link href="/" className="flex items-center gap-2.5 group">
|
||||||
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[var(--brand)] to-[var(--accent)] flex items-center justify-center text-white font-bold text-xs shadow-md shadow-[var(--brand)]/20 group-hover:scale-105 transition-transform">
|
<div className="relative flex items-center justify-center w-8 h-8 rounded-xl bg-gradient-to-br from-indigo-500 via-indigo-600 to-violet-700 text-white shadow-lg shadow-indigo-500/25 group-hover:scale-105 group-hover:shadow-indigo-500/40 transition-all">
|
||||||
M
|
<span className="font-mono font-bold text-sm tracking-tighter">M</span>
|
||||||
|
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-400 ring-2 ring-[var(--bg-app)] animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-[var(--text-primary)] text-base tracking-tight transition-colors">
|
<div className="flex flex-col">
|
||||||
|
<span className="font-bold text-[var(--text-primary)] text-base tracking-tight leading-none group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors">
|
||||||
MCPedia
|
MCPedia
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
<span className="text-[10px] font-mono text-[var(--text-dim)] uppercase tracking-wider leading-tight mt-0.5 hidden sm:block">
|
||||||
<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)]">
|
AI Knowledge Hub
|
||||||
v0.1.0
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Nav */}
|
<div className="hidden xl:flex items-center gap-1.5 ml-2 px-2 py-0.5 rounded-full bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[10px] font-mono text-[var(--text-dim)]">
|
||||||
<nav className="hidden md:flex items-center gap-1">
|
<Activity className="w-3 h-3 text-emerald-500" />
|
||||||
|
<span>MCP :4021</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center: Desktop Dynamic Sections Nav */}
|
||||||
|
<nav className="hidden md:flex items-center gap-1 bg-[var(--bg-elevated)]/60 p-1 rounded-xl border border-[var(--border-color)]">
|
||||||
{sections.map((s) => {
|
{sections.map((s) => {
|
||||||
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
|
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={s.id}
|
key={s.id}
|
||||||
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 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||||||
isActive
|
isActive
|
||||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] border border-[var(--border-color)]"
|
? "text-[var(--text-primary)] bg-[var(--bg-surface)] shadow-xs font-semibold border border-[var(--border-color)]"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface-hover)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="mr-1.5 opacity-80">{s.icon}</span>
|
<span className="text-xs">{s.icon}</span>
|
||||||
{s.label}
|
<span>{s.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Right tools */}
|
{/* Right Tools & Navigation Deck */}
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2">
|
||||||
<CommandMenu />
|
<CommandMenu />
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/search"
|
href="/search"
|
||||||
className="md:hidden p-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-elevated)] transition-colors"
|
className="md:hidden p-2 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg bg-[var(--bg-surface)] border border-[var(--border-color)] transition-colors"
|
||||||
title="Search"
|
title="Search knowledge base"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Search className="w-4 h-4" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/create"
|
href="/create"
|
||||||
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1 bg-[var(--brand)] hover:bg-[var(--accent)] text-white text-xs font-medium rounded-lg transition-all shadow-sm"
|
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white text-xs font-medium rounded-lg shadow-md shadow-[var(--brand)]/20 hover:shadow-[var(--brand)]/35 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>+</span>
|
<Plus className="w-3.5 h-3.5" />
|
||||||
<span>New Doc</span>
|
<span>New Doc</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className={`px-2.5 py-1 text-xs rounded-lg transition-colors ${
|
className={`p-1.5 sm:px-2.5 sm:py-1.5 text-xs rounded-lg transition-colors flex items-center gap-1.5 ${
|
||||||
pathname === "/login"
|
pathname === "/login"
|
||||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] font-medium"
|
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] font-semibold border border-[var(--border-color)]"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
||||||
}`}
|
}`}
|
||||||
|
title="Admin Login"
|
||||||
>
|
>
|
||||||
Login
|
<Lock className="w-3.5 h-3.5" />
|
||||||
|
<span className="hidden sm:inline">Admin</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="border-l border-[var(--border-color)] pl-2.5 flex items-center">
|
<div className="border-l border-[var(--border-color)] pl-2 flex items-center">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu toggle */}
|
{/* Mobile menu hamburger toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
className="md:hidden p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
className="md:hidden p-2 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-elevated)] transition-colors"
|
||||||
aria-label="Toggle navigation"
|
aria-label="Toggle navigation"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||||
{mobileMenuOpen ? (
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
) : (
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Drawer */}
|
{/* Mobile Drawer */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<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">
|
<div className="md:hidden border-t border-[var(--border-color)] bg-[var(--bg-surface)] px-4 py-4 space-y-2 animate-fade-in shadow-xl">
|
||||||
{sections.map((s) => (
|
<div className="text-[11px] font-mono uppercase tracking-wider text-[var(--text-dim)] px-2 mb-1">
|
||||||
|
Sections
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{sections.map((s) => {
|
||||||
|
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
|
||||||
|
return (
|
||||||
<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-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] hover:text-[var(--text-primary)]"
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
|
||||||
|
isActive
|
||||||
|
? "bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30 font-semibold"
|
||||||
|
: "text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] hover:text-[var(--text-primary)] border border-transparent"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span>{s.icon}</span>
|
<span>{s.icon}</span>
|
||||||
<span>{s.label}</span>
|
<span className="truncate">{s.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
);
|
||||||
<div className="pt-2 border-t border-[var(--border-color)] flex gap-2">
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 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-[var(--bg-elevated)] text-[var(--text-secondary)] rounded-lg font-medium border border-[var(--border-color)]"
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 text-xs bg-[var(--bg-elevated)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg font-medium border border-[var(--border-color)]"
|
||||||
>
|
>
|
||||||
Search
|
<Search className="w-3.5 h-3.5" />
|
||||||
|
<span>Search</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/create"
|
href="/create"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
className="flex-1 text-center py-2 text-xs bg-[var(--brand)] text-white rounded-lg font-medium"
|
className="flex-1 flex items-center justify-center gap-1.5 py-2 text-xs bg-[var(--brand)] text-white rounded-lg font-medium shadow-sm"
|
||||||
>
|
>
|
||||||
+ Create
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
<span>Create Doc</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,379 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
Cpu,
|
||||||
|
Database,
|
||||||
|
Layers,
|
||||||
|
Sparkles,
|
||||||
|
Search,
|
||||||
|
Globe,
|
||||||
|
Terminal,
|
||||||
|
Server,
|
||||||
|
ArrowRight,
|
||||||
|
CheckCircle2,
|
||||||
|
FileCode2,
|
||||||
|
ShieldCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface NodeDetail {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
badge: string;
|
||||||
|
type: "client" | "protocol" | "core" | "storage";
|
||||||
|
icon: any;
|
||||||
|
desc: string;
|
||||||
|
metrics: { label: string; value: string }[];
|
||||||
|
features: string[];
|
||||||
|
docLink?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODES: NodeDetail[] = [
|
||||||
|
{
|
||||||
|
id: "ai-agents",
|
||||||
|
title: "AI Coding Agents",
|
||||||
|
badge: "MCP Consumer",
|
||||||
|
type: "client",
|
||||||
|
icon: Sparkles,
|
||||||
|
desc: "Claude Desktop, Cursor, Antigravity, and Zed connect via Model Context Protocol to read and write technical knowledge directly.",
|
||||||
|
metrics: [
|
||||||
|
{ label: "Transport", value: "Stdio / HTTP SSE" },
|
||||||
|
{ label: "Tools Exposed", value: "13 Native Tools" },
|
||||||
|
{ label: "Authentication", value: "x-webhook-secret" },
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
"Semantic similarity search over chunks",
|
||||||
|
"Direct document CRUD into Postgres",
|
||||||
|
"Context retrieval for debugging & CTF writeups",
|
||||||
|
],
|
||||||
|
docLink: "/docs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "web-ui",
|
||||||
|
title: "Human Web Interface",
|
||||||
|
badge: "Next.js 16 + React 19",
|
||||||
|
type: "client",
|
||||||
|
icon: Globe,
|
||||||
|
desc: "Fast, accessible, and responsive developer web interface with dark/light themes, keyboard shortcuts, and instant search.",
|
||||||
|
metrics: [
|
||||||
|
{ label: "Framework", value: "Next.js 16.3" },
|
||||||
|
{ label: "Styling", value: "Tailwind CSS v4" },
|
||||||
|
{ label: "Command Menu", value: "⌘K Omnisearch" },
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
"Hierarchical folder directory tree",
|
||||||
|
"Publication-grade PDF export generator",
|
||||||
|
"Live markdown editor with revision rollback",
|
||||||
|
],
|
||||||
|
docLink: "/docs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mcp-server",
|
||||||
|
title: "MCP Server Gateway",
|
||||||
|
badge: "Streamable HTTP :4021",
|
||||||
|
type: "protocol",
|
||||||
|
icon: Server,
|
||||||
|
desc: "High-throughput streamable Model Context Protocol server exposing tool endpoints and resource URIs.",
|
||||||
|
metrics: [
|
||||||
|
{ label: "Port", value: ":4021" },
|
||||||
|
{ label: "Protocol", value: "MCP 2024-11-05" },
|
||||||
|
{ label: "Runtime", value: "Bun Engine" },
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
"get_document / create_document tools",
|
||||||
|
"search_hybrid / search_semantic tools",
|
||||||
|
"list_sections & list_revisions resources",
|
||||||
|
],
|
||||||
|
docLink: "/api",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "core-engine",
|
||||||
|
title: "@mcpedia/core",
|
||||||
|
badge: "Single Source of Truth",
|
||||||
|
type: "core",
|
||||||
|
icon: Layers,
|
||||||
|
desc: "Unified business logic layer shared across Web, MCP Server, REST API, and Background Workers.",
|
||||||
|
metrics: [
|
||||||
|
{ label: "Architecture", value: "Monorepo Package" },
|
||||||
|
{ label: "Validation", value: "Strict Type Safety" },
|
||||||
|
{ label: "Ranking", value: "RRF Hybrid Fusion" },
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
"Path classification & folder hierarchy",
|
||||||
|
"Markdown document chunking & tokenization",
|
||||||
|
"Drizzle ORM transactional schema mapping",
|
||||||
|
],
|
||||||
|
docLink: "/research",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "search-engine",
|
||||||
|
title: "Multi-Modal Search Engine",
|
||||||
|
badge: "RRF Algorithm",
|
||||||
|
type: "core",
|
||||||
|
icon: Search,
|
||||||
|
desc: "Reciprocal Rank Fusion uniting Postgres Full-Text Search tsvectors with OpenAI/OpenRouter vector embeddings.",
|
||||||
|
metrics: [
|
||||||
|
{ label: "FTS Algorithm", value: "tsvector + GIN" },
|
||||||
|
{ label: "Vector Search", value: "Cosine Similarity" },
|
||||||
|
{ label: "Fusion Constant", value: "k = 60" },
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
"Sub-millisecond keyword retrieval",
|
||||||
|
"Conceptual similarity matching",
|
||||||
|
"Instant snippet generation with highlight markers",
|
||||||
|
],
|
||||||
|
docLink: "/search",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "postgres-db",
|
||||||
|
title: "PostgreSQL 16 + pgvector",
|
||||||
|
badge: "Primary Storage",
|
||||||
|
type: "storage",
|
||||||
|
icon: Database,
|
||||||
|
desc: "ACID-compliant relational database storing document bodies, revision trees, vector embeddings, and full-text indexes.",
|
||||||
|
metrics: [
|
||||||
|
{ label: "Extension", value: "vector (1536 dim)" },
|
||||||
|
{ label: "Backups", value: "Auto .md to disk" },
|
||||||
|
{ label: "Isolation", value: "Transactional" },
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
"HNSW vector index for embeddings",
|
||||||
|
"Complete revision history preservation",
|
||||||
|
"Instant document schema migrations",
|
||||||
|
],
|
||||||
|
docLink: "/docs",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function InteractiveTopology() {
|
||||||
|
const [activeNodeId, setActiveNodeId] = useState<string>("core-engine");
|
||||||
|
|
||||||
|
const activeNode = NODES.find((n) => n.id === activeNodeId) || NODES[0];
|
||||||
|
const ActiveIcon = activeNode.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl overflow-hidden shadow-sm">
|
||||||
|
{/* Header Bar */}
|
||||||
|
<div className="p-5 sm:p-6 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]/50 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1.5">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
|
<span className="text-[11px] font-mono uppercase tracking-wider text-[var(--text-muted)]">
|
||||||
|
Interactive System Topology
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">
|
||||||
|
How MCPedia Unifies Humans & AI Agents
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-2.5 py-1 rounded-md">
|
||||||
|
Click any node below to inspect
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid Layout: Visual Interactive Map & Inspector */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12">
|
||||||
|
{/* Visual Map Column (Left) */}
|
||||||
|
<div className="lg:col-span-7 p-6 border-b lg:border-b-0 lg:border-r border-[var(--border-color)] bg-[var(--bg-app)]/50 flex flex-col justify-between space-y-6">
|
||||||
|
{/* Layer 1: Clients */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
|
||||||
|
1. Ingestion & Access Layer
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{NODES.filter((n) => n.type === "client").map((node) => {
|
||||||
|
const Icon = node.icon;
|
||||||
|
const isSelected = activeNodeId === node.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
onClick={() => setActiveNodeId(node.id)}
|
||||||
|
type="button"
|
||||||
|
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--brand)]/15 border-[var(--brand)] shadow-md shadow-[var(--brand)]/10 ring-1 ring-[var(--brand)]"
|
||||||
|
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--brand)]/50 hover:bg-[var(--bg-elevated)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-secondary)]"}`}>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-mono px-2 py-0.5 rounded bg-[var(--bg-elevated)] text-[var(--text-muted)] border border-[var(--border-color)]">
|
||||||
|
{node.badge}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold text-sm text-[var(--text-primary)]">{node.title}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flow Connector Arrow */}
|
||||||
|
<div className="flex justify-center text-[var(--text-dim)]">
|
||||||
|
<span className="text-xs font-mono px-3 py-0.5 rounded-full bg-[var(--bg-surface)] border border-[var(--border-color)]">
|
||||||
|
↓ Secure Protocol & Gateway
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Layer 2: Protocol & Core Services */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
|
||||||
|
2. Gateway & Intelligence Core
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
{NODES.filter((n) => n.type === "protocol" || n.type === "core").map((node) => {
|
||||||
|
const Icon = node.icon;
|
||||||
|
const isSelected = activeNodeId === node.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
onClick={() => setActiveNodeId(node.id)}
|
||||||
|
type="button"
|
||||||
|
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--brand)]/15 border-[var(--brand)] shadow-md shadow-[var(--brand)]/10 ring-1 ring-[var(--brand)]"
|
||||||
|
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--brand)]/50 hover:bg-[var(--bg-elevated)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-secondary)]"}`}>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold text-xs text-[var(--text-primary)] leading-tight mb-1 truncate">
|
||||||
|
{node.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-[var(--text-muted)] font-mono truncate">
|
||||||
|
{node.badge}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flow Connector Arrow */}
|
||||||
|
<div className="flex justify-center text-[var(--text-dim)]">
|
||||||
|
<span className="text-xs font-mono px-3 py-0.5 rounded-full bg-[var(--bg-surface)] border border-[var(--border-color)]">
|
||||||
|
↓ Persistent Layer (ACID + Vectors)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Layer 3: Database & Vector Storage */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
|
||||||
|
3. Source of Truth Storage
|
||||||
|
</span>
|
||||||
|
{NODES.filter((n) => n.type === "storage").map((node) => {
|
||||||
|
const Icon = node.icon;
|
||||||
|
const isSelected = activeNodeId === node.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
onClick={() => setActiveNodeId(node.id)}
|
||||||
|
type="button"
|
||||||
|
className={`w-full p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--brand)]/15 border-[var(--brand)] shadow-md shadow-[var(--brand)]/10 ring-1 ring-[var(--brand)]"
|
||||||
|
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--brand)]/50 hover:bg-[var(--bg-elevated)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-secondary)]"}`}>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-sm text-[var(--text-primary)]">{node.title}</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)]">Source of truth for all content & embeddings</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-mono px-2 py-0.5 rounded bg-[var(--bg-elevated)] text-[var(--text-muted)] border border-[var(--border-color)]">
|
||||||
|
{node.badge}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Node Inspector Details Column (Right) */}
|
||||||
|
<div className="lg:col-span-5 p-6 bg-[var(--bg-surface)] flex flex-col justify-between space-y-6 animate-fade-in">
|
||||||
|
<div>
|
||||||
|
{/* Inspector Header */}
|
||||||
|
<div className="flex items-start justify-between gap-4 mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-11 h-11 rounded-xl bg-[var(--brand)]/15 border border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)] flex items-center justify-center">
|
||||||
|
<ActiveIcon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-bold text-[var(--text-primary)]">{activeNode.title}</h4>
|
||||||
|
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)]">
|
||||||
|
{activeNode.badge}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed mb-6">
|
||||||
|
{activeNode.desc}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Telemetry Metrics */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-dim)] font-mono mb-2.5 block">
|
||||||
|
Telemetry & Architecture
|
||||||
|
</span>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{activeNode.metrics.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.label}
|
||||||
|
className="flex items-center justify-between p-2.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg text-xs"
|
||||||
|
>
|
||||||
|
<span className="text-[var(--text-muted)]">{m.label}</span>
|
||||||
|
<span className="font-mono font-medium text-[var(--text-primary)]">{m.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Capabilities */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-dim)] font-mono mb-2.5 block">
|
||||||
|
Key Capabilities
|
||||||
|
</span>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{activeNode.features.map((feat, idx) => (
|
||||||
|
<li key={idx} className="flex items-start gap-2 text-xs text-[var(--text-secondary)]">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-500 shrink-0 mt-0.5" />
|
||||||
|
<span>{feat}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Link to Documentation */}
|
||||||
|
{activeNode.docLink && (
|
||||||
|
<div className="pt-4 border-t border-[var(--border-color)]">
|
||||||
|
<Link
|
||||||
|
href={activeNode.docLink}
|
||||||
|
className="inline-flex items-center justify-center gap-2 w-full py-2.5 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-xs font-semibold text-[var(--text-primary)] rounded-xl transition-all shadow-xs group"
|
||||||
|
>
|
||||||
|
<span>Read technical writeup in {activeNode.title}</span>
|
||||||
|
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-0.5 transition-transform" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,15 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import rehypeSlug from "rehype-slug";
|
import rehypeSlug from "rehype-slug";
|
||||||
import rehypeHighlight from "rehype-highlight";
|
import rehypeHighlight from "rehype-highlight";
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
Info,
|
||||||
|
Lightbulb,
|
||||||
|
AlertTriangle,
|
||||||
|
Flame,
|
||||||
|
ShieldAlert,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
function extractText(node: React.ReactNode): string {
|
function extractText(node: React.ReactNode): string {
|
||||||
if (node === null || node === undefined || typeof node === "boolean") {
|
if (node === null || node === undefined || typeof node === "boolean") {
|
||||||
@@ -42,16 +51,16 @@ function PreBlock({ children, ...props }: React.HTMLAttributes<HTMLPreElement>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="code-block-wrapper relative group my-5 rounded-xl overflow-hidden border border-[var(--border-code)] bg-[var(--code-block-bg)] shadow-xs print:my-3 print:rounded-md print:border-slate-300 print:bg-slate-50">
|
<div className="code-block-wrapper relative group my-6 rounded-xl overflow-hidden border border-[var(--border-code)] bg-[var(--code-block-bg)] shadow-sm print:my-3 print:rounded-md print:border-slate-300 print:bg-slate-50">
|
||||||
{/* Code block header bar */}
|
{/* Code block header bar */}
|
||||||
<div className="code-block-header flex items-center justify-between px-4 py-2 bg-[var(--code-header-bg)] border-b border-[var(--border-code)] text-xs font-mono select-none print:px-3 print:py-1 print:bg-slate-100 print:border-slate-300">
|
<div className="code-block-header flex items-center justify-between px-4 py-2.5 bg-[var(--code-header-bg)] border-b border-[var(--border-code)] text-xs font-mono select-none print:px-3 print:py-1 print:bg-slate-100 print:border-slate-300">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="no-print flex items-center gap-1.5 opacity-75 group-hover:opacity-100 transition-opacity">
|
<div className="no-print flex items-center gap-1.5 opacity-75 group-hover:opacity-100 transition-opacity">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-rose-400/80 dark:bg-rose-500/60 inline-block" />
|
<span className="w-2.5 h-2.5 rounded-full bg-rose-500/80 inline-block" />
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-amber-400/80 dark:bg-amber-500/60 inline-block" />
|
<span className="w-2.5 h-2.5 rounded-full bg-amber-500/80 inline-block" />
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400/80 dark:bg-emerald-500/60 inline-block" />
|
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500/80 inline-block" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted)] print:text-slate-700 group-hover:text-[var(--text-secondary)] transition-colors">
|
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-muted)] print:text-slate-700 group-hover:text-[var(--text-primary)] transition-colors">
|
||||||
{language}
|
{language}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,23 +73,19 @@ function PreBlock({ children, ...props }: React.HTMLAttributes<HTMLPreElement>)
|
|||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<>
|
<>
|
||||||
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-emerald-500 font-medium">Copied!</span>
|
<span className="text-emerald-500 font-medium">Copied!</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Copy className="w-3.5 h-3.5" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
<span>Copy</span>
|
<span>Copy</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre className="!m-0 !bg-transparent p-4 print:p-3 overflow-x-auto text-[13.5px] print:text-[8.5pt] text-[var(--pre-text)] print:text-slate-900 font-mono leading-relaxed" {...props}>
|
<pre className="!m-0 !bg-transparent p-4 sm:p-5 print:p-3 overflow-x-auto text-[13px] sm:text-[13.5px] print:text-[8.5pt] text-[var(--pre-text)] print:text-slate-900 font-mono leading-relaxed" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +96,7 @@ export default function Markdown({ source }: { source: string }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="prose max-w-none
|
className="prose max-w-none
|
||||||
prose-headings:text-[var(--text-primary)] prose-headings:font-semibold prose-headings:tracking-tight
|
prose-headings:text-[var(--text-primary)] prose-headings:font-bold prose-headings:tracking-tight
|
||||||
prose-h1:text-2xl sm:prose-h1:text-3xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-[var(--border-color)]
|
prose-h1:text-2xl sm:prose-h1:text-3xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-[var(--border-color)]
|
||||||
prose-h2:text-xl sm:prose-h2:text-2xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:pb-2 prose-h2:border-b prose-h2:border-[var(--border-subtle)]
|
prose-h2:text-xl sm:prose-h2:text-2xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:pb-2 prose-h2:border-b prose-h2:border-[var(--border-subtle)]
|
||||||
prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6
|
prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6
|
||||||
@@ -130,8 +135,48 @@ export default function Markdown({ source }: { source: string }) {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
blockquote: ({ children }) => {
|
blockquote: ({ children }) => {
|
||||||
|
const rawText = extractText(children).trim();
|
||||||
|
const alertMatch = rawText.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i);
|
||||||
|
|
||||||
|
if (alertMatch) {
|
||||||
|
const alertType = alertMatch[1].toUpperCase();
|
||||||
|
let borderClass = "border-indigo-500 bg-indigo-500/10 text-indigo-900 dark:text-indigo-200";
|
||||||
|
let Icon = Info;
|
||||||
|
let title = "Note";
|
||||||
|
|
||||||
|
if (alertType === "TIP") {
|
||||||
|
borderClass = "border-emerald-500 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200";
|
||||||
|
Icon = Lightbulb;
|
||||||
|
title = "Tip";
|
||||||
|
} else if (alertType === "IMPORTANT") {
|
||||||
|
borderClass = "border-violet-500 bg-violet-500/10 text-violet-900 dark:text-violet-200";
|
||||||
|
Icon = Flame;
|
||||||
|
title = "Important";
|
||||||
|
} else if (alertType === "WARNING") {
|
||||||
|
borderClass = "border-amber-500 bg-amber-500/10 text-amber-900 dark:text-amber-200";
|
||||||
|
Icon = AlertTriangle;
|
||||||
|
title = "Warning";
|
||||||
|
} else if (alertType === "CAUTION") {
|
||||||
|
borderClass = "border-rose-500 bg-rose-500/10 text-rose-900 dark:text-rose-200";
|
||||||
|
Icon = ShieldAlert;
|
||||||
|
title = "Caution";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<blockquote className="border-l-4 border-[var(--brand)] bg-[var(--brand)]/5 dark:bg-[var(--brand)]/10 pl-4 py-2.5 my-5 rounded-r-lg text-[var(--text-secondary)] not-italic">
|
<div className={`my-5 p-4 rounded-xl border-l-4 ${borderClass} shadow-xs`}>
|
||||||
|
<div className="flex items-center gap-2 font-bold text-xs uppercase tracking-wider mb-1.5">
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs sm:text-sm text-[var(--text-secondary)] [&>p]:mb-1 [&>p:last-child]:mb-0">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<blockquote className="border-l-4 border-[var(--brand)] bg-[var(--brand)]/5 dark:bg-[var(--brand)]/10 pl-4 py-2.5 my-5 rounded-r-xl text-[var(--text-secondary)] not-italic">
|
||||||
{children}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,23 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Markdown from "@/components/Markdown";
|
import Markdown from "@/components/Markdown";
|
||||||
|
import {
|
||||||
|
Sparkles,
|
||||||
|
Terminal,
|
||||||
|
Server,
|
||||||
|
Code2,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
CheckCircle2,
|
||||||
|
Cpu,
|
||||||
|
Laptop,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
const CONFIGS = [
|
const CONFIGS = [
|
||||||
{
|
{
|
||||||
id: "claude",
|
id: "claude",
|
||||||
label: "Claude Desktop",
|
label: "Claude Desktop",
|
||||||
|
icon: Sparkles,
|
||||||
lang: "json",
|
lang: "json",
|
||||||
filename: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
filename: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
||||||
content: `{
|
content: `{
|
||||||
@@ -15,7 +27,7 @@ const CONFIGS = [
|
|||||||
"command": "bun",
|
"command": "bun",
|
||||||
"args": ["run", "/path/to/mcpedia/apps/mcp/src/index.ts"],
|
"args": ["run", "/path/to/mcpedia/apps/mcp/src/index.ts"],
|
||||||
"env": {
|
"env": {
|
||||||
"DATABASE_URL": "postgresql://...",
|
"DATABASE_URL": "postgresql://postgres:password@localhost:5432/mcpedia",
|
||||||
"OPENROUTER_API_KEY": "sk-or-v1-..."
|
"OPENROUTER_API_KEY": "sk-or-v1-..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,10 +35,29 @@ const CONFIGS = [
|
|||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "http",
|
id: "cursor",
|
||||||
label: "Remote HTTP (SSE)",
|
label: "Cursor AI",
|
||||||
|
icon: Laptop,
|
||||||
lang: "json",
|
lang: "json",
|
||||||
filename: "Streamable HTTP Endpoint",
|
filename: ".cursor/mcp.json",
|
||||||
|
content: `{
|
||||||
|
"mcpServers": {
|
||||||
|
"mcpedia": {
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "/path/to/mcpedia/apps/mcp/src/index.ts"],
|
||||||
|
"env": {
|
||||||
|
"DATABASE_URL": "postgresql://postgres:password@localhost:5432/mcpedia"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "http",
|
||||||
|
label: "Streamable HTTP Endpoint",
|
||||||
|
icon: Server,
|
||||||
|
lang: "json",
|
||||||
|
filename: "Streamable HTTP (SSE) Client Config",
|
||||||
content: `{
|
content: `{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mcpedia-remote": {
|
"mcpedia-remote": {
|
||||||
@@ -40,19 +71,21 @@ const CONFIGS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cli",
|
id: "cli",
|
||||||
label: "CLI Client",
|
label: "CLI & Scripts",
|
||||||
|
icon: Terminal,
|
||||||
lang: "bash",
|
lang: "bash",
|
||||||
filename: "Terminal One-Shot / REPL",
|
filename: "Terminal / MCP Client Subprocess",
|
||||||
content: `# One-shot question
|
content: `# Ask questions over semantic knowledge
|
||||||
bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work?"
|
bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work in x86_64?"
|
||||||
|
|
||||||
# Interactive REPL
|
# Run interactive REPL with native tool calls
|
||||||
bun run apps/mcp-client/src/index.ts`,
|
bun run apps/mcp-client/src/index.ts`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function McpConfigSnippet() {
|
export default function McpConfigSnippet() {
|
||||||
const [activeTab, setActiveTab] = useState("claude");
|
const [activeTab, setActiveTab] = useState("claude");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0];
|
const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0];
|
||||||
|
|
||||||
@@ -60,45 +93,80 @@ export default function McpConfigSnippet() {
|
|||||||
${current.content}
|
${current.content}
|
||||||
\`\`\``;
|
\`\`\``;
|
||||||
|
|
||||||
|
function handleCopyAll() {
|
||||||
|
navigator.clipboard.writeText(current.content);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-sm">
|
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl overflow-hidden shadow-sm">
|
||||||
{/* Header bar with tabs */}
|
{/* Header bar with tabs */}
|
||||||
<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 justify-between px-4 sm:px-6 py-3 bg-[var(--bg-elevated)]/60 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 flex-wrap">
|
||||||
{CONFIGS.map((c) => (
|
{CONFIGS.map((c) => {
|
||||||
|
const Icon = c.icon;
|
||||||
|
const isSelected = activeTab === c.id;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={c.id}
|
key={c.id}
|
||||||
onClick={() => setActiveTab(c.id)}
|
onClick={() => setActiveTab(c.id)}
|
||||||
className={`px-3 py-1.5 text-xs rounded-lg font-medium transition-all cursor-pointer ${
|
type="button"
|
||||||
activeTab === c.id
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-xl font-semibold transition-all cursor-pointer ${
|
||||||
? "bg-[var(--brand)] text-white shadow-xs"
|
isSelected
|
||||||
|
? "bg-[var(--brand)] text-white shadow-md shadow-[var(--brand)]/20"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{c.label}
|
<Icon className="w-3.5 h-3.5" />
|
||||||
|
<span>{c.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-[11px] text-[var(--text-dim)] font-mono hidden sm:block">
|
<button
|
||||||
{current.filename}
|
onClick={handleCopyAll}
|
||||||
</div>
|
type="button"
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||||
|
<span className="text-emerald-500">Copied Config</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
<span>Copy Config</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Code preview via Markdown syntax highlighter */}
|
{/* Code preview via Markdown syntax highlighter */}
|
||||||
<div className="p-4 [&>.prose>.group]:my-0 [&>.prose]:max-w-none">
|
<div className="p-4 sm:p-5 [&>.prose>.group]:my-0 [&>.prose]:max-w-none">
|
||||||
<Markdown source={markdownSnippet} />
|
<Markdown source={markdownSnippet} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)] flex items-center justify-between flex-wrap gap-2">
|
{/* Footer Info */}
|
||||||
<span>
|
<div className="px-4 sm:px-6 py-3 bg-[var(--bg-elevated)]/40 border-t border-[var(--border-color)] text-xs text-[var(--text-dim)] flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||||
Config target: <span className="font-mono text-[var(--text-secondary)]">{current.filename}</span>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono uppercase text-[10px] text-[var(--text-muted)] font-semibold">
|
||||||
|
Target File:
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-[var(--text-muted)]">
|
<span className="font-mono text-[var(--text-secondary)] text-[11px]">
|
||||||
Format: <span className="uppercase font-mono font-semibold">{current.lang}</span>
|
{current.filename}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 text-[11px]">
|
||||||
|
<span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400 font-medium">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
|
<span>13 MCP Tools Ready</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ import {
|
|||||||
type ExportSortOption,
|
type ExportSortOption,
|
||||||
} from "@mcpedia/core/export-helpers";
|
} from "@mcpedia/core/export-helpers";
|
||||||
import { getSectionMeta } from "@mcpedia/config/sections";
|
import { getSectionMeta } from "@mcpedia/config/sections";
|
||||||
|
import {
|
||||||
|
Printer,
|
||||||
|
FileDown,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
ArrowLeft,
|
||||||
|
Filter,
|
||||||
|
ListOrdered,
|
||||||
|
BookOpen,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface PdfExportViewProps {
|
interface PdfExportViewProps {
|
||||||
exportData: ExportData;
|
exportData: ExportData;
|
||||||
@@ -113,23 +123,21 @@ export default function PdfExportView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-16 print:pb-0">
|
<div className="pb-16 print:pb-0">
|
||||||
{/* =========================================================================
|
{/* Export Control Toolbar (Screen Only — Hidden in Print) */}
|
||||||
Export Control Toolbar (Screen Only — Hidden in Print)
|
<aside className="no-print export-toolbar sticky top-16 z-30 mb-8 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-5 shadow-md">
|
||||||
========================================================================= */}
|
|
||||||
<aside className="no-print export-toolbar sticky top-16 z-30 mb-8 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-4 shadow-sm">
|
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||||
{/* Left info & Back button */}
|
{/* Left info & Back button */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3.5">
|
||||||
<Link
|
<Link
|
||||||
href={backHref}
|
href={backHref}
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all shadow-xs"
|
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<span>←</span>
|
<ArrowLeft className="w-3.5 h-3.5" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-mono text-[var(--text-primary)] font-semibold uppercase">
|
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] font-bold uppercase">
|
||||||
{sectionMeta.label}
|
{sectionMeta.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-[var(--text-dim)]">·</span>
|
<span className="text-xs text-[var(--text-dim)]">·</span>
|
||||||
@@ -150,13 +158,19 @@ export default function PdfExportView({
|
|||||||
<button
|
<button
|
||||||
onClick={handleCopyMarkdown}
|
onClick={handleCopyMarkdown}
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium 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-all shadow-xs cursor-pointer"
|
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold 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-xl transition-all shadow-xs cursor-pointer"
|
||||||
title="Copy compiled markdown document"
|
title="Copy compiled markdown document"
|
||||||
>
|
>
|
||||||
{copiedMd ? (
|
{copiedMd ? (
|
||||||
<span className="text-emerald-600 font-bold">✓ Copied</span>
|
<>
|
||||||
|
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||||
|
<span className="text-emerald-500 font-bold">Copied</span>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
<span>Copy Markdown</span>
|
<span>Copy Markdown</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -164,9 +178,10 @@ export default function PdfExportView({
|
|||||||
<button
|
<button
|
||||||
onClick={handleDownloadMarkdown}
|
onClick={handleDownloadMarkdown}
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium 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-all shadow-xs cursor-pointer"
|
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold 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-xl transition-all shadow-xs cursor-pointer"
|
||||||
title="Download compiled .md file"
|
title="Download compiled .md file"
|
||||||
>
|
>
|
||||||
|
<FileDown className="w-3.5 h-3.5" />
|
||||||
<span>Download .md</span>
|
<span>Download .md</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -174,22 +189,10 @@ export default function PdfExportView({
|
|||||||
<button
|
<button
|
||||||
onClick={handlePrint}
|
onClick={handlePrint}
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 text-xs font-semibold bg-slate-900 hover:bg-slate-800 text-white rounded-md shadow-sm transition-all cursor-pointer hover:shadow"
|
className="inline-flex items-center gap-2 px-4 py-2 text-xs font-bold bg-slate-900 hover:bg-slate-800 dark:bg-white dark:hover:bg-slate-100 text-white dark:text-slate-900 rounded-xl shadow-md transition-all cursor-pointer hover:shadow-lg"
|
||||||
title="Open browser print dialog to save as PDF"
|
title="Open browser print dialog to save as PDF"
|
||||||
>
|
>
|
||||||
<svg
|
<Printer className="w-4 h-4" />
|
||||||
className="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Print / Save as PDF</span>
|
<span>Print / Save as PDF</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,11 +202,11 @@ export default function PdfExportView({
|
|||||||
<div className="mt-4 pt-3.5 border-t border-[var(--border-color)] flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs">
|
<div className="mt-4 pt-3.5 border-t border-[var(--border-color)] flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs">
|
||||||
{/* Sort Order Selector */}
|
{/* Sort Order Selector */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[var(--text-muted)] font-medium">Sort by:</span>
|
<span className="text-[var(--text-muted)] font-medium font-mono text-[11px] uppercase">Sort by:</span>
|
||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value as ExportSortOption)}
|
onChange={(e) => setSortBy(e.target.value as ExportSortOption)}
|
||||||
className="bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md px-2.5 py-1 text-xs text-[var(--text-primary)] focus:outline-none focus:border-slate-500"
|
className="bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg px-3 py-1 text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--brand)]"
|
||||||
>
|
>
|
||||||
<option value="category_points">Category & Points (Standard)</option>
|
<option value="category_points">Category & Points (Standard)</option>
|
||||||
<option value="points_desc">Points (Highest First)</option>
|
<option value="points_desc">Points (Highest First)</option>
|
||||||
@@ -217,23 +220,23 @@ export default function PdfExportView({
|
|||||||
{/* Section toggles */}
|
{/* Section toggles */}
|
||||||
<div className="flex items-center gap-3 flex-wrap text-[var(--text-secondary)]">
|
<div className="flex items-center gap-3 flex-wrap text-[var(--text-secondary)]">
|
||||||
{cleanDocs.length > 1 && (
|
{cleanDocs.length > 1 && (
|
||||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
<label className="inline-flex items-center gap-1.5 cursor-pointer select-none">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showTOC}
|
checked={showTOC}
|
||||||
onChange={(e) => setShowTOC(e.target.checked)}
|
onChange={(e) => setShowTOC(e.target.checked)}
|
||||||
className="rounded border-[var(--border-color)] text-slate-800"
|
className="rounded border-[var(--border-color)] text-indigo-600"
|
||||||
/>
|
/>
|
||||||
<span>Table of Contents</span>
|
<span>Table of Contents</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
<label className="inline-flex items-center gap-1.5 cursor-pointer select-none">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pageBreaks}
|
checked={pageBreaks}
|
||||||
onChange={(e) => setPageBreaks(e.target.checked)}
|
onChange={(e) => setPageBreaks(e.target.checked)}
|
||||||
className="rounded border-[var(--border-color)] text-slate-800"
|
className="rounded border-[var(--border-color)] text-indigo-600"
|
||||||
/>
|
/>
|
||||||
<span>Page Break per Challenge</span>
|
<span>Page Break per Challenge</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -243,13 +246,13 @@ export default function PdfExportView({
|
|||||||
{/* Category filter pills */}
|
{/* Category filter pills */}
|
||||||
{allCategories.length > 1 && (
|
{allCategories.length > 1 && (
|
||||||
<div className="mt-3 pt-3 border-t border-[var(--border-color)] flex items-center gap-1.5 flex-wrap text-xs">
|
<div className="mt-3 pt-3 border-t border-[var(--border-color)] flex items-center gap-1.5 flex-wrap text-xs">
|
||||||
<span className="text-[var(--text-muted)] mr-1">Filter:</span>
|
<span className="text-[var(--text-muted)] font-mono text-[11px] uppercase mr-1">Filter:</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedCategory("all")}
|
onClick={() => setSelectedCategory("all")}
|
||||||
type="button"
|
type="button"
|
||||||
className={`px-2.5 py-0.5 rounded-md text-xs font-mono transition-all cursor-pointer ${
|
className={`px-3 py-1 rounded-lg text-xs font-mono transition-all cursor-pointer ${
|
||||||
selectedCategory === "all"
|
selectedCategory === "all"
|
||||||
? "bg-slate-900 text-white font-semibold"
|
? "bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold"
|
||||||
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -264,9 +267,9 @@ export default function PdfExportView({
|
|||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => setSelectedCategory(cat)}
|
onClick={() => setSelectedCategory(cat)}
|
||||||
type="button"
|
type="button"
|
||||||
className={`px-2.5 py-0.5 rounded-md text-xs font-mono transition-all cursor-pointer ${
|
className={`px-3 py-1 rounded-lg text-xs font-mono transition-all cursor-pointer ${
|
||||||
selectedCategory.toLowerCase() === cat.toLowerCase()
|
selectedCategory.toLowerCase() === cat.toLowerCase()
|
||||||
? "bg-slate-900 text-white font-semibold"
|
? "bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold"
|
||||||
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -278,14 +281,11 @@ export default function PdfExportView({
|
|||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* =========================================================================
|
{/* Writeup Document Flow */}
|
||||||
Clean, Simple, Human-Written Writeup Document Flow (No Cards)
|
<main className="print-container bg-white text-slate-900 max-w-4xl mx-auto px-4 sm:px-8 py-6 print:p-0 print:max-w-none rounded-2xl border border-[var(--border-color)] print:border-none shadow-sm print:shadow-none">
|
||||||
========================================================================= */}
|
|
||||||
<main className="print-container bg-white text-slate-900 max-w-4xl mx-auto px-4 sm:px-8 py-6 print:p-0 print:max-w-none">
|
|
||||||
|
|
||||||
{/* Document Header */}
|
{/* Document Header */}
|
||||||
<header className="mb-8 pb-3 border-b border-slate-300">
|
<header className="mb-8 pb-4 border-b border-slate-300">
|
||||||
<h1 className="text-3xl sm:text-4xl font-bold text-slate-950 tracking-tight mb-2">
|
<h1 className="text-3xl sm:text-4xl font-extrabold text-slate-950 tracking-tight mb-2">
|
||||||
{summary.title}
|
{summary.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="text-xs text-slate-500 font-mono flex flex-wrap items-center gap-x-2 gap-y-1">
|
<div className="text-xs text-slate-500 font-mono flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
@@ -297,11 +297,12 @@ export default function PdfExportView({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Table of Contents (Simple & Clean) */}
|
{/* Table of Contents */}
|
||||||
{showTOC && filteredAndSortedDocs.length > 1 && (
|
{showTOC && filteredAndSortedDocs.length > 1 && (
|
||||||
<section className="toc-section mb-10 pb-6 border-b border-slate-200">
|
<section className="toc-section mb-10 pb-6 border-b border-slate-200">
|
||||||
<h2 className="text-sm font-bold uppercase font-mono text-slate-700 mb-3 tracking-wider">
|
<h2 className="text-sm font-bold uppercase font-mono text-slate-700 mb-3 tracking-wider flex items-center gap-1.5">
|
||||||
Table of Contents
|
<BookOpen className="w-4 h-4" />
|
||||||
|
<span>Table of Contents</span>
|
||||||
</h2>
|
</h2>
|
||||||
<ol className="list-decimal list-inside space-y-1.5 text-xs text-slate-800">
|
<ol className="list-decimal list-inside space-y-1.5 text-xs text-slate-800">
|
||||||
{filteredAndSortedDocs.map((doc, idx) => {
|
{filteredAndSortedDocs.map((doc, idx) => {
|
||||||
@@ -326,7 +327,7 @@ export default function PdfExportView({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Challenges / Writeups Flow ("Mirip Writeup Manusia") */}
|
{/* Challenges / Writeups Flow */}
|
||||||
<div className="challenges-flow space-y-10 print:space-y-0">
|
<div className="challenges-flow space-y-10 print:space-y-0">
|
||||||
{filteredAndSortedDocs.map((doc, idx) => {
|
{filteredAndSortedDocs.map((doc, idx) => {
|
||||||
const chNum = idx + 1;
|
const chNum = idx + 1;
|
||||||
@@ -353,7 +354,7 @@ export default function PdfExportView({
|
|||||||
{pts > 0 && <span><strong>Points:</strong> {pts} pts</span>}
|
{pts > 0 && <span><strong>Points:</strong> {pts} pts</span>}
|
||||||
{diff && <span><strong>Difficulty:</strong> {diff}</span>}
|
{diff && <span><strong>Difficulty:</strong> {diff}</span>}
|
||||||
|
|
||||||
{/* Extra fields (excluding author, slug, scope, etc.) */}
|
{/* Extra fields */}
|
||||||
{Object.entries(extra).map(([k, v]) => {
|
{Object.entries(extra).map(([k, v]) => {
|
||||||
const lowerKey = k.toLowerCase();
|
const lowerKey = k.toLowerCase();
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ import { useEffect, useState, useMemo } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { getSectionMeta } from "@mcpedia/config/sections";
|
import { getSectionMeta } from "@mcpedia/config/sections";
|
||||||
|
import {
|
||||||
|
Folder,
|
||||||
|
FolderOpen,
|
||||||
|
FileText,
|
||||||
|
ChevronRight,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
|
Plus,
|
||||||
|
Compass,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface Doc {
|
interface Doc {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -102,11 +112,11 @@ 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-[var(--border-color)]" : ""}`}>
|
<li className={`select-none ${depth > 0 ? "ml-3.5 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 text-xs py-1.5 px-2 rounded-lg transition-all ${
|
||||||
isActive
|
isActive
|
||||||
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30 shadow-sm"
|
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30 shadow-xs"
|
||||||
: isParentOfActive
|
: isParentOfActive
|
||||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
|
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
||||||
@@ -117,15 +127,23 @@ function TreeNodeItem({
|
|||||||
className="flex items-center gap-2 min-w-0 flex-1 py-0.5"
|
className="flex items-center gap-2 min-w-0 flex-1 py-0.5"
|
||||||
title={node.title || node.name}
|
title={node.title || node.name}
|
||||||
>
|
>
|
||||||
<span className="text-xs shrink-0 opacity-80 group-hover:opacity-100">
|
<span className="shrink-0 text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors">
|
||||||
{isFolder ? "📁" : "📄"}
|
{isFolder ? (
|
||||||
|
isCollapsed ? (
|
||||||
|
<Folder className="w-3.5 h-3.5" />
|
||||||
|
) : (
|
||||||
|
<FolderOpen className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate">{node.title || node.name}</span>
|
<span className="truncate">{node.title || node.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{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-[var(--text-muted)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-1 py-0.2 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.2 rounded font-mono">
|
||||||
{node.docCount}
|
{node.docCount}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -135,24 +153,19 @@ function TreeNodeItem({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleCollapse(node.slug);
|
toggleCollapse(node.slug);
|
||||||
}}
|
}}
|
||||||
className="p-0.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded hover:bg-[var(--bg-elevated-hover)] transition-transform"
|
className="p-1 text-[var(--text-dim)] 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
|
<ChevronRight
|
||||||
className={`w-3 h-3 transition-transform duration-200 ${isCollapsed ? "-rotate-90" : "rotate-0"}`}
|
className={`w-3 h-3 transition-transform duration-200 ${isCollapsed ? "rotate-0" : "rotate-90"}`}
|
||||||
fill="none"
|
/>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFolder && !isCollapsed && (
|
{isFolder && !isCollapsed && (
|
||||||
<ul className="mt-0.5 space-y-0.5 animate-fade-in">
|
<ul className="mt-1 space-y-0.5 animate-fade-in">
|
||||||
{node.children.map((child) => (
|
{node.children.map((child) => (
|
||||||
<TreeNodeItem
|
<TreeNodeItem
|
||||||
key={child.slug}
|
key={child.slug}
|
||||||
@@ -212,7 +225,7 @@ export default function Sidebar() {
|
|||||||
}, [filteredDocs]);
|
}, [filteredDocs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="h-full flex flex-col py-4 px-3">
|
<nav className="h-full flex flex-col py-4 px-3 select-none">
|
||||||
{/* Sidebar search filter */}
|
{/* Sidebar search filter */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -220,31 +233,25 @@ export default function Sidebar() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
placeholder="Filter docs..."
|
placeholder="Filter knowledge tree..."
|
||||||
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"
|
className="w-full pl-8 pr-7 py-1.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-xs text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-1 focus:ring-[var(--brand)] transition-all"
|
||||||
/>
|
/>
|
||||||
<svg
|
<Search className="w-3.5 h-3.5 text-[var(--text-dim)] absolute left-2.5 top-2.5 pointer-events-none" />
|
||||||
className="w-3.5 h-3.5 text-[var(--text-muted)] absolute left-2 top-2.5"
|
|
||||||
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" />
|
|
||||||
</svg>
|
|
||||||
{filterText && (
|
{filterText && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilterText("")}
|
onClick={() => setFilterText("")}
|
||||||
className="absolute right-2 top-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
className="absolute right-2 top-2 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded hover:bg-[var(--bg-elevated)]"
|
||||||
|
aria-label="Clear filter"
|
||||||
>
|
>
|
||||||
✕
|
<X className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary count */}
|
{/* Summary count */}
|
||||||
<div className="flex items-center justify-between px-1 mb-3 text-[11px] text-[var(--text-dim)]">
|
<div className="flex items-center justify-between px-1 mb-2 text-[10px] text-[var(--text-dim)] font-mono uppercase tracking-wider">
|
||||||
<span className="uppercase tracking-wider font-mono">Knowledge Base</span>
|
<span>Knowledge Index</span>
|
||||||
<span>{filteredDocs.length} items</span>
|
<span>{filteredDocs.length} items</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -261,17 +268,17 @@ export default function Sidebar() {
|
|||||||
<div key={id} className="border-b border-[var(--border-color)] 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-2 py-1.5 mb-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors ${
|
||||||
isSectionActive
|
isSectionActive
|
||||||
? "text-[var(--brand)] dark:text-[var(--accent)]"
|
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-2">
|
||||||
<span>{icon}</span>
|
<span className="text-xs">{icon}</span>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-[var(--text-dim)] font-normal font-mono">
|
<span className="text-[10px] text-[var(--text-dim)] font-normal font-mono bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)]">
|
||||||
{sectionDocs.length}
|
{sectionDocs.length}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -291,15 +298,22 @@ export default function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{filteredDocs.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-xs text-[var(--text-dim)]">
|
||||||
|
No matching documents
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom quick links */}
|
{/* Bottom quick links */}
|
||||||
<div className="pt-3 border-t border-[var(--border-color)] 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-[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"
|
className="flex items-center justify-center gap-2 w-full py-2 text-xs bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg transition-all font-medium shadow-xs group"
|
||||||
>
|
>
|
||||||
<span>+ Create New Document</span>
|
<Plus className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)] group-hover:scale-110 transition-transform" />
|
||||||
|
<span>Create New Document</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import GithubSlugger from "github-slugger";
|
import GithubSlugger from "github-slugger";
|
||||||
|
import { AlignLeft, ArrowUp, Hash } from "lucide-react";
|
||||||
|
|
||||||
interface TOCEntry {
|
interface TOCEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -58,18 +59,29 @@ export default function TOC({ source }: { source: string }) {
|
|||||||
|
|
||||||
if (toc.length === 0) return null;
|
if (toc.length === 0) return null;
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="text-xs">
|
<nav className="text-xs space-y-4">
|
||||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-[var(--border-color)]">
|
<div className="flex items-center justify-between pb-2 border-b border-[var(--border-color)]">
|
||||||
<svg className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div className="flex items-center gap-1.5 font-semibold uppercase tracking-wider text-[var(--text-muted)] text-[11px] font-mono">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" />
|
<AlignLeft className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
</svg>
|
<span>On This Page</span>
|
||||||
<span className="font-semibold uppercase tracking-wider text-[var(--text-muted)] text-[11px]">
|
</div>
|
||||||
On this page
|
<button
|
||||||
</span>
|
onClick={scrollToTop}
|
||||||
|
type="button"
|
||||||
|
className="text-[10px] text-[var(--text-dim)] hover:text-[var(--text-primary)] flex items-center gap-0.5 hover:underline"
|
||||||
|
title="Scroll to top"
|
||||||
|
>
|
||||||
|
<ArrowUp className="w-3 h-3" />
|
||||||
|
<span>Top</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="space-y-1.5 border-l border-[var(--border-color)] pl-2.5">
|
<ul className="space-y-1.5 border-l border-[var(--border-color)] pl-3">
|
||||||
{toc.map((entry) => {
|
{toc.map((entry) => {
|
||||||
const isActive = activeId === entry.id;
|
const isActive = activeId === entry.id;
|
||||||
return (
|
return (
|
||||||
@@ -90,7 +102,7 @@ export default function TOC({ source }: { source: string }) {
|
|||||||
history.pushState(null, "", `#${entry.id}`);
|
history.pushState(null, "", `#${entry.id}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`block py-0.5 transition-colors line-clamp-1 ${
|
className={`block py-0.5 transition-all line-clamp-1 ${
|
||||||
isActive
|
isActive
|
||||||
? "text-[var(--brand)] dark:text-[var(--accent)] font-semibold translate-x-0.5"
|
? "text-[var(--brand)] dark:text-[var(--accent)] font-semibold translate-x-0.5"
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Sun, Moon } from "lucide-react";
|
||||||
|
|
||||||
const STORAGE_KEY = "mcpedia-theme";
|
const STORAGE_KEY = "mcpedia-theme";
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ export default function ThemeToggle() {
|
|||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<div className="w-8 h-8 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] opacity-50" />
|
<div className="w-8 h-8 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] opacity-40" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,16 +47,12 @@ export default function ThemeToggle() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||||
title={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"
|
className="p-2 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 cursor-pointer"
|
||||||
>
|
>
|
||||||
{isDark ? (
|
{isDark ? (
|
||||||
<svg className="w-4 h-4 text-amber-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Sun className="w-4 h-4 text-amber-400 hover:rotate-45 transition-transform" />
|
||||||
<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">
|
<Moon className="w-4 h-4 text-indigo-600 hover:-rotate-12 transition-transform" />
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { WEBHOOK_SECRET } from "@mcpedia/config";
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import DocForm from "@/components/DocForm";
|
import DocForm from "@/components/DocForm";
|
||||||
|
import { Edit3, ArrowLeft, Plus } from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -33,14 +34,16 @@ export default async function CreatePage() {
|
|||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between pb-4 border-b border-[var(--border-color)]">
|
<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.5">
|
||||||
<div className="w-10 h-10 rounded-lg bg-[var(--brand)]/15 border border-[var(--brand)]/30 flex items-center justify-center text-xl text-[var(--brand)] dark:text-[var(--accent)]">
|
<div className="w-11 h-11 rounded-2xl bg-[var(--brand)]/15 border border-[var(--brand)]/30 flex items-center justify-center text-[var(--brand)] dark:text-[var(--accent)]">
|
||||||
✏️
|
<Plus className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">Create Document</h1>
|
<h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-0.5">
|
Create Knowledge Document
|
||||||
Writes directly to PostgreSQL, records revision history, and computes vector embeddings.
|
</h1>
|
||||||
|
<p className="text-xs text-[var(--text-muted)] mt-0.5 font-mono">
|
||||||
|
Persists directly into PostgreSQL, tracks revision history, and generates vector embeddings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from "react";
|
import React from "next/link";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getExportDocuments, formatScopeTitle, type ExportSortOption } from "@mcpedia/core";
|
import { getExportDocuments, formatScopeTitle, type ExportSortOption } from "@mcpedia/core";
|
||||||
import PdfExportView from "@/components/PdfExportView";
|
import PdfExportView from "@/components/PdfExportView";
|
||||||
|
import { FileText, ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -43,14 +44,16 @@ export default async function ExportPdfPage({
|
|||||||
|
|
||||||
if (exportData.documents.length === 0) {
|
if (exportData.documents.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="py-16 text-center space-y-4 max-w-lg mx-auto">
|
<div className="py-16 text-center space-y-4 max-w-lg mx-auto bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-3xl p-8 shadow-sm">
|
||||||
<div className="text-4xl">📄</div>
|
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-elevated)] text-[var(--text-dim)] flex items-center justify-center mx-auto">
|
||||||
|
<FileText className="w-7 h-7" />
|
||||||
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
||||||
No Documents Found for Export
|
No Documents Found for Export
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--text-muted)]">
|
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed">
|
||||||
Could not find any published documents matching the path{" "}
|
Could not find any published documents matching the path{" "}
|
||||||
<code className="text-[var(--brand)] font-mono">
|
<code className="text-[var(--brand)] dark:text-[var(--accent)] font-mono font-semibold">
|
||||||
{targetPath || section || "root"}
|
{targetPath || section || "root"}
|
||||||
</code>
|
</code>
|
||||||
.
|
.
|
||||||
@@ -58,9 +61,10 @@ export default async function ExportPdfPage({
|
|||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<Link
|
<Link
|
||||||
href={backHref}
|
href={backHref}
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white text-xs font-semibold rounded-lg shadow-sm transition-all"
|
className="inline-flex items-center gap-1.5 px-4 py-2 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white text-xs font-semibold rounded-xl shadow-md shadow-[var(--brand)]/20 transition-all"
|
||||||
>
|
>
|
||||||
<span>← Return to Knowledge Base</span>
|
<ArrowLeft className="w-3.5 h-3.5" />
|
||||||
|
<span>Return to Knowledge Base</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+158
-154
@@ -20,13 +20,15 @@
|
|||||||
|
|
||||||
--brand: #4f46e5;
|
--brand: #4f46e5;
|
||||||
--brand-hover: #4338ca;
|
--brand-hover: #4338ca;
|
||||||
|
--brand-glow: rgba(79, 70, 229, 0.15);
|
||||||
--accent: #4f46e5;
|
--accent: #4f46e5;
|
||||||
--accent-hover: #4338ca;
|
--accent-hover: #4338ca;
|
||||||
|
--cyan-accent: #0284c7;
|
||||||
|
|
||||||
--nav-bg: rgba(255, 255, 255, 0.88);
|
--nav-bg: rgba(255, 255, 255, 0.85);
|
||||||
|
|
||||||
/* Code & Syntax variables (Light Mode) */
|
/* Code & Syntax variables (Light Mode) */
|
||||||
--code-bg: #f1f5f9;
|
--code-bg: #f8fafc;
|
||||||
--code-block-bg: #f8fafc;
|
--code-block-bg: #f8fafc;
|
||||||
--code-header-bg: #f1f5f9;
|
--code-header-bg: #f1f5f9;
|
||||||
--border-code: #e2e8f0;
|
--border-code: #e2e8f0;
|
||||||
@@ -53,47 +55,52 @@
|
|||||||
--hl-operator: #57606a;
|
--hl-operator: #57606a;
|
||||||
--hl-variable: #953800;
|
--hl-variable: #953800;
|
||||||
--hl-meta: #6e7781;
|
--hl-meta: #6e7781;
|
||||||
|
|
||||||
|
/* Grid overlay */
|
||||||
|
--grid-pattern: rgba(15, 23, 42, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode overrides (html.dark) */
|
/* Dark mode overrides (html.dark) */
|
||||||
html.dark {
|
html.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg-app: #08090a;
|
--bg-app: #08090d;
|
||||||
--bg-surface: #0f1011;
|
--bg-surface: #0e1117;
|
||||||
--bg-surface-hover: #131415;
|
--bg-surface-hover: #141822;
|
||||||
--bg-elevated: #141517;
|
--bg-elevated: #151a26;
|
||||||
--bg-elevated-hover: #1b1d20;
|
--bg-elevated-hover: #1e2436;
|
||||||
--border-color: #1f2022;
|
--border-color: #1e2536;
|
||||||
--border-subtle: rgba(255, 255, 255, 0.06);
|
--border-subtle: rgba(255, 255, 255, 0.06);
|
||||||
--border-card: #1f2022;
|
--border-card: #1e2536;
|
||||||
|
|
||||||
--text-primary: #f7f8f8;
|
--text-primary: #f8fafc;
|
||||||
--text-secondary: #d0d6e0;
|
--text-secondary: #cbd5e1;
|
||||||
--text-muted: #8a8f98;
|
--text-muted: #8492a6;
|
||||||
--text-dim: #62666d;
|
--text-dim: #505c70;
|
||||||
|
|
||||||
--brand: #5e6ad2;
|
--brand: #6366f1;
|
||||||
--brand-hover: #7170ff;
|
--brand-hover: #818cf8;
|
||||||
--accent: #7170ff;
|
--brand-glow: rgba(99, 102, 241, 0.22);
|
||||||
--accent-hover: #828fff;
|
--accent: #818cf8;
|
||||||
|
--accent-hover: #a5b4fc;
|
||||||
|
--cyan-accent: #38bdf8;
|
||||||
|
|
||||||
--nav-bg: rgba(8, 9, 10, 0.85);
|
--nav-bg: rgba(8, 9, 13, 0.85);
|
||||||
|
|
||||||
/* Code & Syntax variables (Dark Mode) */
|
/* Code & Syntax variables (Dark Mode) */
|
||||||
--code-bg: #141517;
|
--code-bg: #0b0e14;
|
||||||
--code-block-bg: #0d1117;
|
--code-block-bg: #0a0d14;
|
||||||
--code-header-bg: #161b22;
|
--code-header-bg: #111520;
|
||||||
--border-code: #30363d;
|
--border-code: #202738;
|
||||||
--code-button-bg: #21262d;
|
--code-button-bg: #151a26;
|
||||||
--code-button-hover-bg: #30363d;
|
--code-button-hover-bg: #202738;
|
||||||
--pre-text: #e6edf3;
|
--pre-text: #e2e8f0;
|
||||||
--code-inline-bg: #161b22;
|
--code-inline-bg: #131722;
|
||||||
--code-inline-text: #e6edf3;
|
--code-inline-text: #e2e8f0;
|
||||||
--code-inline-border: #30363d;
|
--code-inline-border: #202738;
|
||||||
|
|
||||||
--table-zebra: rgba(255, 255, 255, 0.02);
|
--table-zebra: rgba(255, 255, 255, 0.02);
|
||||||
--scrollbar-thumb: #23252a;
|
--scrollbar-thumb: #1e2536;
|
||||||
--scrollbar-thumb-hover: #383b42;
|
--scrollbar-thumb-hover: #313c54;
|
||||||
--shadow-card: none;
|
--shadow-card: none;
|
||||||
|
|
||||||
/* Syntax Highlighting (Dark Mode) */
|
/* Syntax Highlighting (Dark Mode) */
|
||||||
@@ -107,6 +114,9 @@ html.dark {
|
|||||||
--hl-operator: #8b949e;
|
--hl-operator: #8b949e;
|
||||||
--hl-variable: #ffa657;
|
--hl-variable: #ffa657;
|
||||||
--hl-meta: #8b949e;
|
--hl-meta: #8b949e;
|
||||||
|
|
||||||
|
/* Grid overlay */
|
||||||
|
--grid-pattern: rgba(255, 255, 255, 0.025);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -141,10 +151,19 @@ body {
|
|||||||
background-color: var(--bg-app);
|
background-color: var(--bg-app);
|
||||||
color: var(--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", "zero";
|
||||||
-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;
|
transition: background-color 0.25s ease, color 0.25s ease;
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background subtle tech grid */
|
||||||
|
.tech-grid-bg {
|
||||||
|
background-size: 32px 32px;
|
||||||
|
background-image: linear-gradient(to right, var(--grid-pattern) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, var(--grid-pattern) 1px, transparent 1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
@@ -169,44 +188,100 @@ body {
|
|||||||
/* Glassmorphism helpers */
|
/* Glassmorphism helpers */
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-nav {
|
.glass-nav {
|
||||||
background: var(--nav-bg);
|
background: var(--nav-bg);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-card {
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--brand);
|
||||||
|
box-shadow: 0 8px 30px -10px var(--brand-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Callout Alert Styles */
|
||||||
|
.callout {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-note {
|
||||||
|
border-left-color: #6366f1;
|
||||||
|
background-color: rgba(99, 102, 241, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-tip {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
background-color: rgba(16, 185, 129, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-important {
|
||||||
|
border-left-color: #8b5cf6;
|
||||||
|
background-color: rgba(139, 92, 246, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-warning {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
background-color: rgba(245, 158, 11, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-caution {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
background-color: rgba(239, 68, 68, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prose markdown typography for both Light and Dark modes */
|
/* Prose markdown typography for both Light and Dark modes */
|
||||||
.prose {
|
.prose {
|
||||||
color: var(--text-secondary) !important;
|
color: var(--text-secondary) !important;
|
||||||
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h1, .prose h2, .prose h3, .prose h4, .prose strong {
|
.prose h1, .prose h2, .prose h3, .prose h4, .prose strong {
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose a {
|
.prose a {
|
||||||
color: var(--accent) !important;
|
color: var(--accent) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a:hover {
|
||||||
|
color: var(--accent-hover) !important;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose table {
|
.prose table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
margin: 1.75rem 0;
|
margin: 2rem 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose th,
|
.prose th,
|
||||||
.prose td {
|
.prose td {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.875rem 1.125rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
@@ -217,7 +292,8 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.06em;
|
||||||
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose td {
|
.prose td {
|
||||||
@@ -228,6 +304,10 @@ body {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prose tbody tr:hover {
|
||||||
|
background: var(--bg-elevated-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.prose tbody tr:nth-child(even) {
|
.prose tbody tr:nth-child(even) {
|
||||||
background: var(--table-zebra);
|
background: var(--table-zebra);
|
||||||
}
|
}
|
||||||
@@ -237,10 +317,11 @@ body {
|
|||||||
color: var(--pre-text) !important;
|
color: var(--pre-text) !important;
|
||||||
border: 1px solid var(--border-code);
|
border: 1px solid var(--border-code);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1.25rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose code {
|
.prose code {
|
||||||
@@ -252,7 +333,7 @@ body {
|
|||||||
background: var(--code-inline-bg);
|
background: var(--code-inline-bg);
|
||||||
color: var(--code-inline-text);
|
color: var(--code-inline-text);
|
||||||
border: 1px solid var(--code-inline-border);
|
border: 1px solid var(--code-inline-border);
|
||||||
padding: 0.15rem 0.375rem;
|
padding: 0.15rem 0.4rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -264,12 +345,12 @@ body {
|
|||||||
|
|
||||||
.prose blockquote {
|
.prose blockquote {
|
||||||
border-left: 3px solid var(--brand);
|
border-left: 3px solid var(--brand);
|
||||||
background: rgba(94, 106, 210, 0.08);
|
background: rgba(99, 102, 241, 0.08);
|
||||||
border-radius: 0 0.375rem 0.375rem 0;
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
margin: 1.5em 0;
|
margin: 1.75em 0;
|
||||||
padding: 0.75em 1.25em;
|
padding: 0.875em 1.25em;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose hr {
|
.prose hr {
|
||||||
@@ -346,14 +427,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hljs-addition {
|
.hljs-addition {
|
||||||
color: #1a7f37;
|
color: #10b981;
|
||||||
background-color: rgba(46, 160, 67, 0.15);
|
background-color: rgba(16, 185, 129, 0.15);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-deletion {
|
.hljs-deletion {
|
||||||
color: #cf222e;
|
color: #ef4444;
|
||||||
background-color: rgba(248, 81, 73, 0.15);
|
background-color: rgba(239, 68, 68, 0.15);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,37 +446,50 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Keyframes & Animation utilities */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(4px); }
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
@keyframes slideDown {
|
||||||
animation: fadeIn 0.2s ease-out forwards;
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
@keyframes pulseGlow {
|
||||||
Print & PDF Export Stylesheet
|
0%, 100% { opacity: 0.6; }
|
||||||
Optimized for publication-grade PDF generation via browser / window.print()
|
50% { opacity: 1; filter: drop-shadow(0 0 8px var(--brand)); }
|
||||||
========================================================================== */
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-down {
|
||||||
|
animation: slideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-glow {
|
||||||
|
animation: pulseGlow 2.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print & PDF Export Stylesheet */
|
||||||
@media print {
|
@media print {
|
||||||
@page {
|
@page {
|
||||||
size: A4 portrait;
|
size: A4 portrait;
|
||||||
margin: 18mm 15mm 18mm 15mm;
|
margin: 18mm 15mm 18mm 15mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Force exact background colors and graphics */
|
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
-webkit-print-color-adjust: exact !important;
|
-webkit-print-color-adjust: exact !important;
|
||||||
print-color-adjust: exact !important;
|
print-color-adjust: exact !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reset document canvas to white for crisp printing & ink economy */
|
|
||||||
html, body {
|
html, body {
|
||||||
background-color: #ffffff !important;
|
background-color: #ffffff !important;
|
||||||
color: #0f172a !important;
|
color: #0f172a !important;
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
||||||
font-size: 10pt !important;
|
font-size: 10pt !important;
|
||||||
line-height: 1.5 !important;
|
line-height: 1.5 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
@@ -403,9 +497,9 @@ body {
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide app navigation, headers, sidebars, interactive controls */
|
|
||||||
header,
|
header,
|
||||||
aside,
|
aside,
|
||||||
|
footer,
|
||||||
nav.site-header,
|
nav.site-header,
|
||||||
.no-print,
|
.no-print,
|
||||||
.export-toolbar,
|
.export-toolbar,
|
||||||
@@ -413,7 +507,6 @@ body {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full width container in print */
|
|
||||||
main,
|
main,
|
||||||
.print-container {
|
.print-container {
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
@@ -425,7 +518,6 @@ body {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page break rules */
|
|
||||||
.page-break-before,
|
.page-break-before,
|
||||||
.chapter-page,
|
.chapter-page,
|
||||||
.challenge-entry {
|
.challenge-entry {
|
||||||
@@ -433,14 +525,6 @@ body {
|
|||||||
page-break-before: always !important;
|
page-break-before: always !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-break-after,
|
|
||||||
.cover-page,
|
|
||||||
.scoreboard-page,
|
|
||||||
.toc-page {
|
|
||||||
break-after: page !important;
|
|
||||||
page-break-after: always !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avoid-break-inside,
|
.avoid-break-inside,
|
||||||
.code-block-wrapper,
|
.code-block-wrapper,
|
||||||
.code-block-container,
|
.code-block-container,
|
||||||
@@ -459,7 +543,6 @@ body {
|
|||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code block wrapping and styling in print */
|
|
||||||
.code-block-wrapper,
|
.code-block-wrapper,
|
||||||
.code-block-container {
|
.code-block-container {
|
||||||
break-inside: avoid !important;
|
break-inside: avoid !important;
|
||||||
@@ -492,87 +575,8 @@ body {
|
|||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
word-break: break-all !important;
|
word-break: break-all !important;
|
||||||
overflow-wrap: anywhere !important;
|
overflow-wrap: anywhere !important;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
|
||||||
font-size: 8.5pt !important;
|
font-size: 8.5pt !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.4 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
|
||||||
background-color: #f1f5f9 !important;
|
|
||||||
color: #0f172a !important;
|
|
||||||
padding: 1pt 3pt !important;
|
|
||||||
border-radius: 3px !important;
|
|
||||||
border: 1px solid #e2e8f0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background-color: #f8fafc !important;
|
|
||||||
color: #0f172a !important;
|
|
||||||
border: 1px solid #cbd5e1 !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
padding: 8pt 10pt !important;
|
|
||||||
margin: 8pt 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre code {
|
|
||||||
background-color: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography tweaks for formal print */
|
|
||||||
.prose {
|
|
||||||
color: #0f172a !important;
|
|
||||||
font-size: 10pt !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h1,
|
|
||||||
.prose h2,
|
|
||||||
.prose h3,
|
|
||||||
.prose h4,
|
|
||||||
.prose h5,
|
|
||||||
.prose h6 {
|
|
||||||
color: #020617 !important;
|
|
||||||
font-weight: 700 !important;
|
|
||||||
page-break-after: avoid !important;
|
|
||||||
break-after: avoid !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose p,
|
|
||||||
.prose li {
|
|
||||||
color: #334155 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose a {
|
|
||||||
color: #0f172a !important;
|
|
||||||
text-decoration: underline !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose blockquote {
|
|
||||||
background-color: #f8fafc !important;
|
|
||||||
border-left: 3pt solid #334155 !important;
|
|
||||||
color: #334155 !important;
|
|
||||||
padding: 6pt 10pt !important;
|
|
||||||
margin: 8pt 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose table {
|
|
||||||
border: 1px solid #cbd5e1 !important;
|
|
||||||
font-size: 9pt !important;
|
|
||||||
width: 100% !important;
|
|
||||||
border-collapse: collapse !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose th {
|
|
||||||
background-color: #f1f5f9 !important;
|
|
||||||
color: #0f172a !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
border-bottom: 1.5px solid #94a3b8 !important;
|
|
||||||
padding: 4pt 6pt !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose td {
|
|
||||||
border-bottom: 1px solid #e2e8f0 !important;
|
|
||||||
padding: 4pt 6pt !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-4
@@ -2,9 +2,11 @@ import type { Metadata } from "next";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Database, Activity, Sparkles, Terminal } from "lucide-react";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "MCPedia — Knowledge base for humans and AI agents",
|
title: "MCPedia — Knowledge Base for Humans and AI Agents",
|
||||||
description:
|
description:
|
||||||
"A content-first knowledge base. Humans read the Web UI; AI agents use the MCP server. Both share one Core.",
|
"A content-first knowledge base. Humans read the Web UI; AI agents use the MCP server. Both share one Core.",
|
||||||
};
|
};
|
||||||
@@ -35,16 +37,20 @@ export default function RootLayout({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className="min-h-screen bg-[var(--bg-app)] text-[var(--text-secondary)] font-sans antialiased selection:bg-[var(--brand)]/30 selection:text-white flex flex-col transition-colors duration-150">
|
<body className="min-h-screen bg-[var(--bg-app)] text-[var(--text-secondary)] font-sans antialiased selection:bg-[var(--brand)]/30 selection:text-white flex flex-col transition-colors duration-150 tech-grid-bg relative overflow-x-hidden">
|
||||||
|
{/* Top ambient radial glow effect */}
|
||||||
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[48rem] h-64 bg-indigo-500/10 dark:bg-indigo-600/15 blur-[120px] pointer-events-none rounded-full" />
|
||||||
|
<div className="absolute top-48 left-1/4 w-80 h-80 bg-cyan-500/5 dark:bg-cyan-500/10 blur-[100px] pointer-events-none rounded-full" />
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<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 relative z-10">
|
||||||
<div className="flex flex-1">
|
<div className="flex flex-1">
|
||||||
<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)]">
|
<aside className="hidden lg:block w-72 shrink-0 border-r border-[var(--border-color)] sticky top-15 h-[calc(100vh-3.75rem)] overflow-y-auto bg-[var(--bg-app)]/80 backdrop-blur-md">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</aside>
|
</aside>
|
||||||
<main className="flex-1 min-w-0">
|
<main className="flex-1 min-w-0">
|
||||||
@@ -54,6 +60,30 @@ export default function RootLayout({
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Global Telemetry & Status Footer */}
|
||||||
|
<footer className="no-print border-t border-[var(--border-color)] bg-[var(--bg-surface)]/60 backdrop-blur-md py-4 text-xs text-[var(--text-dim)] relative z-10">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-8 flex flex-col sm:flex-row items-center justify-between gap-3 font-mono">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
|
<span>MCPedia Intelligence Engine</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span className="text-[var(--text-muted)]">PostgreSQL 16 Core</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-[11px] text-[var(--text-muted)]">
|
||||||
|
<Link href="/search" className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] transition-colors">
|
||||||
|
RRF Hybrid Search
|
||||||
|
</Link>
|
||||||
|
<span>·</span>
|
||||||
|
<Link href="/export/pdf" className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] transition-colors">
|
||||||
|
Publication PDF
|
||||||
|
</Link>
|
||||||
|
<span>·</span>
|
||||||
|
<span className="text-[var(--text-dim)]">Native MCP :4021</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+20
-16
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Lock, Key, Eye, EyeOff, ArrowLeft, ShieldCheck } from "lucide-react";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -38,29 +39,29 @@ 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-[var(--bg-surface)] border border-[var(--border-color)] 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-3xl 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-[var(--brand)]/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-indigo-500/20 blur-2xl pointer-events-none rounded-full" />
|
||||||
|
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="w-12 h-12 rounded-xl bg-[var(--brand)] text-white font-bold text-xl flex items-center justify-center mx-auto mb-3 shadow-lg shadow-[var(--brand)]/25">
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-700 text-white font-bold text-xl flex items-center justify-center mx-auto mb-3 shadow-lg shadow-indigo-500/25">
|
||||||
M
|
<Lock className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">Admin Authentication</h1>
|
<h1 className="text-2xl font-extrabold text-[var(--text-primary)] tracking-tight">Admin Vault Authentication</h1>
|
||||||
<p className="text-xs text-[var(--text-muted)] 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 manage documents and trigger schema mutations.
|
||||||
</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-500 dark:text-rose-400 text-xs">
|
<div className="p-3.5 mb-6 rounded-xl border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-xs font-medium animate-fade-in">
|
||||||
{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-[var(--text-muted)] mb-1.5">
|
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
|
||||||
Admin Password
|
Admin Password
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -69,16 +70,18 @@ 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-[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-[var(--brand)] transition-colors"
|
className="w-full pl-10 pr-10 py-3 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[var(--brand)]/20 transition-all"
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
<Key className="w-4 h-4 text-[var(--text-dim)] absolute left-3.5 top-3.5 pointer-events-none" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="absolute right-3 top-2.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
className="absolute right-3.5 top-3 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] p-1 cursor-pointer"
|
||||||
|
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||||
>
|
>
|
||||||
{showPassword ? "Hide" : "Show"}
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,23 +89,24 @@ export default function LoginPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg transition-all font-medium text-sm shadow-md shadow-[var(--brand)]/20 disabled:opacity-50 flex items-center justify-center gap-2"
|
className="w-full py-3 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-xl transition-all font-semibold text-sm shadow-md shadow-[var(--brand)]/20 hover:shadow-[var(--brand)]/35 disabled:opacity-50 flex items-center justify-center gap-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
)}
|
)}
|
||||||
<span>{loading ? "Authenticating..." : "Sign In"}</span>
|
<span>{loading ? "Authenticating Session..." : "Authorize Access"}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-8 pt-6 border-t border-[var(--border-color)] flex items-center justify-between text-xs text-[var(--text-muted)]">
|
<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-[var(--text-primary)] transition-colors"
|
className="hover:text-[var(--text-primary)] transition-colors flex items-center gap-1"
|
||||||
>
|
>
|
||||||
← Back to Home
|
<ArrowLeft className="w-3.5 h-3.5" />
|
||||||
|
<span>Return to Knowledge Base</span>
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-[11px] text-[var(--text-dim)]">HTTP-only Secure Cookie</span>
|
<span className="text-[11px] text-[var(--text-dim)] font-mono">Secure HTTP Cookie</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+123
-71
@@ -2,6 +2,23 @@ import Link from "next/link";
|
|||||||
import { listDocuments, listSections } from "@mcpedia/core";
|
import { listDocuments, listSections } from "@mcpedia/core";
|
||||||
import { getSectionMeta, type SectionConfig } from "@mcpedia/config";
|
import { getSectionMeta, type SectionConfig } from "@mcpedia/config";
|
||||||
import McpConfigSnippet from "@/components/McpConfigSnippet";
|
import McpConfigSnippet from "@/components/McpConfigSnippet";
|
||||||
|
import InteractiveTopology from "@/components/InteractiveTopology";
|
||||||
|
import {
|
||||||
|
Sparkles,
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
BookOpen,
|
||||||
|
ArrowRight,
|
||||||
|
Database,
|
||||||
|
Cpu,
|
||||||
|
Layers,
|
||||||
|
Activity,
|
||||||
|
Folder,
|
||||||
|
FileText,
|
||||||
|
Clock,
|
||||||
|
CheckCircle2,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -22,7 +39,6 @@ function slugToTitle(segment: string): string {
|
|||||||
|
|
||||||
function buildFolderTree(docs: { slug: string; title: string }[], section: string): TreeNode[] {
|
function buildFolderTree(docs: { slug: string; title: string }[], section: string): TreeNode[] {
|
||||||
const tree: TreeNode[] = [];
|
const tree: TreeNode[] = [];
|
||||||
const treeIndex = new Map<string, TreeNode>();
|
|
||||||
|
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
const parts = doc.slug.split("/");
|
const parts = doc.slug.split("/");
|
||||||
@@ -44,7 +60,6 @@ function buildFolderTree(docs: { slug: string; title: string }[], section: strin
|
|||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
};
|
};
|
||||||
current.push(node);
|
current.push(node);
|
||||||
treeIndex.set(currentPath, node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLeaf) {
|
if (isLeaf) {
|
||||||
@@ -77,26 +92,28 @@ function buildFolderTree(docs: { slug: string; title: string }[], section: strin
|
|||||||
return sortAll(tree);
|
return sortAll(tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTree(nodes: TreeNode[], pathname: string) {
|
function renderTree(nodes: TreeNode[]) {
|
||||||
return (
|
return (
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{nodes.map((node) => (
|
{nodes.slice(0, 5).map((node) => (
|
||||||
<li key={node.slug}>
|
<li key={node.slug}>
|
||||||
<Link
|
<Link
|
||||||
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-2 text-xs py-1 px-2 rounded-md text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)] transition-colors group"
|
||||||
pathname === `/${node.slug}`
|
|
||||||
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-medium"
|
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
|
||||||
}`}
|
|
||||||
title={node.title}
|
title={node.title}
|
||||||
>
|
>
|
||||||
<span className="opacity-80">{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span>
|
<span className="shrink-0 text-[var(--text-dim)] group-hover:text-[var(--brand)] transition-colors">
|
||||||
|
{node.isLeaf && node.children.length === 0 ? (
|
||||||
|
<FileText className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<Folder className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</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-[var(--border-color)] pl-2 mt-0.5">
|
<div className="ml-3 border-l border-[var(--border-color)] pl-2 mt-0.5">
|
||||||
{renderTree(node.children, pathname)}
|
{renderTree(node.children)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
@@ -105,13 +122,7 @@ function renderTree(nodes: TreeNode[], pathname: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SectionTreeProps {
|
function SectionTree({ section, docs }: { section: string; docs: { slug: string; title: string }[] }) {
|
||||||
section: string;
|
|
||||||
docs: { slug: string; title: string }[];
|
|
||||||
pathname: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SectionTree({ section, docs, pathname }: SectionTreeProps) {
|
|
||||||
const tree = buildFolderTree(
|
const tree = buildFolderTree(
|
||||||
docs.filter((d) => d.slug.startsWith(`${section}/`)),
|
docs.filter((d) => d.slug.startsWith(`${section}/`)),
|
||||||
section,
|
section,
|
||||||
@@ -120,9 +131,9 @@ function SectionTree({ section, docs, pathname }: SectionTreeProps) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{tree.length > 0 ? (
|
{tree.length > 0 ? (
|
||||||
renderTree(tree, pathname)
|
renderTree(tree)
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-[var(--text-dim)] pl-1">No documents</p>
|
<p className="text-xs text-[var(--text-dim)] pl-2">No documents yet</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -144,77 +155,113 @@ export default async function HomePage() {
|
|||||||
return (
|
return (
|
||||||
<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-2 pb-4">
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--brand)]/10 border border-[var(--brand)]/25 text-[var(--brand)] dark:text-[var(--accent)] text-xs font-medium mb-6">
|
{/* Floating status pill */}
|
||||||
<span className="w-2 h-2 rounded-full bg-[var(--brand)] animate-pulse" />
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--brand)]/10 border border-[var(--brand)]/25 text-[var(--brand)] dark:text-[var(--accent)] text-xs font-semibold mb-6 shadow-xs animate-fade-in">
|
||||||
<span>Model Context Protocol + PostgreSQL Core</span>
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
|
<span>Model Context Protocol · PostgreSQL Source of Truth</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-[var(--text-primary)] tracking-tight mb-5 leading-tight">
|
{/* Primary Title */}
|
||||||
|
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-[var(--text-primary)] tracking-tight mb-5 leading-[1.12]">
|
||||||
Knowledge Base for <br />
|
Knowledge Base for <br />
|
||||||
<span className="bg-gradient-to-r from-[var(--brand)] via-[var(--accent)] to-[var(--accent-hover)] dark:to-white bg-clip-text text-transparent">
|
<span className="bg-gradient-to-r from-indigo-500 via-indigo-600 to-cyan-500 dark:to-cyan-400 bg-clip-text text-transparent">
|
||||||
Humans and AI Agents
|
Humans and AI Agents
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
<p className="text-base sm:text-lg text-[var(--text-muted)] 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, database-backed documentation system. Browse hierarchical notes and technical writeups in your browser, or connect AI coding assistants directly via native MCP tools.
|
A high-performance technical knowledge repository. Browse structured notes and CTF writeups in your browser, or equip AI coding assistants directly via native MCP tools.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Quick action buttons & stats */}
|
{/* Quick Action Buttons */}
|
||||||
<div className="flex flex-wrap items-center gap-3 mb-10">
|
<div className="flex flex-wrap items-center gap-3 mb-10">
|
||||||
<Link
|
<Link
|
||||||
href="/docs"
|
href="/docs"
|
||||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg font-medium text-sm transition-all shadow-md shadow-[var(--brand)]/20"
|
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-xl font-semibold text-sm transition-all shadow-md shadow-[var(--brand)]/25 hover:shadow-[var(--brand)]/40 hover:-translate-y-0.5 cursor-pointer"
|
||||||
>
|
>
|
||||||
|
<BookOpen className="w-4 h-4" />
|
||||||
<span>Browse Documentation</span>
|
<span>Browse Documentation</span>
|
||||||
<span>→</span>
|
<ArrowRight className="w-4 h-4 ml-0.5" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/search"
|
href="/search"
|
||||||
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"
|
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-xl font-semibold text-sm transition-all shadow-xs hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4 text-[var(--text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Search className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<span>Hybrid Search Console</span>
|
||||||
</svg>
|
|
||||||
<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-[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"
|
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)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-xl text-sm transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<span>+ Create Document</span>
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Create Document</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats bar */}
|
{/* Live Telemetry Metric Strip */}
|
||||||
<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 className="grid grid-cols-2 sm:grid-cols-4 gap-3 p-4 sm:p-5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400">
|
||||||
|
<Database className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-bold text-[var(--text-primary)]">{all.length}</div>
|
<div className="text-xl font-bold text-[var(--text-primary)]">{all.length}</div>
|
||||||
<div className="text-xs text-[var(--text-muted)]">Documents in DB</div>
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">Documents in DB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400">
|
||||||
|
<Layers className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-bold text-[var(--text-primary)]">{sections.length}</div>
|
<div className="text-xl font-bold text-[var(--text-primary)]">{sections.length}</div>
|
||||||
<div className="text-xs text-[var(--text-muted)]">Active sections</div>
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">Active Sections</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400">
|
||||||
|
<Search className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-bold text-[var(--brand)] dark:text-[var(--accent)]">RRF Hybrid</div>
|
<div className="text-xl font-bold text-[var(--brand)] dark:text-[var(--accent)]">RRF Hybrid</div>
|
||||||
<div className="text-xs text-[var(--text-muted)]">FTS + Cosine Vectors</div>
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">FTS + Cosine Vectors</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
|
||||||
|
<Terminal className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">13 MCP Tools</div>
|
<div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">13 Tools</div>
|
||||||
<div className="text-xs text-[var(--text-muted)]">Streamable HTTP :4021</div>
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">Streamable HTTP :4021</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Browse by Section */}
|
{/* Interactive System Architecture & Topology */}
|
||||||
|
<section>
|
||||||
|
<InteractiveTopology />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Browse Knowledge 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-[var(--text-muted)] uppercase tracking-wider">
|
<div className="flex items-center gap-2">
|
||||||
Browse by Section
|
<Layers className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Browse Knowledge by Section
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{sections.map((s) => {
|
{sections.map((s) => {
|
||||||
@@ -222,19 +269,19 @@ export default async function HomePage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={s.id}
|
key={s.id}
|
||||||
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"
|
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-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<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-[var(--brand)] dark:text-[var(--accent)] hover:underline"
|
className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-semibold"
|
||||||
>
|
>
|
||||||
{sectionDocs.length} docs →
|
{sectionDocs.length} docs →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-base text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-1.5">
|
<h3 className="font-bold text-base text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-1.5">
|
||||||
{s.label}
|
{s.label}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-[var(--text-muted)] mb-4 line-clamp-2 leading-relaxed">
|
<p className="text-xs text-[var(--text-muted)] mb-4 line-clamp-2 leading-relaxed">
|
||||||
@@ -242,12 +289,11 @@ export default async function HomePage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Inline folder tree */}
|
{/* Inline folder directory preview */}
|
||||||
<div className="pt-3 border-t border-[var(--border-color)]">
|
<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 }))}
|
||||||
pathname="/"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,18 +302,22 @@ export default async function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Recent Documents */}
|
{/* Recently Updated Knowledge */}
|
||||||
{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-[var(--text-muted)] uppercase tracking-wider">
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
Recently Updated Knowledge
|
Recently Updated Knowledge
|
||||||
</h2>
|
</h2>
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/docs"
|
href="/docs"
|
||||||
className="text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-medium"
|
className="text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-semibold flex items-center gap-1"
|
||||||
>
|
>
|
||||||
View all ({all.length}) →
|
<span>View all ({all.length})</span>
|
||||||
|
<ArrowRight className="w-3.5 h-3.5" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -278,11 +328,12 @@ export default async function HomePage() {
|
|||||||
<Link
|
<Link
|
||||||
key={d.slug}
|
key={d.slug}
|
||||||
href={`/${d.slug}`}
|
href={`/${d.slug}`}
|
||||||
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"
|
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2 mb-2">
|
<div className="flex items-center justify-between gap-2 mb-2.5">
|
||||||
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] uppercase">
|
<span className="text-[11px] font-mono font-semibold text-[var(--brand)] dark:text-[var(--accent)] uppercase flex items-center gap-1">
|
||||||
{sInfo?.icon} {d.section}
|
<span>{sInfo?.icon}</span>
|
||||||
|
<span>{d.section}</span>
|
||||||
</span>
|
</span>
|
||||||
<time className="text-[11px] text-[var(--text-dim)] font-mono">
|
<time className="text-[11px] text-[var(--text-dim)] font-mono">
|
||||||
{new Date(d.updatedAt).toLocaleDateString(undefined, {
|
{new Date(d.updatedAt).toLocaleDateString(undefined, {
|
||||||
@@ -291,15 +342,15 @@ export default async function HomePage() {
|
|||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-medium text-sm text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-2">
|
<h3 className="font-bold text-sm text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-2">
|
||||||
{d.title}
|
{d.title}
|
||||||
</h3>
|
</h3>
|
||||||
{d.tags.length > 0 && (
|
{d.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1 mt-3">
|
||||||
{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-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]"
|
className="text-[10px] px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -313,17 +364,18 @@ export default async function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* MCP Agent Integration Showcase */}
|
{/* Connect AI Assistant Showcase */}
|
||||||
<section className="border-t border-[var(--border-color)] 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-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] text-xs font-mono mb-2">
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] text-xs font-mono mb-3">
|
||||||
<span>⚡ Native Model Context Protocol (MCP)</span>
|
<Sparkles className="w-3.5 h-3.5 text-indigo-500" />
|
||||||
|
<span>Connect Coding Assistants</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight mb-2">
|
<h2 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight mb-2">
|
||||||
Connect AI Coding Assistants
|
Model Context Protocol Configuration
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-[var(--text-muted)] max-w-xl">
|
<p className="text-xs sm:text-sm text-[var(--text-muted)] max-w-xl leading-relaxed">
|
||||||
Give Claude Desktop, Cursor, Antigravity, or Zed direct access to your knowledge base via semantic search, read resources, and document tools.
|
Equip Claude Desktop, Cursor, Antigravity, or custom agents with direct access to your knowledge base via semantic search, read resources, and document tools.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,19 @@ import { useState, useEffect, useCallback, Suspense } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
|
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Sparkles,
|
||||||
|
Zap,
|
||||||
|
Cpu,
|
||||||
|
Layers,
|
||||||
|
FileText,
|
||||||
|
Clock,
|
||||||
|
ArrowRight,
|
||||||
|
Filter,
|
||||||
|
X,
|
||||||
|
Compass,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface DocHit {
|
interface DocHit {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -78,7 +91,8 @@ function SearchContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredResults = selectedSection === "all"
|
const filteredResults =
|
||||||
|
selectedSection === "all"
|
||||||
? results
|
? results
|
||||||
: results.filter((r) => r.section === selectedSection);
|
: results.filter((r) => r.section === selectedSection);
|
||||||
|
|
||||||
@@ -86,65 +100,64 @@ 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-[var(--text-primary)] tracking-tight mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
Search Knowledge Base
|
<span className="p-2 rounded-xl bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)]">
|
||||||
|
<Search className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
<h1 className="text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
|
||||||
|
Multi-Modal Search Console
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--text-muted)]">
|
</div>
|
||||||
Find answers across all documentation, CTF writeups, research notes, and semantic vector chunks.
|
<p className="text-xs sm:text-sm text-[var(--text-muted)]">
|
||||||
|
Search across documentation, CTF writeups, and research notes using PostgreSQL Full-Text Search, Cosine Embeddings, or Reciprocal Rank Fusion.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Box & Controls */}
|
{/* Search Box & Controls */}
|
||||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-4 sm:p-5 shadow-sm space-y-4">
|
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-5 sm:p-6 shadow-sm space-y-5">
|
||||||
{/* Search Input */}
|
{/* Search Input Bar */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
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-[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-[var(--brand)]/20 text-base transition-all"
|
className="w-full pl-12 pr-10 py-3.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[var(--brand)]/20 text-base transition-all"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<svg
|
<Search className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] absolute left-4 top-4 pointer-events-none" />
|
||||||
className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] absolute left-3.5 top-3.5"
|
|
||||||
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" />
|
|
||||||
</svg>
|
|
||||||
{query && (
|
{query && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setQuery("");
|
setQuery("");
|
||||||
setResults([]);
|
setResults([]);
|
||||||
}}
|
}}
|
||||||
className="absolute right-3.5 top-3.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] p-0.5"
|
className="absolute right-3.5 top-3.5 p-1 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-surface)]"
|
||||||
|
aria-label="Clear query"
|
||||||
>
|
>
|
||||||
✕
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Mode & Section Filters */}
|
{/* Algorithm & Filter Controls */}
|
||||||
<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">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pt-3 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-[var(--text-muted)] mr-1">Algorithm:</span>
|
<span className="text-[var(--text-dim)] font-mono uppercase font-semibold mr-1">Algorithm:</span>
|
||||||
{[
|
{[
|
||||||
{ id: "hybrid", label: "⚡ Hybrid (RRF)", desc: "FTS + Vector Fusion" },
|
{ id: "hybrid", label: "⚡ Hybrid (RRF)", desc: "Reciprocal Rank Fusion (FTS + Vector)" },
|
||||||
{ id: "keyword", label: "🔍 Keyword", desc: "PostgreSQL FTS" },
|
{ id: "keyword", label: "🔍 Keyword (FTS)", desc: "PostgreSQL GIN tsvector" },
|
||||||
{ id: "semantic", label: "🧠 Semantic", desc: "Embeddings Cosine" },
|
{ id: "semantic", label: "🧠 Semantic Vector", desc: "Cosine Similarity over Embeddings" },
|
||||||
].map((m) => (
|
].map((m) => (
|
||||||
<button
|
<button
|
||||||
key={m.id}
|
key={m.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleModeChange(m.id as SearchMode)}
|
onClick={() => handleModeChange(m.id as SearchMode)}
|
||||||
title={m.desc}
|
title={m.desc}
|
||||||
className={`px-3 py-1 rounded-md border font-medium transition-all ${
|
className={`px-3 py-1.5 rounded-lg border font-semibold transition-all cursor-pointer ${
|
||||||
mode === m.id
|
mode === m.id
|
||||||
? "bg-[var(--brand)] border-[var(--brand)] text-white shadow-sm"
|
? "bg-[var(--brand)] border-[var(--brand)] text-white shadow-sm shadow-[var(--brand)]/25"
|
||||||
: "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
: "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -158,22 +171,22 @@ function SearchContent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
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-lg text-xs font-medium transition-colors ${
|
||||||
selectedSection === "all"
|
selectedSection === "all"
|
||||||
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-medium"
|
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-bold"
|
||||||
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All Sections
|
All
|
||||||
</button>
|
</button>
|
||||||
{sections.map((s) => (
|
{sections.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s.id}
|
key={s.id}
|
||||||
type="button"
|
type="button"
|
||||||
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-lg text-xs font-medium transition-colors ${
|
||||||
selectedSection === s.id
|
selectedSection === s.id
|
||||||
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-medium"
|
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-bold"
|
||||||
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -184,38 +197,48 @@ function SearchContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading State */}
|
{/* Loading Spinner */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center gap-3 py-8 text-[var(--text-muted)] text-sm">
|
<div className="flex items-center justify-center gap-3 py-12 text-[var(--text-muted)] text-sm animate-fade-in">
|
||||||
<div className="w-5 h-5 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin" />
|
<div className="w-5 h-5 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin" />
|
||||||
<span>Searching knowledge base...</span>
|
<span>Searching knowledge vector space...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
{!loading && query && filteredResults.length === 0 && (
|
{!loading && query && filteredResults.length === 0 && (
|
||||||
<div className="text-center py-16 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-sm">
|
<div className="text-center py-16 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-sm space-y-3 animate-fade-in">
|
||||||
<div className="text-3xl mb-3">🔍</div>
|
<div className="p-3 w-12 h-12 rounded-2xl bg-[var(--bg-elevated)] text-[var(--text-dim)] mx-auto flex items-center justify-center">
|
||||||
<h3 className="text-base font-semibold text-[var(--text-primary)] mb-1">No matching documents</h3>
|
<Search className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-base font-bold text-[var(--text-primary)]">No matching documents found</h3>
|
||||||
<p className="text-xs text-[var(--text-muted)] 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Initial Hint */}
|
{/* Initial Suggestions */}
|
||||||
{!loading && !query && (
|
{!loading && !query && (
|
||||||
<div className="p-8 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-4 shadow-sm">
|
<div className="p-6 sm:p-8 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl space-y-4 shadow-sm">
|
||||||
<p className="text-xs text-[var(--text-muted)]">Popular search queries:</p>
|
<p className="text-xs font-mono uppercase tracking-wider text-[var(--text-dim)]">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 exploit",
|
||||||
|
"bullmq worker",
|
||||||
|
"streamable http",
|
||||||
|
"full-text search",
|
||||||
|
"caddy reverse proxy",
|
||||||
|
"pgvector cosine",
|
||||||
|
].map((term) => (
|
||||||
<button
|
<button
|
||||||
key={term}
|
key={term}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setQuery(term);
|
setQuery(term);
|
||||||
handleSearch(term, mode);
|
handleSearch(term, mode);
|
||||||
}}
|
}}
|
||||||
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"
|
className="px-3.5 py-1.5 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all font-mono cursor-pointer"
|
||||||
>
|
>
|
||||||
{term}
|
{term}
|
||||||
</button>
|
</button>
|
||||||
@@ -226,10 +249,10 @@ function SearchContent() {
|
|||||||
|
|
||||||
{/* Results List */}
|
{/* Results List */}
|
||||||
{!loading && filteredResults.length > 0 && (
|
{!loading && filteredResults.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 animate-fade-in">
|
||||||
<div className="flex items-center justify-between text-xs text-[var(--text-dim)]">
|
<div className="flex items-center justify-between text-xs text-[var(--text-dim)] font-mono">
|
||||||
<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>Ranked by {mode} score</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -237,29 +260,29 @@ function SearchContent() {
|
|||||||
<Link
|
<Link
|
||||||
key={r.slug}
|
key={r.slug}
|
||||||
href={`/${r.slug}`}
|
href={`/${r.slug}`}
|
||||||
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"
|
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3 mb-2">
|
<div className="flex items-center justify-between gap-3 mb-2.5">
|
||||||
<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-[var(--brand)]/10 border border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)] uppercase">
|
<span className="text-[10px] font-mono px-2 py-0.5 rounded-md bg-[var(--brand)]/10 border border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)] uppercase font-bold">
|
||||||
{r.section}
|
{r.section}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-[var(--text-dim)] 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-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] 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.5 py-0.5 rounded-md font-semibold">
|
||||||
Score: {r.score.toFixed(3)}
|
Score: {r.score.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-base font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-2">
|
<h2 className="text-base font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-2">
|
||||||
{r.title}
|
{r.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{r.snippet && (
|
{r.snippet && (
|
||||||
<p
|
<p
|
||||||
className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--brand)]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:px-1 [&>mark]:rounded [&>mark]:font-medium"
|
className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--brand)]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:px-1 [&>mark]:rounded [&>mark]:font-semibold"
|
||||||
dangerouslySetInnerHTML={{ __html: r.snippet }}
|
dangerouslySetInnerHTML={{ __html: r.snippet }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@mcpedia/core": "workspace:*",
|
"@mcpedia/core": "workspace:*",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
|
"lucide-react": "^1.33.0",
|
||||||
"next": "16.3.1",
|
"next": "16.3.1",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"@mcpedia/core": "workspace:*",
|
"@mcpedia/core": "workspace:*",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
|
"lucide-react": "^1.33.0",
|
||||||
"next": "16.3.1",
|
"next": "16.3.1",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
@@ -1120,6 +1121,8 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@1.33.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg=="],
|
||||||
|
|
||||||
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user