Author SHA1 Message Date
mytheclipsebotreview 5a00fd1909 fix: auto-fix code quality [skip ci] 2026-08-29 10:33:38 +07:00
mytheclipsebotreview[bot] 48566081f0 Auto-merge PR #10
chore(deps): bump next from 16.3.2 to 16.3.3 in the production group
2026-08-29 03:31:14 +00:00
mytheclipsebotreview 10a15a133c fix: auto-fix code quality [skip ci] 2026-08-29 10:30:56 +07:00
mytheclipsebotreview 5b1345cf9e chore: sync bun.lock after dependabot bump [skip ci] 2026-08-29 10:27:30 +07:00
mytheclipsebotreview 4ca5f0af39 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/production-2c8a7f9a5b 2026-08-29 10:27:29 +07:00
mytheclipsebotreview 283d66b5e0 chore: sync bun.lock after @types/node@26 bump [skip ci] 2026-08-29 11:21:52 +08:00
mytheclipsebotreview[bot] b311c363c5 Auto-merge PR #12
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
chore(deps-dev): bump @types/node from 20.19.43 to 26.3.0
2026-08-29 02:57:10 +00:00
mytheclipsebotreview 006bf79655 fix: auto-fix code quality [skip ci] 2026-08-29 09:56:57 +07:00
dependabot[bot] 1a45ba9fdb chore(deps-dev): bump @types/node from 20.19.43 to 26.3.0
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.43 to 26.3.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.3.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 17:55:21 +00:00
dependabot[bot] b2fa2a98e0 chore(deps-dev): bump eslint-config-next in the development group
Bumps the development group with 1 update: [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).


Updates `eslint-config-next` from 16.3.2 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 17:55:09 +00:00
dependabot[bot] 4d424e67ad chore(deps): bump next from 16.3.2 to 16.3.3 in the production group
Bumps the production group with 1 update: [next](https://github.com/vercel/next.js).


Updates `next` from 16.3.2 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.2...v16.3.3)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 17:54:52 +00:00
asepharyana 263a92f368 Merge pull request #8 from asepharyana/dependabot/npm_and_yarn/production-7059fa7707
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
chore(deps): bump the production group across 1 directory with 2 updates
2026-08-26 09:12:50 +07:00
Asep Hariyana f44e66bcd5 chore: sync lockfile for dependabot update 2026-08-26 10:12:44 +08:00
asepharyana 66e6fad876 Merge pull request #9 from asepharyana/dependabot/npm_and_yarn/development-9df26a4104
chore(deps-dev): bump the development group across 1 directory with 2 updates
2026-08-26 09:12:39 +07:00
Asep Hariyana befb898c45 chore: sync lockfile for dependabot update 2026-08-26 10:12:33 +08:00
dependabot[bot] 0638c4a82d chore(deps): bump the production group across 1 directory with 2 updates
Bumps the production group with 2 updates in the / directory: [next](https://github.com/vercel/next.js) and [drizzle-orm](https://github.com/drizzle-team/drizzle-orm).


Updates `next` from 16.3.1 to 16.3.2
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.1...v16.3.2)

Updates `drizzle-orm` from 0.38.4 to 0.45.2
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.38.4...0.45.2)

---
updated-dependencies:
- dependency-name: drizzle-orm
  dependency-version: 0.45.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: next
  dependency-version: 16.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 01:55:57 +00:00
dependabot[bot] e412767847 chore(deps-dev): bump the development group across 1 directory with 2 updates
Bumps the development group with 2 updates in the / directory: [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) and [drizzle-kit](https://github.com/drizzle-team/drizzle-orm).


Updates `eslint-config-next` from 16.3.1 to 16.3.2
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.2/packages/eslint-config-next)

Updates `drizzle-kit` from 0.30.6 to 0.31.10
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/drizzle-kit@0.30.6...drizzle-kit@0.31.10)

---
updated-dependencies:
- dependency-name: drizzle-kit
  dependency-version: 0.31.10
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: eslint-config-next
  dependency-version: 16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 01:55:46 +00:00
asepharyana ea4f34271c chore: update dependabot-auto-merge.yml
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-26 08:53:44 +07:00
asepharyana caabe05c68 chore: update dependabot.yml 2026-08-26 08:53:43 +07:00
asepharyana 9d8d876ba7 feat: add dependabot auto-merge workflow
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-25 20:32:58 +07:00
asepharyana 8f98bed7dd feat: update dependabot config
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-25 18:16:53 +07:00
asepharyana bd3a0894b4 feat: update dependabot config 2026-08-25 18:13:06 +07:00
asepharyana 03616b4e60 feat: add dependabot config (npm auto-deps)
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-25 18:08:45 +07:00
asepharyana 5ccba958d2 feat: add Word (.docx) export, enhance print density, and fix auth security guards
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-22 17:59:32 +07:00
asepharyana 067115ba13 style(web): refine aesthetic to archival precision, remove generic gradients and glowing blobs
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-22 15:12:00 +07:00
asepharyana 011571e12d feat(web): overhaul frontend with cyber-intelligence design and interactive topology
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-22 14:55:19 +07:00
asepharyana 9591c67478 feat(export): remove author, scope, and slug metadata for cleaner minimal writeup look
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-22 14:06:36 +07:00
asepharyana d65c5613d3 fix(print): prevent code block header/body page break split and ensure atomic code block flow 2026-08-22 14:03:35 +07:00
asepharyana 4f0d2da876 feat(export): simplify writeup export to pure human-like markdown layout without cards and exclude _index documents
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-22 13:49:00 +07:00
asepharyana 72b53afe06 feat(export): remove cover header banner and confidential footer line from PDF view 2026-08-22 13:45:57 +07:00
asepharyana 5d85226ff0 feat(export): redesign PDF view for formal publication styling without colors or watermarks
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-22 12:22:01 +07:00
asepharyana 5195a65ca5 ci: use sudo when syncing .next on VPS
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
2026-08-22 12:08:20 +07:00
asepharyana 666ba855a2 fix(web): fix working directory in mcpedia-web wrapper script 2026-08-22 12:04:22 +07:00
asepharyana 8adfffb6e7 fix(deploy): correctly copy prebuilt .next to apps/web/.next on VPS during deployment 2026-08-22 12:00:30 +07:00
41 changed files with 3314 additions and 1491 deletions

No files matched your search

+17
View File
@@ -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 }}
+18
View File
@@ -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"
+5 -5
View File
@@ -304,15 +304,15 @@ jobs:
VPS_HOST: ${{ secrets.SSH_DEPLOY_HOST }} VPS_HOST: ${{ secrets.SSH_DEPLOY_HOST }}
VPS_USER: ${{ secrets.SSH_DEPLOY_USER }} VPS_USER: ${{ secrets.SSH_DEPLOY_USER }}
run: | run: |
# Substitute from Attic cache (fast) # Update profile + sync git repo + sync .next + restart
ssh "$VPS_USER@$VPS_HOST" \ ssh "$VPS_USER@$VPS_HOST" \
"sudo /nix/var/nix/profiles/default/bin/nix-store --realise '$STORE_PATH'" "sudo /nix/var/nix/profiles/default/bin/nix-store --realise '$STORE_PATH' && \
# Update profile + sync git repo + restart sudo /nix/var/nix/profiles/default/bin/nix-env \
ssh "$VPS_USER@$VPS_HOST" \
"sudo /nix/var/nix/profiles/default/bin/nix-env \
--profile /nix/var/nix/profiles/mcpedia-web \ --profile /nix/var/nix/profiles/mcpedia-web \
--set '$STORE_PATH' && \ --set '$STORE_PATH' && \
(cd /home/code/mcpedia && git fetch origin main && git reset --hard origin/main) && \ (cd /home/code/mcpedia && git fetch origin main && git reset --hard origin/main) && \
sudo rm -rf /home/code/mcpedia/apps/web/.next && \
sudo cp -r '$STORE_PATH/share/mcpedia-web/.next' /home/code/mcpedia/apps/web/.next && \
sudo systemctl restart mcpedia-web && \ sudo systemctl restart mcpedia-web && \
sleep 3 && \ sleep 3 && \
sudo systemctl status mcpedia-web --no-pager --no-legend | head -5" sudo systemctl status mcpedia-web --no-pager --no-legend | head -5"
+2 -2
View File
@@ -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"
} }
} }
+1 -1
View File
@@ -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"
} }
} }
+141 -112
View File
@@ -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>
+63 -40
View File
@@ -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 -8
View File
@@ -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 -2
View File
@@ -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.");
+6 -2
View File
@@ -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.");
+25 -6
View File
@@ -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) {
+140 -47
View File
@@ -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 &quot;{query}&quot; <p>No knowledge base items matched &quot;{query}&quot;</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>
+33 -27
View File
@@ -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
View File
@@ -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>
+72 -54
View File
@@ -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>
);
}
+71 -26
View File
@@ -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="relative group my-5 rounded-xl overflow-hidden border border-[var(--border-code)] bg-[var(--code-block-bg)] shadow-xs"> <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="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"> <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="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)] 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>
@@ -60,27 +69,23 @@ function PreBlock({ children, ...props }: React.HTMLAttributes<HTMLPreElement>)
onClick={handleCopy} onClick={handleCopy}
type="button" type="button"
aria-label="Copy code to clipboard" aria-label="Copy code to clipboard"
className="flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--code-button-bg)] hover:bg-[var(--code-button-hover-bg)] px-2.5 py-1 rounded-md border border-[var(--border-code)] transition-all font-sans shadow-xs cursor-pointer" className="no-print flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--code-button-bg)] hover:bg-[var(--code-button-hover-bg)] px-2.5 py-1 rounded-md border border-[var(--border-code)] transition-all font-sans shadow-xs cursor-pointer"
> >
{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 overflow-x-auto text-[13.5px] text-[var(--pre-text)] 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>
); );
+101 -37
View File
@@ -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>
); );
} }
+180 -494
View File
@@ -7,15 +7,22 @@ import {
extractDocCategory, extractDocCategory,
extractDocPoints, extractDocPoints,
extractDocDifficulty, extractDocDifficulty,
extractDocSolved,
sortExportDocuments, sortExportDocuments,
compileExportMarkdown, compileExportMarkdown,
type Document, type Document,
type ExportData, type ExportData,
type ExportSortOption, type ExportSortOption,
type ExportCategorySummary,
} 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;
@@ -28,11 +35,23 @@ export default function PdfExportView({
}: PdfExportViewProps) { }: PdfExportViewProps) {
const { summary, documents: initialDocs } = exportData; const { summary, documents: initialDocs } = exportData;
// Filter out any leftover index / meta files
const cleanDocs = useMemo(() => {
return initialDocs.filter((d) => {
const slug = d.slug.toLowerCase();
return (
!slug.endsWith("/_index") &&
!slug.endsWith("/index") &&
slug !== "_index" &&
slug !== "index" &&
d.type !== "writeup-index"
);
});
}, [initialDocs]);
// Configuration and interactive state // Configuration and interactive state
const [sortBy, setSortBy] = useState<ExportSortOption>("category_points"); const [sortBy, setSortBy] = useState<ExportSortOption>("category_points");
const [showCover, setShowCover] = useState(true); const [showTOC, setShowTOC] = useState(cleanDocs.length > 1);
const [showScoreboard, setShowScoreboard] = useState(initialDocs.length > 1);
const [showTOC, setShowTOC] = useState(initialDocs.length > 1);
const [pageBreaks, setPageBreaks] = useState(true); const [pageBreaks, setPageBreaks] = useState(true);
const [selectedCategory, setSelectedCategory] = useState<string>("all"); const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [copiedMd, setCopiedMd] = useState(false); const [copiedMd, setCopiedMd] = useState(false);
@@ -42,22 +61,22 @@ export default function PdfExportView({
// Available categories for filtering // Available categories for filtering
const allCategories = useMemo(() => { const allCategories = useMemo(() => {
const set = new Set<string>(); const set = new Set<string>();
for (const doc of initialDocs) { for (const doc of cleanDocs) {
set.add(extractDocCategory(doc)); set.add(extractDocCategory(doc));
} }
return Array.from(set).sort(); return Array.from(set).sort();
}, [initialDocs]); }, [cleanDocs]);
// Filtered & Sorted documents // Filtered & Sorted documents
const filteredAndSortedDocs = useMemo(() => { const filteredAndSortedDocs = useMemo(() => {
let docs = [...initialDocs]; let docs = [...cleanDocs];
if (selectedCategory !== "all") { if (selectedCategory !== "all") {
docs = docs.filter( docs = docs.filter(
(d) => extractDocCategory(d).toLowerCase() === selectedCategory.toLowerCase(), (d) => extractDocCategory(d).toLowerCase() === selectedCategory.toLowerCase(),
); );
} }
return sortExportDocuments(docs, sortBy); return sortExportDocuments(docs, sortBy);
}, [initialDocs, selectedCategory, sortBy]); }, [cleanDocs, selectedCategory, sortBy]);
// Calculate dynamic stats for currently filtered view // Calculate dynamic stats for currently filtered view
const currentTotalPoints = useMemo(() => { const currentTotalPoints = useMemo(() => {
@@ -101,76 +120,55 @@ export default function PdfExportView({
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
function getCategoryBadgeClass(category: string): string { const [downloadingDocx, setDownloadingDocx] = useState(false);
const cat = category.toLowerCase();
if (cat.includes("web")) {
return "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/30";
}
if (cat.includes("crypto")) {
return "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/30";
}
if (cat.includes("pwn") || cat.includes("binary")) {
return "bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/30";
}
if (cat.includes("reverse") || cat.includes("rev")) {
return "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/30";
}
if (cat.includes("forensic") || cat.includes("dfir")) {
return "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30";
}
if (cat.includes("osint")) {
return "bg-teal-500/10 text-teal-600 dark:text-teal-400 border-teal-500/30";
}
return "bg-[var(--brand)]/10 text-[var(--brand)] dark:text-[var(--accent)] border-[var(--brand)]/30";
}
function getCategoryIcon(category: string): string { async function handleDownloadDocx() {
const cat = category.toLowerCase(); setDownloadingDocx(true);
if (cat.includes("web")) return "🌐"; try {
if (cat.includes("crypto")) return "🔐"; const slugs = filteredAndSortedDocs.map((d) => d.slug).join(",");
if (cat.includes("pwn") || cat.includes("binary")) return "⚡"; const safeName =
if (cat.includes("reverse") || cat.includes("rev")) return "⚙️"; summary.title
if (cat.includes("forensic") || cat.includes("dfir")) return "🔍"; .toLowerCase()
if (cat.includes("osint")) return "🛰️"; .replace(/[^a-z0-9]+/g, "-")
if (cat.includes("mobile")) return "📱"; .replace(/^-|-$/g, "") || "export";
if (cat.includes("hardware")) return "🔌"; const url = `/api/export?format=docx&slugs=${encodeURIComponent(slugs)}&sort=${sortBy}&pageBreaks=${pageBreaks}`;
return "🚩"; const res = await fetch(url);
} if (!res.ok) throw new Error("Failed to generate Word document");
const blob = await res.blob();
function getDifficultyBadgeClass(difficulty: string): string { const blobUrl = URL.createObjectURL(blob);
const diff = difficulty.toLowerCase(); const a = document.createElement("a");
if (/easy|simple|beginner/i.test(diff)) { a.href = blobUrl;
return "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/30"; 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);
} }
if (/medium|intermediate/i.test(diff)) {
return "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30";
}
if (/hard|expert|advanced|insane/i.test(diff)) {
return "bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/30";
}
return "bg-[var(--bg-elevated)] text-[var(--text-muted)] border-[var(--border-color)]";
} }
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 (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-2xl p-4 sm:p-5 shadow-md">
<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-lg 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(--brand)] dark:text-[var(--accent)] font-semibold uppercase"> <span className="text-xs font-mono text-[var(--text-primary)] font-bold uppercase">
{sectionMeta.icon} {sectionMeta.label} {sectionMeta.label}
</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">
@@ -190,17 +188,18 @@ 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)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg 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-500 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>📋</span> <Copy className="w-3.5 h-3.5" />
<span>Copy Markdown</span> <span>Copy MD</span>
</> </>
)} )}
</button> </button>
@@ -209,50 +208,53 @@ 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)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg 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"
> >
<span>💾</span> <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-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg shadow-sm transition-all cursor-pointer hover:shadow-md active:scale-98" 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)}
aria-label="Sort documents" 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"
className="bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-primary)] rounded-md px-2.5 py-1 text-xs focus:outline-none focus:border-[var(--brand)] cursor-pointer"
> >
<option value="category_points">Category & Points (CTF 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>
<option value="difficulty">Difficulty (Easy to Hard)</option> <option value="difficulty">Difficulty (Easy to Hard)</option>
<option value="title">Challenge Title (A-Z)</option> <option value="title">Challenge Title (A-Z)</option>
@@ -261,84 +263,63 @@ export default function PdfExportView({
</select> </select>
</div> </div>
{/* Toggle Switches */} {/* Section toggles */}
<div className="flex items-center gap-4 flex-wrap text-[var(--text-muted)]"> <div className="flex items-center gap-3 flex-wrap text-[var(--text-secondary)]">
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]"> {cleanDocs.length > 1 && (
<input <label className="inline-flex items-center gap-1.5 cursor-pointer select-none">
type="checkbox" <input
checked={showCover} type="checkbox"
onChange={(e) => setShowCover(e.target.checked)} checked={showTOC}
className="rounded border-[var(--border-color)] text-[var(--brand)] focus:ring-[var(--brand)]" onChange={(e) => setShowTOC(e.target.checked)}
/> className="rounded border-[var(--border-color)]"
<span>Cover Page</span> />
</label> <span>Table of Contents</span>
</label>
{initialDocs.length > 1 && (
<>
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]">
<input
type="checkbox"
checked={showScoreboard}
onChange={(e) => setShowScoreboard(e.target.checked)}
className="rounded border-[var(--border-color)] text-[var(--brand)] focus:ring-[var(--brand)]"
/>
<span>Scoreboard Table</span>
</label>
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]">
<input
type="checkbox"
checked={showTOC}
onChange={(e) => setShowTOC(e.target.checked)}
className="rounded border-[var(--border-color)] text-[var(--brand)] focus:ring-[var(--brand)]"
/>
<span>Table of Contents</span>
</label>
</>
)} )}
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]"> <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-[var(--brand)] focus:ring-[var(--brand)]" className="rounded border-[var(--border-color)]"
/> />
<span>Page Break per Ch</span> <span>Page Break per Challenge</span>
</label> </label>
</div> </div>
</div> </div>
{/* Category Filter Chips */} {/* 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"> <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-[11px] text-[var(--text-muted)] font-medium mr-1">Category:</span> <span className="text-[var(--text-muted)] font-mono text-[10px] uppercase mr-1">Filter:</span>
<button <button
onClick={() => setSelectedCategory("all")} onClick={() => setSelectedCategory("all")}
className={`px-2 py-0.5 rounded text-xs font-mono transition-colors ${ type="button"
className={`px-2.5 py-0.5 rounded text-xs font-mono transition-all cursor-pointer ${
selectedCategory === "all" selectedCategory === "all"
? "bg-[var(--brand)] text-white font-medium" ? "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)]"
}`} }`}
> >
All ({initialDocs.length}) All ({cleanDocs.length})
</button> </button>
{allCategories.map((cat: string) => { {allCategories.map((cat) => {
const count = initialDocs.filter((d: Document) => extractDocCategory(d) === cat).length; const count = cleanDocs.filter(
const isActive = selectedCategory.toLowerCase() === cat.toLowerCase(); (d) => extractDocCategory(d).toLowerCase() === cat.toLowerCase(),
).length;
return ( return (
<button <button
key={cat} key={cat}
onClick={() => setSelectedCategory(isActive ? "all" : cat)} onClick={() => setSelectedCategory(cat)}
className={`px-2 py-0.5 rounded text-xs font-mono transition-colors flex items-center gap-1 ${ type="button"
isActive className={`px-2.5 py-0.5 rounded text-xs font-mono transition-all cursor-pointer ${
? "bg-[var(--brand)] text-white font-medium" selectedCategory.toLowerCase() === cat.toLowerCase()
? "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)]"
}`} }`}
> >
<span>{getCategoryIcon(cat)}</span> {cat} ({count})
<span>{cat}</span>
<span className="text-[10px] opacity-75 font-sans">({count})</span>
</button> </button>
); );
})} })}
@@ -346,374 +327,80 @@ export default function PdfExportView({
)} )}
</aside> </aside>
{/* ========================================================================= {/* Writeup Document Flow */}
Main Printable Document <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">
========================================================================= */} {/* Document Header */}
<div className="print-container bg-[var(--bg-surface)] print:bg-white border border-[var(--border-color)] print:border-none rounded-2xl print:rounded-none p-6 sm:p-10 print:p-0 shadow-sm print:shadow-none space-y-12 print:space-y-0"> <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 print:text-lg font-extrabold text-slate-950 tracking-tight mb-2 print:mb-0.5">
{summary.title}
</h1>
<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>
{currentTotalPoints > 0 && (
<span>· {currentTotalPoints.toLocaleString()} Total Points</span>
)}
<span>· {new Date(summary.generatedAt).toLocaleDateString(undefined, { dateStyle: "long" })}</span>
</div>
</header>
{/* ======================================================================= {/* Table of Contents */}
1. Cover Page
======================================================================= */}
{showCover && (
<section className="cover-page flex flex-col justify-between min-h-[600px] print:min-h-[92vh] p-8 sm:p-12 print:p-10 border border-[var(--border-color)] print:border-none rounded-xl print:rounded-none bg-[var(--bg-elevated)] print:bg-transparent">
<div>
{/* Header Badge */}
<div className="flex items-center justify-between gap-4 mb-8">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-[var(--brand)] text-white flex items-center justify-center font-bold text-sm">
M
</div>
<div>
<span className="text-xs font-bold uppercase tracking-wider text-[var(--text-primary)]">
MCPedia
</span>
<p className="text-[10px] text-[var(--text-dim)] font-mono">
Knowledge Base Documentation
</p>
</div>
</div>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-mono font-semibold bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30">
<span>{sectionMeta.icon}</span>
<span className="uppercase">{sectionMeta.label}</span>
</span>
</div>
{/* Title and Subtitle */}
<div className="space-y-3 mb-10 pt-4">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-[var(--text-primary)] tracking-tight leading-tight">
{summary.title}
</h1>
<p className="text-sm sm:text-base text-[var(--text-muted)] leading-relaxed max-w-2xl">
Comprehensive challenge solutions, technical writeups, and security research findings.
</p>
</div>
{/* Stats Metric Cards Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 my-8">
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
<div className="text-2xl font-bold text-[var(--text-primary)] font-mono">
{filteredAndSortedDocs.length}
</div>
<div className="text-xs text-[var(--text-muted)] mt-0.5">
Challenges / Ch
</div>
</div>
{currentTotalPoints > 0 && (
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
<div className="text-2xl font-bold text-[var(--brand)] dark:text-[var(--accent)] font-mono">
{currentTotalPoints.toLocaleString()}
</div>
<div className="text-xs text-[var(--text-muted)] mt-0.5">
Total Points
</div>
</div>
)}
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
<div className="text-2xl font-bold text-[var(--text-primary)] font-mono">
{summary.categories.length}
</div>
<div className="text-xs text-[var(--text-muted)] mt-0.5">
Categories
</div>
</div>
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
<div className="text-2xl font-bold text-[var(--text-primary)] font-mono">
{summary.authors.length || 1}
</div>
<div className="text-xs text-[var(--text-muted)] mt-0.5">
Contributors
</div>
</div>
</div>
{/* Categories Distribution */}
{summary.categories.length > 0 && (
<div className="space-y-2 mt-6">
<span className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
Categories Covered:
</span>
<div className="flex flex-wrap gap-2">
{summary.categories.map((cat: ExportCategorySummary) => (
<span
key={cat.name}
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-mono border ${getCategoryBadgeClass(cat.name)}`}
>
<span>{getCategoryIcon(cat.name)}</span>
<span className="font-semibold">{cat.name}:</span>
<span>{cat.count} chall{cat.count !== 1 ? "s" : ""}</span>
{cat.points > 0 && <span className="opacity-75 font-sans">({cat.points} pts)</span>}
</span>
))}
</div>
</div>
)}
</div>
{/* Cover Footer */}
<div className="pt-8 border-t border-[var(--border-color)] flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs text-[var(--text-muted)]">
<div>
{summary.authors.length > 0 && (
<p>
<span className="font-medium text-[var(--text-secondary)]">Authors:</span>{" "}
{summary.authors.map((a: string) => `@${a}`).join(", ")}
</p>
)}
<p className="mt-0.5 font-mono text-[11px] text-[var(--text-dim)]">
Path: {summary.scope}
</p>
</div>
<div className="font-mono text-right sm:text-right">
<p>
{new Date(summary.generatedAt).toLocaleDateString(undefined, {
dateStyle: "long",
})}
</p>
<p className="text-[10px] text-[var(--text-dim)]">Generated by MCPedia</p>
</div>
</div>
</section>
)}
{/* =======================================================================
2. Scoreboard / Challenge Matrix Table
======================================================================= */}
{showScoreboard && filteredAndSortedDocs.length > 1 && (
<section className="scoreboard-page py-6 print:py-8 border-t border-[var(--border-color)] print:border-none">
<div className="flex items-center justify-between gap-4 mb-4">
<h2 className="text-lg font-bold text-[var(--text-primary)] tracking-tight flex items-center gap-2">
<span>📊</span>
<span>Challenge Overview & Scoreboard</span>
</h2>
<span className="text-xs font-mono text-[var(--text-muted)]">
{filteredAndSortedDocs.length} Total Challenges
</span>
</div>
<div className="overflow-x-auto border border-[var(--border-color)] rounded-xl shadow-xs">
<table className="w-full text-xs text-left border-collapse">
<thead>
<tr className="bg-[var(--bg-elevated)] print:bg-slate-100 border-b border-[var(--border-color)] text-[var(--text-muted)] font-mono uppercase">
<th className="py-2.5 px-3 w-12 text-center">#</th>
<th className="py-2.5 px-3">Challenge</th>
<th className="py-2.5 px-3">Category</th>
<th className="py-2.5 px-3">Difficulty</th>
<th className="py-2.5 px-3 text-right">Points</th>
<th className="py-2.5 px-3">Author</th>
<th className="py-2.5 px-3 text-center">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--border-color)]">
{filteredAndSortedDocs.map((doc, idx) => {
const chNum = idx + 1;
const cat = extractDocCategory(doc);
const pts = extractDocPoints(doc);
const diff = extractDocDifficulty(doc);
const isSolved = extractDocSolved(doc);
return (
<tr
key={doc.slug}
className="hover:bg-[var(--bg-elevated)]/50 transition-colors"
>
<td className="py-2.5 px-3 text-center font-mono text-[var(--text-dim)]">
{chNum}
</td>
<td className="py-2.5 px-3 font-medium text-[var(--text-primary)]">
<a
href={`#ch-${chNum}`}
className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] hover:underline"
>
{doc.title}
</a>
</td>
<td className="py-2.5 px-3">
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-mono border ${getCategoryBadgeClass(cat)}`}
>
<span>{getCategoryIcon(cat)}</span>
<span>{cat}</span>
</span>
</td>
<td className="py-2.5 px-3">
{diff ? (
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium border ${getDifficultyBadgeClass(diff)}`}
>
{diff}
</span>
) : (
<span className="text-[var(--text-dim)]">-</span>
)}
</td>
<td className="py-2.5 px-3 text-right font-mono font-semibold text-[var(--brand)] dark:text-[var(--accent)]">
{pts > 0 ? `${pts} pts` : "-"}
</td>
<td className="py-2.5 px-3 text-[var(--text-secondary)]">
{doc.author ? `@${doc.author}` : "-"}
</td>
<td className="py-2.5 px-3 text-center">
{isSolved ? (
<span className="text-emerald-600 font-bold" title="Solved">
✓
</span>
) : (
<span className="text-[var(--text-dim)]">-</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
)}
{/* =======================================================================
3. Table of Contents (TOC)
======================================================================= */}
{showTOC && filteredAndSortedDocs.length > 1 && ( {showTOC && filteredAndSortedDocs.length > 1 && (
<section className="toc-page py-6 print:py-8 border-t border-[var(--border-color)] print:border-none"> <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-lg font-bold text-[var(--text-primary)] tracking-tight mb-4 flex items-center gap-2"> <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">
<span>📑</span> <BookOpen className="w-3.5 h-3.5 print:w-3 print:h-3" />
<span>Table of Contents</span> <span>Table of Contents</span>
</h2> </h2>
<ol className="list-decimal list-inside space-y-1.5 print:space-y-0.5 text-xs print:text-[7.5pt] text-slate-800">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2.5">
{filteredAndSortedDocs.map((doc, idx) => { {filteredAndSortedDocs.map((doc, idx) => {
const chNum = idx + 1;
const cat = extractDocCategory(doc); const cat = extractDocCategory(doc);
const pts = extractDocPoints(doc); const pts = extractDocPoints(doc);
return ( return (
<a <li key={doc.slug} className="print:leading-tight">
key={doc.slug} <a
href={`#ch-${chNum}`} href={`#ch-${idx + 1}`}
className="group flex items-center justify-between p-3 rounded-lg bg-[var(--bg-elevated)] print:bg-slate-50 hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-xs transition-colors" className="hover:underline font-medium text-slate-900"
> >
<div className="flex items-center gap-2.5 min-w-0 pr-2"> {doc.title}
<span className="font-mono text-[11px] font-bold text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 px-1.5 py-0.5 rounded shrink-0"> </a>
Ch {chNum} <span className="text-slate-500 font-mono ml-2 print:ml-1">
</span> [{cat}{pts > 0 ? ` · ${pts} pts` : ""}]
<span className="font-medium text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] truncate"> </span>
{doc.title} </li>
</span>
</div>
<div className="flex items-center gap-1.5 shrink-0 font-mono text-[11px]">
<span className="text-[var(--text-muted)]">{cat}</span>
{pts > 0 && (
<span className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">
· {pts}p
</span>
)}
</div>
</a>
); );
})} })}
</div> </ol>
</section> </section>
)} )}
{/* ======================================================================= {/* Challenges Flow */}
4. Challenge Chapters ("Urutan Rapih Per Ch") <div className="challenges-flow space-y-10 print:space-y-0">
======================================================================= */}
<div className="space-y-12 print:space-y-0">
{filteredAndSortedDocs.map((doc, idx) => { {filteredAndSortedDocs.map((doc, idx) => {
const chNum = idx + 1; const chNum = idx + 1;
const cat = extractDocCategory(doc); const cat = extractDocCategory(doc);
const pts = extractDocPoints(doc); const pts = extractDocPoints(doc);
const diff = extractDocDifficulty(doc); const diff = extractDocDifficulty(doc);
const isSolved = extractDocSolved(doc);
const extra = doc.extraFields || {}; const extra = doc.extraFields || {};
return ( return (
<article <article
key={doc.slug} key={doc.slug}
id={`ch-${chNum}`} id={`ch-${chNum}`}
className={`chapter-page ${ className={`challenge-entry ${
pageBreaks && idx > 0 ? "page-break-before" : "" pageBreaks && idx > 0 ? "page-break-before" : ""
} pt-8 first:pt-0 print:pt-6`} } pt-6 first:pt-0 print:pt-2.5 print:pb-2 print:border-b print:border-slate-200 print:last:border-none`}
> >
{/* Chapter Header Card */} {/* Challenge Title & Metadata */}
<div className="challenge-header-card bg-[var(--bg-elevated)] print:bg-slate-50 border border-[var(--border-color)] print:border-slate-300 rounded-xl p-5 sm:p-6 mb-6 shadow-xs"> <div className="challenge-header mb-4 pb-2 print:mb-1.5 print:pb-1 border-b border-slate-200 print:border-slate-300">
{/* Chapter index label */} <h2 className="text-2xl print:text-[11pt] font-bold text-slate-950 tracking-tight leading-tight mb-1.5 print:mb-0.5">
<div className="flex items-center justify-between gap-3 mb-2 flex-wrap"> {chNum}. {doc.title}
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-[var(--brand)] text-white">
CHAPTER {chNum}
</span>
<span className="text-xs font-mono text-[var(--text-dim)]">
Challenge {chNum} of {filteredAndSortedDocs.length}
</span>
</div>
<span className="text-xs font-mono text-[var(--text-dim)]">
{doc.slug}
</span>
</div>
{/* Challenge Title */}
<h2 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight leading-snug mb-4">
{doc.title}
</h2> </h2>
<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>
{pts > 0 && <span><strong>Points:</strong> {pts} pts</span>}
{diff && <span><strong>Difficulty:</strong> {diff}</span>}
{/* Metadata Chips Bar */} {/* Extra fields */}
<div className="badge-container flex flex-wrap items-center gap-2 pt-3 border-t border-[var(--border-color)]">
{/* Category Chip */}
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-mono font-semibold border ${getCategoryBadgeClass(cat)}`}
>
<span>{getCategoryIcon(cat)}</span>
<span>Category: {cat}</span>
</span>
{/* Points Chip */}
{pts > 0 && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-mono font-semibold bg-[var(--brand)]/10 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30">
<span>🎯</span>
<span>{pts} pts</span>
</span>
)}
{/* Difficulty Chip */}
{diff && (
<span
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium border ${getDifficultyBadgeClass(diff)}`}
>
<span>Difficulty: {diff}</span>
</span>
)}
{/* Solved Status */}
{isSolved && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
<span>✓</span>
<span>Solved</span>
</span>
)}
{/* Author Chip */}
{doc.author && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-secondary)]">
<span>👤</span>
<span>@{doc.author}</span>
</span>
)}
{/* Date Chip */}
{doc.updatedAt && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-dim)] font-mono">
<span>📅</span>
<span>{new Date(doc.updatedAt).toLocaleDateString()}</span>
</span>
)}
{/* Custom Extra Fields badges (excluding already rendered keys) */}
{Object.entries(extra).map(([k, v]) => { {Object.entries(extra).map(([k, v]) => {
const lowerKey = k.toLowerCase(); const lowerKey = k.toLowerCase();
if ( if (
@@ -725,7 +412,12 @@ export default function PdfExportView({
"difficulty", "difficulty",
"solved", "solved",
"author", "author",
"authors",
"scope",
"slug",
"event", "event",
"challenge",
"path",
].includes(lowerKey) || ].includes(lowerKey) ||
v === null || v === null ||
v === undefined || v === undefined ||
@@ -734,32 +426,26 @@ export default function PdfExportView({
return null; return null;
} }
return ( return (
<span <span key={k}>
key={k} <strong>{k}:</strong> {String(v)}
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-muted)] font-mono"
>
<span className="text-[var(--text-dim)]">{k}:</span>
<span className="text-[var(--text-secondary)] font-semibold">
{String(v)}
</span>
</span> </span>
); );
})} })}
</div> </div>
</div> </div>
{/* Markdown Writeup Body */} {/* Markdown Writeup Content */}
<div className="prose-content mb-10"> <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>
{/* Chapter End Divider */} {/* Divider between challenges (screen only) */}
<div className="print:hidden border-b border-[var(--border-color)] my-8" /> <hr className="print:hidden border-slate-200 my-8" />
</article> </article>
); );
})} })}
</div> </div>
</div> </main>
</div> </div>
); );
} }
+53 -40
View File
@@ -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>
+22 -10
View File
@@ -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)]"
+5 -8
View File
@@ -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>
); );
+9 -6
View File
@@ -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>
+11 -7
View File
@@ -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>
+348 -174
View File
@@ -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,46 +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: 15mm 15mm 15mm 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-size: 10.5pt !important; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
line-height: 1.5 !important; font-size: 8.5pt !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,
@@ -412,107 +470,223 @@ 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;
padding: 0 !important; padding: 0 !important;
border: none !important; border: none !important;
box-shadow: none !important; box-shadow: none !important;
background: transparent !important;
} }
/* Page break rules */ /* Explicit page break classes */
.page-break-before, .page-break-before,
.chapter-page { .chapter-page {
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,
.challenge-header-card, .code-block-wrapper,
.badge-container, .code-block-container,
.callout,
blockquote,
table, table,
pre, tr,
blockquote { img {
break-inside: avoid !important; break-inside: avoid !important;
page-break-inside: avoid !important; page-break-inside: avoid !important;
} }
/* Code block wrapping and styling in print */ /* Prevent headings from being orphaned at the bottom of pages */
.challenge-header,
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-wrapper,
.code-block-container {
break-inside: avoid !important;
page-break-inside: avoid !important;
border: 1px solid #cbd5e1 !important;
background-color: #f8fafc !important;
border-radius: 3px !important;
margin: 2.5pt 0 !important;
overflow: hidden !important;
}
.code-block-header {
border-bottom: 1px solid #cbd5e1 !important;
background-color: #f1f5f9 !important;
padding: 1.5pt 5pt !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 {
border: none !important;
background-color: transparent !important;
padding: 3pt 5pt !important;
margin: 0 !important;
border-radius: 0 !important;
}
pre, pre,
pre code, pre code,
code { code {
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-size: 8.5pt !important; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
line-height: 1.4 !important; font-size: 7.5pt !important;
} line-height: 1.3 !important;
pre {
background-color: #f8fafc !important;
color: #0f172a !important; color: #0f172a !important;
border: 1px solid #cbd5e1 !important;
border-radius: 6px !important;
padding: 8pt 10pt !important;
margin: 10pt 0 !important;
} }
/* Typography tweaks for high-contrast print */ :not(pre) > code {
.prose { padding: 0.5pt 2pt !important;
color: #1e293b !important; font-size: 7.5pt !important;
font-size: 10.5pt !important;
}
.prose h1,
.prose h2,
.prose h3,
.prose h4 {
color: #0f172a !important;
page-break-after: avoid !important;
break-after: avoid !important;
}
.prose p,
.prose li {
color: #334155 !important;
}
.prose a {
color: #312e81 !important;
text-decoration: underline !important;
}
/* Links display clean href only if needed or retain normal link text */
.prose blockquote {
background-color: #f1f5f9 !important; background-color: #f1f5f9 !important;
border-left: 3.5pt solid #4f46e5 !important; border: 1px solid #e2e8f0 !important;
color: #334155 !important; border-radius: 2px !important;
padding: 6pt 10pt !important;
}
.prose table {
border: 1px solid #cbd5e1 !important;
font-size: 9pt !important;
}
.prose th {
background-color: #f1f5f9 !important;
color: #0f172a !important;
border-bottom: 1.5px solid #94a3b8 !important;
}
.prose td {
border-bottom: 1px solid #e2e8f0 !important;
} }
} }
+33 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+61 -55
View File
@@ -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 }}
/> />
)} )}
+4 -3
View File
@@ -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"
} }
+165 -81
View File
@@ -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=="],
} }
} }
+2 -1
View File
@@ -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"
+103
View File
@@ -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);
});
});
+875
View File
@@ -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);
}
+1 -3
View File
@@ -376,7 +376,7 @@ export function compileExportMarkdown(exportData: ExportData): string {
// Cover / Header // Cover / Header
lines.push(`# ${summary.title}`); lines.push(`# ${summary.title}`);
lines.push(""); lines.push("");
lines.push(`*Generated from MCPedia Knowledge Base on ${new Date(summary.generatedAt).toLocaleDateString(undefined, { dateStyle: "long" })}*`); lines.push(`*Date: ${new Date(summary.generatedAt).toLocaleDateString(undefined, { dateStyle: "long" })}*`);
lines.push(""); lines.push("");
if (summary.totalDocuments > 1) { if (summary.totalDocuments > 1) {
@@ -416,8 +416,6 @@ export function compileExportMarkdown(exportData: ExportData): string {
if (cat) badges.push(`**Category:** \`${cat}\``); if (cat) badges.push(`**Category:** \`${cat}\``);
if (pts > 0) badges.push(`**Points:** \`${pts} pts\``); if (pts > 0) badges.push(`**Points:** \`${pts} pts\``);
if (diff) badges.push(`**Difficulty:** \`${diff}\``); if (diff) badges.push(`**Difficulty:** \`${diff}\``);
if (doc.author) badges.push(`**Author:** @${doc.author}`);
if (doc.slug) badges.push(`**Slug:** \`${doc.slug}\``);
if (badges.length > 0) { if (badges.length > 0) {
lines.push(badges.join(" · ")); lines.push(badges.join(" · "));
+20 -1
View File
@@ -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
@@ -104,8 +105,26 @@ export async function getExportDocuments(
return { ...toMeta(row), body: body || "" }; return { ...toMeta(row), body: body || "" };
}); });
// Filter out meta index documents (like _index or index) when exporting multiple docs
let filteredDocs = loadedDocs;
if (loadedDocs.length > 1) {
const withoutIndex = loadedDocs.filter((d) => {
const slug = d.slug.toLowerCase();
return (
!slug.endsWith("/_index") &&
!slug.endsWith("/index") &&
slug !== "_index" &&
slug !== "index" &&
d.type !== "writeup-index"
);
});
if (withoutIndex.length > 0) {
filteredDocs = withoutIndex;
}
}
// Sort neatly // Sort neatly
const sortedDocs = sortExportDocuments(loadedDocs, sortBy); const sortedDocs = sortExportDocuments(filteredDocs, sortBy);
const summary = buildExportSummary(sortedDocs, opts.path, opts.section); const summary = buildExportSummary(sortedDocs, opts.path, opts.section);
return { return {
+3 -3
View File
@@ -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",
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
} }
} }
+10 -13
View File
@@ -3,23 +3,11 @@
# so .next is built in CI via `bun run build` and packaged here. # so .next is built in CI via `bun run build` and packaged here.
{ pkgs { pkgs
, stdenv , stdenv
, writeShellScriptBin
, bun , bun
, webNextDir , webNextDir
, ... , ...
}: }:
let
mcpedia-web-script = writeShellScriptBin "mcpedia-web" ''
#!${pkgs.bash}/bin/bash
export NODE_ENV=production
export PORT=4016
cd /home/code/mcpedia/apps/web
rm -f .next
ln -s "${webNextDir}" .next
exec "${bun}/bin/bun" x next start -p 4016
'';
in
stdenv.mkDerivation { stdenv.mkDerivation {
pname = "mcpedia-web"; pname = "mcpedia-web";
version = "1.0.0"; version = "1.0.0";
@@ -31,6 +19,15 @@ stdenv.mkDerivation {
cp -r "${webNextDir}/." $out/share/mcpedia-web/.next/ cp -r "${webNextDir}/." $out/share/mcpedia-web/.next/
mkdir -p $out/bin mkdir -p $out/bin
cp ${mcpedia-web-script}/bin/mcpedia-web $out/bin/mcpedia-web cat > $out/bin/mcpedia-web <<EOF
#!${pkgs.bash}/bin/bash
export NODE_ENV=production
export PORT=4016
cd /home/code/mcpedia
rm -rf apps/web/.next
cp -r "$out/share/mcpedia-web/.next" apps/web/.next
exec "${bun}/bin/bun" run --cwd apps/web start
EOF
chmod +x $out/bin/mcpedia-web
''; '';
} }