Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a00fd1909 | ||
|
|
48566081f0 | ||
|
|
10a15a133c | ||
|
|
5b1345cf9e | ||
|
|
4ca5f0af39 | ||
|
|
283d66b5e0 | ||
|
|
b311c363c5 | ||
|
|
006bf79655 | ||
|
|
1a45ba9fdb | ||
|
|
b2fa2a98e0 | ||
|
|
4d424e67ad | ||
|
|
263a92f368 | ||
|
|
f44e66bcd5 | ||
|
|
66e6fad876 | ||
|
|
befb898c45 | ||
|
|
0638c4a82d | ||
|
|
e412767847 | ||
|
|
ea4f34271c | ||
|
|
caabe05c68 | ||
|
|
9d8d876ba7 | ||
|
|
8f98bed7dd | ||
|
|
bd3a0894b4 | ||
|
|
03616b4e60 | ||
|
|
5ccba958d2 | ||
|
|
067115ba13 | ||
|
|
011571e12d |
No files matched your search
@@ -0,0 +1,17 @@
|
|||||||
|
name: Dependabot Auto-Merge
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.actor == 'dependabot[bot]'
|
||||||
|
steps:
|
||||||
|
- name: Enable auto-merge for Dependabot PR
|
||||||
|
run: gh pr merge --auto --merge "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
groups:
|
||||||
|
production:
|
||||||
|
dependency-type: "production"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
development:
|
||||||
|
dependency-type: "development"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/index.ts",
|
||||||
"chat": "bun run src/index.ts",
|
"chat": "bun run src/index.ts",
|
||||||
"ask": "bun run src/ask.ts",
|
"ask": "bun run src/ask.ts",
|
||||||
"lint": "bun run lint",
|
"lint": "tsc --noEmit",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "bun build src/index.ts --outdir=dist --format=esm",
|
"build": "bun build src/index.ts --outdir=dist --format=esm",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"zod": "^4.0.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^26",
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"zod": "^4.0.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^26",
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,21 @@ 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,
|
||||||
|
History,
|
||||||
|
FileDown,
|
||||||
|
ArrowLeft,
|
||||||
|
Sparkles,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Target,
|
||||||
|
Trophy,
|
||||||
|
Zap,
|
||||||
|
Tag,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -49,44 +64,41 @@ function CustomFieldBadges({
|
|||||||
{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(--bg-elevated)] border-[var(--border-color)] text-[var(--text-primary)] font-semibold";
|
||||||
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 font-semibold"
|
||||||
: "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 font-semibold";
|
||||||
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(--bg-elevated)] border-[var(--border-color)] text-[var(--text-secondary)]";
|
||||||
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 font-semibold";
|
||||||
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 font-semibold";
|
||||||
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 font-semibold";
|
||||||
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-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-primary)] font-bold";
|
||||||
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(--bg-elevated)] border-[var(--border-color)] text-[var(--text-primary)]";
|
||||||
icon = "🏆";
|
IconComponent = Trophy;
|
||||||
displayValue = String(value);
|
displayValue = String(value);
|
||||||
} else {
|
} else {
|
||||||
displayValue = String(value);
|
displayValue = String(value);
|
||||||
@@ -98,9 +110,9 @@ function CustomFieldBadges({
|
|||||||
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-2.5 py-1 border rounded-md 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>{displayValue}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -146,9 +158,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 +183,59 @@ 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-xl p-6 shadow-xs">
|
||||||
<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-10 h-10 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-[var(--text-primary)]">
|
||||||
📁
|
<Folder className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight">
|
<h1 className="text-2xl font-bold 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-0.5 font-mono">
|
||||||
Directory path: <code className="text-[var(--brand)] dark:text-[var(--accent)] font-mono">{section}/{slug}</code>
|
Directory: <code>{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-2.5 py-1 rounded">
|
||||||
{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:opacity-90 text-[var(--brand-fg)] px-3 py-1.5 rounded font-semibold transition-all shadow-xs"
|
||||||
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</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 tracking-wider font-mono flex items-center gap-2">
|
||||||
<span>Subfolders</span>
|
<Folder className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||||
<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 p-3.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg transition-colors shadow-xs"
|
||||||
>
|
>
|
||||||
<span className="text-2xl group-hover:scale-110 transition-transform">📁</span>
|
<Folder className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
<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-xs font-semibold text-[var(--text-primary)] group-hover:underline 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-[11px] 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 +245,32 @@ 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 tracking-wider font-mono flex items-center gap-2">
|
||||||
<span>Documents</span>
|
<FileText className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||||
<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">
|
||||||
{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 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg transition-colors shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<span className="text-xl opacity-70 group-hover:opacity-100">📄</span>
|
<FileText className="w-4 h-4 text-[var(--text-muted)] shrink-0" />
|
||||||
<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-xs sm:text-sm font-semibold text-[var(--text-primary)] group-hover:underline 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 mt-1.5">
|
||||||
{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-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -266,11 +279,15 @@ function FolderIndexPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<time className="text-xs text-[var(--text-dim)] shrink-0 font-mono">
|
|
||||||
{new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
<time className="text-xs text-[var(--text-dim)] shrink-0 font-mono flex items-center gap-1">
|
||||||
month: "short",
|
<Clock className="w-3 h-3" />
|
||||||
day: "numeric",
|
<span>
|
||||||
})}
|
{new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</time>
|
</time>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -279,8 +296,8 @@ 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-12 border border-[var(--border-color)] rounded-lg 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 in this directory.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -292,7 +309,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
const searchParamsObj = await searchParams;
|
const searchParamsObj = await searchParams;
|
||||||
const { edit, path: queryPath, sort } = searchParamsObj;
|
const { edit, path: queryPath, sort } = searchParamsObj;
|
||||||
|
|
||||||
// Handle direct export views: /[section]/export, /[section]/[...folder]/export, or /export/pdf
|
// Handle direct export views
|
||||||
if (section === "export" || (slug && slug[slug.length - 1] === "export")) {
|
if (section === "export" || (slug && slug[slug.length - 1] === "export")) {
|
||||||
const isTrailingExport = slug && slug[slug.length - 1] === "export";
|
const isTrailingExport = slug && slug[slug.length - 1] === "export";
|
||||||
const targetSlug = isTrailingExport ? slug.slice(0, -1) : slug;
|
const targetSlug = isTrailingExport ? slug.slice(0, -1) : slug;
|
||||||
@@ -331,14 +348,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 font-bold text-[var(--text-primary)] tracking-tight">
|
||||||
Edit: {doc.title}
|
Edit: {doc.title}
|
||||||
</h1>
|
</h1>
|
||||||
<DocForm
|
<DocForm
|
||||||
@@ -373,8 +391,8 @@ 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 font-mono">
|
||||||
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
||||||
MCPedia
|
MCPedia
|
||||||
</Link>
|
</Link>
|
||||||
@@ -389,7 +407,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
<span>/</span>
|
<span>/</span>
|
||||||
<Link
|
<Link
|
||||||
href={`/${crumbPath}`}
|
href={`/${crumbPath}`}
|
||||||
className={isLast ? "text-[var(--text-primary)] font-semibold" : "hover:text-[var(--text-primary)] transition-colors"}
|
className={isLast ? "text-[var(--text-primary)] font-semibold font-sans" : "hover:text-[var(--text-primary)] transition-colors font-sans"}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -399,24 +417,26 @@ 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-xl p-6 sm:p-7 mb-8 shadow-xs">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4">
|
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
<div className="flex items-center gap-2 mb-2 flex-wrap text-xs">
|
||||||
<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 px-2 py-0.5 rounded text-xs font-mono font-bold bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-primary)] uppercase">
|
||||||
<span>{sectionInfo.icon}</span>
|
<span>{sectionInfo.icon}</span>
|
||||||
<span className="uppercase">{sectionInfo.label}</span>
|
<span>{sectionInfo.label}</span>
|
||||||
</span>
|
|
||||||
<span className="text-xs text-[var(--text-dim)]">·</span>
|
|
||||||
<span className="text-xs text-[var(--text-muted)]">
|
|
||||||
{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">
|
||||||
{readingTime}
|
{doc.type}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[var(--text-dim)]">·</span>
|
||||||
|
<span className="text-xs text-[var(--text-muted)] font-mono flex items-center gap-1">
|
||||||
|
<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 font-extrabold text-[var(--text-primary)] tracking-tight leading-tight">
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -425,24 +445,27 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
<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, 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-3.5 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 bg-[var(--brand)] text-[var(--brand-fg)] flex items-center justify-center text-[10px] font-bold font-mono">
|
||||||
{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>
|
<span className="flex items-center gap-1 font-mono text-[11px]">
|
||||||
Updated {new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
<span>
|
||||||
month: "long",
|
Updated {new Date(doc.updatedAt).toLocaleDateString(undefined, {
|
||||||
day: "numeric",
|
month: "short",
|
||||||
year: "numeric",
|
day: "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 +473,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-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] text-[10px] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -464,22 +487,25 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
<CustomFieldBadges doc={{ ...doc }} standardKeys={STANDARD_KEYS} />
|
<CustomFieldBadges doc={{ ...doc }} standardKeys={STANDARD_KEYS} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile TOC (rendered inline on smaller screens) */}
|
{/* Mobile TOC */}
|
||||||
<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-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl 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-12 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-3.5">
|
||||||
<span>Related Knowledge</span>
|
<Sparkles className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
</h2>
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Related Knowledge
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{related.map((r) => {
|
{related.map((r) => {
|
||||||
const rInfo = getSectionInfo(r.section);
|
const rInfo = getSectionInfo(r.section);
|
||||||
@@ -487,10 +513,10 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
<Link
|
<Link
|
||||||
key={r.slug}
|
key={r.slug}
|
||||||
href={`/${r.slug}`}
|
href={`/${r.slug}`}
|
||||||
className="group block bg-[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(--text-muted)] rounded-lg p-4 transition-colors shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2 mb-2">
|
<div className="flex items-center justify-between gap-2 mb-1.5">
|
||||||
<span className="text-xs text-[var(--brand)] dark:text-[var(--accent)] font-mono uppercase">
|
<span className="text-[10px] text-[var(--text-muted)] 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 +526,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-xs sm:text-sm font-semibold text-[var(--text-primary)] group-hover:underline line-clamp-1">
|
||||||
{r.title}
|
{r.title}
|
||||||
</h3>
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -513,25 +539,28 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
{/* Revision History */}
|
{/* Revision History */}
|
||||||
{revisions.length > 0 && (
|
{revisions.length > 0 && (
|
||||||
<aside className="mt-10 pt-8 border-t border-[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-3.5">
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
|
<div className="flex items-center gap-2">
|
||||||
Revision History ({revisions.length})
|
<History className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
</h2>
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Revision History ({revisions.length})
|
||||||
|
</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 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-xs shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0 font-mono">
|
||||||
<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="text-[var(--text-primary)] bg-[var(--bg-elevated)] px-1.5 py-0.5 rounded border border-[var(--border-color)] font-bold text-[10px]">
|
||||||
v{rev.revisionNo}
|
v{rev.revisionNo}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[var(--text-primary)] truncate">
|
<span className="text-[var(--text-secondary)] font-sans truncate text-xs">
|
||||||
{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 text-[11px]">
|
||||||
· {new Date(rev.createdAt).toLocaleString()}
|
· {new Date(rev.createdAt).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -540,7 +569,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-primary)] hover:text-black dark:hover:text-white bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] transition-colors font-medium cursor-pointer"
|
||||||
>
|
>
|
||||||
Restore
|
Restore
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
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,
|
||||||
|
FileDown,
|
||||||
|
Plus,
|
||||||
|
Clock,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -81,26 +88,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-1.5 px-2.5 rounded-md transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30"
|
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] font-semibold border border-[var(--border-color)] 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 truncate">
|
||||||
<span>{isFolder ? "📁" : "📄"}</span>
|
<span className="text-[var(--text-dim)]">
|
||||||
<span className="truncate">{node.doc?.title || node.name}</span>
|
{isFolder ? <Folder className="w-3.5 h-3.5" /> : <FileText className="w-3.5 h-3.5" />}
|
||||||
|
</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-1.5 py-0.2 rounded font-mono">
|
||||||
{node.docCount} docs
|
{node.docCount} docs
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
{node.children.length > 0 && (
|
{node.children.length > 0 && (
|
||||||
<div className="ml-4 border-l border-[var(--border-color)] pl-2 mt-1">
|
<div className="ml-3.5 border-l border-[var(--border-color)] pl-2.5 mt-0.5 space-y-0.5">
|
||||||
{renderTree(node.children, pathname)}
|
{renderTree(node.children, pathname)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -124,67 +133,73 @@ export default async function SectionIndexPage({
|
|||||||
const tree = buildFolderTree(allDocs, section);
|
const tree = buildFolderTree(allDocs, section);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-8">
|
||||||
{/* 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)] font-mono">
|
||||||
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
|
||||||
MCPedia
|
MCPedia
|
||||||
</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 font-sans">{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-xl p-6 shadow-xs">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-3">
|
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3.5">
|
||||||
<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-12 h-12 rounded-xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-2xl">
|
||||||
{sectionInfo.icon}
|
{sectionInfo.icon}
|
||||||
</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">
|
||||||
{sectionInfo.label}
|
{sectionInfo.label}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-0.5 font-mono uppercase">
|
<p className="text-xs text-[var(--text-muted)] font-mono uppercase tracking-wider mt-0.5">
|
||||||
Section: {section}
|
Section: <code>{section}</code>
|
||||||
</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(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-2.5 py-1 rounded">
|
||||||
{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)] text-[var(--text-primary)] px-3 py-1 rounded-md 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:opacity-90 text-[var(--brand-fg)] px-3 py-1 rounded-md font-semibold transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<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-2">
|
||||||
{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-xl p-5 shadow-xs">
|
||||||
<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-3 pb-2 border-b border-[var(--border-color)]">
|
||||||
<span>Folder Structure</span>
|
<Folder className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||||
</h2>
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Folder & Chapter Structure
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
{renderTree(tree, section)}
|
{renderTree(tree, section)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@@ -192,9 +207,13 @@ 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-3.5">
|
||||||
All Documents in {sectionInfo.label}
|
<FileText className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||||
</h2>
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
All Documents in {sectionInfo.label}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[...sectionDocs]
|
{[...sectionDocs]
|
||||||
.sort(
|
.sort(
|
||||||
@@ -205,12 +224,12 @@ export default async function SectionIndexPage({
|
|||||||
<Link
|
<Link
|
||||||
key={d.slug}
|
key={d.slug}
|
||||||
href={`/${d.slug}`}
|
href={`/${d.slug}`}
|
||||||
className="group flex items-center justify-between gap-4 p-4 bg-[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 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-xl transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<span className="text-lg opacity-70 group-hover:opacity-100">📄</span>
|
<FileText className="w-4 h-4 text-[var(--text-muted)] shrink-0" />
|
||||||
<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-xs sm:text-sm font-semibold text-[var(--text-primary)] group-hover:underline truncate">
|
||||||
{d.title}
|
{d.title}
|
||||||
</h3>
|
</h3>
|
||||||
{d.tags.length > 0 && (
|
{d.tags.length > 0 && (
|
||||||
@@ -218,7 +237,7 @@ export default async function SectionIndexPage({
|
|||||||
{d.tags.slice(0, 4).map((t) => (
|
{d.tags.slice(0, 4).map((t) => (
|
||||||
<span
|
<span
|
||||||
key={t}
|
key={t}
|
||||||
className="text-[10px] px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]"
|
className="text-[10px] px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -227,12 +246,16 @@ export default async function SectionIndexPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0">
|
|
||||||
{new Date(d.updatedAt).toLocaleDateString(undefined, {
|
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0 flex items-center gap-1">
|
||||||
month: "short",
|
<Clock className="w-3 h-3" />
|
||||||
day: "numeric",
|
<span>
|
||||||
year: "numeric",
|
{new Date(d.updatedAt).toLocaleDateString(undefined, {
|
||||||
})}
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</time>
|
</time>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -13,15 +13,23 @@ function signCookie(value: string): string {
|
|||||||
return `${value}.${sig}`;
|
return `${value}.${sig}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeTimingEqual(a: string, b: string): boolean {
|
||||||
|
try {
|
||||||
|
const bufA = Buffer.from(a);
|
||||||
|
const bufB = Buffer.from(b);
|
||||||
|
if (bufA.length !== bufB.length) return false;
|
||||||
|
return timingSafeEqual(bufA, bufB);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function verifyCookie(cookieValue: string | undefined): boolean {
|
function verifyCookie(cookieValue: string | undefined): boolean {
|
||||||
if (!cookieValue) return false;
|
if (!cookieValue) return false;
|
||||||
const [value, sig] = cookieValue.split(".");
|
const [value, sig] = cookieValue.split(".");
|
||||||
if (!value || !sig) return false;
|
if (!value || !sig) return false;
|
||||||
const expected = signCookie(value);
|
const expected = signCookie(value);
|
||||||
return timingSafeEqual(
|
return safeTimingEqual(cookieValue, expected);
|
||||||
Buffer.from(cookieValue),
|
|
||||||
Buffer.from(expected),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
@@ -32,10 +40,7 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = timingSafeEqual(
|
const ok = safeTimingEqual(password, ADMIN_PASSWORD);
|
||||||
Buffer.from(password),
|
|
||||||
Buffer.from(ADMIN_PASSWORD),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import { timingSafeEqual } from "node:crypto";
|
|||||||
function isAuthorized(req: NextRequest): boolean {
|
function isAuthorized(req: NextRequest): boolean {
|
||||||
if (!WEBHOOK_SECRET) return false;
|
if (!WEBHOOK_SECRET) return false;
|
||||||
const headerSecret = req.headers.get("x-webhook-secret") ?? "";
|
const headerSecret = req.headers.get("x-webhook-secret") ?? "";
|
||||||
if (headerSecret && timingSafeEqual(Buffer.from(headerSecret), Buffer.from(WEBHOOK_SECRET))) {
|
if (headerSecret) {
|
||||||
return true;
|
const bufA = Buffer.from(headerSecret);
|
||||||
|
const bufB = Buffer.from(WEBHOOK_SECRET);
|
||||||
|
if (bufA.length === bufB.length && timingSafeEqual(bufA, bufB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const cookie = req.cookies.get("mcpedia_admin")?.value ?? "";
|
const cookie = req.cookies.get("mcpedia_admin")?.value ?? "";
|
||||||
return cookie.startsWith("admin.");
|
return cookie.startsWith("admin.");
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ import { timingSafeEqual } from "node:crypto";
|
|||||||
function isAuthorized(req: NextRequest): boolean {
|
function isAuthorized(req: NextRequest): boolean {
|
||||||
if (!WEBHOOK_SECRET) return false;
|
if (!WEBHOOK_SECRET) return false;
|
||||||
const headerSecret = req.headers.get("x-webhook-secret") ?? "";
|
const headerSecret = req.headers.get("x-webhook-secret") ?? "";
|
||||||
if (headerSecret && timingSafeEqual(Buffer.from(headerSecret), Buffer.from(WEBHOOK_SECRET))) {
|
if (headerSecret) {
|
||||||
return true;
|
const bufA = Buffer.from(headerSecret);
|
||||||
|
const bufB = Buffer.from(WEBHOOK_SECRET);
|
||||||
|
if (bufA.length === bufB.length && timingSafeEqual(bufA, bufB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const cookie = req.cookies.get("mcpedia_admin")?.value ?? "";
|
const cookie = req.cookies.get("mcpedia_admin")?.value ?? "";
|
||||||
return cookie.startsWith("admin.");
|
return cookie.startsWith("admin.");
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getExportDocuments, compileExportMarkdown, type ExportSortOption } from "@mcpedia/core";
|
import {
|
||||||
|
getExportDocuments,
|
||||||
|
compileExportMarkdown,
|
||||||
|
generateExportDocx,
|
||||||
|
type ExportSortOption,
|
||||||
|
} from "@mcpedia/core";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -11,6 +16,8 @@ export async function GET(req: NextRequest) {
|
|||||||
const sortBy = (searchParams.get("sort") as ExportSortOption) || "category_points";
|
const sortBy = (searchParams.get("sort") as ExportSortOption) || "category_points";
|
||||||
const slugsParam = searchParams.get("slugs");
|
const slugsParam = searchParams.get("slugs");
|
||||||
const slugs = slugsParam ? slugsParam.split(",").map((s) => s.trim()) : undefined;
|
const slugs = slugsParam ? slugsParam.split(",").map((s) => s.trim()) : undefined;
|
||||||
|
const pageBreaksParam = searchParams.get("pageBreaks");
|
||||||
|
const pageBreaks = pageBreaksParam !== "false" && pageBreaksParam !== "0";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getExportDocuments({
|
const data = await getExportDocuments({
|
||||||
@@ -20,13 +27,25 @@ export async function GET(req: NextRequest) {
|
|||||||
sortBy,
|
sortBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const safeFilename = data.summary.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "") || "mcpedia-export";
|
||||||
|
|
||||||
|
if (format === "docx" || format === "word") {
|
||||||
|
const buffer = await generateExportDocx(data, { pageBreaks });
|
||||||
|
const uint8 = new Uint8Array(buffer);
|
||||||
|
return new NextResponse(uint8, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type":
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"Content-Disposition": `attachment; filename="${safeFilename}.docx"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (format === "markdown" || format === "md") {
|
if (format === "markdown" || format === "md") {
|
||||||
const md = compileExportMarkdown(data);
|
const md = compileExportMarkdown(data);
|
||||||
const safeFilename = data.summary.title
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
|
||||||
.replace(/^-|-$/g, "");
|
|
||||||
|
|
||||||
return new NextResponse(md, {
|
return new NextResponse(md, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/markdown; charset=utf-8",
|
"Content-Type": "text/markdown; charset=utf-8",
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { restoreRevision, getRevision } from "@mcpedia/core";
|
import { restoreRevision, getRevision } from "@mcpedia/core";
|
||||||
|
import { WEBHOOK_SECRET } from "@mcpedia/config";
|
||||||
|
import { timingSafeEqual } from "node:crypto";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
function isAuthorized(req: NextRequest): boolean {
|
||||||
|
if (!WEBHOOK_SECRET) return false;
|
||||||
|
const headerSecret = req.headers.get("x-webhook-secret") ?? "";
|
||||||
|
if (headerSecret) {
|
||||||
|
const bufA = Buffer.from(headerSecret);
|
||||||
|
const bufB = Buffer.from(WEBHOOK_SECRET);
|
||||||
|
if (bufA.length === bufB.length && timingSafeEqual(bufA, bufB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cookie = req.cookies.get("mcpedia_admin")?.value ?? "";
|
||||||
|
return cookie.startsWith("admin.");
|
||||||
|
}
|
||||||
|
|
||||||
// POST /api/revisions/restore — restore a document to a past revision.
|
// POST /api/revisions/restore — restore a document to a past revision.
|
||||||
// Body (form-urlencoded): id=<revision uuid>
|
// Body (form-urlencoded): id=<revision uuid>
|
||||||
// After restoring, we rebuild semantic chunks (handled inside restoreRevision)
|
// After restoring, we rebuild semantic chunks (handled inside restoreRevision)
|
||||||
// and revalidate the doc page so the Web UI reflects the restored body.
|
// and revalidate the doc page so the Web UI reflects the restored body.
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
if (!isAuthorized(req)) {
|
||||||
|
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
const form = await req.formData().catch(() => null);
|
const form = await req.formData().catch(() => null);
|
||||||
const id = form?.get("id");
|
const id = form?.get("id");
|
||||||
if (typeof id !== "string" || id.length === 0) {
|
if (typeof id !== "string" || id.length === 0) {
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
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,
|
||||||
|
Plus,
|
||||||
|
ArrowRight,
|
||||||
|
BookOpen,
|
||||||
|
Flag,
|
||||||
|
Sparkles,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface Hit {
|
interface Hit {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -12,6 +21,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 +39,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 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 & Writeups Directory",
|
||||||
|
subtitle: "Pwn, Reverse, Web, Crypto writeups and solutions",
|
||||||
|
icon: Flag,
|
||||||
|
href: "/writeups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browse-research",
|
||||||
|
title: "Research & Benchmarks",
|
||||||
|
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 +112,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 +121,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 +153,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(--text-muted)] text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md 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(--text-primary)] 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...</span>
|
||||||
</svg>
|
<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)]">
|
||||||
<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 +166,115 @@ 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-black/60 backdrop-blur-xs transition-opacity"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* 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-xl shadow-2xl overflow-hidden animate-fade-in">
|
||||||
{/* Search input bar */}
|
{/* Search input bar */}
|
||||||
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
|
<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)]">
|
||||||
<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-4 h-4 text-[var(--text-muted)] 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 tools..."
|
||||||
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 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(--text-muted)] 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-[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
|
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-1 space-y-0.5">
|
||||||
<p className="text-xs text-[var(--text-muted)]">Search full-text, semantic embeddings, and CTF writeups.</p>
|
<div className="px-2.5 py-1 text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">
|
||||||
<div className="flex justify-center gap-2 mt-3 text-[11px] text-[var(--text-dim)]">
|
Quick Shortcuts
|
||||||
<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 rounded-lg cursor-pointer transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--bg-elevated)] text-[var(--text-primary)]"
|
||||||
|
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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 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(--text-primary)]" : ""}`} />
|
||||||
|
</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-10 text-center text-xs text-[var(--text-muted)] space-y-1">
|
||||||
No documents matching "{query}"
|
<p>No knowledge base items matched "{query}"</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-lg cursor-pointer transition-colors ${
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)]"
|
? "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"
|
: "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-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] 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(--text-primary)]" : "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(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
||||||
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 +284,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 border-t border-[var(--border-color)] bg-[var(--bg-elevated)] text-[10px] text-[var(--text-muted)] font-mono">
|
||||||
<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 ]</span>
|
||||||
|
<span>[ ↵ SELECT ]</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="hover:text-[var(--text-primary)] 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,
|
||||||
|
FileText,
|
||||||
|
Edit3,
|
||||||
|
} 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"
|
||||||
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-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 ? (
|
{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" />
|
<span className="text-emerald-500 font-medium">Copied</span>
|
||||||
</svg>
|
|
||||||
<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-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 ? (
|
{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" />
|
<span className="text-emerald-500 font-medium">Copied</span>
|
||||||
</svg>
|
|
||||||
<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,29 @@ 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 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-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"
|
||||||
>
|
>
|
||||||
<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" />
|
|
||||||
</svg>
|
|
||||||
<span>PDF</span>
|
<span>PDF</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{/* Export Word (.docx) */}
|
||||||
|
<a
|
||||||
|
href={`/api/export?path=${encodeURIComponent(slug)}&format=docx`}
|
||||||
|
title="Download editable Microsoft Word (.docx) file"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
<span>Word (.docx)</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* 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-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"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|||||||
+144
-109
@@ -3,6 +3,19 @@
|
|||||||
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,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Eye,
|
||||||
|
Edit3,
|
||||||
|
Sliders,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface DocFormProps {
|
interface DocFormProps {
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
@@ -22,8 +35,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 +119,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 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 = "") {
|
function addCustomField(key = "", value = "") {
|
||||||
setCustomFields([...customFields, { key, value }]);
|
setCustomFields([...customFields, { key, value }]);
|
||||||
@@ -128,7 +159,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 +206,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,75 +221,73 @@ 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-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}
|
{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 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, research"
|
||||||
placeholder="e.g. docs, writeups, ctf, guides"
|
className={baseInputCls}
|
||||||
className={baseInputCls}
|
required
|
||||||
required
|
/>
|
||||||
/>
|
<datalist id="section-list">
|
||||||
<datalist id="section-list">
|
{sectionOptions.map((s) => (
|
||||||
{sectionOptions.map((s) => (
|
<option key={s} value={s}>
|
||||||
<option key={s} value={s}>
|
{s.toUpperCase()}
|
||||||
{s.toUpperCase()}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</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. writeup, documentation, note"
|
||||||
placeholder="e.g. documentation, writeup, note"
|
className={baseInputCls}
|
||||||
className={baseInputCls}
|
/>
|
||||||
/>
|
<datalist id="type-list">
|
||||||
<datalist id="type-list">
|
{DEFAULT_TYPE_LIST.map((t) => (
|
||||||
{DEFAULT_TYPE_LIST.map((t) => (
|
<option key={t} value={t}>
|
||||||
<option key={t} value={t}>
|
{t}
|
||||||
{t}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</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 +296,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 +304,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,9 +321,10 @@ 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
|
||||||
@@ -309,12 +339,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(--text-primary)] font-semibold">{effectiveSlug || section}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +352,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 +360,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 +372,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 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3.5 shadow-xs">
|
||||||
<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(--text-muted)]" />
|
||||||
|
<span>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 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-[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}
|
+{preset.key}
|
||||||
</button>
|
</button>
|
||||||
@@ -393,10 +424,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 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,14 +435,15 @@ 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(--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"
|
||||||
>
|
>
|
||||||
+ 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-xl overflow-hidden bg-[var(--bg-surface)] shadow-xs">
|
||||||
{/* 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)] border-b border-[var(--border-color)] flex-wrap gap-2">
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
@@ -419,90 +451,92 @@ export default function DocForm({
|
|||||||
<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 text-xs rounded font-semibold transition-all cursor-pointer ${
|
||||||
activeTab === "write"
|
activeTab === "write"
|
||||||
? "bg-[var(--brand)] text-white shadow-sm"
|
? "bg-[var(--brand)] text-[var(--brand-fg)] 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 text-xs rounded font-semibold transition-all cursor-pointer ${
|
||||||
activeTab === "preview"
|
activeTab === "preview"
|
||||||
? "bg-[var(--brand)] text-white shadow-sm"
|
? "bg-[var(--brand)] text-[var(--brand-fg)] 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>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 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||||
title="Bold"
|
title="Bold"
|
||||||
>
|
>
|
||||||
<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 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||||
title="Italic"
|
title="Italic"
|
||||||
>
|
>
|
||||||
<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 hover:bg-[var(--bg-surface)] rounded 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 hover:bg-[var(--bg-surface)] rounded 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 hover:bg-[var(--bg-surface)] rounded 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 hover:bg-[var(--bg-surface)] rounded hover:text-[var(--text-primary)]"
|
||||||
title="Link"
|
title="Link"
|
||||||
>
|
>
|
||||||
Link
|
<LinkIcon className="w-3.5 h-3.5" />
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
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)]"
|
|
||||||
title="Note Callout"
|
|
||||||
>
|
|
||||||
Note
|
|
||||||
</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 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 +544,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 font-mono">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: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 && (
|
{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 ? "Saving document..." : mode === "create" ? "Create Document" : "Save Changes"}</span>
|
<span>{loading ? "PERSISTING..." : mode === "create" ? "PUBLISH_DOCUMENT" : "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(--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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ 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 {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
|
Lock,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -24,24 +31,26 @@ export default function Header() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-30 border-b border-[var(--border-color)] glass-nav">
|
<header className="site-header no-print 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-14 flex items-center justify-between gap-3 sm:gap-4">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<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="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
|
M
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-[var(--text-primary)] text-base tracking-tight transition-colors">
|
<div className="flex items-center gap-2">
|
||||||
MCPedia
|
<span className="font-bold text-[var(--text-primary)] text-sm tracking-tight leading-none">
|
||||||
</span>
|
MCPedia
|
||||||
|
</span>
|
||||||
|
<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>
|
</Link>
|
||||||
<span className="hidden sm:inline-block text-[10px] uppercase font-mono px-1.5 py-0.5 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)]">
|
|
||||||
v0.1.0
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Nav */}
|
{/* Center: Desktop Sections Nav */}
|
||||||
<nav className="hidden md:flex items-center gap-1">
|
<nav className="hidden md:flex items-center gap-1">
|
||||||
{sections.map((s) => {
|
{sections.map((s) => {
|
||||||
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
|
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
|
||||||
@@ -49,101 +58,110 @@ export default function Header() {
|
|||||||
<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-md text-xs font-medium transition-colors 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-elevated)] border border-[var(--border-color)] font-semibold"
|
||||||
: "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-elevated)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="mr-1.5 opacity-80">{s.icon}</span>
|
<span>{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-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"
|
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:opacity-90 text-[var(--brand-fg)] text-xs font-semibold rounded-md shadow-xs 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-md 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 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-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md 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-lg">
|
||||||
{sections.map((s) => (
|
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)] px-1 mb-1">
|
||||||
<Link
|
Knowledge Sections
|
||||||
key={s.id}
|
</div>
|
||||||
href={`/${s.id}`}
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
{sections.map((s) => {
|
||||||
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)]"
|
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
|
||||||
>
|
return (
|
||||||
<span>{s.icon}</span>
|
<Link
|
||||||
<span>{s.label}</span>
|
key={s.id}
|
||||||
</Link>
|
href={`/${s.id}`}
|
||||||
))}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
<div className="pt-2 border-t border-[var(--border-color)] flex gap-2">
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-xs font-medium transition-colors ${
|
||||||
|
isActive
|
||||||
|
? "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>
|
||||||
|
<span className="truncate">{s.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</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-md 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-[var(--brand-fg)] rounded-md font-semibold shadow-xs"
|
||||||
>
|
>
|
||||||
+ Create
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
<span>Create Doc</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
Cpu,
|
||||||
|
Database,
|
||||||
|
Layers,
|
||||||
|
Sparkles,
|
||||||
|
Search,
|
||||||
|
Globe,
|
||||||
|
Server,
|
||||||
|
ArrowRight,
|
||||||
|
CheckCircle2,
|
||||||
|
} 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::CLIENT",
|
||||||
|
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: "UI::NEXTJS",
|
||||||
|
type: "client",
|
||||||
|
icon: Globe,
|
||||||
|
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" },
|
||||||
|
{ 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: "PORT::4021",
|
||||||
|
type: "protocol",
|
||||||
|
icon: Server,
|
||||||
|
desc: "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: "CORE::PACKAGE",
|
||||||
|
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: "SEARCH::RRF",
|
||||||
|
type: "core",
|
||||||
|
icon: Search,
|
||||||
|
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" },
|
||||||
|
{ 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: "DB::POSTGRES",
|
||||||
|
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-xl overflow-hidden shadow-xs">
|
||||||
|
{/* Header Bar */}
|
||||||
|
<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">
|
||||||
|
<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-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-[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 */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12">
|
||||||
|
{/* Visual Map Column (Left) */}
|
||||||
|
<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-muted)] mb-2 block">
|
||||||
|
1. Ingestion & Access Layer
|
||||||
|
</span>
|
||||||
|
<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;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
onClick={() => setActiveNodeId(node.id)}
|
||||||
|
type="button"
|
||||||
|
className={`p-3 rounded-lg border text-left transition-all cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? "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-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-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-xs 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-[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-muted)] mb-2 block">
|
||||||
|
2. Core Engine & Protocol Layer
|
||||||
|
</span>
|
||||||
|
<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;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
onClick={() => setActiveNodeId(node.id)}
|
||||||
|
type="button"
|
||||||
|
className={`p-3 rounded-lg border text-left transition-all cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? "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-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-0.5 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-[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: Storage */}
|
||||||
|
<div>
|
||||||
|
<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;
|
||||||
|
const isSelected = activeNodeId === node.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
onClick={() => setActiveNodeId(node.id)}
|
||||||
|
type="button"
|
||||||
|
className={`w-full p-3 rounded-lg border text-left transition-all cursor-pointer ${
|
||||||
|
isSelected
|
||||||
|
? "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-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-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-1.5 py-0.2 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-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-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-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 text-[var(--text-secondary)] leading-relaxed mb-5">
|
||||||
|
{activeNode.desc}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Telemetry Metrics */}
|
||||||
|
<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-1.5">
|
||||||
|
{activeNode.metrics.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.label}
|
||||||
|
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)] text-[11px]">{m.label}</span>
|
||||||
|
<span className="font-mono font-medium text-[var(--text-primary)] text-[11px]">{m.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Capabilities */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)] font-mono mb-2 block">
|
||||||
|
Capabilities
|
||||||
|
</span>
|
||||||
|
<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)]">
|
||||||
|
<span className="text-emerald-500 shrink-0 font-mono text-[11px]">✓</span>
|
||||||
|
<span>{feat}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Link to Documentation */}
|
||||||
|
{activeNode.docLink && (
|
||||||
|
<div className="pt-3 border-t border-[var(--border-color)]">
|
||||||
|
<Link
|
||||||
|
href={activeNode.docLink}
|
||||||
|
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 documentation</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 print:my-2 rounded-xl print:rounded border border-[var(--border-code)] print:border-slate-300 bg-[var(--code-block-bg)] print:bg-slate-50 shadow-sm print:shadow-none">
|
||||||
{/* 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 print:px-2.5 print:py-0.5 bg-[var(--code-header-bg)] print:bg-slate-100 border-b border-[var(--border-code)] print:border-slate-300 text-xs font-mono select-none">
|
||||||
<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] print:text-[7pt] 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-2 overflow-x-auto text-[13px] sm:text-[13.5px] print:text-[7.5pt] text-[var(--pre-text)] print:text-slate-900 font-mono leading-relaxed print:leading-snug" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,16 +96,16 @@ 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)] print:prose-h1:text-[13pt] print:prose-h1:mt-2 print:prose-h1:mb-1 print:prose-h1:pb-0.5
|
||||||
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)] print:prose-h2:text-[10.5pt] print:prose-h2:mt-2 print:prose-h2:mb-1 print:prose-h2:pb-0.5
|
||||||
prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6
|
prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6 print:prose-h3:text-[9.5pt] print:prose-h3:mt-1.5 print:prose-h3:mb-0.5
|
||||||
prose-p:text-[var(--text-secondary)] prose-p:leading-relaxed prose-p:mb-4
|
prose-p:text-[var(--text-secondary)] prose-p:leading-relaxed prose-p:mb-4 print:prose-p:text-[8.5pt] print:prose-p:leading-snug print:prose-p:mb-1.5
|
||||||
prose-a:text-[var(--accent)] prose-a:font-medium prose-a:no-underline hover:prose-a:underline hover:prose-a:text-[var(--accent-hover)]
|
prose-a:text-[var(--accent)] prose-a:font-medium prose-a:no-underline hover:prose-a:underline hover:prose-a:text-[var(--accent-hover)]
|
||||||
prose-ul:text-[var(--text-secondary)] prose-ul:my-4 prose-li:my-1.5
|
prose-ul:text-[var(--text-secondary)] prose-ul:my-4 prose-li:my-1.5 print:prose-ul:my-1 print:prose-li:my-0.5
|
||||||
prose-ol:text-[var(--text-secondary)] prose-ol:my-4
|
prose-ol:text-[var(--text-secondary)] prose-ol:my-4 print:prose-ol:my-1
|
||||||
prose-strong:text-[var(--text-primary)] prose-strong:font-semibold
|
prose-strong:text-[var(--text-primary)] prose-strong:font-semibold
|
||||||
prose-hr:border-[var(--border-color)] prose-hr:my-8"
|
prose-hr:border-[var(--border-color)] prose-hr:my-8 print:prose-hr:my-2"
|
||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
@@ -117,7 +122,7 @@ export default function Markdown({ source }: { source: string }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<code
|
<code
|
||||||
className="px-1.5 py-0.5 rounded-md bg-[var(--code-inline-bg)] text-[var(--code-inline-text)] border border-[var(--code-inline-border)] font-mono text-[0.875em] font-medium"
|
className="px-1.5 py-0.5 rounded-md print:rounded-none bg-[var(--code-inline-bg)] print:bg-slate-100 text-[var(--code-inline-text)] print:text-slate-900 border border-[var(--code-inline-border)] print:border-slate-200 font-mono text-[0.875em] print:text-[7.5pt] font-medium"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -125,13 +130,53 @@ export default function Markdown({ source }: { source: string }) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
table: ({ node, ...props }) => (
|
table: ({ node, ...props }) => (
|
||||||
<div className="overflow-x-auto my-6 border border-[var(--border-color)] rounded-xl shadow-xs">
|
<div className="overflow-x-auto my-6 print:my-1.5 border border-[var(--border-color)] print:border-slate-300 rounded-xl print:rounded-none shadow-xs print:shadow-none">
|
||||||
<table className="w-full border-collapse" {...props} />
|
<table className="w-full border-collapse" {...props} />
|
||||||
</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 print:bg-indigo-50 print:text-indigo-950";
|
||||||
|
let Icon = Info;
|
||||||
|
let title = "Note";
|
||||||
|
|
||||||
|
if (alertType === "TIP") {
|
||||||
|
borderClass = "border-emerald-500 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200 print:bg-emerald-50 print:text-emerald-950";
|
||||||
|
Icon = Lightbulb;
|
||||||
|
title = "Tip";
|
||||||
|
} else if (alertType === "IMPORTANT") {
|
||||||
|
borderClass = "border-violet-500 bg-violet-500/10 text-violet-900 dark:text-violet-200 print:bg-violet-50 print:text-violet-950";
|
||||||
|
Icon = Flame;
|
||||||
|
title = "Important";
|
||||||
|
} else if (alertType === "WARNING") {
|
||||||
|
borderClass = "border-amber-500 bg-amber-500/10 text-amber-900 dark:text-amber-200 print:bg-amber-50 print:text-amber-950";
|
||||||
|
Icon = AlertTriangle;
|
||||||
|
title = "Warning";
|
||||||
|
} else if (alertType === "CAUTION") {
|
||||||
|
borderClass = "border-rose-500 bg-rose-500/10 text-rose-900 dark:text-rose-200 print:bg-rose-50 print:text-rose-950";
|
||||||
|
Icon = ShieldAlert;
|
||||||
|
title = "Caution";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`my-5 print:my-1.5 p-4 print:p-2 rounded-xl print:rounded border-l-4 ${borderClass} shadow-xs print:shadow-none`}>
|
||||||
|
<div className="flex items-center gap-2 font-bold text-xs print:text-[7.5pt] uppercase tracking-wider mb-1.5 print:mb-0.5">
|
||||||
|
<Icon className="w-4 h-4 print:w-3 print:h-3" />
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs sm:text-sm print:text-[8pt] text-[var(--text-secondary)] print:text-slate-900 [&>p]:mb-1 [&>p:last-child]:mb-0 print:[&>p]:mb-0.5">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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">
|
<blockquote className="border-l-4 border-[var(--brand)] print:border-slate-500 bg-[var(--brand)]/5 dark:bg-[var(--brand)]/10 print:bg-slate-50 pl-4 print:pl-2.5 py-2.5 print:py-1 my-5 print:my-1.5 rounded-r-xl print:rounded-none text-[var(--text-secondary)] print:text-slate-900 not-italic print:text-[8pt]">
|
||||||
{children}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,20 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Markdown from "@/components/Markdown";
|
import Markdown from "@/components/Markdown";
|
||||||
|
import {
|
||||||
|
Sparkles,
|
||||||
|
Terminal,
|
||||||
|
Server,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
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 +24,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 +32,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 (SSE)",
|
||||||
|
icon: Server,
|
||||||
|
lang: "json",
|
||||||
|
filename: "Streamable HTTP Client Config",
|
||||||
content: `{
|
content: `{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mcpedia-remote": {
|
"mcpedia-remote": {
|
||||||
@@ -40,19 +68,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 Subprocess / REPL",
|
||||||
content: `# One-shot question
|
content: `# Semantic question answering via MCP client
|
||||||
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
|
# Interactive REPL with tool discovery
|
||||||
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 +90,79 @@ 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-xl overflow-hidden shadow-xs">
|
||||||
{/* 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 py-2.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
{CONFIGS.map((c) => (
|
{CONFIGS.map((c) => {
|
||||||
<button
|
const Icon = c.icon;
|
||||||
key={c.id}
|
const isSelected = activeTab === c.id;
|
||||||
onClick={() => setActiveTab(c.id)}
|
return (
|
||||||
className={`px-3 py-1.5 text-xs rounded-lg font-medium transition-all cursor-pointer ${
|
<button
|
||||||
activeTab === c.id
|
key={c.id}
|
||||||
? "bg-[var(--brand)] text-white shadow-xs"
|
onClick={() => setActiveTab(c.id)}
|
||||||
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
|
type="button"
|
||||||
}`}
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md font-medium transition-all cursor-pointer ${
|
||||||
>
|
isSelected
|
||||||
{c.label}
|
? "bg-[var(--brand)] text-[var(--brand-fg)] font-semibold shadow-xs"
|
||||||
</button>
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
|
||||||
))}
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5" />
|
||||||
|
<span>{c.label}</span>
|
||||||
|
</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-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</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
<span>Copy Config</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Code preview via Markdown syntax highlighter */}
|
{/* Code preview */}
|
||||||
<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 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">
|
||||||
Config target: <span className="font-mono text-[var(--text-secondary)]">{current.filename}</span>
|
<div className="flex items-center gap-2">
|
||||||
</span>
|
<span className="font-mono uppercase text-[10px] text-[var(--text-muted)]">
|
||||||
<span className="text-[10px] text-[var(--text-muted)]">
|
Target File:
|
||||||
Format: <span className="uppercase font-mono font-semibold">{current.lang}</span>
|
</span>
|
||||||
</span>
|
<span className="font-mono text-[var(--text-secondary)] text-[11px]">
|
||||||
|
{current.filename}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +14,15 @@ 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,
|
||||||
|
FileText,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
ArrowLeft,
|
||||||
|
BookOpen,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface PdfExportViewProps {
|
interface PdfExportViewProps {
|
||||||
exportData: ExportData;
|
exportData: ExportData;
|
||||||
@@ -111,25 +120,54 @@ export default function PdfExportView({
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [downloadingDocx, setDownloadingDocx] = useState(false);
|
||||||
|
|
||||||
|
async function handleDownloadDocx() {
|
||||||
|
setDownloadingDocx(true);
|
||||||
|
try {
|
||||||
|
const slugs = filteredAndSortedDocs.map((d) => d.slug).join(",");
|
||||||
|
const safeName =
|
||||||
|
summary.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "") || "export";
|
||||||
|
const url = `/api/export?format=docx&slugs=${encodeURIComponent(slugs)}&sort=${sortBy}&pageBreaks=${pageBreaks}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error("Failed to generate Word document");
|
||||||
|
const blob = await res.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = blobUrl;
|
||||||
|
a.download = `${safeName}.docx`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("DOCX download error:", err);
|
||||||
|
alert("Failed to generate .docx file. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setDownloadingDocx(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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-xl p-4 sm:p-5 shadow-xs">
|
||||||
========================================================================= */}
|
|
||||||
<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">
|
||||||
<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 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"
|
||||||
>
|
>
|
||||||
<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(--text-primary)] 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 +188,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 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"
|
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>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span>Copy Markdown</span>
|
<>
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
<span>Copy MD</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -164,46 +208,51 @@ 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 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"
|
title="Download compiled .md file"
|
||||||
>
|
>
|
||||||
|
<FileDown className="w-3.5 h-3.5" />
|
||||||
<span>Download .md</span>
|
<span>Download .md</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Download Word (.docx) */}
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadDocx}
|
||||||
|
disabled={downloadingDocx}
|
||||||
|
type="button"
|
||||||
|
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 disabled:opacity-50"
|
||||||
|
title="Download editable Microsoft Word (.docx) document"
|
||||||
|
>
|
||||||
|
{downloadingDocx ? (
|
||||||
|
<div className="w-3.5 h-3.5 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
<span>Download .docx</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Print / Save as PDF Button */}
|
{/* Print / Save as PDF Button */}
|
||||||
<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-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"
|
title="Open browser print dialog to save as PDF"
|
||||||
>
|
>
|
||||||
<svg
|
<Printer className="w-4 h-4" />
|
||||||
className="w-4 h-4"
|
<span>Print / Save PDF</span>
|
||||||
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>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Options & Filters Bar */}
|
{/* 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 */}
|
{/* 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-mono text-[10px] uppercase">Sort:</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 px-2.5 py-1 text-xs text-[var(--text-primary)] focus:outline-none"
|
||||||
>
|
>
|
||||||
<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 +266,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)]"
|
||||||
/>
|
/>
|
||||||
<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)]"
|
||||||
/>
|
/>
|
||||||
<span>Page Break per Challenge</span>
|
<span>Page Break per Challenge</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -243,13 +292,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-[10px] 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-2.5 py-0.5 rounded text-xs font-mono transition-all cursor-pointer ${
|
||||||
selectedCategory === "all"
|
selectedCategory === "all"
|
||||||
? "bg-slate-900 text-white font-semibold"
|
? "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)]"
|
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -264,9 +313,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-2.5 py-0.5 rounded text-xs font-mono transition-all cursor-pointer ${
|
||||||
selectedCategory.toLowerCase() === cat.toLowerCase()
|
selectedCategory.toLowerCase() === cat.toLowerCase()
|
||||||
? "bg-slate-900 text-white font-semibold"
|
? "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)]"
|
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -278,17 +327,14 @@ 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:m-0 print:max-w-none rounded-xl border border-[var(--border-color)] print:border-none shadow-xs 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="doc-header mb-8 pb-4 print:mb-2.5 print:pb-1.5 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 print:text-lg font-extrabold text-slate-950 tracking-tight mb-2 print:mb-0.5">
|
||||||
{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 print:text-[7.5pt] text-slate-500 font-mono flex flex-wrap items-center gap-x-2 gap-y-1 print:gap-x-1.5 print:gap-y-0.5">
|
||||||
<span>{filteredAndSortedDocs.length} Challenges</span>
|
<span>{filteredAndSortedDocs.length} Challenges</span>
|
||||||
{currentTotalPoints > 0 && (
|
{currentTotalPoints > 0 && (
|
||||||
<span>· {currentTotalPoints.toLocaleString()} Total Points</span>
|
<span>· {currentTotalPoints.toLocaleString()} Total Points</span>
|
||||||
@@ -297,26 +343,27 @@ 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 print:mb-2.5 print:pb-1.5 border-b border-slate-200 print:border-slate-300">
|
||||||
<h2 className="text-sm font-bold uppercase font-mono text-slate-700 mb-3 tracking-wider">
|
<h2 className="text-xs print:text-[8pt] font-bold uppercase font-mono text-slate-700 mb-3 print:mb-1 tracking-wider flex items-center gap-1.5">
|
||||||
Table of Contents
|
<BookOpen className="w-3.5 h-3.5 print:w-3 print:h-3" />
|
||||||
|
<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 print:space-y-0.5 text-xs print:text-[7.5pt] text-slate-800">
|
||||||
{filteredAndSortedDocs.map((doc, idx) => {
|
{filteredAndSortedDocs.map((doc, idx) => {
|
||||||
const cat = extractDocCategory(doc);
|
const cat = extractDocCategory(doc);
|
||||||
const pts = extractDocPoints(doc);
|
const pts = extractDocPoints(doc);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={doc.slug}>
|
<li key={doc.slug} className="print:leading-tight">
|
||||||
<a
|
<a
|
||||||
href={`#ch-${idx + 1}`}
|
href={`#ch-${idx + 1}`}
|
||||||
className="hover:underline font-medium text-slate-900"
|
className="hover:underline font-medium text-slate-900"
|
||||||
>
|
>
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</a>
|
</a>
|
||||||
<span className="text-slate-500 font-mono ml-2">
|
<span className="text-slate-500 font-mono ml-2 print:ml-1">
|
||||||
[{cat}{pts > 0 ? ` · ${pts} pts` : ""}]
|
[{cat}{pts > 0 ? ` · ${pts} pts` : ""}]
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -326,7 +373,7 @@ export default function PdfExportView({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Challenges / Writeups Flow ("Mirip Writeup Manusia") */}
|
{/* Challenges 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;
|
||||||
@@ -341,19 +388,19 @@ export default function PdfExportView({
|
|||||||
id={`ch-${chNum}`}
|
id={`ch-${chNum}`}
|
||||||
className={`challenge-entry ${
|
className={`challenge-entry ${
|
||||||
pageBreaks && idx > 0 ? "page-break-before" : ""
|
pageBreaks && idx > 0 ? "page-break-before" : ""
|
||||||
} pt-6 first:pt-0`}
|
} pt-6 first:pt-0 print:pt-2.5 print:pb-2 print:border-b print:border-slate-200 print:last:border-none`}
|
||||||
>
|
>
|
||||||
{/* Challenge Title & Metadata */}
|
{/* Challenge Title & Metadata */}
|
||||||
<div className="mb-4 pb-2 border-b border-slate-200">
|
<div className="challenge-header mb-4 pb-2 print:mb-1.5 print:pb-1 border-b border-slate-200 print:border-slate-300">
|
||||||
<h2 className="text-2xl font-bold text-slate-950 tracking-tight leading-tight mb-1.5">
|
<h2 className="text-2xl print:text-[11pt] font-bold text-slate-950 tracking-tight leading-tight mb-1.5 print:mb-0.5">
|
||||||
{chNum}. {doc.title}
|
{chNum}. {doc.title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-xs font-mono text-slate-600 flex flex-wrap items-center gap-x-3 gap-y-1">
|
<div className="text-xs print:text-[7.5pt] font-mono text-slate-600 flex flex-wrap items-center gap-x-3 print:gap-x-2 gap-y-1 print:gap-y-0.5">
|
||||||
<span><strong>Category:</strong> {cat}</span>
|
<span><strong>Category:</strong> {cat}</span>
|
||||||
{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 (
|
||||||
@@ -388,7 +435,7 @@ export default function PdfExportView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Markdown Writeup Content */}
|
{/* Markdown Writeup Content */}
|
||||||
<div className="prose prose-slate max-w-none text-slate-900 mb-8">
|
<div className="prose prose-slate max-w-none text-slate-900 mb-8 print:mb-1">
|
||||||
<Markdown source={doc.body} />
|
<Markdown source={doc.body} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ 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,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface Doc {
|
interface Doc {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -102,13 +111,13 @@ 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-md 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(--text-primary)] bg-[var(--bg-elevated)] font-semibold border border-[var(--border-color)] shadow-xs"
|
||||||
: isParentOfActive
|
: 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)]"
|
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -117,15 +126,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(--text-primary)] transition-colors">
|
||||||
{isFolder ? "📁" : "📄"}
|
{isFolder ? (
|
||||||
|
isCollapsed ? (
|
||||||
|
<Folder className="w-3.5 h-3.5" />
|
||||||
|
) : (
|
||||||
|
<FolderOpen className="w-3.5 h-3.5 text-[var(--text-primary)]" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<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 +152,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 cursor-pointer"
|
||||||
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 +224,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 +232,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 index..."
|
||||||
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(--text-muted)] rounded-md text-xs text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--text-primary)] 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"
|
||||||
|
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-muted)] 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 +267,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 mb-1 rounded-md text-xs font-semibold uppercase tracking-wider transition-colors ${
|
||||||
isSectionActive
|
isSectionActive
|
||||||
? "text-[var(--brand)] dark:text-[var(--accent)]"
|
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
|
||||||
: "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-muted)] font-normal font-mono bg-[var(--bg-surface)] px-1.5 py-0.2 rounded border border-[var(--border-color)]">
|
||||||
{sectionDocs.length}
|
{sectionDocs.length}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -291,15 +297,22 @@ export default function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{filteredDocs.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-xs text-[var(--text-dim)] font-mono">
|
||||||
|
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(--text-muted)] text-[var(--text-primary)] rounded-md transition-all font-medium shadow-xs"
|
||||||
>
|
>
|
||||||
<span>+ Create New Document</span>
|
<Plus className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||||
|
<span>Create 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>
|
||||||
|
|||||||
+321
-207
@@ -1,32 +1,42 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin "@tailwindcss/typography";
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
/* Light mode defaults (:root) */
|
/*
|
||||||
|
MCPedia Design Philosophy: Archival Precision & Technical Monolith
|
||||||
|
- Strict monochromatic foundation (Graphite, Zinc, Charcoal)
|
||||||
|
- Zero decorative AI gradients or blurred blobs
|
||||||
|
- Typographic hierarchy: Strong contrast, razor-sharp hairline borders, monospace telemetry
|
||||||
|
- Functional signal indicators (Status Green, Alert Amber, Critical Coral)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Light mode defaults (:root) - Technical Paper / Archival Slate */
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--bg-app: #f8fafc;
|
--bg-app: #f9fafb;
|
||||||
--bg-surface: #ffffff;
|
--bg-surface: #ffffff;
|
||||||
--bg-surface-hover: #f1f5f9;
|
--bg-surface-hover: #f3f4f6;
|
||||||
--bg-elevated: #f1f5f9;
|
--bg-elevated: #f3f4f6;
|
||||||
--bg-elevated-hover: #e2e8f0;
|
--bg-elevated-hover: #e5e7eb;
|
||||||
--border-color: #e2e8f0;
|
--border-color: #e5e7eb;
|
||||||
--border-subtle: #edf2f7;
|
--border-subtle: #f3f4f6;
|
||||||
--border-card: #e2e8f0;
|
--border-card: #e5e7eb;
|
||||||
|
|
||||||
--text-primary: #0f172a;
|
--text-primary: #111827;
|
||||||
--text-secondary: #334155;
|
--text-secondary: #374151;
|
||||||
--text-muted: #64748b;
|
--text-muted: #6b7280;
|
||||||
--text-dim: #94a3b8;
|
--text-dim: #9ca3af;
|
||||||
|
|
||||||
--brand: #4f46e5;
|
/* Accent: Precision Technical Ink */
|
||||||
--brand-hover: #4338ca;
|
--brand: #18181b;
|
||||||
--accent: #4f46e5;
|
--brand-hover: #27272a;
|
||||||
--accent-hover: #4338ca;
|
--brand-fg: #ffffff;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-hover: #1d4ed8;
|
||||||
|
|
||||||
--nav-bg: rgba(255, 255, 255, 0.88);
|
--nav-bg: rgba(255, 255, 255, 0.92);
|
||||||
|
|
||||||
/* Code & Syntax variables (Light Mode) */
|
/* Code & Syntax (Light Mode) */
|
||||||
--code-bg: #f1f5f9;
|
--code-bg: #f3f4f6;
|
||||||
--code-block-bg: #f8fafc;
|
--code-block-bg: #f8fafc;
|
||||||
--code-header-bg: #f1f5f9;
|
--code-header-bg: #f1f5f9;
|
||||||
--border-code: #e2e8f0;
|
--border-code: #e2e8f0;
|
||||||
@@ -37,15 +47,15 @@
|
|||||||
--code-inline-text: #0f172a;
|
--code-inline-text: #0f172a;
|
||||||
--code-inline-border: #e2e8f0;
|
--code-inline-border: #e2e8f0;
|
||||||
|
|
||||||
--table-zebra: rgba(241, 245, 249, 0.6);
|
--table-zebra: rgba(243, 244, 246, 0.5);
|
||||||
--scrollbar-thumb: #cbd5e1;
|
--scrollbar-thumb: #d1d5db;
|
||||||
--scrollbar-thumb-hover: #94a3b8;
|
--scrollbar-thumb-hover: #9ca3af;
|
||||||
--shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05);
|
--shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
/* Syntax Highlighting (Light Mode) */
|
/* Syntax Highlighting */
|
||||||
--hl-keyword: #cf222e;
|
--hl-keyword: #cf222e;
|
||||||
--hl-string: #0a3069;
|
--hl-string: #0a3069;
|
||||||
--hl-title: #8250df;
|
--hl-title: #6f42c1;
|
||||||
--hl-attr: #953800;
|
--hl-attr: #953800;
|
||||||
--hl-number: #0550ae;
|
--hl-number: #0550ae;
|
||||||
--hl-comment: #6e7781;
|
--hl-comment: #6e7781;
|
||||||
@@ -55,48 +65,50 @@
|
|||||||
--hl-meta: #6e7781;
|
--hl-meta: #6e7781;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode overrides (html.dark) */
|
/* Dark mode overrides (html.dark) - Deep Basalt / Precision Carbon */
|
||||||
html.dark {
|
html.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg-app: #08090a;
|
--bg-app: #090a0c;
|
||||||
--bg-surface: #0f1011;
|
--bg-surface: #101216;
|
||||||
--bg-surface-hover: #131415;
|
--bg-surface-hover: #16181f;
|
||||||
--bg-elevated: #141517;
|
--bg-elevated: #161920;
|
||||||
--bg-elevated-hover: #1b1d20;
|
--bg-elevated-hover: #1e222b;
|
||||||
--border-color: #1f2022;
|
--border-color: #222630;
|
||||||
--border-subtle: rgba(255, 255, 255, 0.06);
|
--border-subtle: #181b22;
|
||||||
--border-card: #1f2022;
|
--border-card: #222630;
|
||||||
|
|
||||||
--text-primary: #f7f8f8;
|
--text-primary: #f3f4f6;
|
||||||
--text-secondary: #d0d6e0;
|
--text-secondary: #d1d5db;
|
||||||
--text-muted: #8a8f98;
|
--text-muted: #9ca3af;
|
||||||
--text-dim: #62666d;
|
--text-dim: #6b7280;
|
||||||
|
|
||||||
--brand: #5e6ad2;
|
/* Accent: Monolithic Snow Ink */
|
||||||
--brand-hover: #7170ff;
|
--brand: #f3f4f6;
|
||||||
--accent: #7170ff;
|
--brand-hover: #ffffff;
|
||||||
--accent-hover: #828fff;
|
--brand-fg: #090a0c;
|
||||||
|
--accent: #60a5fa;
|
||||||
|
--accent-hover: #93c5fd;
|
||||||
|
|
||||||
--nav-bg: rgba(8, 9, 10, 0.85);
|
--nav-bg: rgba(9, 10, 12, 0.90);
|
||||||
|
|
||||||
/* Code & Syntax variables (Dark Mode) */
|
/* Code & Syntax (Dark Mode) */
|
||||||
--code-bg: #141517;
|
--code-bg: #0c0e12;
|
||||||
--code-block-bg: #0d1117;
|
--code-block-bg: #0c0e12;
|
||||||
--code-header-bg: #161b22;
|
--code-header-bg: #14171d;
|
||||||
--border-code: #30363d;
|
--border-code: #222630;
|
||||||
--code-button-bg: #21262d;
|
--code-button-bg: #181b22;
|
||||||
--code-button-hover-bg: #30363d;
|
--code-button-hover-bg: #222630;
|
||||||
--pre-text: #e6edf3;
|
--pre-text: #e5e7eb;
|
||||||
--code-inline-bg: #161b22;
|
--code-inline-bg: #161920;
|
||||||
--code-inline-text: #e6edf3;
|
--code-inline-text: #e5e7eb;
|
||||||
--code-inline-border: #30363d;
|
--code-inline-border: #222630;
|
||||||
|
|
||||||
--table-zebra: rgba(255, 255, 255, 0.02);
|
--table-zebra: rgba(255, 255, 255, 0.015);
|
||||||
--scrollbar-thumb: #23252a;
|
--scrollbar-thumb: #222630;
|
||||||
--scrollbar-thumb-hover: #383b42;
|
--scrollbar-thumb-hover: #374151;
|
||||||
--shadow-card: none;
|
--shadow-card: none;
|
||||||
|
|
||||||
/* Syntax Highlighting (Dark Mode) */
|
/* Syntax Highlighting */
|
||||||
--hl-keyword: #ff7b72;
|
--hl-keyword: #ff7b72;
|
||||||
--hl-string: #a5d6ff;
|
--hl-string: #a5d6ff;
|
||||||
--hl-title: #d2a8ff;
|
--hl-title: #d2a8ff;
|
||||||
@@ -110,7 +122,7 @@ html.dark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||||||
|
|
||||||
--color-background: var(--bg-app);
|
--color-background: var(--bg-app);
|
||||||
@@ -141,16 +153,17 @@ 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", "calt";
|
||||||
-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.15s ease, color 0.15s ease;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom hairline scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 5px;
|
||||||
height: 6px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@@ -159,49 +172,88 @@ body {
|
|||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--scrollbar-thumb);
|
background: var(--scrollbar-thumb);
|
||||||
border-radius: 9999px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--scrollbar-thumb-hover);
|
background: var(--scrollbar-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glassmorphism helpers */
|
/* Glass navigation */
|
||||||
.glass-panel {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
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(16px);
|
||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prose markdown typography for both Light and Dark modes */
|
/* Tactical Architectural Callouts */
|
||||||
|
.callout {
|
||||||
|
border-left-width: 3px;
|
||||||
|
border-radius: 0 0.375rem 0.375rem 0;
|
||||||
|
padding: 0.875rem 1.125rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-note {
|
||||||
|
border-left-color: var(--text-muted);
|
||||||
|
background-color: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-tip {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
background-color: rgba(16, 185, 129, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-important {
|
||||||
|
border-left-color: #3b82f6;
|
||||||
|
background-color: rgba(59, 130, 246, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-warning {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
background-color: rgba(245, 158, 11, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-caution {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
background-color: rgba(239, 68, 68, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prose Typography */
|
||||||
.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(--text-primary) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: var(--border-color);
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a:hover {
|
||||||
|
text-decoration-color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose th,
|
.prose th,
|
||||||
@@ -215,9 +267,10 @@ body {
|
|||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.08em;
|
||||||
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose td {
|
.prose td {
|
||||||
@@ -228,6 +281,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);
|
||||||
}
|
}
|
||||||
@@ -236,11 +293,12 @@ body {
|
|||||||
background: var(--code-block-bg) !important;
|
background: var(--code-block-bg) !important;
|
||||||
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.5rem;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 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,8 +310,8 @@ 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.35rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.25rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,13 +321,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose blockquote {
|
.prose blockquote {
|
||||||
border-left: 3px solid var(--brand);
|
border-left: 2px solid var(--text-muted);
|
||||||
background: rgba(94, 106, 210, 0.08);
|
background: var(--bg-elevated);
|
||||||
border-radius: 0 0.375rem 0.375rem 0;
|
border-radius: 0 0.375rem 0.375rem 0;
|
||||||
margin: 1.5em 0;
|
margin: 1.5em 0;
|
||||||
padding: 0.75em 1.25em;
|
padding: 0.75em 1.25em;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: var(--text-muted);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose hr {
|
.prose hr {
|
||||||
@@ -277,7 +335,7 @@ body {
|
|||||||
margin: 2.5rem 0;
|
margin: 2.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Syntax Highlighting Tokens (Highlight.js) */
|
/* Syntax Highlighting */
|
||||||
.hljs {
|
.hljs {
|
||||||
color: var(--pre-text);
|
color: var(--pre-text);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -346,14 +404,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,47 +423,46 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Transitions */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(4px); }
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.2s ease-out forwards;
|
animation: fadeIn 0.18s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* Print & PDF Export Stylesheet */
|
||||||
Print & PDF Export Stylesheet
|
|
||||||
Optimized for publication-grade PDF generation via browser / window.print()
|
|
||||||
========================================================================== */
|
|
||||||
@media print {
|
@media print {
|
||||||
@page {
|
@page {
|
||||||
size: A4 portrait;
|
size: A4 portrait;
|
||||||
margin: 18mm 15mm 18mm 15mm;
|
margin: 10mm 12mm 10mm 12mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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;
|
||||||
|
box-sizing: border-box !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: 8.5pt !important;
|
||||||
line-height: 1.5 !important;
|
line-height: 1.35 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide app navigation, headers, sidebars, interactive controls */
|
/* Hide navigation, toolbars, sidebar, and web UI controls */
|
||||||
header,
|
header.site-header,
|
||||||
aside,
|
header.glass-nav,
|
||||||
|
aside.site-sidebar,
|
||||||
|
aside:not(.print-include),
|
||||||
|
footer,
|
||||||
nav.site-header,
|
nav.site-header,
|
||||||
.no-print,
|
.no-print,
|
||||||
.export-toolbar,
|
.export-toolbar,
|
||||||
@@ -413,9 +470,9 @@ body {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full width container in print */
|
|
||||||
main,
|
main,
|
||||||
.print-container {
|
.print-container,
|
||||||
|
main > div {
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
@@ -425,63 +482,190 @@ body {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page break rules */
|
/* Explicit page break classes */
|
||||||
.page-break-before,
|
.page-break-before,
|
||||||
.chapter-page,
|
.chapter-page {
|
||||||
.challenge-entry {
|
|
||||||
break-before: page !important;
|
break-before: page !important;
|
||||||
page-break-before: always !important;
|
page-break-before: always !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-break-after,
|
/* Prevent awkward mid-element page breaks */
|
||||||
.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,
|
||||||
.challenge-entry,
|
.callout,
|
||||||
|
blockquote,
|
||||||
table,
|
table,
|
||||||
tr,
|
tr,
|
||||||
img,
|
img {
|
||||||
pre,
|
|
||||||
blockquote {
|
|
||||||
break-inside: avoid !important;
|
break-inside: avoid !important;
|
||||||
page-break-inside: avoid !important;
|
page-break-inside: avoid !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
/* Prevent headings from being orphaned at the bottom of pages */
|
||||||
max-width: 100% !important;
|
.challenge-header,
|
||||||
height: auto !important;
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
break-after: avoid !important;
|
||||||
|
page-break-after: avoid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dense / compact challenge flow */
|
||||||
|
.challenge-entry {
|
||||||
|
break-inside: auto !important;
|
||||||
|
page-break-inside: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact typography & prose overrides */
|
||||||
|
p,
|
||||||
|
.prose p {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 2.5pt !important;
|
||||||
|
font-size: 8.5pt !important;
|
||||||
|
line-height: 1.35 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
.prose h1 {
|
||||||
|
font-size: 13pt !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #020617 !important;
|
||||||
|
margin-top: 6pt !important;
|
||||||
|
margin-bottom: 2pt !important;
|
||||||
|
padding-bottom: 1pt !important;
|
||||||
|
border-bottom: 1px solid #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2,
|
||||||
|
.prose h2 {
|
||||||
|
font-size: 10.5pt !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #020617 !important;
|
||||||
|
margin-top: 5pt !important;
|
||||||
|
margin-bottom: 2pt !important;
|
||||||
|
padding-bottom: 1pt !important;
|
||||||
|
border-bottom: 1px solid #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3,
|
||||||
|
.prose h3 {
|
||||||
|
font-size: 9.5pt !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #0f172a !important;
|
||||||
|
margin-top: 4pt !important;
|
||||||
|
margin-bottom: 1.5pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4, h5, h6,
|
||||||
|
.prose h4, .prose h5, .prose h6 {
|
||||||
|
font-size: 8.5pt !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
margin-top: 3pt !important;
|
||||||
|
margin-bottom: 1pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol,
|
||||||
|
.prose ul, .prose ol {
|
||||||
|
margin-top: 1pt !important;
|
||||||
|
margin-bottom: 2.5pt !important;
|
||||||
|
padding-left: 13pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li,
|
||||||
|
.prose li {
|
||||||
|
margin-top: 0.5pt !important;
|
||||||
|
margin-bottom: 0.5pt !important;
|
||||||
|
font-size: 8.5pt !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > p {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
.prose blockquote,
|
||||||
|
.callout {
|
||||||
|
margin: 2.5pt 0 !important;
|
||||||
|
padding: 2pt 5pt !important;
|
||||||
|
font-size: 8pt !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
border-left: 2.5px solid #64748b !important;
|
||||||
|
background-color: #f8fafc !important;
|
||||||
|
border-radius: 0 2px 2px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
.prose table {
|
||||||
|
margin: 2.5pt 0 !important;
|
||||||
|
font-size: 7.5pt !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
width: 100% !important;
|
||||||
|
border: 1px solid #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td,
|
||||||
|
.prose th, .prose td {
|
||||||
|
padding: 2pt 4pt !important;
|
||||||
|
border: 1px solid #cbd5e1 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
.prose th {
|
||||||
|
background-color: #f1f5f9 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.04em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr,
|
||||||
|
.prose hr {
|
||||||
|
margin: 3pt 0 !important;
|
||||||
|
border-color: #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 220px !important;
|
||||||
|
height: auto !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
margin: 2.5pt auto !important;
|
||||||
|
display: block !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;
|
||||||
page-break-inside: avoid !important;
|
page-break-inside: avoid !important;
|
||||||
border: 1px solid #cbd5e1 !important;
|
border: 1px solid #cbd5e1 !important;
|
||||||
background-color: #f8fafc !important;
|
background-color: #f8fafc !important;
|
||||||
border-radius: 4px !important;
|
border-radius: 3px !important;
|
||||||
margin: 8pt 0 !important;
|
margin: 2.5pt 0 !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-block-header {
|
.code-block-header {
|
||||||
border-bottom: 1px solid #cbd5e1 !important;
|
border-bottom: 1px solid #cbd5e1 !important;
|
||||||
background-color: #f1f5f9 !important;
|
background-color: #f1f5f9 !important;
|
||||||
padding: 3pt 8pt !important;
|
padding: 1.5pt 5pt !important;
|
||||||
color: #334155 !important;
|
color: #334155 !important;
|
||||||
|
font-size: 7pt !important;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-block-wrapper pre {
|
.code-block-wrapper pre {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
padding: 6pt 8pt !important;
|
padding: 3pt 5pt !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -492,87 +676,17 @@ 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: 7.5pt !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.3 !important;
|
||||||
|
color: #0f172a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
:not(pre) > code {
|
||||||
|
padding: 0.5pt 2pt !important;
|
||||||
|
font-size: 7.5pt !important;
|
||||||
background-color: #f1f5f9 !important;
|
background-color: #f1f5f9 !important;
|
||||||
color: #0f172a !important;
|
|
||||||
padding: 1pt 3pt !important;
|
|
||||||
border-radius: 3px !important;
|
|
||||||
border: 1px solid #e2e8f0 !important;
|
border: 1px solid #e2e8f0 !important;
|
||||||
}
|
border-radius: 2px !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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+33
-5
@@ -2,11 +2,13 @@ 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 } from "lucide-react";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "MCPedia — Knowledge base for humans and AI agents",
|
title: "MCPedia — Archival Knowledge Base for Humans & 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 Model Context Protocol (MCP). Both share one Core.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -35,7 +37,7 @@ 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(--text-primary)] selection:text-[var(--bg-app)] flex flex-col transition-colors duration-150">
|
||||||
<div hidden>
|
<div hidden>
|
||||||
<div id="a11y-live-region" aria-live="polite" aria-atomic="true" />
|
<div id="a11y-live-region" aria-live="polite" aria-atomic="true" />
|
||||||
</div>
|
</div>
|
||||||
@@ -44,16 +46,42 @@ export default function RootLayout({
|
|||||||
|
|
||||||
<div className="mx-auto max-w-7xl w-full flex-1 flex flex-col">
|
<div className="mx-auto max-w-7xl w-full flex-1 flex flex-col">
|
||||||
<div className="flex flex-1">
|
<div className="flex flex-1">
|
||||||
<aside className="hidden lg:block w-72 shrink-0 border-r border-[var(--border-color)] sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto bg-[var(--bg-app)]">
|
<aside className="no-print hidden lg:block w-72 shrink-0 border-r border-[var(--border-color)] sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto bg-[var(--bg-app)]">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</aside>
|
</aside>
|
||||||
<main className="flex-1 min-w-0">
|
<main className="flex-1 min-w-0">
|
||||||
<div className="px-4 sm:px-8 py-8 sm:py-10 max-w-4xl mx-auto xl:max-w-5xl">
|
<div className="px-4 sm:px-8 py-8 sm:py-10 print:p-0 print:m-0 print:max-w-none max-w-4xl mx-auto xl:max-w-5xl">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Global Precision Status Footer */}
|
||||||
|
<footer className="no-print border-t border-[var(--border-color)] bg-[var(--bg-surface)] py-4 text-xs text-[var(--text-muted)]">
|
||||||
|
<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-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||||
|
<span className="text-[var(--text-primary)] font-semibold">MCPedia</span>
|
||||||
|
<span className="text-[var(--border-color)]">/</span>
|
||||||
|
<span>PostgreSQL 16 Core</span>
|
||||||
|
<span className="text-[var(--border-color)]">/</span>
|
||||||
|
<span>pgvector</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-[11px] text-[var(--text-muted)]">
|
||||||
|
<Link href="/search" className="hover:text-[var(--text-primary)] transition-colors">
|
||||||
|
RRF Hybrid Search
|
||||||
|
</Link>
|
||||||
|
<span>·</span>
|
||||||
|
<Link href="/export/pdf" className="hover:text-[var(--text-primary)] transition-colors">
|
||||||
|
Publication PDF
|
||||||
|
</Link>
|
||||||
|
<span>·</span>
|
||||||
|
<span>HTTP :4021</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+25
-24
@@ -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 } from "lucide-react";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -38,47 +39,46 @@ 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-xl p-6 sm:p-8 shadow-xs relative">
|
||||||
{/* Top decorative glow */}
|
<div className="text-center mb-7">
|
||||||
<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="w-10 h-10 rounded-lg bg-[var(--brand)] text-[var(--brand-fg)] font-bold text-sm flex items-center justify-center mx-auto mb-3 shadow-xs font-mono">
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
<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">
|
|
||||||
M
|
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">Admin Authentication</h1>
|
<h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">Admin Authentication</h1>
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-1.5">
|
<p className="text-xs text-[var(--text-muted)] mt-1">
|
||||||
Enter your admin password to create, edit, or delete documents.
|
Enter administrative password to authorize document 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 mb-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}
|
{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
|
Password
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Enter password..."
|
placeholder="Enter admin 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-9 pr-10 py-2.5 bg-[var(--bg-elevated)] 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 font-mono"
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
<Key className="w-3.5 h-3.5 text-[var(--text-dim)] absolute left-3 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 top-2.5 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-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,23 +86,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-2.5 bg-[var(--brand)] hover:opacity-90 text-[var(--brand-fg)] rounded-lg transition-all font-semibold text-xs shadow-xs disabled:opacity-50 flex items-center justify-center gap-2 cursor-pointer uppercase tracking-wider font-mono"
|
||||||
>
|
>
|
||||||
{loading && (
|
{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 ? "Authenticating..." : "Sign In"}</span>
|
<span>{loading ? "AUTHENTICATING..." : "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-7 pt-5 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 Home</span>
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-[11px] text-[var(--text-dim)]">HTTP-only Secure Cookie</span>
|
<span className="text-[10px] text-[var(--text-dim)] font-mono">HTTP Cookie</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+128
-83
@@ -2,6 +2,19 @@ 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 {
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
BookOpen,
|
||||||
|
ArrowRight,
|
||||||
|
Database,
|
||||||
|
Layers,
|
||||||
|
Folder,
|
||||||
|
FileText,
|
||||||
|
Clock,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -22,7 +35,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 +56,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 +88,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(--text-primary)] transition-colors">
|
||||||
|
{node.isLeaf && node.children.length === 0 ? (
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
) : (
|
||||||
|
<Folder className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
</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 +118,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 +127,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</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -144,76 +151,110 @@ 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">
|
{/* Status indicator stamp */}
|
||||||
<span className="w-2 h-2 rounded-full bg-[var(--brand)] animate-pulse" />
|
<div className="inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-secondary)] text-xs font-mono mb-6">
|
||||||
<span>Model Context Protocol + PostgreSQL Core</span>
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||||
|
<span>PostgreSQL 16 Core · Model Context Protocol</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 with strong monolithic weight */}
|
||||||
|
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-[var(--text-primary)] tracking-tight mb-5 leading-[1.1]">
|
||||||
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">
|
Humans and AI Agents.
|
||||||
Humans and AI Agents
|
|
||||||
</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. Humans read the web interface; AI assistants connect directly via native MCP tools. Both share one unified database source of truth.
|
||||||
</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:opacity-90 text-[var(--brand-fg)] rounded-lg font-semibold text-sm transition-all shadow-xs 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(--text-muted)] text-[var(--text-primary)] rounded-lg font-semibold text-sm transition-colors shadow-xs"
|
||||||
>
|
>
|
||||||
<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(--text-muted)]" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>Hybrid Search</span>
|
<span>Hybrid Search</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/create"
|
href="/create"
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-[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)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg text-sm transition-colors 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-xl shadow-xs">
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-xl font-bold text-[var(--text-primary)]">{all.length}</div>
|
<div className="p-2 rounded-lg bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)]">
|
||||||
<div className="text-xs text-[var(--text-muted)]">Documents in DB</div>
|
<Database className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xl font-bold text-[var(--text-primary)] font-mono">{all.length}</div>
|
||||||
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">DB Documents</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="text-xl font-bold text-[var(--text-primary)]">{sections.length}</div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-xs text-[var(--text-muted)]">Active sections</div>
|
<div className="p-2 rounded-lg bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)]">
|
||||||
|
<Layers className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xl font-bold text-[var(--text-primary)] font-mono">{sections.length}</div>
|
||||||
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">Active Sections</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="text-xl font-bold text-[var(--brand)] dark:text-[var(--accent)]">RRF Hybrid</div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-xs text-[var(--text-muted)]">FTS + Cosine Vectors</div>
|
<div className="p-2 rounded-lg bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)]">
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xl font-bold text-[var(--text-primary)] font-mono">RRF</div>
|
||||||
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">FTS + Cosine Vectors</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">13 MCP Tools</div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-xs text-[var(--text-muted)]">Streamable HTTP :4021</div>
|
<div className="p-2 rounded-lg bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)]">
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xl font-bold text-[var(--text-primary)] font-mono">13 Tools</div>
|
||||||
|
<div className="text-[11px] text-[var(--text-muted)] font-mono">HTTP :4021 MCP</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Browse by Section */}
|
{/* Interactive System Architecture & Topology */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<InteractiveTopology />
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
|
</section>
|
||||||
Browse by Section
|
|
||||||
</h2>
|
{/* Browse Knowledge by Section */}
|
||||||
|
<section>
|
||||||
|
<div className="flex items-center justify-between mb-5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Layers className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Knowledge Sections
|
||||||
|
</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">
|
||||||
@@ -222,19 +263,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(--text-muted)] rounded-xl p-5 transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<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(--text-muted)] hover:text-[var(--text-primary)] 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-sm text-[var(--text-primary)] mb-1">
|
||||||
{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 +283,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,33 +296,38 @@ 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-5">
|
||||||
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
|
<div className="flex items-center gap-2">
|
||||||
Recently Updated Knowledge
|
<Clock className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
</h2>
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Recently Updated
|
||||||
|
</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(--text-muted)] hover:text-[var(--text-primary)] font-semibold flex items-center gap-1 font-mono"
|
||||||
>
|
>
|
||||||
View all ({all.length}) →
|
<span>View all ({all.length})</span>
|
||||||
|
<ArrowRight className="w-3 h-3" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5">
|
||||||
{recent.map((d) => {
|
{recent.map((d) => {
|
||||||
const sInfo = getSectionMeta(d.section);
|
const sInfo = getSectionMeta(d.section);
|
||||||
return (
|
return (
|
||||||
<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(--text-muted)] rounded-xl p-4.5 transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2 mb-2">
|
<div className="flex items-center justify-between gap-2 mb-2">
|
||||||
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] uppercase">
|
<span className="text-[11px] font-mono font-semibold text-[var(--text-muted)] 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 +336,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-semibold text-sm text-[var(--text-primary)] group-hover:underline 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-2.5">
|
||||||
{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-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] font-mono"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -313,17 +358,17 @@ 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-2.5 py-0.5 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] text-xs font-mono mb-2">
|
||||||
<span>⚡ Native Model Context Protocol (MCP)</span>
|
<span>[ MCP_INTEGRATION ]</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight mb-2">
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight mb-1.5">
|
||||||
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,10 @@ 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,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface DocHit {
|
interface DocHit {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -78,73 +82,68 @@ function SearchContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredResults = selectedSection === "all"
|
const filteredResults =
|
||||||
? results
|
selectedSection === "all"
|
||||||
: results.filter((r) => r.section === selectedSection);
|
? results
|
||||||
|
: results.filter((r) => r.section === selectedSection);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<h1 className="text-3xl font-extrabold text-[var(--text-primary)] tracking-tight mb-2">
|
||||||
Search Knowledge Base
|
Multi-Modal Search Console
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--text-muted)]">
|
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed">
|
||||||
Find answers across all documentation, CTF writeups, research notes, and semantic vector chunks.
|
Query across knowledge documents and CTF writeups using PostgreSQL Full-Text Search (tsvector), Cosine Vector Embeddings, or Reciprocal Rank Fusion (RRF).
|
||||||
</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-xl p-4 sm:p-5 shadow-xs space-y-4">
|
||||||
{/* 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-10 pr-9 py-3 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--text-primary)] text-sm transition-all"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<svg
|
<Search className="w-4 h-4 text-[var(--text-muted)] absolute left-3.5 top-3.5 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 top-3 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded"
|
||||||
|
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-3 pt-2.5 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 text-[10px] 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 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 rounded-md border text-xs font-medium 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-[var(--brand-fg)] font-semibold shadow-xs"
|
||||||
: "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 +157,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-md 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-semibold"
|
||||||
: "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-md 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-semibold"
|
||||||
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -184,38 +183,45 @@ 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-10 text-[var(--text-muted)] text-xs font-mono">
|
||||||
<div className="w-5 h-5 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-[var(--text-muted)] border-t-transparent rounded-full animate-spin" />
|
||||||
<span>Searching knowledge base...</span>
|
<span>SEARCHING_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-12 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-xs space-y-2 animate-fade-in">
|
||||||
<div className="text-3xl mb-3">🔍</div>
|
<h3 className="text-sm font-bold text-[var(--text-primary)]">No matching documents found</h3>
|
||||||
<h3 className="text-base font-semibold text-[var(--text-primary)] mb-1">No matching documents</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 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3 shadow-xs">
|
||||||
<p className="text-xs text-[var(--text-muted)]">Popular search queries:</p>
|
<p className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">Popular 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 py-1 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all font-mono text-xs cursor-pointer"
|
||||||
>
|
>
|
||||||
{term}
|
{term}
|
||||||
</button>
|
</button>
|
||||||
@@ -226,40 +232,40 @@ function SearchContent() {
|
|||||||
|
|
||||||
{/* Results List */}
|
{/* Results List */}
|
||||||
{!loading && filteredResults.length > 0 && (
|
{!loading && filteredResults.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-3 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-2.5">
|
||||||
{filteredResults.map((r) => (
|
{filteredResults.map((r) => (
|
||||||
<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(--text-muted)] rounded-xl p-4.5 transition-all shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3 mb-2">
|
<div className="flex items-center justify-between gap-3 mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-mono px-2 py-0.5 rounded bg-[var(--brand)]/10 border border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)] uppercase">
|
<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)] uppercase font-semibold">
|
||||||
{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-dim)] bg-[var(--bg-elevated)] px-2 py-0.5 rounded">
|
||||||
Score: {r.score.toFixed(3)}
|
{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-sm font-semibold text-[var(--text-primary)] group-hover:underline mb-1.5">
|
||||||
{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(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
||||||
dangerouslySetInnerHTML={{ __html: r.snippet }}
|
dangerouslySetInnerHTML={{ __html: r.snippet }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"@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",
|
||||||
"next": "16.3.1",
|
"lucide-react": "^1.33.0",
|
||||||
|
"next": "16.3.3",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
@@ -25,11 +26,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/hast": "^3.0.5",
|
"@types/hast": "^3.0.5",
|
||||||
"@types/node": "^20",
|
"@types/node": "^26",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.3.1",
|
"eslint-config-next": "16.3.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"zod": "^4.0.0",
|
"zod": "^4.0.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^26",
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"zod": "^4.0.0",
|
"zod": "^4.0.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^26",
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -72,7 +72,8 @@
|
|||||||
"@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",
|
||||||
"next": "16.3.1",
|
"lucide-react": "^1.33.0",
|
||||||
|
"next": "16.3.3",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
@@ -83,11 +84,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/hast": "^3.0.5",
|
"@types/hast": "^3.0.5",
|
||||||
"@types/node": "^20",
|
"@types/node": "^26",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.3.1",
|
"eslint-config-next": "16.3.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
},
|
},
|
||||||
@@ -122,7 +123,8 @@
|
|||||||
"@mcpedia/parser": "workspace:*",
|
"@mcpedia/parser": "workspace:*",
|
||||||
"@mcpedia/search": "workspace:*",
|
"@mcpedia/search": "workspace:*",
|
||||||
"@mcpedia/types": "workspace:*",
|
"@mcpedia/types": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0",
|
"docx": "^9.7.1",
|
||||||
|
"drizzle-orm": "^0.45.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/db": {
|
"packages/db": {
|
||||||
@@ -130,12 +132,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mcpedia/config": "workspace:*",
|
"@mcpedia/config": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.45.2",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^26",
|
||||||
"drizzle-kit": "^0.30.0",
|
"drizzle-kit": "^0.31.10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/embeddings": {
|
"packages/embeddings": {
|
||||||
@@ -178,7 +180,7 @@
|
|||||||
"@mcpedia/db": "workspace:*",
|
"@mcpedia/db": "workspace:*",
|
||||||
"@mcpedia/embeddings": "workspace:*",
|
"@mcpedia/embeddings": "workspace:*",
|
||||||
"@mcpedia/types": "workspace:*",
|
"@mcpedia/types": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.45.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/types": {
|
"packages/types": {
|
||||||
@@ -196,7 +198,7 @@
|
|||||||
"@mcpedia/parser": "workspace:*",
|
"@mcpedia/parser": "workspace:*",
|
||||||
"@mcpedia/queue": "workspace:*",
|
"@mcpedia/queue": "workspace:*",
|
||||||
"@mcpedia/search": "workspace:*",
|
"@mcpedia/search": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.45.2",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -248,51 +250,57 @@
|
|||||||
|
|
||||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="],
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="],
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="],
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="],
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="],
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="],
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="],
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="],
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="],
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="],
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="],
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="],
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="],
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="],
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="],
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="],
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="],
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="],
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="],
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="],
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
||||||
|
|
||||||
@@ -434,25 +442,25 @@
|
|||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@16.3.1", "", {}, "sha512-35G3xwkQUb2oETSDjFXGrVugknoayLFBh7vSE+yAcl9IP2zT9wyGwq7297AYHR11kJld807t5f8AJBs6WBzXsQ=="],
|
"@next/env": ["@next/env@16.3.3", "", {}, "sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.3.1", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "fast-glob": "3.3.1" } }, "sha512-B4SznlXwVpaLDa7Tbi6zLuueria2d/PmFDhXyDymPGrk2r1n/RMJmcn5FZq1L64k+Jsyte1lKvOr7lP9Xo80mQ=="],
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.3.3", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "fast-glob": "3.3.1" } }, "sha512-pbEh30vvjKpDoTAmo1v3q2uM4JUi8QaEBpbmjWvGfoec2jLghy/WNtvzAT0bk+Ik9oz6etjt4YjXEk4BQnicCw=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ABMIu2zQ7cnNIHm5ivKGwZwUrm0pAai3yiJ/gK/rF1c1VP9UOnj7XECbMKFdVKp9I9eMYq9NoDs1WXOoowxzJw=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw=="],
|
||||||
|
|
||||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gNG21e/UnrroeScbY/QndUEdl0mF1FRibW7BBeYUz/5ABCepjqDdEdgr592vpzMtCn/m7FTjYq3TN4TpyDnutw=="],
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-6B6Lw016iwNUQuaJoraMMTLh6TwHzFUtxipSScD1F3YyymcrRWkobodRS2ftIOkF5vrs4zNlyUrTC5YZQ9Lz5w=="],
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JUiPXZKK9wOhjf4MgDiH29GZLxfqOesbLtHq2pDxwH/WwscTRV2ToymnOTh1egzaZf0ueUf8T2+CeYTGHjW0Iw=="],
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Uog9jsrmIRIL/lfvIp9htmskSNC7JcQsMVucXL2V2YY1y/D9IUN3LPEafqy0zRJ2cIU1SQ0V6F6TlffQ+pLAGg=="],
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-6yy3FT13KgUFOj5H8bl8w/6nKiJwHIvbtwh1V+1acsu+7y4tJjnemSa6mhsh53BeoVrlozE+fMgZhXH46WmjMA=="],
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg=="],
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iOoN1QecUoGNZik536U/vtK43YwgyrCsGIkth52yIkl612n+0C9MjSnJbQAikISpb+WYRooBVhaDlUW7iZoKog=="],
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg=="],
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d/k+PpAriUPaeMJJOG7HUSdqfEX46FEPWU1p3/nm2ACmXhj9hFEWdFODUBIpkuijXYkfL90qZzTqVPRp4BW/hw=="],
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
@@ -462,8 +470,6 @@
|
|||||||
|
|
||||||
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
||||||
|
|
||||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
|
|
||||||
|
|
||||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
|
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
|
||||||
@@ -718,6 +724,8 @@
|
|||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||||
|
|
||||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
"cron-parser": ["cron-parser@5.10.0", "", { "dependencies": { "luxon": "^3.7.2" } }, "sha512-izNAxJyRWUP8ljBoDSub5WyrVOUlT4SLGShswE7eoRBpp6QUsSycYxLBMJlbshgPBMcPT/nrfgjNY2918ayv2A=="],
|
"cron-parser": ["cron-parser@5.10.0", "", { "dependencies": { "luxon": "^3.7.2" } }, "sha512-izNAxJyRWUP8ljBoDSub5WyrVOUlT4SLGShswE7eoRBpp6QUsSycYxLBMJlbshgPBMcPT/nrfgjNY2918ayv2A=="],
|
||||||
@@ -758,9 +766,11 @@
|
|||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="],
|
"docx": ["docx@9.7.1", "", { "dependencies": { "@types/node": "^25.2.3", "hash.js": "^1.1.7", "jszip": "^3.10.1", "nanoid": "^5.1.3", "xml": "^1.0.1", "xml-js": "^1.6.8" } }, "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.38.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": "Line truncated
|
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
|
||||||
|
|
||||||
|
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.jsLine truncated
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
@@ -774,8 +784,6 @@
|
|||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||||
|
|
||||||
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
|
||||||
|
|
||||||
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name":Line truncated
|
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name":Line truncated
|
||||||
|
|
||||||
"es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="],
|
"es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="],
|
||||||
@@ -794,9 +802,7 @@
|
|||||||
|
|
||||||
"es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="],
|
"es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/lLine truncated
|
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/lLine truncated
|
||||||
|
|
||||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
|
||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
@@ -806,7 +812,7 @@
|
|||||||
|
|
||||||
"eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "crosLine truncated
|
"eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "crosLine truncated
|
||||||
|
|
||||||
"eslint-config-next": ["eslint-config-next@16.3.1", "", { "dependencies": { "@next/eslint-plugin-next": "16.3.1", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typeLine truncated
|
"eslint-config-next": ["eslint-config-next@16.3.3", "", { "dependencies": { "@next/eslint-plugin-next": "16.3.3", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typeLine truncated
|
||||||
|
|
||||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
|
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
|
||||||
|
|
||||||
@@ -886,14 +892,14 @@
|
|||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="],
|
"function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="],
|
||||||
|
|
||||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||||
|
|
||||||
"gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="],
|
|
||||||
|
|
||||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||||
|
|
||||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
@@ -932,6 +938,8 @@
|
|||||||
|
|
||||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||||
|
|
||||||
"hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="],
|
"hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="],
|
||||||
@@ -962,6 +970,8 @@
|
|||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
|
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|
||||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
@@ -1046,9 +1056,9 @@
|
|||||||
|
|
||||||
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||||
|
|
||||||
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||||
|
|
||||||
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
||||||
|
|
||||||
@@ -1074,6 +1084,8 @@
|
|||||||
|
|
||||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||||
|
|
||||||
|
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
|
||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||||
@@ -1084,6 +1096,8 @@
|
|||||||
|
|
||||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
|
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcsLine truncated
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcsLine truncated
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
@@ -1120,6 +1134,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=="],
|
||||||
@@ -1226,6 +1242,8 @@
|
|||||||
|
|
||||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
|
|
||||||
|
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
@@ -1236,7 +1254,7 @@
|
|||||||
|
|
||||||
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "Line truncated
|
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "Line truncated
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
"nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||||
|
|
||||||
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
|
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
|
||||||
|
|
||||||
@@ -1244,7 +1262,7 @@
|
|||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"next": ["next@16.3.1", "", { "dependencies": { "@next/env": "16.3.1", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.1", "@next/swc-darwin-x64": "16.3.1", "@next/swc-linux-arm64-gnu": "16.3.1", "@next/swc-linux-arm64-musl": "16.3.1", "@next/swc-linux-x64-gnu": "16.3.1", "@next/swc-linux-x64-musl": "16.3.1", "@next/swc-win32-arm64-msvc": Line truncated
|
"next": ["next@16.3.3", "", { "dependencies": { "@next/env": "16.3.3", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.3", "@next/swc-darwin-x64": "16.3.3", "@next/swc-linux-arm64-gnu": "16.3.3", "@next/swc-linux-arm64-musl": "16.3.3", "@next/swc-linux-x64-gnu": "16.3.3", "@next/swc-linux-x64-musl": "16.3.3", "@next/swc-win32-arm64-msvc": Line truncated
|
||||||
|
|
||||||
"node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="],
|
"node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="],
|
||||||
|
|
||||||
@@ -1282,6 +1300,8 @@
|
|||||||
|
|
||||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||||
@@ -1314,6 +1334,8 @@
|
|||||||
|
|
||||||
"prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
|
"prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
|
||||||
|
|
||||||
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||||
|
|
||||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
|
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
|
||||||
@@ -1338,6 +1360,8 @@
|
|||||||
|
|
||||||
"react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqePLine truncated
|
"react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqePLine truncated
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||||
|
|
||||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||||
@@ -1372,12 +1396,16 @@
|
|||||||
|
|
||||||
"safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
|
"safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||||
|
|
||||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||||
|
|
||||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||||
@@ -1394,6 +1422,8 @@
|
|||||||
|
|
||||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||||
|
|
||||||
|
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
|
||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linLine truncated
|
"sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linLine truncated
|
||||||
@@ -1402,8 +1432,6 @@
|
|||||||
|
|
||||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
"shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="],
|
|
||||||
|
|
||||||
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||||
|
|
||||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||||
@@ -1442,6 +1470,8 @@
|
|||||||
|
|
||||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||||
@@ -1480,6 +1510,8 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.10.11", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.11", "@turbo/darwin-arm64": "2.10.11", "@turbo/linux-64": "2.10.11", "@turbo/linux-arm64": "2.10.11", "@turbo/windows-64": "2.10.11", "@turbo/windows-arm64": "2.10.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-yQfwQVoRXwOuyX1LxiJFBFNg6VfuYh+/RyZLd82+isgyLkBXw3S5XRRzvcck1FAjSCG5sVyLd+O1eDMvYa3J7g=="],
|
"turbo": ["turbo@2.10.11", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.11", "@turbo/darwin-arm64": "2.10.11", "@turbo/linux-64": "2.10.11", "@turbo/linux-arm64": "2.10.11", "@turbo/windows-64": "2.10.11", "@turbo/windows-arm64": "2.10.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-yQfwQVoRXwOuyX1LxiJFBFNg6VfuYh+/RyZLd82+isgyLkBXw3S5XRRzvcck1FAjSCG5sVyLd+O1eDMvYa3J7g=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
@@ -1532,7 +1564,7 @@
|
|||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||||
|
|
||||||
@@ -1546,6 +1578,10 @@
|
|||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="],
|
||||||
|
|
||||||
|
"xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
|
||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
@@ -1576,18 +1612,10 @@
|
|||||||
|
|
||||||
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@mcpedia/db/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
|
||||||
|
|
||||||
"@mcpedia/mcp/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
|
||||||
|
|
||||||
"@mcpedia/mcp/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"@mcpedia/mcp/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
"@mcpedia/mcp-client/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
|
||||||
|
|
||||||
"@mcpedia/mcp-client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"@mcpedia/mcp-client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
"@mcpedia/web/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="],
|
"@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
@@ -1614,7 +1642,7 @@
|
|||||||
|
|
||||||
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||||
|
|
||||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"docx/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="],
|
||||||
|
|
||||||
"eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
"eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|
||||||
@@ -1642,8 +1670,18 @@
|
|||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
|
"postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||||
|
|
||||||
|
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
|
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
|
"tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mipsLine truncated
|
||||||
|
|
||||||
"type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
"type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||||
|
|
||||||
|
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||||
@@ -1692,22 +1730,68 @@
|
|||||||
|
|
||||||
"@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"@mcpedia/db/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"@mcpedia/mcp-client/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"@mcpedia/mcp/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"@mcpedia/web/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
||||||
|
|
||||||
"@next/eslint-plugin-next/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@next/eslint-plugin-next/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
||||||
|
|
||||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"docx/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"next/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||||
|
|
||||||
|
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"@mcpedia/parser": "workspace:*",
|
"@mcpedia/parser": "workspace:*",
|
||||||
"@mcpedia/search": "workspace:*",
|
"@mcpedia/search": "workspace:*",
|
||||||
"@mcpedia/types": "workspace:*",
|
"@mcpedia/types": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0"
|
"docx": "^9.7.1",
|
||||||
|
"drizzle-orm": "^0.45.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
parseInlineMarkdown,
|
||||||
|
convertMarkdownToDocx,
|
||||||
|
generateExportDocx,
|
||||||
|
} from "./export.docx";
|
||||||
|
import type { ExportData } from "@mcpedia/types";
|
||||||
|
|
||||||
|
describe("DOCX Export Generator", () => {
|
||||||
|
it("parseInlineMarkdown: formats plain, bold, italic, inline code, and links", () => {
|
||||||
|
const text = "Hello **world** and *italic* with `const x = 1;` and [Link](https://example.com)";
|
||||||
|
const runs = parseInlineMarkdown(text);
|
||||||
|
expect(runs.length).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("convertMarkdownToDocx: handles headings, tables, callouts, and code blocks", () => {
|
||||||
|
const markdown = `
|
||||||
|
# Title
|
||||||
|
Some paragraph text with **bold** words.
|
||||||
|
|
||||||
|
## Subsection
|
||||||
|
> [!NOTE]
|
||||||
|
> This is a crucial note for testing.
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
def solve():
|
||||||
|
return "flag{test}"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
| Challenge | Points |
|
||||||
|
| --- | --- |
|
||||||
|
| Chall 1 | 100 |
|
||||||
|
| Chall 2 | 200 |
|
||||||
|
|
||||||
|
- Item 1
|
||||||
|
- Item 2
|
||||||
|
|
||||||
|
1. First
|
||||||
|
2. Second
|
||||||
|
`;
|
||||||
|
const elements = convertMarkdownToDocx(markdown);
|
||||||
|
expect(elements.length).toBeGreaterThan(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateExportDocx: builds a valid Word document buffer with OpenXML signature", async () => {
|
||||||
|
const mockExportData: ExportData = {
|
||||||
|
summary: {
|
||||||
|
title: "CTF 2026 Writeups",
|
||||||
|
scope: "ctf/2026",
|
||||||
|
section: "ctf",
|
||||||
|
totalDocuments: 2,
|
||||||
|
totalPoints: 600,
|
||||||
|
categories: [
|
||||||
|
{ name: "Web", count: 1, points: 200 },
|
||||||
|
{ name: "Pwn", count: 1, points: 400 },
|
||||||
|
],
|
||||||
|
authors: ["asepharyana"],
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
documents: [
|
||||||
|
{
|
||||||
|
id: "ctf/2026/web/baby-web",
|
||||||
|
slug: "ctf/2026/web/baby-web",
|
||||||
|
title: "Baby Web",
|
||||||
|
section: "ctf",
|
||||||
|
type: "writeup",
|
||||||
|
status: "published",
|
||||||
|
author: "asepharyana",
|
||||||
|
tags: ["web", "sqli"],
|
||||||
|
path: "ctf/2026/web/baby-web.md",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
extraFields: { category: "Web", points: 200, difficulty: "Easy" },
|
||||||
|
body: "# Baby Web\n\nSolution explanation here.\n\n```sql\nSELECT * FROM users;\n```",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ctf/2026/pwn/ret2win",
|
||||||
|
slug: "ctf/2026/pwn/ret2win",
|
||||||
|
title: "Ret2Win Challenge",
|
||||||
|
section: "ctf",
|
||||||
|
type: "writeup",
|
||||||
|
status: "published",
|
||||||
|
author: "asepharyana",
|
||||||
|
tags: ["pwn", "rop"],
|
||||||
|
path: "ctf/2026/pwn/ret2win.md",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
extraFields: { category: "Pwn", points: 400, difficulty: "Medium" },
|
||||||
|
body: "## Challenge Overview\n\nStack buffer overflow vulnerability.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const buffer = await generateExportDocx(mockExportData, { pageBreaks: true });
|
||||||
|
expect(buffer).toBeDefined();
|
||||||
|
expect(buffer.length).toBeGreaterThan(1000);
|
||||||
|
// Standard ZIP/DOCX magic bytes: 0x50, 0x4B, 0x03, 0x04 ('PK\x03\x04')
|
||||||
|
expect(buffer[0]).toBe(0x50);
|
||||||
|
expect(buffer[1]).toBe(0x4b);
|
||||||
|
expect(buffer[2]).toBe(0x03);
|
||||||
|
expect(buffer[3]).toBe(0x04);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,875 @@
|
|||||||
|
import {
|
||||||
|
Document as DocxDocument,
|
||||||
|
Paragraph,
|
||||||
|
TextRun,
|
||||||
|
Table,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
HeadingLevel,
|
||||||
|
AlignmentType,
|
||||||
|
BorderStyle,
|
||||||
|
WidthType,
|
||||||
|
ShadingType,
|
||||||
|
PageBreak,
|
||||||
|
Header,
|
||||||
|
Footer,
|
||||||
|
PageNumber,
|
||||||
|
Packer,
|
||||||
|
ExternalHyperlink,
|
||||||
|
} from "docx";
|
||||||
|
import type {
|
||||||
|
Document as CoreDocument,
|
||||||
|
ExportData,
|
||||||
|
ExportSummary,
|
||||||
|
} from "@mcpedia/types";
|
||||||
|
import {
|
||||||
|
extractDocCategory,
|
||||||
|
extractDocPoints,
|
||||||
|
extractDocDifficulty,
|
||||||
|
} from "./export.helpers";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Markdown to DOCX Inlines & Blocks Parser
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type InlineElement = TextRun | ExternalHyperlink;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse inline markdown formatting (bold, italic, code, links) into docx TextRuns / Hyperlinks.
|
||||||
|
*/
|
||||||
|
export function parseInlineMarkdown(text: string): InlineElement[] {
|
||||||
|
if (!text) return [];
|
||||||
|
|
||||||
|
const elements: InlineElement[] = [];
|
||||||
|
|
||||||
|
// Regex pattern matching:
|
||||||
|
// 1. Links: [text](url)
|
||||||
|
// 2. Inline code: `code`
|
||||||
|
// 3. Bold + Italic: ***text*** or ___text___
|
||||||
|
// 4. Bold: **text** or __text__
|
||||||
|
// 5. Italic: *text* or _text_
|
||||||
|
const inlineRegex =
|
||||||
|
/(\[([^\]]+)\]\(([^)]+)\)|`([^`]+)`|\*\*\*([^*]+)\*\*\*|___([^_]+)___|\*\*([^*]+)\*\*|__([^_]+)__|(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_))/g;
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = inlineRegex.exec(text)) !== null) {
|
||||||
|
// Add any preceding plain text
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
elements.push(
|
||||||
|
new TextRun({
|
||||||
|
text: text.slice(lastIndex, match.index),
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20, // 10pt
|
||||||
|
color: "1E293B",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match[2] && match[3]) {
|
||||||
|
// Hyperlink [text](url)
|
||||||
|
elements.push(
|
||||||
|
new ExternalHyperlink({
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: match[2],
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20,
|
||||||
|
color: "2563EB",
|
||||||
|
underline: {},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
link: match[3],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (match[4]) {
|
||||||
|
// Inline code `code`
|
||||||
|
elements.push(
|
||||||
|
new TextRun({
|
||||||
|
text: match[4],
|
||||||
|
font: "Consolas, Courier New",
|
||||||
|
size: 18, // 9pt
|
||||||
|
color: "0F172A",
|
||||||
|
shading: {
|
||||||
|
type: ShadingType.CLEAR,
|
||||||
|
fill: "F1F5F9",
|
||||||
|
color: "auto",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (match[5] || match[6]) {
|
||||||
|
// Bold + Italic ***text***
|
||||||
|
elements.push(
|
||||||
|
new TextRun({
|
||||||
|
text: match[5] || match[6],
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20,
|
||||||
|
bold: true,
|
||||||
|
italics: true,
|
||||||
|
color: "0F172A",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (match[7] || match[8]) {
|
||||||
|
// Bold **text**
|
||||||
|
elements.push(
|
||||||
|
new TextRun({
|
||||||
|
text: match[7] || match[8],
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20,
|
||||||
|
bold: true,
|
||||||
|
color: "0F172A",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (match[9] || match[10]) {
|
||||||
|
// Italic *text*
|
||||||
|
elements.push(
|
||||||
|
new TextRun({
|
||||||
|
text: match[9] || match[10],
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20,
|
||||||
|
italics: true,
|
||||||
|
color: "334155",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = inlineRegex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing plain text
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
elements.push(
|
||||||
|
new TextRun({
|
||||||
|
text: text.slice(lastIndex),
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20,
|
||||||
|
color: "1E293B",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements.length > 0
|
||||||
|
? elements
|
||||||
|
: [
|
||||||
|
new TextRun({
|
||||||
|
text,
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20,
|
||||||
|
color: "1E293B",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse markdown table lines into a Docx Table.
|
||||||
|
*/
|
||||||
|
function parseMarkdownTable(tableLines: string[]): Table | null {
|
||||||
|
if (tableLines.length < 2) return null;
|
||||||
|
|
||||||
|
const rows: string[][] = [];
|
||||||
|
for (const line of tableLines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed.startsWith("|") && !trimmed.endsWith("|")) continue;
|
||||||
|
// Skip separator line (|---|---|)
|
||||||
|
if (/^\|[\s\-:|]+\|$/.test(trimmed)) continue;
|
||||||
|
|
||||||
|
const cells = trimmed
|
||||||
|
.split("|")
|
||||||
|
.slice(1, -1)
|
||||||
|
.map((c) => c.trim());
|
||||||
|
rows.push(cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
|
||||||
|
const colCount = Math.max(...rows.map((r) => r.length));
|
||||||
|
|
||||||
|
return new Table({
|
||||||
|
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||||
|
margins: {
|
||||||
|
top: 100,
|
||||||
|
bottom: 100,
|
||||||
|
left: 140,
|
||||||
|
right: 140,
|
||||||
|
},
|
||||||
|
rows: rows.map((rowCells, rowIdx) => {
|
||||||
|
const isHeader = rowIdx === 0;
|
||||||
|
return new TableRow({
|
||||||
|
tableHeader: isHeader,
|
||||||
|
children: Array.from({ length: colCount }).map((_, colIdx) => {
|
||||||
|
const cellText = rowCells[colIdx] || "";
|
||||||
|
return new TableCell({
|
||||||
|
shading: isHeader
|
||||||
|
? { type: ShadingType.CLEAR, fill: "F1F5F9", color: "auto" }
|
||||||
|
: rowIdx % 2 === 1
|
||||||
|
? { type: ShadingType.CLEAR, fill: "FFFFFF", color: "auto" }
|
||||||
|
: { type: ShadingType.CLEAR, fill: "F8FAFC", color: "auto" },
|
||||||
|
borders: {
|
||||||
|
top: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" },
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" },
|
||||||
|
left: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" },
|
||||||
|
right: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 40, after: 40 },
|
||||||
|
children: isHeader
|
||||||
|
? [
|
||||||
|
new TextRun({
|
||||||
|
text: cellText,
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 18,
|
||||||
|
bold: true,
|
||||||
|
color: "0F172A",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: parseInlineMarkdown(cellText),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a markdown body string into an array of docx Paragraphs and Tables.
|
||||||
|
*/
|
||||||
|
export function convertMarkdownToDocx(markdown: string): (Paragraph | Table)[] {
|
||||||
|
const elements: (Paragraph | Table)[] = [];
|
||||||
|
const lines = markdown.split(/\r?\n/);
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
// 1. Blank lines
|
||||||
|
if (trimmed === "") {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fenced Code Blocks (```lang ... ```)
|
||||||
|
if (trimmed.startsWith("```")) {
|
||||||
|
const lang = trimmed.slice(3).trim();
|
||||||
|
const codeLines: string[] = [];
|
||||||
|
i++;
|
||||||
|
while (i < lines.length && !lines[i].trim().startsWith("```")) {
|
||||||
|
codeLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
i++; // skip closing ```
|
||||||
|
|
||||||
|
const codeParagraphs = codeLines.map(
|
||||||
|
(codeLine) =>
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 0, after: 0, line: 240 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: codeLine.replace(/\t/g, " ") || " ",
|
||||||
|
font: "Consolas, Courier New",
|
||||||
|
size: 17, // 8.5pt
|
||||||
|
color: "0F172A",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrap in a shaded callout table for code box look
|
||||||
|
const codeBox = new Table({
|
||||||
|
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||||
|
margins: { top: 120, bottom: 120, left: 160, right: 160 },
|
||||||
|
rows: [
|
||||||
|
new TableRow({
|
||||||
|
children: [
|
||||||
|
new TableCell({
|
||||||
|
shading: {
|
||||||
|
type: ShadingType.CLEAR,
|
||||||
|
fill: "F8FAFC",
|
||||||
|
color: "auto",
|
||||||
|
},
|
||||||
|
borders: {
|
||||||
|
top: { style: BorderStyle.SINGLE, size: 6, color: "CBD5E1" },
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 6, color: "CBD5E1" },
|
||||||
|
left: { style: BorderStyle.SINGLE, size: 18, color: "64748B" }, // darker left accent
|
||||||
|
right: { style: BorderStyle.SINGLE, size: 6, color: "CBD5E1" },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
...(lang
|
||||||
|
? [
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 0, after: 60 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: lang.toUpperCase(),
|
||||||
|
font: "Consolas, Courier New",
|
||||||
|
size: 15,
|
||||||
|
bold: true,
|
||||||
|
color: "64748B",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...codeParagraphs,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.push(codeBox);
|
||||||
|
elements.push(new Paragraph({ spacing: { before: 80, after: 0 } }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Markdown Tables (| col1 | col2 |)
|
||||||
|
if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
|
||||||
|
const tableLines: string[] = [];
|
||||||
|
while (i < lines.length && lines[i].trim().startsWith("|")) {
|
||||||
|
tableLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
const table = parseMarkdownTable(tableLines);
|
||||||
|
if (table) {
|
||||||
|
elements.push(table);
|
||||||
|
elements.push(new Paragraph({ spacing: { before: 80, after: 0 } }));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Blockquotes & GitHub Alert Callouts (> [!NOTE], > [!TIP], etc.)
|
||||||
|
if (trimmed.startsWith(">")) {
|
||||||
|
const quoteLines: string[] = [];
|
||||||
|
while (i < lines.length && lines[i].trim().startsWith(">")) {
|
||||||
|
quoteLines.push(lines[i].trim().replace(/^>\s?/, ""));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullQuote = quoteLines.join("\n");
|
||||||
|
const alertMatch = fullQuote.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i);
|
||||||
|
|
||||||
|
let borderColor = "3B82F6"; // Blue
|
||||||
|
let fillColor = "EFF6FF";
|
||||||
|
let title = "NOTE";
|
||||||
|
|
||||||
|
if (alertMatch) {
|
||||||
|
const alertType = alertMatch[1].toUpperCase();
|
||||||
|
if (alertType === "TIP") {
|
||||||
|
borderColor = "10B981";
|
||||||
|
fillColor = "ECFDF5";
|
||||||
|
title = "TIP";
|
||||||
|
} else if (alertType === "IMPORTANT") {
|
||||||
|
borderColor = "8B5CF6";
|
||||||
|
fillColor = "F5F3FF";
|
||||||
|
title = "IMPORTANT";
|
||||||
|
} else if (alertType === "WARNING") {
|
||||||
|
borderColor = "F59E0B";
|
||||||
|
fillColor = "FFFBEB";
|
||||||
|
title = "WARNING";
|
||||||
|
} else if (alertType === "CAUTION") {
|
||||||
|
borderColor = "EF4444";
|
||||||
|
fillColor = "FEF2F2";
|
||||||
|
title = "CAUTION";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLines = alertMatch
|
||||||
|
? fullQuote.replace(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n?/i, "").split("\n")
|
||||||
|
: quoteLines;
|
||||||
|
|
||||||
|
const calloutTable = new Table({
|
||||||
|
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||||
|
margins: { top: 100, bottom: 100, left: 160, right: 160 },
|
||||||
|
rows: [
|
||||||
|
new TableRow({
|
||||||
|
children: [
|
||||||
|
new TableCell({
|
||||||
|
shading: {
|
||||||
|
type: ShadingType.CLEAR,
|
||||||
|
fill: fillColor,
|
||||||
|
color: "auto",
|
||||||
|
},
|
||||||
|
borders: {
|
||||||
|
top: { style: BorderStyle.NONE, size: 0, color: "auto" },
|
||||||
|
bottom: { style: BorderStyle.NONE, size: 0, color: "auto" },
|
||||||
|
left: { style: BorderStyle.SINGLE, size: 24, color: borderColor },
|
||||||
|
right: { style: BorderStyle.NONE, size: 0, color: "auto" },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
...(alertMatch
|
||||||
|
? [
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 0, after: 40 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: `[ ${title} ]`,
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 18,
|
||||||
|
bold: true,
|
||||||
|
color: borderColor,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...contentLines.map(
|
||||||
|
(qLine) =>
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 20, after: 40 },
|
||||||
|
children: parseInlineMarkdown(qLine),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.push(calloutTable);
|
||||||
|
elements.push(new Paragraph({ spacing: { before: 80, after: 0 } }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Headings
|
||||||
|
if (trimmed.startsWith("#### ")) {
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
heading: HeadingLevel.HEADING_4,
|
||||||
|
spacing: { before: 180, after: 60 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: trimmed.slice(5),
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 22, // 11pt
|
||||||
|
bold: true,
|
||||||
|
color: "1E293B",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith("### ")) {
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
heading: HeadingLevel.HEADING_3,
|
||||||
|
spacing: { before: 220, after: 80 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: trimmed.slice(4),
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 24, // 12pt
|
||||||
|
bold: true,
|
||||||
|
color: "0F172A",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith("## ")) {
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
heading: HeadingLevel.HEADING_2,
|
||||||
|
spacing: { before: 280, after: 100 },
|
||||||
|
border: {
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 6, color: "E2E8F0" },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: trimmed.slice(3),
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 28, // 14pt
|
||||||
|
bold: true,
|
||||||
|
color: "020617",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith("# ")) {
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
heading: HeadingLevel.HEADING_1,
|
||||||
|
spacing: { before: 320, after: 120 },
|
||||||
|
border: {
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 8, color: "CBD5E1" },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: trimmed.slice(2),
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 34, // 17pt
|
||||||
|
bold: true,
|
||||||
|
color: "020617",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Horizontal Rule (--- or ***)
|
||||||
|
if (/^(\-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 120, after: 120 },
|
||||||
|
border: {
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 6, color: "CBD5E1" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Bullet Lists (- item or * item)
|
||||||
|
if (/^[-*+]\s+/.test(trimmed)) {
|
||||||
|
const itemText = trimmed.replace(/^[-*+]\s+/, "");
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
bullet: { level: 0 },
|
||||||
|
spacing: { before: 20, after: 40 },
|
||||||
|
children: parseInlineMarkdown(itemText),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Numbered Lists (1. item)
|
||||||
|
if (/^\d+\.\s+/.test(trimmed)) {
|
||||||
|
const numMatch = trimmed.match(/^(\d+)\.\s+(.*)$/);
|
||||||
|
const num = numMatch ? numMatch[1] : "1";
|
||||||
|
const itemText = numMatch ? numMatch[2] : trimmed;
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 20, after: 40 },
|
||||||
|
indent: { left: 360 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: `${num}. `,
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 20,
|
||||||
|
bold: true,
|
||||||
|
color: "475569",
|
||||||
|
}),
|
||||||
|
...parseInlineMarkdown(itemText),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Standard Paragraph
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 40, after: 100, line: 276 }, // 1.15 line spacing
|
||||||
|
children: parseInlineMarkdown(line),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DOCX Document Builder
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface DocxExportOptions {
|
||||||
|
pageBreaks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a complete, publication-grade Microsoft Word (.docx) document
|
||||||
|
* from MCPedia ExportData.
|
||||||
|
*/
|
||||||
|
export async function generateExportDocx(
|
||||||
|
exportData: ExportData,
|
||||||
|
options: DocxExportOptions = {},
|
||||||
|
): Promise<Buffer> {
|
||||||
|
const { summary, documents: docs } = exportData;
|
||||||
|
const pageBreaks = options.pageBreaks ?? true;
|
||||||
|
|
||||||
|
const docChildren: (Paragraph | Table)[] = [];
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. Document Cover & Header
|
||||||
|
// ==========================================
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
heading: HeadingLevel.TITLE,
|
||||||
|
spacing: { before: 0, after: 120 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: summary.title,
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 44, // 22pt
|
||||||
|
bold: true,
|
||||||
|
color: "020617",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Metadata subtitle
|
||||||
|
const metaParts: string[] = [
|
||||||
|
`${summary.totalDocuments} Challenge${summary.totalDocuments !== 1 ? "s" : ""}`,
|
||||||
|
];
|
||||||
|
if (summary.totalPoints > 0) {
|
||||||
|
metaParts.push(`${summary.totalPoints.toLocaleString()} Total Points`);
|
||||||
|
}
|
||||||
|
metaParts.push(
|
||||||
|
new Date(summary.generatedAt).toLocaleDateString(undefined, {
|
||||||
|
dateStyle: "long",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (summary.authors && summary.authors.length > 0) {
|
||||||
|
metaParts.push(`Authors: ${summary.authors.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 0, after: 240 },
|
||||||
|
border: {
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 8, color: "CBD5E1" },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: metaParts.join(" · "),
|
||||||
|
font: "Consolas, Courier New",
|
||||||
|
size: 18, // 9pt
|
||||||
|
color: "64748B",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. Table of Contents / Matrix (if > 1 doc)
|
||||||
|
// ==========================================
|
||||||
|
if (docs.length > 1) {
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
heading: HeadingLevel.HEADING_2,
|
||||||
|
spacing: { before: 200, after: 120 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: "Table of Contents & Overview",
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 26, // 13pt
|
||||||
|
bold: true,
|
||||||
|
color: "0F172A",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tocRows: string[][] = [
|
||||||
|
["#", "Challenge Title", "Category", "Points", "Difficulty"],
|
||||||
|
];
|
||||||
|
|
||||||
|
docs.forEach((doc, idx) => {
|
||||||
|
const chNum = String(idx + 1);
|
||||||
|
const cat = extractDocCategory(doc);
|
||||||
|
const pts = extractDocPoints(doc);
|
||||||
|
const diff = extractDocDifficulty(doc) || "-";
|
||||||
|
tocRows.push([
|
||||||
|
chNum,
|
||||||
|
doc.title,
|
||||||
|
cat,
|
||||||
|
pts > 0 ? `${pts} pts` : "-",
|
||||||
|
diff,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tocTable = parseMarkdownTable(
|
||||||
|
tocRows.map((r) => `| ${r.join(" | ")} |`),
|
||||||
|
);
|
||||||
|
if (tocTable) {
|
||||||
|
docChildren.push(tocTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 200, after: 200 },
|
||||||
|
border: {
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 6, color: "E2E8F0" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. Challenge Writeup Chapters
|
||||||
|
// ==========================================
|
||||||
|
docs.forEach((doc, idx) => {
|
||||||
|
const chNum = idx + 1;
|
||||||
|
const cat = extractDocCategory(doc);
|
||||||
|
const pts = extractDocPoints(doc);
|
||||||
|
const diff = extractDocDifficulty(doc);
|
||||||
|
|
||||||
|
// Page break between challenges
|
||||||
|
if (pageBreaks && idx > 0) {
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [new PageBreak()],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapter Title
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
heading: HeadingLevel.HEADING_1,
|
||||||
|
spacing: { before: pageBreaks ? 0 : 280, after: 80 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: `${chNum}. ${doc.title}`,
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 32, // 16pt
|
||||||
|
bold: true,
|
||||||
|
color: "020617",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Metadata Badge Ribbon
|
||||||
|
const badgeTextParts: string[] = [`CATEGORY: ${cat.toUpperCase()}`];
|
||||||
|
if (pts > 0) badgeTextParts.push(`POINTS: ${pts} PTS`);
|
||||||
|
if (diff) badgeTextParts.push(`DIFFICULTY: ${diff.toUpperCase()}`);
|
||||||
|
if (doc.author) badgeTextParts.push(`AUTHOR: ${doc.author}`);
|
||||||
|
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 0, after: 180 },
|
||||||
|
border: {
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 6, color: "CBD5E1" },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: badgeTextParts.join(" | "),
|
||||||
|
font: "Consolas, Courier New",
|
||||||
|
size: 16, // 8pt
|
||||||
|
bold: true,
|
||||||
|
color: "475569",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Markdown Writeup Content
|
||||||
|
const contentElements = convertMarkdownToDocx(doc.body);
|
||||||
|
docChildren.push(...contentElements);
|
||||||
|
|
||||||
|
// Subtle divider after challenge (when not page breaking)
|
||||||
|
if (!pageBreaks && idx < docs.length - 1) {
|
||||||
|
docChildren.push(
|
||||||
|
new Paragraph({
|
||||||
|
spacing: { before: 180, after: 180 },
|
||||||
|
border: {
|
||||||
|
bottom: { style: BorderStyle.DASHED, size: 6, color: "CBD5E1" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. Construct Final DOCX Package
|
||||||
|
// ==========================================
|
||||||
|
const docx = new DocxDocument({
|
||||||
|
creator: "MCPedia Knowledge Base",
|
||||||
|
title: summary.title,
|
||||||
|
description: `Export of ${summary.title} generated by MCPedia`,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
margin: {
|
||||||
|
top: 1440, // 1 inch (1440 dxa)
|
||||||
|
bottom: 1440,
|
||||||
|
left: 1440,
|
||||||
|
right: 1440,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
default: new Header({
|
||||||
|
children: [
|
||||||
|
new Paragraph({
|
||||||
|
alignment: AlignmentType.RIGHT,
|
||||||
|
spacing: { after: 120 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: `MCPedia · ${summary.title}`,
|
||||||
|
font: "Consolas, Courier New",
|
||||||
|
size: 16,
|
||||||
|
color: "94A3B8",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
footers: {
|
||||||
|
default: new Footer({
|
||||||
|
children: [
|
||||||
|
new Paragraph({
|
||||||
|
alignment: AlignmentType.RIGHT,
|
||||||
|
spacing: { before: 120 },
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: "Page ",
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 16,
|
||||||
|
color: "94A3B8",
|
||||||
|
}),
|
||||||
|
new TextRun({
|
||||||
|
children: [PageNumber.CURRENT],
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 16,
|
||||||
|
color: "94A3B8",
|
||||||
|
}),
|
||||||
|
new TextRun({
|
||||||
|
text: " of ",
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 16,
|
||||||
|
color: "94A3B8",
|
||||||
|
}),
|
||||||
|
new TextRun({
|
||||||
|
children: [PageNumber.TOTAL_PAGES],
|
||||||
|
font: "Inter, Calibri, Arial",
|
||||||
|
size: 16,
|
||||||
|
color: "94A3B8",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
children: docChildren,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Packer.toBuffer(docx);
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from "./export.helpers";
|
} from "./export.helpers";
|
||||||
|
|
||||||
export * from "./export.helpers";
|
export * from "./export.helpers";
|
||||||
|
export * from "./export.docx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all documents for export with full bodies directly from PostgreSQL
|
* Fetch all documents for export with full bodies directly from PostgreSQL
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mcpedia/config": "workspace:*",
|
"@mcpedia/config": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.45.2",
|
||||||
"postgres": "^3.4.5"
|
"postgres": "^3.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"drizzle-kit": "^0.30.0",
|
"@types/node": "^26",
|
||||||
"@types/node": "^20"
|
"drizzle-kit": "^0.31.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"@mcpedia/db": "workspace:*",
|
"@mcpedia/db": "workspace:*",
|
||||||
"@mcpedia/embeddings": "workspace:*",
|
"@mcpedia/embeddings": "workspace:*",
|
||||||
"@mcpedia/types": "workspace:*",
|
"@mcpedia/types": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0"
|
"drizzle-orm": "^0.45.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"@mcpedia/parser": "workspace:*",
|
"@mcpedia/parser": "workspace:*",
|
||||||
"@mcpedia/search": "workspace:*",
|
"@mcpedia/search": "workspace:*",
|
||||||
"@mcpedia/queue": "workspace:*",
|
"@mcpedia/queue": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.45.2",
|
||||||
"postgres": "^3.4.5"
|
"postgres": "^3.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user