style(web): refine aesthetic to archival precision, remove generic gradients and glowing blobs
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:
@@ -5,18 +5,12 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Search,
|
||||
FileText,
|
||||
Plus,
|
||||
Compass,
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
Flag,
|
||||
Layers,
|
||||
SunMoon,
|
||||
Command,
|
||||
Sparkles,
|
||||
X,
|
||||
Flame,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Hit {
|
||||
@@ -49,7 +43,7 @@ export default function CommandMenu() {
|
||||
{
|
||||
id: "create-doc",
|
||||
title: "Create New Document",
|
||||
subtitle: "Write technical doc, writeup, or research note",
|
||||
subtitle: "Write technical doc, writeup, or note",
|
||||
icon: Plus,
|
||||
href: "/create",
|
||||
},
|
||||
@@ -69,14 +63,14 @@ export default function CommandMenu() {
|
||||
},
|
||||
{
|
||||
id: "browse-writeups",
|
||||
title: "CTF & Challenge Writeups",
|
||||
title: "CTF & Writeups Directory",
|
||||
subtitle: "Pwn, Reverse, Web, Crypto writeups and solutions",
|
||||
icon: Flag,
|
||||
href: "/writeups",
|
||||
},
|
||||
{
|
||||
id: "browse-research",
|
||||
title: "Deep-Dive Research",
|
||||
title: "Research & Benchmarks",
|
||||
subtitle: "Architecture notes, benchmarks, experiments",
|
||||
icon: Sparkles,
|
||||
href: "/research",
|
||||
@@ -160,11 +154,11 @@ export default function CommandMenu() {
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
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"
|
||||
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(--text-muted)] text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-xs cursor-pointer group"
|
||||
>
|
||||
<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" />
|
||||
<span className="font-sans">Search knowledge...</span>
|
||||
<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">
|
||||
<Search className="w-3.5 h-3.5 text-[var(--text-dim)] group-hover:text-[var(--text-primary)] transition-colors" />
|
||||
<span className="font-sans">Search...</span>
|
||||
<kbd className="ml-2 font-mono bg-[var(--bg-surface)] px-1.5 py-0.2 rounded border border-[var(--border-color)] text-[10px] text-[var(--text-muted)]">
|
||||
⌘K
|
||||
</kbd>
|
||||
</button>
|
||||
@@ -175,31 +169,31 @@ export default function CommandMenu() {
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-slate-950/60 dark:bg-black/80 backdrop-blur-md transition-opacity"
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-xs transition-opacity"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Omnisearch Modal */}
|
||||
<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">
|
||||
{/* 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">
|
||||
{/* Search input bar */}
|
||||
<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">
|
||||
<Search className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] shrink-0" />
|
||||
<div className="flex items-center gap-3 px-4 sm:px-5 py-3.5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
|
||||
<Search className="w-4 h-4 text-[var(--text-muted)] shrink-0" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyNavigation}
|
||||
placeholder="Search documents, writeups, concepts, or commands..."
|
||||
className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm sm:text-base focus:outline-none"
|
||||
placeholder="Search documents, writeups, concepts, or tools..."
|
||||
className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm focus:outline-none"
|
||||
/>
|
||||
{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(--text-muted)] border-t-transparent rounded-full animate-spin shrink-0" />
|
||||
)}
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
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)]"
|
||||
className="text-[10px] font-mono text-[var(--text-dim)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)]"
|
||||
>
|
||||
ESC
|
||||
</button>
|
||||
@@ -208,9 +202,9 @@ export default function CommandMenu() {
|
||||
{/* Results / Quick Actions List */}
|
||||
<div className="max-h-[22rem] overflow-y-auto p-2">
|
||||
{query.trim() === "" ? (
|
||||
<div className="p-2 space-y-1">
|
||||
<div className="px-3 py-1.5 text-[10px] font-mono uppercase tracking-wider text-[var(--text-dim)]">
|
||||
Quick Shortcuts & Actions
|
||||
<div className="p-1 space-y-0.5">
|
||||
<div className="px-2.5 py-1 text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">
|
||||
Quick Shortcuts
|
||||
</div>
|
||||
{quickActions.map((action, idx) => {
|
||||
const Icon = action.icon;
|
||||
@@ -220,18 +214,18 @@ export default function CommandMenu() {
|
||||
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 ${
|
||||
className={`flex items-center justify-between gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors ${
|
||||
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"
|
||||
? "bg-[var(--bg-elevated)] text-[var(--text-primary)]"
|
||||
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)]"
|
||||
}`}
|
||||
>
|
||||
<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 className="flex items-center gap-2.5">
|
||||
<div className={`p-1.5 rounded ${isSelected ? "bg-[var(--brand)] text-[var(--brand-fg)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs sm:text-sm font-semibold text-[var(--text-primary)]">
|
||||
<div className="text-xs font-semibold text-[var(--text-primary)]">
|
||||
{action.title}
|
||||
</div>
|
||||
<div className="text-[11px] text-[var(--text-muted)]">
|
||||
@@ -239,17 +233,14 @@ export default function CommandMenu() {
|
||||
</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`} />
|
||||
<ArrowRight className={`w-3.5 h-3.5 text-[var(--text-dim)] ${isSelected ? "text-[var(--text-primary)]" : ""}`} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : results.length === 0 && !loading ? (
|
||||
<div className="px-4 py-12 text-center text-xs text-[var(--text-muted)] space-y-2">
|
||||
<div className="px-4 py-10 text-center text-xs text-[var(--text-muted)] space-y-1">
|
||||
<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>
|
||||
) : (
|
||||
<ul className="space-y-1 p-1">
|
||||
@@ -260,24 +251,24 @@ export default function CommandMenu() {
|
||||
key={hit.slug}
|
||||
onClick={() => handleNavigate(`/${hit.slug}`)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-all ${
|
||||
className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors ${
|
||||
isSelected
|
||||
? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)] shadow-xs"
|
||||
? "bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<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">
|
||||
<span className="text-[10px] font-mono px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] uppercase font-semibold">
|
||||
{hit.section}
|
||||
</span>
|
||||
<span className={`text-xs sm:text-sm font-semibold ${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(--text-primary)]" : "text-[var(--text-primary)]"} truncate`}>
|
||||
{hit.title}
|
||||
</span>
|
||||
</div>
|
||||
{hit.snippet && (
|
||||
<p
|
||||
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"
|
||||
className="text-xs text-[var(--text-muted)] line-clamp-1 mt-1 [&>mark]:bg-[var(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
||||
dangerouslySetInnerHTML={{ __html: hit.snippet }}
|
||||
/>
|
||||
)}
|
||||
@@ -293,15 +284,15 @@ export default function CommandMenu() {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<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)]">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-t border-[var(--border-color)] bg-[var(--bg-elevated)] text-[10px] text-[var(--text-muted)] font-mono">
|
||||
<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>
|
||||
<span>[ ↑↓ NAVIGATE ]</span>
|
||||
<span>[ ↵ SELECT ]</span>
|
||||
</div>
|
||||
<Link
|
||||
href={`/search?q=${encodeURIComponent(query)}`}
|
||||
onClick={() => setOpen(false)}
|
||||
className="text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-medium flex items-center gap-1"
|
||||
className="hover:text-[var(--text-primary)] font-medium flex items-center gap-1"
|
||||
>
|
||||
<span>Full Search Console</span>
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Share2,
|
||||
FileDown,
|
||||
Edit3,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
|
||||
interface DocActionsProps {
|
||||
@@ -39,13 +38,13 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
|
||||
<button
|
||||
onClick={handleCopyMarkdown}
|
||||
type="button"
|
||||
title="Copy raw markdown content to clipboard"
|
||||
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"
|
||||
title="Copy raw markdown content"
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-colors shadow-xs cursor-pointer"
|
||||
>
|
||||
{copiedMd ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span className="text-emerald-500 font-medium">Copied MD</span>
|
||||
<span className="text-emerald-500 font-medium">Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -60,12 +59,12 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
|
||||
onClick={handleCopyLink}
|
||||
type="button"
|
||||
title="Copy link to document"
|
||||
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"
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-colors shadow-xs cursor-pointer"
|
||||
>
|
||||
{copiedLink ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span className="text-emerald-500 font-medium">Copied Link</span>
|
||||
<span className="text-emerald-500 font-medium">Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -78,17 +77,17 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
|
||||
{/* Export PDF */}
|
||||
<Link
|
||||
href={`/${slug}/export`}
|
||||
title="Export document or parent folder to publication-grade PDF"
|
||||
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"
|
||||
title="Export document to publication-grade PDF"
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-colors shadow-xs"
|
||||
>
|
||||
<FileDown className="w-3.5 h-3.5" />
|
||||
<span>Export PDF</span>
|
||||
<span>PDF</span>
|
||||
</Link>
|
||||
|
||||
{/* Edit Link */}
|
||||
<Link
|
||||
href={canEdit ? `/${slug}?edit=1` : `/login?redirect=/${slug}`}
|
||||
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"
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-primary)] rounded-md transition-colors font-medium shadow-xs"
|
||||
>
|
||||
<Edit3 className="w-3.5 h-3.5" />
|
||||
<span>Edit</span>
|
||||
|
||||
@@ -10,18 +10,10 @@ import {
|
||||
Heading3,
|
||||
Code,
|
||||
Link as LinkIcon,
|
||||
Table as TableIcon,
|
||||
Info,
|
||||
Lightbulb,
|
||||
AlertTriangle,
|
||||
Plus,
|
||||
Trash2,
|
||||
Check,
|
||||
Eye,
|
||||
Edit3,
|
||||
FileCode,
|
||||
Tag,
|
||||
Folder,
|
||||
Sliders,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -127,7 +119,7 @@ export default function DocForm({
|
||||
const availableFolders = existingFolders?.[section] ?? [];
|
||||
|
||||
const baseInputCls =
|
||||
"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";
|
||||
"w-full px-3.5 py-2 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--text-primary)] transition-colors";
|
||||
|
||||
function addCustomField(key = "", value = "") {
|
||||
setCustomFields([...customFields, { key, value }]);
|
||||
@@ -229,7 +221,7 @@ export default function DocForm({
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6 max-w-4xl">
|
||||
{error && (
|
||||
<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">
|
||||
<div className="p-3.5 rounded-lg border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-xs font-medium animate-fade-in">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -244,7 +236,7 @@ export default function DocForm({
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="e.g. Exploiting Stack Alignment Issues in x86_64"
|
||||
className={`${baseInputCls} text-base sm:text-lg font-semibold`}
|
||||
className={`${baseInputCls} text-base font-semibold`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -339,7 +331,7 @@ export default function DocForm({
|
||||
type="text"
|
||||
value={slug ?? ""}
|
||||
readOnly
|
||||
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"
|
||||
className="w-full px-3.5 py-2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg text-sm text-[var(--text-muted)] font-mono"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@@ -352,7 +344,7 @@ export default function DocForm({
|
||||
/>
|
||||
)}
|
||||
<p className="text-[11px] text-[var(--text-dim)] mt-1.5 font-mono">
|
||||
Full Slug: <span className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">{effectiveSlug || section}</span>
|
||||
Full Slug: <span className="text-[var(--text-primary)] font-semibold">{effectiveSlug || section}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -386,26 +378,26 @@ export default function DocForm({
|
||||
</div>
|
||||
|
||||
{/* Custom Metadata Fields */}
|
||||
<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="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3.5 shadow-xs">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] font-mono flex items-center gap-1.5">
|
||||
<Sliders className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
|
||||
<span>Dynamic Technical Metadata</span>
|
||||
<Sliders className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||
<span>Technical Metadata</span>
|
||||
</h3>
|
||||
<p className="text-[11px] text-[var(--text-dim)] mt-0.5">
|
||||
Custom fields (points, difficulty, event, solved) will render as styled badges in the document hero.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<span className="text-[10px] font-mono text-[var(--text-dim)] uppercase">Presets:</span>
|
||||
{PRESET_FIELDS.map((preset) => (
|
||||
<button
|
||||
key={preset.key}
|
||||
type="button"
|
||||
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)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-colors font-mono cursor-pointer"
|
||||
className="px-2 py-0.5 text-[10px] 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 font-mono cursor-pointer"
|
||||
>
|
||||
+{preset.key}
|
||||
</button>
|
||||
@@ -432,7 +424,7 @@ export default function DocForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeCustomField(i)}
|
||||
className="p-2 text-rose-500 hover:text-rose-600 rounded-lg hover:bg-[var(--bg-elevated)] transition-colors cursor-pointer"
|
||||
className="p-2 text-rose-500 hover:text-rose-600 rounded hover:bg-[var(--bg-elevated)] transition-colors cursor-pointer"
|
||||
title="Remove field"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -443,7 +435,7 @@ export default function DocForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addCustomField()}
|
||||
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"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-[var(--text-primary)] hover:underline px-3 py-1 rounded-md border border-[var(--border-color)] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] transition-colors font-medium cursor-pointer"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Add Custom Field</span>
|
||||
@@ -451,17 +443,17 @@ export default function DocForm({
|
||||
</div>
|
||||
|
||||
{/* Markdown Body Editor */}
|
||||
<div className="border border-[var(--border-color)] rounded-2xl overflow-hidden bg-[var(--bg-surface)] shadow-sm">
|
||||
<div className="border border-[var(--border-color)] rounded-xl overflow-hidden bg-[var(--bg-surface)] shadow-xs">
|
||||
{/* Editor Toolbar */}
|
||||
<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">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2">
|
||||
{/* Tabs */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("write")}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-semibold transition-all cursor-pointer ${
|
||||
className={`flex items-center gap-1.5 px-3 py-1 text-xs rounded font-semibold transition-all cursor-pointer ${
|
||||
activeTab === "write"
|
||||
? "bg-[var(--brand)] text-white shadow-xs"
|
||||
? "bg-[var(--brand)] text-[var(--brand-fg)] shadow-xs"
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
@@ -471,14 +463,14 @@ export default function DocForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("preview")}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-semibold transition-all cursor-pointer ${
|
||||
className={`flex items-center gap-1.5 px-3 py-1 text-xs rounded font-semibold transition-all cursor-pointer ${
|
||||
activeTab === "preview"
|
||||
? "bg-[var(--brand)] text-white shadow-xs"
|
||||
? "bg-[var(--brand)] text-[var(--brand-fg)] shadow-xs"
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
<span>Live Preview</span>
|
||||
<span>Preview</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -488,23 +480,23 @@ export default function DocForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => insertMarkdown("**", "**")}
|
||||
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||
title="Bold (**text**)"
|
||||
className="p-1 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||
title="Bold"
|
||||
>
|
||||
<Bold 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="Italic (*text*)"
|
||||
className="p-1 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||
title="Italic"
|
||||
>
|
||||
<Italic 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)]"
|
||||
className="p-1 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||
title="Heading 2"
|
||||
>
|
||||
<Heading2 className="w-3.5 h-3.5" />
|
||||
@@ -512,7 +504,7 @@ export default function DocForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => insertMarkdown("### ")}
|
||||
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||
className="p-1 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||
title="Heading 3"
|
||||
>
|
||||
<Heading3 className="w-3.5 h-3.5" />
|
||||
@@ -520,7 +512,7 @@ export default function DocForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => insertMarkdown("```\n", "\n```")}
|
||||
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||
className="p-1 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||
title="Code Block"
|
||||
>
|
||||
<Code className="w-3.5 h-3.5" />
|
||||
@@ -528,27 +520,11 @@ export default function DocForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => insertMarkdown("[", "](https://)")}
|
||||
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
|
||||
className="p-1 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||
title="Link"
|
||||
>
|
||||
<LinkIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => insertMarkdown("> [!NOTE]\n> ")}
|
||||
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)] text-indigo-500"
|
||||
title="Note Callout"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -559,7 +535,7 @@ export default function DocForm({
|
||||
ref={textareaRef}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
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"
|
||||
className="w-full h-96 p-4 bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] font-mono text-sm focus:outline-none resize-y leading-relaxed"
|
||||
placeholder="# Write your technical documentation or writeup here in Markdown..."
|
||||
required
|
||||
/>
|
||||
@@ -568,7 +544,7 @@ export default function DocForm({
|
||||
{body.trim() ? (
|
||||
<Markdown source={body} />
|
||||
) : (
|
||||
<p className="text-xs text-[var(--text-dim)] italic">No content to preview yet.</p>
|
||||
<p className="text-xs text-[var(--text-dim)] italic font-mono">No content to preview yet.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -579,18 +555,18 @@ export default function DocForm({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
className="px-6 py-2.5 bg-[var(--brand)] hover:opacity-90 text-[var(--brand-fg)] rounded-lg font-semibold text-xs transition-all shadow-xs disabled:opacity-50 flex items-center gap-2 cursor-pointer font-mono uppercase tracking-wider"
|
||||
>
|
||||
{loading && (
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-3.5 h-3.5 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
)}
|
||||
<span>{loading ? "Persisting Document..." : mode === "create" ? "Publish to PostgreSQL" : "Save Changes"}</span>
|
||||
<span>{loading ? "PERSISTING..." : mode === "create" ? "PUBLISH_DOCUMENT" : "SAVE_CHANGES"}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
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-xl text-sm transition-colors shadow-xs cursor-pointer"
|
||||
className="px-4 py-2.5 border border-[var(--border-color)] hover:border-[var(--text-muted)] text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] rounded-lg text-xs transition-colors shadow-xs cursor-pointer font-mono"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -7,16 +7,11 @@ import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
import CommandMenu from "@/components/CommandMenu";
|
||||
import {
|
||||
Sparkles,
|
||||
Plus,
|
||||
Search,
|
||||
Menu,
|
||||
X,
|
||||
Lock,
|
||||
Layers,
|
||||
FileText,
|
||||
Activity,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
|
||||
export default function Header() {
|
||||
@@ -37,45 +32,39 @@ export default function Header() {
|
||||
|
||||
return (
|
||||
<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-15 flex items-center justify-between gap-3 sm:gap-4">
|
||||
{/* Left: Brand Identity */}
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 h-14 flex items-center justify-between gap-3 sm:gap-4">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/" className="flex items-center gap-2.5 group">
|
||||
<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">
|
||||
<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 className="flex items-center justify-center w-7 h-7 rounded-md bg-[var(--brand)] text-[var(--brand-fg)] font-mono font-bold text-xs shadow-xs group-hover:opacity-90 transition-opacity">
|
||||
M
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-[var(--text-primary)] text-sm tracking-tight leading-none">
|
||||
MCPedia
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-[var(--text-dim)] uppercase tracking-wider leading-tight mt-0.5 hidden sm:block">
|
||||
AI Knowledge Hub
|
||||
<span className="text-[10px] font-mono px-1.5 py-0.2 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] hidden sm:inline-block">
|
||||
CORE::DB
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<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)]">
|
||||
<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)]">
|
||||
{/* Center: Desktop Sections Nav */}
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
{sections.map((s) => {
|
||||
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
|
||||
return (
|
||||
<Link
|
||||
key={s.id}
|
||||
href={`/${s.id}`}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||||
className={`px-3 py-1 rounded-md text-xs font-medium transition-colors flex items-center gap-1.5 ${
|
||||
isActive
|
||||
? "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)]/50"
|
||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] border border-[var(--border-color)] font-semibold"
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
||||
}`}
|
||||
>
|
||||
<span className="text-xs">{s.icon}</span>
|
||||
<span>{s.icon}</span>
|
||||
<span>{s.label}</span>
|
||||
</Link>
|
||||
);
|
||||
@@ -88,7 +77,7 @@ export default function Header() {
|
||||
|
||||
<Link
|
||||
href="/search"
|
||||
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"
|
||||
className="md:hidden p-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md bg-[var(--bg-surface)] border border-[var(--border-color)] transition-colors"
|
||||
title="Search knowledge base"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
@@ -96,7 +85,7 @@ export default function Header() {
|
||||
|
||||
<Link
|
||||
href="/create"
|
||||
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"
|
||||
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 bg-[var(--brand)] hover:opacity-90 text-[var(--brand-fg)] text-xs font-semibold rounded-md shadow-xs transition-all cursor-pointer"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>New Doc</span>
|
||||
@@ -104,7 +93,7 @@ export default function Header() {
|
||||
|
||||
<Link
|
||||
href="/login"
|
||||
className={`p-1.5 sm:px-2.5 sm:py-1.5 text-xs rounded-lg transition-colors flex items-center gap-1.5 ${
|
||||
className={`p-1.5 sm:px-2.5 sm:py-1.5 text-xs rounded-md transition-colors flex items-center gap-1.5 ${
|
||||
pathname === "/login"
|
||||
? "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)]"
|
||||
@@ -119,10 +108,10 @@ export default function Header() {
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Mobile menu hamburger toggle */}
|
||||
{/* Mobile menu toggle */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden p-2 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-elevated)] transition-colors"
|
||||
className="md:hidden p-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md hover:bg-[var(--bg-elevated)] transition-colors"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
@@ -132,9 +121,9 @@ export default function Header() {
|
||||
|
||||
{/* Mobile Drawer */}
|
||||
{mobileMenuOpen && (
|
||||
<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">
|
||||
<div className="text-[11px] font-mono uppercase tracking-wider text-[var(--text-dim)] px-2 mb-1">
|
||||
Sections
|
||||
<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-lg">
|
||||
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)] px-1 mb-1">
|
||||
Knowledge Sections
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{sections.map((s) => {
|
||||
@@ -144,10 +133,10 @@ export default function Header() {
|
||||
key={s.id}
|
||||
href={`/${s.id}`}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md 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"
|
||||
? "bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)] font-semibold"
|
||||
: "text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
<span>{s.icon}</span>
|
||||
@@ -161,7 +150,7 @@ export default function Header() {
|
||||
<Link
|
||||
href="/search"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
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)]"
|
||||
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-md font-medium border border-[var(--border-color)]"
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" />
|
||||
<span>Search</span>
|
||||
@@ -169,7 +158,7 @@ export default function Header() {
|
||||
<Link
|
||||
href="/create"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
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"
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 text-xs bg-[var(--brand)] text-[var(--brand-fg)] rounded-md font-semibold shadow-xs"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Create Doc</span>
|
||||
|
||||
@@ -9,12 +9,9 @@ import {
|
||||
Sparkles,
|
||||
Search,
|
||||
Globe,
|
||||
Terminal,
|
||||
Server,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
FileCode2,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
interface NodeDetail {
|
||||
@@ -33,7 +30,7 @@ const NODES: NodeDetail[] = [
|
||||
{
|
||||
id: "ai-agents",
|
||||
title: "AI Coding Agents",
|
||||
badge: "MCP Consumer",
|
||||
badge: "MCP::CLIENT",
|
||||
type: "client",
|
||||
icon: Sparkles,
|
||||
desc: "Claude Desktop, Cursor, Antigravity, and Zed connect via Model Context Protocol to read and write technical knowledge directly.",
|
||||
@@ -52,10 +49,10 @@ const NODES: NodeDetail[] = [
|
||||
{
|
||||
id: "web-ui",
|
||||
title: "Human Web Interface",
|
||||
badge: "Next.js 16 + React 19",
|
||||
badge: "UI::NEXTJS",
|
||||
type: "client",
|
||||
icon: Globe,
|
||||
desc: "Fast, accessible, and responsive developer web interface with dark/light themes, keyboard shortcuts, and instant search.",
|
||||
desc: "Fast, accessible 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" },
|
||||
@@ -71,10 +68,10 @@ const NODES: NodeDetail[] = [
|
||||
{
|
||||
id: "mcp-server",
|
||||
title: "MCP Server Gateway",
|
||||
badge: "Streamable HTTP :4021",
|
||||
badge: "PORT::4021",
|
||||
type: "protocol",
|
||||
icon: Server,
|
||||
desc: "High-throughput streamable Model Context Protocol server exposing tool endpoints and resource URIs.",
|
||||
desc: "Streamable Model Context Protocol server exposing tool endpoints and resource URIs.",
|
||||
metrics: [
|
||||
{ label: "Port", value: ":4021" },
|
||||
{ label: "Protocol", value: "MCP 2024-11-05" },
|
||||
@@ -90,7 +87,7 @@ const NODES: NodeDetail[] = [
|
||||
{
|
||||
id: "core-engine",
|
||||
title: "@mcpedia/core",
|
||||
badge: "Single Source of Truth",
|
||||
badge: "CORE::PACKAGE",
|
||||
type: "core",
|
||||
icon: Layers,
|
||||
desc: "Unified business logic layer shared across Web, MCP Server, REST API, and Background Workers.",
|
||||
@@ -109,10 +106,10 @@ const NODES: NodeDetail[] = [
|
||||
{
|
||||
id: "search-engine",
|
||||
title: "Multi-Modal Search Engine",
|
||||
badge: "RRF Algorithm",
|
||||
badge: "SEARCH::RRF",
|
||||
type: "core",
|
||||
icon: Search,
|
||||
desc: "Reciprocal Rank Fusion uniting Postgres Full-Text Search tsvectors with OpenAI/OpenRouter vector embeddings.",
|
||||
desc: "Reciprocal Rank Fusion uniting Postgres Full-Text Search tsvectors with vector embeddings.",
|
||||
metrics: [
|
||||
{ label: "FTS Algorithm", value: "tsvector + GIN" },
|
||||
{ label: "Vector Search", value: "Cosine Similarity" },
|
||||
@@ -128,7 +125,7 @@ const NODES: NodeDetail[] = [
|
||||
{
|
||||
id: "postgres-db",
|
||||
title: "PostgreSQL 16 + pgvector",
|
||||
badge: "Primary Storage",
|
||||
badge: "DB::POSTGRES",
|
||||
type: "storage",
|
||||
icon: Database,
|
||||
desc: "ACID-compliant relational database storing document bodies, revision trees, vector embeddings, and full-text indexes.",
|
||||
@@ -153,38 +150,38 @@ export default function InteractiveTopology() {
|
||||
const ActiveIcon = activeNode.icon;
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl overflow-hidden shadow-sm">
|
||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-xs">
|
||||
{/* 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 className="p-4 sm:p-5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)] flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<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
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">
|
||||
Architecture Schematic
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">
|
||||
How MCPedia Unifies Humans & AI Agents
|
||||
<h3 className="text-base sm:text-lg font-bold text-[var(--text-primary)] tracking-tight">
|
||||
System Topology: Single Core Architecture
|
||||
</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 className="text-[11px] font-mono text-[var(--text-muted)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-2 py-1 rounded">
|
||||
Click nodes to inspect
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid Layout: Visual Interactive Map & Inspector */}
|
||||
{/* Grid Layout */}
|
||||
<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">
|
||||
<div className="lg:col-span-7 p-5 border-b lg:border-b-0 lg:border-r border-[var(--border-color)] bg-[var(--bg-app)] flex flex-col justify-between space-y-5">
|
||||
{/* Layer 1: Clients */}
|
||||
<div>
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-muted)] mb-2 block">
|
||||
1. Ingestion & Access Layer
|
||||
</span>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||
{NODES.filter((n) => n.type === "client").map((node) => {
|
||||
const Icon = node.icon;
|
||||
const isSelected = activeNodeId === node.id;
|
||||
@@ -193,21 +190,21 @@ export default function InteractiveTopology() {
|
||||
key={node.id}
|
||||
onClick={() => setActiveNodeId(node.id)}
|
||||
type="button"
|
||||
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
|
||||
className={`p-3 rounded-lg 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)]"
|
||||
? "bg-[var(--bg-surface)] border-[var(--text-primary)] shadow-xs ring-1 ring-[var(--text-primary)]"
|
||||
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--text-muted)]"
|
||||
}`}
|
||||
>
|
||||
<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 className={`p-1.5 rounded ${isSelected ? "bg-[var(--brand)] text-[var(--brand-fg)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
</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)]">
|
||||
<span className="text-[10px] font-mono px-1.5 py-0.2 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>
|
||||
<div className="font-semibold text-xs text-[var(--text-primary)]">{node.title}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -216,17 +213,17 @@ export default function InteractiveTopology() {
|
||||
|
||||
{/* 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 className="text-[10px] font-mono px-2.5 py-0.5 rounded bg-[var(--bg-surface)] border border-[var(--border-color)]">
|
||||
↓ 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 className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-muted)] mb-2 block">
|
||||
2. Core Engine & Protocol Layer
|
||||
</span>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||
{NODES.filter((n) => n.type === "protocol" || n.type === "core").map((node) => {
|
||||
const Icon = node.icon;
|
||||
const isSelected = activeNodeId === node.id;
|
||||
@@ -235,18 +232,18 @@ export default function InteractiveTopology() {
|
||||
key={node.id}
|
||||
onClick={() => setActiveNodeId(node.id)}
|
||||
type="button"
|
||||
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
|
||||
className={`p-3 rounded-lg 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)]"
|
||||
? "bg-[var(--bg-surface)] border-[var(--text-primary)] shadow-xs ring-1 ring-[var(--text-primary)]"
|
||||
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--text-muted)]"
|
||||
}`}
|
||||
>
|
||||
<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 className={`p-1.5 rounded ${isSelected ? "bg-[var(--brand)] text-[var(--brand-fg)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-semibold text-xs text-[var(--text-primary)] leading-tight mb-1 truncate">
|
||||
<div className="font-semibold text-xs text-[var(--text-primary)] leading-tight mb-0.5 truncate">
|
||||
{node.title}
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--text-muted)] font-mono truncate">
|
||||
@@ -260,15 +257,15 @@ export default function InteractiveTopology() {
|
||||
|
||||
{/* 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 className="text-[10px] font-mono px-2.5 py-0.5 rounded bg-[var(--bg-surface)] border border-[var(--border-color)]">
|
||||
↓ Persistent Storage Layer
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Layer 3: Database & Vector Storage */}
|
||||
{/* Layer 3: Storage */}
|
||||
<div>
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
|
||||
3. Source of Truth Storage
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-muted)] mb-2 block">
|
||||
3. Database (Source of Truth)
|
||||
</span>
|
||||
{NODES.filter((n) => n.type === "storage").map((node) => {
|
||||
const Icon = node.icon;
|
||||
@@ -278,23 +275,23 @@ export default function InteractiveTopology() {
|
||||
key={node.id}
|
||||
onClick={() => setActiveNodeId(node.id)}
|
||||
type="button"
|
||||
className={`w-full p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
|
||||
className={`w-full p-3 rounded-lg 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)]"
|
||||
? "bg-[var(--bg-surface)] border-[var(--text-primary)] shadow-xs ring-1 ring-[var(--text-primary)]"
|
||||
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--text-muted)]"
|
||||
}`}
|
||||
>
|
||||
<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)]"}`}>
|
||||
<div className={`p-1.5 rounded ${isSelected ? "bg-[var(--brand)] text-[var(--brand-fg)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
|
||||
<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 className="font-semibold text-xs text-[var(--text-primary)]">{node.title}</div>
|
||||
<div className="text-[11px] 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)]">
|
||||
<span className="text-[10px] font-mono px-1.5 py-0.2 rounded bg-[var(--bg-elevated)] text-[var(--text-muted)] border border-[var(--border-color)]">
|
||||
{node.badge}
|
||||
</span>
|
||||
</div>
|
||||
@@ -305,40 +302,40 @@ export default function InteractiveTopology() {
|
||||
</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 className="lg:col-span-5 p-5 bg-[var(--bg-surface)] flex flex-col justify-between space-y-5 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 className="flex items-start justify-between gap-3 mb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-primary)] flex items-center justify-center">
|
||||
<ActiveIcon className="w-4 h-4" />
|
||||
</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)]">
|
||||
<h4 className="text-sm font-bold text-[var(--text-primary)]">{activeNode.title}</h4>
|
||||
<span className="text-[10px] font-mono text-[var(--text-muted)]">
|
||||
{activeNode.badge}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed mb-6">
|
||||
<p className="text-xs text-[var(--text-secondary)] leading-relaxed mb-5">
|
||||
{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
|
||||
<div className="mb-5">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)] font-mono mb-2 block">
|
||||
Specifications
|
||||
</span>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{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"
|
||||
className="flex items-center justify-between p-2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-xs"
|
||||
>
|
||||
<span className="text-[var(--text-muted)]">{m.label}</span>
|
||||
<span className="font-mono font-medium text-[var(--text-primary)]">{m.value}</span>
|
||||
<span className="text-[var(--text-muted)] text-[11px]">{m.label}</span>
|
||||
<span className="font-mono font-medium text-[var(--text-primary)] text-[11px]">{m.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -346,13 +343,13 @@ export default function InteractiveTopology() {
|
||||
|
||||
{/* 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 className="text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)] font-mono mb-2 block">
|
||||
Capabilities
|
||||
</span>
|
||||
<ul className="space-y-2">
|
||||
<ul className="space-y-1.5">
|
||||
{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 className="text-emerald-500 shrink-0 font-mono text-[11px]">✓</span>
|
||||
<span>{feat}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -362,12 +359,12 @@ export default function InteractiveTopology() {
|
||||
|
||||
{/* Link to Documentation */}
|
||||
{activeNode.docLink && (
|
||||
<div className="pt-4 border-t border-[var(--border-color)]">
|
||||
<div className="pt-3 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"
|
||||
className="inline-flex items-center justify-center gap-1.5 w-full py-2 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-xs font-semibold text-[var(--text-primary)] rounded-lg transition-colors group"
|
||||
>
|
||||
<span>Read technical writeup in {activeNode.title}</span>
|
||||
<span>Read documentation</span>
|
||||
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,8 @@ import {
|
||||
Sparkles,
|
||||
Terminal,
|
||||
Server,
|
||||
Code2,
|
||||
Copy,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
Cpu,
|
||||
Laptop,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -54,10 +51,10 @@ const CONFIGS = [
|
||||
},
|
||||
{
|
||||
id: "http",
|
||||
label: "Streamable HTTP Endpoint",
|
||||
label: "Streamable HTTP (SSE)",
|
||||
icon: Server,
|
||||
lang: "json",
|
||||
filename: "Streamable HTTP (SSE) Client Config",
|
||||
filename: "Streamable HTTP Client Config",
|
||||
content: `{
|
||||
"mcpServers": {
|
||||
"mcpedia-remote": {
|
||||
@@ -74,11 +71,11 @@ const CONFIGS = [
|
||||
label: "CLI & Scripts",
|
||||
icon: Terminal,
|
||||
lang: "bash",
|
||||
filename: "Terminal / MCP Client Subprocess",
|
||||
content: `# Ask questions over semantic knowledge
|
||||
filename: "Terminal Subprocess / REPL",
|
||||
content: `# Semantic question answering via MCP client
|
||||
bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work in x86_64?"
|
||||
|
||||
# Run interactive REPL with native tool calls
|
||||
# Interactive REPL with tool discovery
|
||||
bun run apps/mcp-client/src/index.ts`,
|
||||
},
|
||||
];
|
||||
@@ -100,9 +97,9 @@ ${current.content}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl overflow-hidden shadow-sm">
|
||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-xs">
|
||||
{/* Header bar with tabs */}
|
||||
<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 justify-between px-4 py-2.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{CONFIGS.map((c) => {
|
||||
const Icon = c.icon;
|
||||
@@ -112,9 +109,9 @@ ${current.content}
|
||||
key={c.id}
|
||||
onClick={() => setActiveTab(c.id)}
|
||||
type="button"
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-xl font-semibold transition-all cursor-pointer ${
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md font-medium transition-all cursor-pointer ${
|
||||
isSelected
|
||||
? "bg-[var(--brand)] text-white shadow-md shadow-[var(--brand)]/20"
|
||||
? "bg-[var(--brand)] text-[var(--brand-fg)] font-semibold shadow-xs"
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
|
||||
}`}
|
||||
>
|
||||
@@ -128,12 +125,12 @@ ${current.content}
|
||||
<button
|
||||
onClick={handleCopyAll}
|
||||
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"
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-primary)] rounded-md transition-colors 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>
|
||||
<span className="text-emerald-500">Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -144,15 +141,15 @@ ${current.content}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Code preview via Markdown syntax highlighter */}
|
||||
{/* Code preview */}
|
||||
<div className="p-4 sm:p-5 [&>.prose>.group]:my-0 [&>.prose]:max-w-none">
|
||||
<Markdown source={markdownSnippet} />
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<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">
|
||||
<div className="px-4 py-2.5 bg-[var(--bg-elevated)] 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">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono uppercase text-[10px] text-[var(--text-muted)] font-semibold">
|
||||
<span className="font-mono uppercase text-[10px] text-[var(--text-muted)]">
|
||||
Target File:
|
||||
</span>
|
||||
<span className="font-mono text-[var(--text-secondary)] text-[11px]">
|
||||
@@ -160,10 +157,9 @@ ${current.content}
|
||||
</span>
|
||||
</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>
|
||||
<div className="flex items-center gap-3 text-[11px] font-mono">
|
||||
<span className="flex items-center gap-1 text-[var(--text-muted)]">
|
||||
<span>[ 13_TOOLS_ACTIVE ]</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
Copy,
|
||||
Check,
|
||||
ArrowLeft,
|
||||
Filter,
|
||||
ListOrdered,
|
||||
BookOpen,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -124,20 +122,20 @@ export default function PdfExportView({
|
||||
return (
|
||||
<div className="pb-16 print:pb-0">
|
||||
{/* 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 sm:p-5 shadow-xs">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
{/* Left info & Back button */}
|
||||
<div className="flex items-center gap-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href={backHref}
|
||||
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"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold 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-colors shadow-xs"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
<span>Back</span>
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] font-bold uppercase">
|
||||
<span className="text-xs font-mono text-[var(--text-primary)] font-bold uppercase">
|
||||
{sectionMeta.label}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--text-dim)]">·</span>
|
||||
@@ -158,7 +156,7 @@ export default function PdfExportView({
|
||||
<button
|
||||
onClick={handleCopyMarkdown}
|
||||
type="button"
|
||||
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"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 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-md transition-colors shadow-xs cursor-pointer"
|
||||
title="Copy compiled markdown document"
|
||||
>
|
||||
{copiedMd ? (
|
||||
@@ -169,7 +167,7 @@ export default function PdfExportView({
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
<span>Copy Markdown</span>
|
||||
<span>Copy MD</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -178,7 +176,7 @@ export default function PdfExportView({
|
||||
<button
|
||||
onClick={handleDownloadMarkdown}
|
||||
type="button"
|
||||
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"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 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-md transition-colors shadow-xs cursor-pointer"
|
||||
title="Download compiled .md file"
|
||||
>
|
||||
<FileDown className="w-3.5 h-3.5" />
|
||||
@@ -189,24 +187,24 @@ export default function PdfExportView({
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
type="button"
|
||||
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"
|
||||
className="inline-flex items-center gap-2 px-3.5 py-1.5 text-xs font-bold bg-[var(--brand)] hover:opacity-90 text-[var(--brand-fg)] rounded-md shadow-xs transition-all cursor-pointer"
|
||||
title="Open browser print dialog to save as PDF"
|
||||
>
|
||||
<Printer className="w-4 h-4" />
|
||||
<span>Print / Save as PDF</span>
|
||||
<span>Print / Save PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options & Filters Bar */}
|
||||
<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-3.5 pt-3 border-t border-[var(--border-color)] flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs">
|
||||
{/* Sort Order Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[var(--text-muted)] font-medium font-mono text-[11px] uppercase">Sort by:</span>
|
||||
<span className="text-[var(--text-muted)] font-mono text-[10px] uppercase">Sort:</span>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as ExportSortOption)}
|
||||
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)]"
|
||||
className="bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded px-2.5 py-1 text-xs text-[var(--text-primary)] focus:outline-none"
|
||||
>
|
||||
<option value="category_points">Category & Points (Standard)</option>
|
||||
<option value="points_desc">Points (Highest First)</option>
|
||||
@@ -225,7 +223,7 @@ export default function PdfExportView({
|
||||
type="checkbox"
|
||||
checked={showTOC}
|
||||
onChange={(e) => setShowTOC(e.target.checked)}
|
||||
className="rounded border-[var(--border-color)] text-indigo-600"
|
||||
className="rounded border-[var(--border-color)]"
|
||||
/>
|
||||
<span>Table of Contents</span>
|
||||
</label>
|
||||
@@ -236,7 +234,7 @@ export default function PdfExportView({
|
||||
type="checkbox"
|
||||
checked={pageBreaks}
|
||||
onChange={(e) => setPageBreaks(e.target.checked)}
|
||||
className="rounded border-[var(--border-color)] text-indigo-600"
|
||||
className="rounded border-[var(--border-color)]"
|
||||
/>
|
||||
<span>Page Break per Challenge</span>
|
||||
</label>
|
||||
@@ -246,13 +244,13 @@ export default function PdfExportView({
|
||||
{/* Category filter pills */}
|
||||
{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">
|
||||
<span className="text-[var(--text-muted)] font-mono text-[11px] uppercase mr-1">Filter:</span>
|
||||
<span className="text-[var(--text-muted)] font-mono text-[10px] uppercase mr-1">Filter:</span>
|
||||
<button
|
||||
onClick={() => setSelectedCategory("all")}
|
||||
type="button"
|
||||
className={`px-3 py-1 rounded-lg text-xs font-mono transition-all cursor-pointer ${
|
||||
className={`px-2.5 py-0.5 rounded text-xs font-mono transition-all cursor-pointer ${
|
||||
selectedCategory === "all"
|
||||
? "bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold"
|
||||
? "bg-[var(--brand)] text-[var(--brand-fg)] font-bold"
|
||||
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||
}`}
|
||||
>
|
||||
@@ -267,9 +265,9 @@ export default function PdfExportView({
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
type="button"
|
||||
className={`px-3 py-1 rounded-lg text-xs font-mono transition-all cursor-pointer ${
|
||||
className={`px-2.5 py-0.5 rounded text-xs font-mono transition-all cursor-pointer ${
|
||||
selectedCategory.toLowerCase() === cat.toLowerCase()
|
||||
? "bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold"
|
||||
? "bg-[var(--brand)] text-[var(--brand-fg)] font-bold"
|
||||
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||
}`}
|
||||
>
|
||||
@@ -282,7 +280,7 @@ export default function PdfExportView({
|
||||
</aside>
|
||||
|
||||
{/* Writeup Document Flow */}
|
||||
<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 rounded-xl border border-[var(--border-color)] print:border-none shadow-xs print:shadow-none">
|
||||
{/* Document Header */}
|
||||
<header className="mb-8 pb-4 border-b border-slate-300">
|
||||
<h1 className="text-3xl sm:text-4xl font-extrabold text-slate-950 tracking-tight mb-2">
|
||||
@@ -300,8 +298,8 @@ export default function PdfExportView({
|
||||
{/* Table of Contents */}
|
||||
{showTOC && filteredAndSortedDocs.length > 1 && (
|
||||
<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 flex items-center gap-1.5">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<h2 className="text-xs font-bold uppercase font-mono text-slate-700 mb-3 tracking-wider flex items-center gap-1.5">
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
<span>Table of Contents</span>
|
||||
</h2>
|
||||
<ol className="list-decimal list-inside space-y-1.5 text-xs text-slate-800">
|
||||
@@ -327,7 +325,7 @@ export default function PdfExportView({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Challenges / Writeups Flow */}
|
||||
{/* Challenges Flow */}
|
||||
<div className="challenges-flow space-y-10 print:space-y-0">
|
||||
{filteredAndSortedDocs.map((doc, idx) => {
|
||||
const chNum = idx + 1;
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Search,
|
||||
X,
|
||||
Plus,
|
||||
Compass,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Doc {
|
||||
@@ -114,11 +113,11 @@ function TreeNodeItem({
|
||||
return (
|
||||
<li className={`select-none ${depth > 0 ? "ml-3.5 pl-2 border-l border-[var(--border-color)]" : ""}`}>
|
||||
<div
|
||||
className={`group flex items-center justify-between gap-1 text-xs py-1.5 px-2 rounded-lg transition-all ${
|
||||
className={`group flex items-center justify-between gap-1 text-xs py-1.5 px-2 rounded-md transition-all ${
|
||||
isActive
|
||||
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30 shadow-xs"
|
||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] font-semibold border border-[var(--border-color)] shadow-xs"
|
||||
: isParentOfActive
|
||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
|
||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]/60"
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
||||
}`}
|
||||
>
|
||||
@@ -127,12 +126,12 @@ function TreeNodeItem({
|
||||
className="flex items-center gap-2 min-w-0 flex-1 py-0.5"
|
||||
title={node.title || node.name}
|
||||
>
|
||||
<span className="shrink-0 text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors">
|
||||
<span className="shrink-0 text-[var(--text-dim)] group-hover:text-[var(--text-primary)] transition-colors">
|
||||
{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)]" />
|
||||
<FolderOpen className="w-3.5 h-3.5 text-[var(--text-primary)]" />
|
||||
)
|
||||
) : (
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
@@ -153,7 +152,7 @@ function TreeNodeItem({
|
||||
e.stopPropagation();
|
||||
toggleCollapse(node.slug);
|
||||
}}
|
||||
className="p-1 text-[var(--text-dim)] 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 cursor-pointer"
|
||||
aria-label={isCollapsed ? "Expand folder" : "Collapse folder"}
|
||||
>
|
||||
<ChevronRight
|
||||
@@ -233,14 +232,14 @@ export default function Sidebar() {
|
||||
type="text"
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
placeholder="Filter knowledge tree..."
|
||||
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"
|
||||
placeholder="Filter index..."
|
||||
className="w-full pl-8 pr-7 py-1.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-md text-xs text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--text-primary)] transition-all"
|
||||
/>
|
||||
<Search className="w-3.5 h-3.5 text-[var(--text-dim)] absolute left-2.5 top-2.5 pointer-events-none" />
|
||||
{filterText && (
|
||||
<button
|
||||
onClick={() => setFilterText("")}
|
||||
className="absolute right-2 top-2 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded hover:bg-[var(--bg-elevated)]"
|
||||
className="absolute right-2 top-2 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded"
|
||||
aria-label="Clear filter"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
@@ -250,7 +249,7 @@ export default function Sidebar() {
|
||||
</div>
|
||||
|
||||
{/* Summary count */}
|
||||
<div className="flex items-center justify-between px-1 mb-2 text-[10px] text-[var(--text-dim)] font-mono uppercase tracking-wider">
|
||||
<div className="flex items-center justify-between px-1 mb-2 text-[10px] text-[var(--text-muted)] font-mono uppercase tracking-wider">
|
||||
<span>Knowledge Index</span>
|
||||
<span>{filteredDocs.length} items</span>
|
||||
</div>
|
||||
@@ -268,9 +267,9 @@ export default function Sidebar() {
|
||||
<div key={id} className="border-b border-[var(--border-color)] pb-3 last:border-0">
|
||||
<Link
|
||||
href={`/${id}`}
|
||||
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 ${
|
||||
className={`flex items-center justify-between gap-1.5 px-2 py-1 mb-1 rounded-md text-xs font-semibold uppercase tracking-wider transition-colors ${
|
||||
isSectionActive
|
||||
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10"
|
||||
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
|
||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
||||
}`}
|
||||
>
|
||||
@@ -278,7 +277,7 @@ export default function Sidebar() {
|
||||
<span className="text-xs">{icon}</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<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)]">
|
||||
<span className="text-[10px] text-[var(--text-muted)] font-normal font-mono bg-[var(--bg-surface)] px-1.5 py-0.2 rounded border border-[var(--border-color)]">
|
||||
{sectionDocs.length}
|
||||
</span>
|
||||
</Link>
|
||||
@@ -300,7 +299,7 @@ export default function Sidebar() {
|
||||
})}
|
||||
|
||||
{filteredDocs.length === 0 && (
|
||||
<div className="text-center py-8 text-xs text-[var(--text-dim)]">
|
||||
<div className="text-center py-8 text-xs text-[var(--text-dim)] font-mono">
|
||||
No matching documents
|
||||
</div>
|
||||
)}
|
||||
@@ -310,10 +309,10 @@ export default function Sidebar() {
|
||||
<div className="pt-3 border-t border-[var(--border-color)] mt-auto">
|
||||
<Link
|
||||
href="/create"
|
||||
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"
|
||||
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(--text-muted)] text-[var(--text-primary)] rounded-md transition-all font-medium shadow-xs"
|
||||
>
|
||||
<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>
|
||||
<Plus className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||
<span>Create Document</span>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user