Update InternalDocsLayout to Gitbook-style document page features
cgen-bb86dd35ff284fe6bb1c385349c83dce
This commit is contained in:
parent
384b958a04
commit
9cc7cdd7cd
1 changed files with 129 additions and 54 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useLocation, Navigate } from "react-router-dom";
|
||||
import { Menu, X, ChevronRight, Lock, Home } from "lucide-react";
|
||||
import { Menu, X, ChevronRight, Lock, Home, ExternalLink } from "lucide-react";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
||||
interface NavSpace {
|
||||
|
|
@ -151,15 +151,89 @@ interface InternalDocsLayoutProps {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
export default function InternalDocsLayout({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
}: InternalDocsLayoutProps) {
|
||||
// Map routes -> source file path so "Edit this page" can link to the component in the repo
|
||||
const SOURCE_MAP: Record<string, string> = {
|
||||
"/internal-docs": "code/client/pages/internal-docs/Space1Welcome.tsx",
|
||||
"/internal-docs/axiom-model": "code/client/pages/internal-docs/Space1AxiomModel.tsx",
|
||||
"/internal-docs/find-your-role": "code/client/pages/internal-docs/Space1FindYourRole.tsx",
|
||||
"/internal-docs/code-of-conduct": "code/client/pages/internal-docs/Space2CodeOfConduct.tsx",
|
||||
"/internal-docs/communication": "code/client/pages/internal-docs/Space2Communication.tsx",
|
||||
"/internal-docs/meetings": "code/client/pages/internal-docs/Space2MeetingCadence.tsx",
|
||||
"/internal-docs/brand": "code/client/pages/internal-docs/Space2BrandVoice.tsx",
|
||||
"/internal-docs/tech-stack": "code/client/pages/internal-docs/Space2TechStack.tsx",
|
||||
"/internal-docs/foundation-governance": "code/client/pages/internal-docs/Space3FoundationGovernance.tsx",
|
||||
"/internal-docs/foundation-protocol": "code/client/pages/internal-docs/Space3OpenSourceProtocol.tsx",
|
||||
"/internal-docs/foundation-programs": "code/client/pages/internal-docs/Space3CommunityPrograms.tsx",
|
||||
"/internal-docs/corp-product": "code/client/pages/internal-docs/Space4ProductOps.tsx",
|
||||
"/internal-docs/corp-blueprints": "code/client/pages/internal-docs/Space4CorpBlueprints.tsx",
|
||||
"/internal-docs/corp-clients": "code/client/pages/internal-docs/Space4ClientOps.tsx",
|
||||
"/internal-docs/corp-platform": "code/client/pages/internal-docs/Space4PlatformStrategy.tsx",
|
||||
"/internal-docs/onboarding": "code/client/pages/internal-docs/Space5Onboarding.tsx",
|
||||
"/internal-docs/finance": "code/client/pages/internal-docs/Space5Finance.tsx",
|
||||
};
|
||||
|
||||
export default function InternalDocsLayout({ children, title, description }: InternalDocsLayoutProps) {
|
||||
const { user, loading } = useAuth();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// Add copy buttons to code blocks
|
||||
const addCopyButtons = () => {
|
||||
document.querySelectorAll("pre").forEach((pre) => {
|
||||
if ((pre as HTMLElement).dataset.hasCopy === "true") return;
|
||||
(pre as HTMLElement).dataset.hasCopy = "true";
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "copy-code-btn absolute right-3 top-3 bg-slate-800/70 text-slate-200 text-xs px-2 py-1 rounded-md hover:bg-slate-800";
|
||||
btn.innerText = "Copy";
|
||||
btn.onclick = async () => {
|
||||
const code = pre.querySelector("code")?.textContent || pre.textContent || "";
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
btn.innerText = "Copied";
|
||||
setTimeout(() => (btn.innerText = "Copy"), 1500);
|
||||
} catch {
|
||||
btn.innerText = "Copy";
|
||||
}
|
||||
};
|
||||
(pre as HTMLElement).style.position = "relative";
|
||||
(pre as HTMLElement).appendChild(btn);
|
||||
});
|
||||
};
|
||||
|
||||
// Add heading anchors for h2/h3
|
||||
const addAnchors = () => {
|
||||
const container = document.querySelector(".internal-docs-content");
|
||||
if (!container) return;
|
||||
container.querySelectorAll("h2, h3").forEach((el) => {
|
||||
if ((el as HTMLElement).querySelector(".heading-anchor")) return;
|
||||
const text = (el.textContent || "").trim();
|
||||
const id = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
||||
(el as HTMLElement).id = id;
|
||||
const anchor = document.createElement("a");
|
||||
anchor.className = "heading-anchor ml-2 text-slate-500 text-sm opacity-0 hover:opacity-100 transition-opacity";
|
||||
anchor.href = `#${id}`;
|
||||
anchor.innerText = "#";
|
||||
(el as HTMLElement).appendChild(anchor);
|
||||
});
|
||||
};
|
||||
|
||||
addCopyButtons();
|
||||
addAnchors();
|
||||
|
||||
const obs = new MutationObserver(() => {
|
||||
addCopyButtons();
|
||||
addAnchors();
|
||||
});
|
||||
obs.observe(document.querySelector(".internal-docs-content") || document.body, { childList: true, subtree: true });
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// close sidebar on navigation (mobile)
|
||||
setSidebarOpen(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white flex items-center justify-center">
|
||||
|
|
@ -177,6 +251,11 @@ export default function InternalDocsLayout({
|
|||
|
||||
const isCurrentPage = (path: string) => location.pathname === path;
|
||||
|
||||
const sourcePath = SOURCE_MAP[location.pathname];
|
||||
const editUrl = sourcePath
|
||||
? `https://github.com/AeThex-Corporation/aethex-forge/blob/main/${sourcePath}`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
|
||||
{/* Sidebar */}
|
||||
|
|
@ -187,11 +266,7 @@ export default function InternalDocsLayout({
|
|||
>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-slate-700 sticky top-0 bg-slate-900">
|
||||
<Link
|
||||
to="/internal-docs"
|
||||
className="flex items-center gap-3 mb-6"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<Link to="/internal-docs" className="flex items-center gap-3 mb-6" onClick={() => setSidebarOpen(false)}>
|
||||
<Lock className="h-5 w-5 text-blue-400" />
|
||||
<div>
|
||||
<div className="font-bold text-sm text-white">Internal Hub</div>
|
||||
|
|
@ -199,10 +274,7 @@ export default function InternalDocsLayout({
|
|||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-800 transition-colors w-full justify-center"
|
||||
>
|
||||
<Link to="/" className="inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-800 transition-colors w-full justify-center">
|
||||
<Home className="h-4 w-4" />
|
||||
Back to Site
|
||||
</Link>
|
||||
|
|
@ -215,12 +287,8 @@ export default function InternalDocsLayout({
|
|||
<div className="flex items-center gap-2 mb-3 px-2">
|
||||
<span className="text-lg">{space.emoji}</span>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-blue-400 uppercase tracking-wider">
|
||||
{space.title}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{space.description}
|
||||
</div>
|
||||
<div className="text-xs font-semibold text-blue-400 uppercase tracking-wider">{space.title}</div>
|
||||
<div className="text-xs text-slate-500">{space.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -231,17 +299,11 @@ export default function InternalDocsLayout({
|
|||
to={page.path}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`block px-3 py-2 rounded-lg text-sm transition-all group ${
|
||||
isCurrentPage(page.path)
|
||||
? "bg-blue-600 text-white font-medium"
|
||||
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800"
|
||||
isCurrentPage(page.path) ? "bg-blue-600 text-white font-medium" : "text-slate-400 hover:text-slate-200 hover:bg-slate-800"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">{page.title}</div>
|
||||
{page.description && (
|
||||
<div className="text-xs text-slate-500 group-hover:text-slate-400">
|
||||
{page.description}
|
||||
</div>
|
||||
)}
|
||||
{page.description && <div className="text-xs text-slate-500 group-hover:text-slate-400">{page.description}</div>}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -260,47 +322,60 @@ export default function InternalDocsLayout({
|
|||
<main className="md:ml-72">
|
||||
{/* Mobile Header */}
|
||||
<div className="md:hidden sticky top-0 z-20 border-b border-slate-700 bg-slate-900/95 backdrop-blur p-4 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="p-2 hover:bg-slate-800 rounded-lg transition-colors"
|
||||
>
|
||||
{sidebarOpen ? (
|
||||
<X className="h-5 w-5" />
|
||||
) : (
|
||||
<Menu className="h-5 w-5" />
|
||||
)}
|
||||
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="p-2 hover:bg-slate-800 rounded-lg transition-colors">
|
||||
{sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</button>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-sm font-medium text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
<Link to="/" className="text-sm font-medium text-blue-400 hover:text-blue-300">
|
||||
← Back
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="p-6 md:p-8 max-w-4xl">
|
||||
{title && (
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-2">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-lg text-slate-400">{description}</p>
|
||||
<div className="p-6 md:p-8 max-w-5xl mx-auto">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
{title && (
|
||||
<div className="mb-4">
|
||||
<h1 className="text-4xl font-bold text-white mb-2">{title}</h1>
|
||||
{description && <p className="text-lg text-slate-400">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="prose prose-invert max-w-none">{children}</div>
|
||||
<div className="ml-6 flex-shrink-0 pt-1">
|
||||
{editUrl && (
|
||||
<a href={editUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-2 text-xs text-slate-300 hover:text-white bg-slate-800/40 px-3 py-2 rounded-md">
|
||||
<ExternalLink className="h-4 w-4" /> Edit this page
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="internal-docs-content prose prose-invert mx-auto max-w-none lg:max-w-3xl">{children}</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-slate-700 bg-slate-900/50 mt-12">
|
||||
<div className="max-w-4xl mx-auto px-6 md:px-8 py-8">
|
||||
<div className="max-w-5xl mx-auto px-6 md:px-8 py-8">
|
||||
<p className="text-sm text-slate-500">
|
||||
© 2025 AeThex. This is an internal operations hub. Information
|
||||
here is confidential and for authorized personnel only.
|
||||
© 2025 AeThex. This is an internal operations hub. Information here is confidential and for authorized personnel only.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
{/* Inline styles for print and copy button */}
|
||||
<style>{`
|
||||
@media print {
|
||||
body { background: white !important; color: #111 !important }
|
||||
.internal-docs-content { color: #111 !important }
|
||||
aside, .copy-code-btn, .heading-anchor, a[href^="http"] { display: none !important }
|
||||
.prose h1, .prose h2 { page-break-after: avoid }
|
||||
}
|
||||
.copy-code-btn { opacity: 0.95 }
|
||||
.internal-docs-content pre { position: relative }
|
||||
.heading-anchor { margin-left: 8px }
|
||||
.prose a[href^="#"] { text-decoration: none }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue