refactor(public-pages): unify docs services and about styling
This commit is contained in:
parent
0967ac6d9b
commit
d6d062daa9
@ -1,161 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { translations } from "../../i18n/translations";
|
||||
import { useLanguage } from "../../i18n/LanguageProvider";
|
||||
import UnifiedNavigation from "../../components/UnifiedNavigation";
|
||||
import Footer from "../../components/Footer";
|
||||
import { AlertTriangle, ArrowUpRight, Heart, Sparkles } from "lucide-react";
|
||||
|
||||
import {
|
||||
PublicPageIntro,
|
||||
PublicPageShell,
|
||||
} from "@/components/public/PublicPageShell";
|
||||
import { useLanguage } from "@/i18n/LanguageProvider";
|
||||
import { translations } from "@/i18n/translations";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function AboutPage() {
|
||||
const { language } = useLanguage();
|
||||
const isChinese = language === "zh";
|
||||
const t = translations[language].about;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-text transition-colors duration-150">
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-app-from opacity-20"
|
||||
aria-hidden
|
||||
/>
|
||||
<PublicPageShell>
|
||||
<section className="rounded-[2.4rem] border border-slate-900/10 bg-[linear-gradient(180deg,#ffffff,#faf7f2)] p-6 shadow-[0_22px_50px_rgba(15,23,42,0.05)] sm:p-8 lg:p-10">
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem] lg:items-end">
|
||||
<PublicPageIntro
|
||||
eyebrow={isChinese ? "项目说明" : "Project note"}
|
||||
title={t.title}
|
||||
subtitle={t.subtitle}
|
||||
titleClassName={cn(
|
||||
isChinese
|
||||
? "text-[2.7rem] tracking-[-0.08em] sm:text-[3.4rem]"
|
||||
: "editorial-display text-[2.9rem] tracking-[-0.06em] sm:text-[3.6rem]",
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="relative mx-auto max-w-7xl px-6 pb-20">
|
||||
<UnifiedNavigation />
|
||||
|
||||
<main className="pt-20 lg:pt-32">
|
||||
<div className="mx-auto max-w-3xl space-y-12">
|
||||
{/* Header */}
|
||||
<div className="space-y-4 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-heading sm:text-5xl">
|
||||
{t.title}
|
||||
</h1>
|
||||
<p className="text-lg text-text-muted">{t.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{/* Disclaimer Section */}
|
||||
<div className="rounded-2xl border border-warning/20 bg-warning/5 p-8 shadow-inner shadow-warning/10">
|
||||
<div className="flex gap-4">
|
||||
<div className="mt-1 shrink-0 text-warning">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-alert-triangle"
|
||||
>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-warning-foreground">
|
||||
Disclaimer
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-warning-foreground/80 whitespace-pre-wrap">
|
||||
{t.disclaimer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Acknowledgments */}
|
||||
<div className="space-y-8 rounded-3xl border border-surface-border bg-surface p-8 lg:p-12 shadow-2xl backdrop-blur-sm">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-heading">
|
||||
{t.acknowledgmentsTitle}
|
||||
</h2>
|
||||
<p className="text-lg leading-relaxed text-text-muted whitespace-pre-wrap">
|
||||
{t.acknowledgments}
|
||||
</p>
|
||||
|
||||
<div className="space-y-12 pt-4">
|
||||
{t.sections.map((section, sIndex) => (
|
||||
<div key={sIndex} className="space-y-4">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-primary border-b border-primary/20 pb-2">
|
||||
{section.title}
|
||||
</h3>
|
||||
|
||||
{section.content && (
|
||||
<p className="text-sm text-text-muted leading-relaxed whitespace-pre-wrap">
|
||||
{section.content}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{section.items && (
|
||||
<div className="grid gap-4 sm:grid-cols-1">
|
||||
{section.items.map((item, iIndex) => (
|
||||
<div key={iIndex} className="group relative rounded-xl border border-surface-border bg-surface-hover/30 p-4 transition-all hover:border-primary/20 hover:bg-surface-hover/50">
|
||||
<div className="flex flex-col gap-1">
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-text hover:text-primary transition-colors flex items-center gap-2"
|
||||
>
|
||||
{item.label}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-external-link opacity-0 group-hover:opacity-100 transition-opacity"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
|
||||
</a>
|
||||
<p className="text-sm text-text-muted leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section.links && (
|
||||
<ul className="grid gap-3 sm:grid-cols-2">
|
||||
{section.links.map((link, lIndex) => (
|
||||
<li key={lIndex} className="flex items-center gap-2 text-sm text-text-muted">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-colors hover:text-text hover:underline hover:decoration-primary"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden rounded-xl bg-primary/10 p-6">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 opacity-50" />
|
||||
<div className="relative flex items-center gap-4 text-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-heart h-6 w-6"
|
||||
>
|
||||
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
|
||||
</svg>
|
||||
<p className="font-medium whitespace-pre-wrap">{t.opensource}</p>
|
||||
</div>
|
||||
<div className="grid gap-3 rounded-[1.75rem] border border-slate-900/10 bg-white/85 p-5">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
{isChinese ? "维护方式" : "Maintenance"}
|
||||
</p>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-900/[0.04] text-primary">
|
||||
<Sparkles className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
{isChinese
|
||||
? "独立开发者维护,围绕 AI 服务、可观测性与云原生控制面持续演进。"
|
||||
: "Maintained independently, evolving around AI services, observability, and cloud-native control planes."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<section className="rounded-[2rem] border border-warning/25 bg-[#fffaf0] p-5 shadow-[0_18px_40px_rgba(15,23,42,0.04)] lg:p-7">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-warning/10 text-warning">
|
||||
<AlertTriangle className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-warning-foreground/70">
|
||||
{isChinese ? "免责声明" : "Disclaimer"}
|
||||
</p>
|
||||
<p className="text-sm leading-7 text-warning-foreground/85 whitespace-pre-wrap">
|
||||
{t.disclaimer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[2rem] border border-slate-900/10 bg-white/90 p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:p-7">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
{isChinese ? "致谢与驱动力" : "Acknowledgements"}
|
||||
</p>
|
||||
<h2 className="text-[2rem] font-semibold tracking-[-0.05em] text-heading sm:text-[2.35rem]">
|
||||
{t.acknowledgmentsTitle}
|
||||
</h2>
|
||||
<p className="max-w-3xl text-[1rem] leading-8 text-text-muted whitespace-pre-wrap">
|
||||
{t.acknowledgments}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
{t.sections.map((section, index) => (
|
||||
<div
|
||||
key={`${section.title}-${index}`}
|
||||
className="flex h-full flex-col gap-4 rounded-[1.6rem] border border-slate-900/10 bg-[#fcfbf8] p-5 transition duration-200 hover:-translate-y-[1px] hover:bg-white"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="inline-flex w-fit rounded-full border border-slate-900/10 bg-white px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||
{isChinese ? "章节" : "Section"} {index + 1}
|
||||
</div>
|
||||
<h3 className="text-[1.08rem] font-semibold leading-7 tracking-[-0.03em] text-slate-900">
|
||||
{section.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{section.content ? (
|
||||
<p className="text-sm leading-7 text-slate-600 whitespace-pre-wrap">
|
||||
{section.content}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{section.items ? (
|
||||
<div className="grid gap-3">
|
||||
{section.items.map((item) => (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group rounded-[1.25rem] border border-slate-900/10 bg-white/80 p-4 transition hover:bg-white"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-slate-900">
|
||||
{item.label}
|
||||
</p>
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowUpRight className="mt-1 h-4 w-4 shrink-0 text-slate-400 transition group-hover:text-primary" />
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{section.links ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{section.links.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 transition hover:border-primary/20 hover:text-primary"
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[2rem] border border-primary/15 bg-[linear-gradient(135deg,rgba(255,255,255,0.96),rgba(240,244,255,0.92))] p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:p-7">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<Heart className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
{isChinese ? "开源协作" : "Open source"}
|
||||
</p>
|
||||
<p className="max-w-4xl text-[1rem] leading-8 text-slate-700 whitespace-pre-wrap">
|
||||
{t.opensource}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PublicPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,36 +1,44 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ThumbsUp, ThumbsDown } from 'lucide-react'
|
||||
import { useState } from "react";
|
||||
import { ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
|
||||
export default function Feedback() {
|
||||
const [voted, setVoted] = useState<'yes' | 'no' | null>(null)
|
||||
const [voted, setVoted] = useState<"yes" | "no" | null>(null);
|
||||
|
||||
return (
|
||||
<div className="mt-16 border-t border-surface-border pt-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="text-lg font-semibold text-heading">Is this page helpful?</h3>
|
||||
{voted === null ? (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setVoted('yes')}
|
||||
className="flex items-center gap-2 rounded-md border border-surface-border bg-surface px-4 py-2 text-sm font-medium text-text transition hover:border-primary hover:text-primary"
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setVoted('no')}
|
||||
className="flex items-center gap-2 rounded-md border border-surface-border bg-surface px-4 py-2 text-sm font-medium text-text transition hover:border-danger hover:text-danger"
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">Thanks for your feedback!</p>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<section className="rounded-[1.6rem] border border-slate-900/10 bg-[#fcfbf8] p-5 shadow-[0_14px_30px_rgba(15,23,42,0.04)]">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
Feedback
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold tracking-[-0.03em] text-heading">
|
||||
Is this page helpful?
|
||||
</h3>
|
||||
</div>
|
||||
)
|
||||
|
||||
{voted === null ? (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setVoted("yes")}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-4 py-2 text-sm font-semibold text-slate-800 transition hover:border-primary/20 hover:text-primary"
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setVoted("no")}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-4 py-2 text-sm font-semibold text-slate-800 transition hover:border-danger/20 hover:text-danger"
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">Thanks for your feedback.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,104 +1,128 @@
|
||||
export const dynamic = 'error'
|
||||
export const revalidate = false
|
||||
export const dynamic = "error";
|
||||
export const revalidate = false;
|
||||
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import DocArticle from '@/components/doc/DocArticle'
|
||||
import DocMetaPanel from '@/components/doc/DocMetaPanel'
|
||||
import Feedback from '../../Feedback'
|
||||
import { getDocVersionParams, getDocVersion } from '../../resources.server'
|
||||
import { isFeatureEnabled } from '@lib/featureToggles'
|
||||
import Link from 'next/link'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import DocArticle from "@/components/doc/DocArticle";
|
||||
import DocMetaPanel from "@/components/doc/DocMetaPanel";
|
||||
import { PublicPageIntro } from "@/components/public/PublicPageShell";
|
||||
import { isFeatureEnabled } from "@lib/featureToggles";
|
||||
|
||||
// Simple Breadcrumbs Component inline (or could be separate)
|
||||
function DocsBreadcrumbs({ items }: { items: { label: string; href: string }[] }) {
|
||||
import Feedback from "../../Feedback";
|
||||
import { getDocVersion, getDocVersionParams } from "../../resources.server";
|
||||
|
||||
function DocsBreadcrumbs({
|
||||
items,
|
||||
}: {
|
||||
items: { label: string; href: string }[];
|
||||
}) {
|
||||
return (
|
||||
<nav className="flex items-center gap-2 text-sm text-text-muted mb-6">
|
||||
<nav className="mb-5 flex flex-wrap items-center gap-2 text-sm text-text-muted">
|
||||
{items.map((item, index) => (
|
||||
<div key={item.href} className="flex items-center gap-2">
|
||||
{index > 0 && <ChevronRight className="h-4 w-4" />}
|
||||
{index > 0 ? (
|
||||
<ChevronRight className="h-4 w-4 text-slate-400" />
|
||||
) : null}
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`transition hover:text-primary ${index === items.length - 1 ? 'font-medium text-text' : ''}`}
|
||||
className={`rounded-full border px-3 py-1.5 transition ${
|
||||
index === items.length - 1
|
||||
? "border-slate-900/10 bg-[#f8f4ec] font-medium text-slate-900"
|
||||
: "border-slate-900/10 bg-white text-slate-600 hover:text-primary"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
if (!isFeatureEnabled('appModules', '/docs')) {
|
||||
return []
|
||||
if (!isFeatureEnabled("appModules", "/docs")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getDocVersionParams()
|
||||
}
|
||||
return getDocVersionParams();
|
||||
};
|
||||
|
||||
export const dynamicParams = false
|
||||
export const dynamicParams = false;
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ collection: string; slug: string[] }>;
|
||||
}): Promise<Metadata> {
|
||||
const resolvedParams = await params;
|
||||
const doc = await getDocVersion(
|
||||
resolvedParams.collection,
|
||||
resolvedParams.slug,
|
||||
);
|
||||
if (!doc) return {};
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ collection: string; slug: string[] }> }): Promise<Metadata> {
|
||||
const resolvedParams = await params
|
||||
const doc = await getDocVersion(resolvedParams.collection, resolvedParams.slug)
|
||||
if (!doc) return {}
|
||||
return {
|
||||
title: `${doc.version.title} - ${doc.collection.title} | Documentation`,
|
||||
description: doc.version.description,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DocVersionPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ collection: string; slug: string[] }>
|
||||
params: Promise<{ collection: string; slug: string[] }>;
|
||||
}) {
|
||||
if (!isFeatureEnabled('appModules', '/docs')) {
|
||||
notFound()
|
||||
if (!isFeatureEnabled("appModules", "/docs")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const resolvedParams = await params
|
||||
const doc = await getDocVersion(resolvedParams.collection, resolvedParams.slug)
|
||||
const resolvedParams = await params;
|
||||
const doc = await getDocVersion(
|
||||
resolvedParams.collection,
|
||||
resolvedParams.slug,
|
||||
);
|
||||
if (!doc) {
|
||||
notFound()
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { collection, version } = doc
|
||||
|
||||
const { collection, version } = doc;
|
||||
const breadcrumbs = [
|
||||
{ label: 'Documentation', href: '/docs' },
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: collection.title, href: `/docs/${collection.slug}` },
|
||||
{ label: version.title, href: `/docs/${collection.slug}/${version.slug}` },
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex gap-12 xl:gap-16">
|
||||
{/* Center Content */}
|
||||
<article className="min-w-0 flex-1">
|
||||
<DocsBreadcrumbs items={breadcrumbs} />
|
||||
<div className="flex gap-8 xl:gap-10">
|
||||
<article className="min-w-0 flex-1 space-y-6">
|
||||
<section className="rounded-[2rem] border border-slate-900/10 bg-[linear-gradient(180deg,#ffffff,#faf7f2)] p-6 shadow-[0_20px_48px_rgba(15,23,42,0.05)] lg:p-7">
|
||||
<DocsBreadcrumbs items={breadcrumbs} />
|
||||
<PublicPageIntro
|
||||
eyebrow="Documentation"
|
||||
title={version.title}
|
||||
subtitle={version.description}
|
||||
titleClassName="text-[2.3rem] tracking-[-0.06em] sm:text-[2.9rem]"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<header className="mb-10 border-b border-surface-border pb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-heading sm:text-4xl">{version.title}</h1>
|
||||
{version.description && <p className="mt-4 text-lg text-text-muted">{version.description}</p>}
|
||||
</header>
|
||||
|
||||
<div className="prose prose-slate max-w-none dark:prose-invert prose-headings:scroll-mt-20 prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline">
|
||||
<section className="rounded-[2rem] border border-slate-900/10 bg-white/92 p-6 shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:p-8">
|
||||
<DocArticle content={version.content} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Feedback />
|
||||
</article>
|
||||
|
||||
{/* Right Sidebar */}
|
||||
<aside className="hidden w-64 shrink-0 lg:block xl:w-72">
|
||||
<div className="sticky top-[100px] space-y-8 border-l border-surface-border pl-6">
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-text-subtle">Metadata</h3>
|
||||
<div className="sticky top-[100px]">
|
||||
<div className="rounded-[1.6rem] border border-slate-900/10 bg-white/90 p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)]">
|
||||
<p className="mb-4 text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
Metadata
|
||||
</p>
|
||||
<DocMetaPanel
|
||||
description={undefined} // Description already shown in header
|
||||
description={undefined}
|
||||
updatedAt={version.updatedAt}
|
||||
tags={version.tags}
|
||||
/>
|
||||
@ -106,5 +130,5 @@ export default async function DocVersionPage({
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,45 +1,154 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import { marked } from 'marked'
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import matter from "gray-matter";
|
||||
import { ArrowRight, BookCopy, Files } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import DocArticle from "@/components/doc/DocArticle";
|
||||
import { PublicPageIntro } from "@/components/public/PublicPageShell";
|
||||
|
||||
import { getDocCollections } from "./resources.server";
|
||||
|
||||
export default async function DocsHome() {
|
||||
try {
|
||||
// Read the index.md file
|
||||
const indexPath = path.join(process.cwd(), 'src', 'content', 'doc', 'index.md')
|
||||
const fileContent = await fs.readFile(indexPath, 'utf-8')
|
||||
const { data: frontmatter, content } = matter(fileContent)
|
||||
|
||||
// Convert markdown to HTML
|
||||
const htmlContent = await marked(content)
|
||||
const indexPath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"content",
|
||||
"doc",
|
||||
"index.md",
|
||||
);
|
||||
const [fileContent, collections] = await Promise.all([
|
||||
fs.readFile(indexPath, "utf-8"),
|
||||
getDocCollections(),
|
||||
]);
|
||||
const { data: frontmatter, content } = matter(fileContent);
|
||||
const articleCount = collections.reduce(
|
||||
(sum, collection) => sum + collection.versions.length,
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<header className="mb-10 border-b border-surface-border pb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-heading sm:text-4xl">
|
||||
{frontmatter.title || 'Documentation'}
|
||||
</h1>
|
||||
{frontmatter.description && (
|
||||
<p className="mt-4 text-lg text-text-muted">{frontmatter.description}</p>
|
||||
)}
|
||||
</header>
|
||||
<div className="space-y-8">
|
||||
<section className="rounded-[2.2rem] border border-slate-900/10 bg-[linear-gradient(180deg,#ffffff,#faf7f2)] p-6 shadow-[0_22px_50px_rgba(15,23,42,0.05)] lg:p-8">
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem] lg:items-end">
|
||||
<PublicPageIntro
|
||||
eyebrow="Documentation"
|
||||
title={frontmatter.title || "Documentation"}
|
||||
subtitle={
|
||||
frontmatter.description ||
|
||||
"Unified references for Cloud-Neutral Toolkit services."
|
||||
}
|
||||
titleClassName="editorial-display text-[2.8rem] tracking-[-0.06em] sm:text-[3.4rem]"
|
||||
/>
|
||||
|
||||
<article
|
||||
className="prose prose-slate max-w-none dark:prose-invert prose-headings:scroll-mt-20 prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline"
|
||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||
/>
|
||||
<div className="grid gap-3 rounded-[1.75rem] border border-slate-900/10 bg-white/85 p-5">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
Library snapshot
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<div className="rounded-[1.25rem] border border-slate-900/10 bg-[#fcfbf8] p-4">
|
||||
<div className="flex items-center gap-2 text-slate-900">
|
||||
<BookCopy className="h-4 w-4 text-primary" aria-hidden />
|
||||
<span className="text-sm font-semibold">Collections</span>
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-semibold tracking-[-0.05em] text-slate-900">
|
||||
{collections.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[1.25rem] border border-slate-900/10 bg-[#fcfbf8] p-4">
|
||||
<div className="flex items-center gap-2 text-slate-900">
|
||||
<Files className="h-4 w-4 text-primary" aria-hidden />
|
||||
<span className="text-sm font-semibold">Articles</span>
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-semibold tracking-[-0.05em] text-slate-900">
|
||||
{articleCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{collections.length > 0 ? (
|
||||
<section className="rounded-[2rem] border border-slate-900/10 bg-white/90 p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:p-7">
|
||||
<div className="mb-5 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
Browse collections
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-6 text-text-muted">
|
||||
Documentation sections now use the same card language as the
|
||||
rest of the public site.
|
||||
</p>
|
||||
</div>
|
||||
<span className="inline-flex w-fit rounded-full border border-slate-900/10 bg-[#f8f4ec] px-3 py-1 text-xs font-semibold text-slate-700">
|
||||
{collections.length} collections
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 xl:grid-cols-2">
|
||||
{collections.map((collection) => (
|
||||
<Link
|
||||
key={collection.slug}
|
||||
href={`/docs/${collection.slug}/${collection.defaultVersionSlug}`}
|
||||
className="group rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] p-4 transition duration-200 hover:-translate-y-[1px] hover:bg-white"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[1.05rem] font-semibold leading-7 tracking-[-0.03em] text-slate-900">
|
||||
{collection.title}
|
||||
</h2>
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
{collection.description}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="mt-1 h-4 w-4 shrink-0 text-slate-400 transition group-hover:text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<span className="rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-semibold text-slate-600">
|
||||
{collection.versions.length} articles
|
||||
</span>
|
||||
{collection.tags.slice(0, 2).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-medium text-slate-500"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="rounded-[2rem] border border-slate-900/10 bg-white/92 p-6 shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:p-8">
|
||||
<div className="mb-5 border-b border-slate-900/10 pb-4">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
Overview
|
||||
</p>
|
||||
</div>
|
||||
<DocArticle content={content} />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to load docs index:', error)
|
||||
console.error("Failed to load docs index:", error);
|
||||
|
||||
return (
|
||||
<div className="flex h-64 flex-col items-center justify-center rounded-lg border border-dashed border-surface-border bg-surface p-8 text-center">
|
||||
<h3 className="text-lg font-semibold text-heading">No Documentation Found</h3>
|
||||
<p className="max-w-md text-sm text-text-muted mt-2">
|
||||
We could not find any documentation files. Please ensure content is synced to <code>src/content/doc</code>.
|
||||
<div className="rounded-[2rem] border border-dashed border-slate-900/12 bg-white/80 p-8 text-center shadow-[0_18px_40px_rgba(15,23,42,0.04)]">
|
||||
<h3 className="text-xl font-semibold tracking-[-0.03em] text-heading">
|
||||
No Documentation Found
|
||||
</h3>
|
||||
<p className="mx-auto mt-3 max-w-xl text-sm leading-6 text-text-muted">
|
||||
We could not find any documentation files. Please ensure content is
|
||||
synced to <code>src/content/doc</code>.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,6 +161,156 @@ button {
|
||||
font-family: var(--font-editorial-display);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.public-doc-prose {
|
||||
max-width: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.public-doc-prose > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.public-doc-prose > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.public-doc-prose h1,
|
||||
.public-doc-prose h2,
|
||||
.public-doc-prose h3,
|
||||
.public-doc-prose h4,
|
||||
.public-doc-prose h5,
|
||||
.public-doc-prose h6 {
|
||||
scroll-margin-top: calc(var(--app-shell-nav-offset) + 1rem);
|
||||
margin-top: 2.75rem;
|
||||
margin-bottom: 0.9rem;
|
||||
color: var(--color-heading);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.public-doc-prose h1 {
|
||||
font-size: clamp(2rem, 4vw, 2.9rem);
|
||||
}
|
||||
|
||||
.public-doc-prose h2 {
|
||||
font-size: clamp(1.55rem, 2.2vw, 2rem);
|
||||
}
|
||||
|
||||
.public-doc-prose h3 {
|
||||
font-size: clamp(1.25rem, 1.8vw, 1.5rem);
|
||||
}
|
||||
|
||||
.public-doc-prose p,
|
||||
.public-doc-prose ul,
|
||||
.public-doc-prose ol,
|
||||
.public-doc-prose blockquote,
|
||||
.public-doc-prose pre,
|
||||
.public-doc-prose table {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.public-doc-prose p,
|
||||
.public-doc-prose li {
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.public-doc-prose strong {
|
||||
color: var(--color-heading);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.public-doc-prose a {
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.public-doc-prose a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.public-doc-prose ul,
|
||||
.public-doc-prose ol {
|
||||
padding-left: 1.35rem;
|
||||
}
|
||||
|
||||
.public-doc-prose li + li {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.public-doc-prose code {
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 999px;
|
||||
background: #f8f4ec;
|
||||
padding: 0.12rem 0.42rem;
|
||||
color: var(--color-heading);
|
||||
font-family: var(--font-geist-mono);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.public-doc-prose pre {
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 1.25rem;
|
||||
background: #f5f2eb;
|
||||
padding: 1rem 1.15rem;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.public-doc-prose pre code {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.public-doc-prose blockquote {
|
||||
border-left: 2px solid rgba(51, 102, 255, 0.24);
|
||||
border-radius: 0 1rem 1rem 0;
|
||||
background: rgba(248, 244, 236, 0.78);
|
||||
padding: 0.75rem 0 0.75rem 1rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
.public-doc-prose hr {
|
||||
margin: 2rem 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-divider);
|
||||
}
|
||||
|
||||
.public-doc-prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.public-doc-prose th,
|
||||
.public-doc-prose td {
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
padding: 0.8rem 1rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.public-doc-prose th {
|
||||
background: rgba(248, 244, 236, 0.82);
|
||||
color: var(--color-heading);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.public-doc-prose img {
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
|
||||
@ -7,77 +7,56 @@ import {
|
||||
Bot,
|
||||
Box,
|
||||
CloudCog,
|
||||
Command,
|
||||
Database,
|
||||
FileEdit,
|
||||
Gauge,
|
||||
type LucideIcon,
|
||||
MessageCircle,
|
||||
Network,
|
||||
} from "lucide-react";
|
||||
import Footer from "../../components/Footer";
|
||||
import UnifiedNavigation from "../../components/UnifiedNavigation";
|
||||
import { useLanguage } from "../../i18n/LanguageProvider";
|
||||
import { useViewStore } from "../../components/theme/viewStore";
|
||||
import Material3Layout from "./Material3Layout";
|
||||
|
||||
import {
|
||||
PublicPageIntro,
|
||||
PublicPageShell,
|
||||
} from "@/components/public/PublicPageShell";
|
||||
import { useLanguage } from "@/i18n/LanguageProvider";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const placeholderCount = 1;
|
||||
|
||||
type ServiceCardData = {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
href: string;
|
||||
icon: any;
|
||||
icon: LucideIcon;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
const ServiceCard = ({
|
||||
function ServiceCard({
|
||||
service,
|
||||
view,
|
||||
isChinese,
|
||||
}: {
|
||||
service: ServiceCardData;
|
||||
view: "classic" | "material";
|
||||
isChinese: boolean;
|
||||
}) => {
|
||||
const isMaterial = view === "material";
|
||||
|
||||
const cardContent = (
|
||||
<div
|
||||
className={`group flex h-full flex-col justify-between rounded-xl p-5 transition ${
|
||||
isMaterial
|
||||
? "border border-surface-border bg-surface hover:-translate-y-[1px] hover:border-primary/50 hover:bg-background-muted"
|
||||
: "border border-white/10 bg-white/5 hover:-translate-y-[1px] hover:border-indigo-400/50 hover:bg-slate-900/60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={`flex h-10 w-10 items-center justify-center rounded-full ${
|
||||
isMaterial
|
||||
? "bg-primary/15 text-primary"
|
||||
: "bg-indigo-500/15 text-indigo-200"
|
||||
}`}
|
||||
>
|
||||
}) {
|
||||
const content = (
|
||||
<div className="group flex h-full flex-col justify-between rounded-[1.6rem] border border-slate-900/10 bg-[#fcfbf8] p-5 transition duration-200 hover:-translate-y-[1px] hover:bg-white">
|
||||
<div className="space-y-4">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-slate-900/[0.04] text-primary">
|
||||
<service.icon className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div
|
||||
className={`text-sm font-semibold ${isMaterial ? "text-heading" : "text-white"}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[1.02rem] font-semibold leading-7 tracking-[-0.03em] text-slate-900">
|
||||
{service.name}
|
||||
</div>
|
||||
<p
|
||||
className={`text-sm ${isMaterial ? "text-text-muted" : "text-slate-300"}`}
|
||||
>
|
||||
</h2>
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`mt-4 inline-flex items-center gap-1 text-xs font-semibold transition ${
|
||||
isMaterial
|
||||
? "text-primary group-hover:text-primary-hover"
|
||||
: "text-indigo-200 group-hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<span className="mt-6 inline-flex items-center gap-1 text-sm font-semibold text-primary transition group-hover:text-primary-hover">
|
||||
{isChinese ? "打开" : "Open"}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden />
|
||||
</span>
|
||||
@ -92,111 +71,44 @@ const ServiceCard = ({
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
{cardContent}
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={service.href} className="block">
|
||||
{cardContent}
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const PlaceholderCard = ({
|
||||
view,
|
||||
isChinese,
|
||||
}: {
|
||||
view: "classic" | "material";
|
||||
isChinese: boolean;
|
||||
}) => {
|
||||
const isMaterial = view === "material";
|
||||
const placeholderLabel = isChinese
|
||||
? "更多服务即将上线"
|
||||
: "More services coming soon";
|
||||
const placeholderDescription = isChinese
|
||||
? "预留卡片位置,持续扩充入口。"
|
||||
: "Reserved slots for new service entries.";
|
||||
}
|
||||
|
||||
function PlaceholderCard({ isChinese }: { isChinese: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={`flex h-full flex-col justify-between rounded-xl border border-dashed p-5 ${
|
||||
isMaterial
|
||||
? "border-surface-border-strong bg-surface text-text-muted"
|
||||
: "border-white/15 bg-white/5 text-slate-300"
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
className={`flex h-10 w-10 items-center justify-center rounded-full border border-dashed text-sm ${
|
||||
isMaterial
|
||||
? "border-surface-border-strong text-text-subtle"
|
||||
: "border-white/20 text-slate-400"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full flex-col justify-between rounded-[1.6rem] border border-dashed border-slate-900/12 bg-white/70 p-5">
|
||||
<div className="space-y-4">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-full border border-dashed border-slate-900/12 text-slate-400">
|
||||
<Box className="h-4 w-4" aria-hidden />
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-semibold ${isMaterial ? "text-heading" : "text-white/80"}`}
|
||||
>
|
||||
{placeholderLabel}
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[1.02rem] font-semibold leading-7 tracking-[-0.03em] text-slate-800">
|
||||
{isChinese ? "更多服务即将上线" : "More services coming soon"}
|
||||
</h2>
|
||||
<p className="text-sm leading-6 text-slate-500">
|
||||
{isChinese
|
||||
? "预留卡片位置,持续扩充入口。"
|
||||
: "Reserved slots for new service entries."}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className={`text-sm ${isMaterial ? "text-text-subtle" : "text-slate-400"}`}
|
||||
>
|
||||
{placeholderDescription}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`mt-4 text-xs font-semibold ${isMaterial ? "text-text-subtle" : "text-slate-400"}`}
|
||||
>
|
||||
<span className="mt-6 text-sm font-semibold text-slate-500">
|
||||
{isChinese ? "敬请期待" : "Stay tuned"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ServiceGrid = ({
|
||||
view,
|
||||
services,
|
||||
isChinese,
|
||||
}: {
|
||||
view: "classic" | "material";
|
||||
services: ServiceCardData[];
|
||||
isChinese: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<section className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{services.map((service) => (
|
||||
<ServiceCard
|
||||
key={service.key}
|
||||
service={service}
|
||||
view={view}
|
||||
isChinese={isChinese}
|
||||
/>
|
||||
))}
|
||||
{Array.from({ length: placeholderCount }).map((_, index) => (
|
||||
<PlaceholderCard
|
||||
key={`placeholder-${index}`}
|
||||
view={view}
|
||||
isChinese={isChinese}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const ClawdbotLogo = (props: any) => (
|
||||
<img
|
||||
src="https://mintcdn.com/clawdhub/4rYvG-uuZrMK_URE/assets/pixel-lobster.svg?fit=max&auto=format&n=4rYvG-uuZrMK_URE&q=85&s=da2032e9eac3b5d9bfe7eb96ca6a8a26"
|
||||
alt="Clawdbot"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ServicesPage() {
|
||||
const { view, isHydrated } = useViewStore();
|
||||
const { language } = useLanguage();
|
||||
const isChinese = language === "zh";
|
||||
|
||||
@ -299,71 +211,83 @@ export default function ServicesPage() {
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
key: "moltbot",
|
||||
key: "xworkmate",
|
||||
name: "XWorkmate",
|
||||
description: isChinese
|
||||
? "在线版 XWorkmate 工作区,底层由 OpenClaw gateway 驱动。"
|
||||
: "Online XWorkmate workspace powered by the OpenClaw gateway.",
|
||||
href: "/xworkmate",
|
||||
icon: ClawdbotLogo,
|
||||
icon: Command,
|
||||
},
|
||||
];
|
||||
|
||||
if (!isHydrated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (view === "material") {
|
||||
return (
|
||||
<Material3Layout>
|
||||
<div className="mb-10">
|
||||
<h2 className="text-heading text-4xl font-black tracking-tight mb-2">
|
||||
Service Overview
|
||||
</h2>
|
||||
<p className="text-text-muted text-lg max-w-2xl">
|
||||
Real-time metrics and system health for your current production
|
||||
environment.
|
||||
</p>
|
||||
</div>
|
||||
<ServiceGrid
|
||||
view="material"
|
||||
services={services}
|
||||
isChinese={isChinese}
|
||||
/>
|
||||
</Material3Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
<div
|
||||
className="absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(56,189,248,0.18),transparent_35%),radial-gradient(circle_at_80%_0,rgba(168,85,247,0.15),transparent_30%),radial-gradient(circle_at_50%_60%,rgba(52,211,153,0.08),transparent_35%)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="relative w-full px-8 pb-20">
|
||||
<UnifiedNavigation />
|
||||
<main className="space-y-10 pt-10">
|
||||
<header className="space-y-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-slate-400">
|
||||
{isChinese ? "更多服务" : "More services"}
|
||||
</p>
|
||||
<h1 className="text-3xl font-semibold text-white sm:text-4xl">
|
||||
{isChinese ? "扩展服务与工具箱" : "Extended Services & Toolbox"}
|
||||
</h1>
|
||||
<p className="max-w-2xl text-sm text-slate-300">
|
||||
{isChinese
|
||||
? "汇聚开发辅助、运维监控与核心制品,构建无缝衔接的云原生工作台。"
|
||||
: "A unified hub for development aids, operations monitoring, and core artifacts."}
|
||||
</p>
|
||||
</header>
|
||||
<ServiceGrid
|
||||
view="classic"
|
||||
services={services}
|
||||
isChinese={isChinese}
|
||||
<PublicPageShell>
|
||||
<section className="rounded-[2.4rem] border border-slate-900/10 bg-[linear-gradient(180deg,#ffffff,#faf7f2)] p-6 shadow-[0_22px_50px_rgba(15,23,42,0.05)] sm:p-8 lg:p-10">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
|
||||
<PublicPageIntro
|
||||
eyebrow={isChinese ? "更多服务" : "More services"}
|
||||
title={
|
||||
isChinese ? "扩展服务与工具箱" : "Extended Services & Toolbox"
|
||||
}
|
||||
subtitle={
|
||||
isChinese
|
||||
? "把开发辅助、运维观测与核心资源整理成一组统一入口,延续首页同一套公开页语法。"
|
||||
: "A unified set of entry points for development tools, observability, and core resources, using the same public-page language as the homepage."
|
||||
}
|
||||
titleClassName={cn(
|
||||
isChinese
|
||||
? "text-[2.7rem] tracking-[-0.08em] sm:text-[3.4rem]"
|
||||
: "editorial-display text-[2.9rem] tracking-[-0.06em] sm:text-[3.6rem]",
|
||||
)}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 rounded-[1.75rem] border border-slate-900/10 bg-white/85 p-5 sm:min-w-[18rem]">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
{isChinese ? "页面原则" : "Page rhythm"}
|
||||
</p>
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
{isChinese
|
||||
? "保持结构不变,但去掉 classic / material 的风格分裂。"
|
||||
: "Keep the structure, remove the classic/material visual split."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[2rem] border border-slate-900/10 bg-white/90 p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:p-7">
|
||||
<div className="mb-5 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-text-subtle">
|
||||
{isChinese ? "服务目录" : "Service directory"}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-6 text-text-muted">
|
||||
{isChinese
|
||||
? "每个入口都使用同一种卡片语法:白底、细边框、轻阴影、明确标题。"
|
||||
: "Every entry now follows the same card grammar: pale surface, fine border, light shadow, and clear hierarchy."}
|
||||
</p>
|
||||
</div>
|
||||
<span className="inline-flex w-fit rounded-full border border-slate-900/10 bg-[#f8f4ec] px-3 py-1 text-xs font-semibold text-slate-700">
|
||||
{services.length} {isChinese ? "个入口" : "entries"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{services.map((service) => (
|
||||
<ServiceCard
|
||||
key={service.key}
|
||||
service={service}
|
||||
isChinese={isChinese}
|
||||
/>
|
||||
))}
|
||||
{Array.from({ length: placeholderCount }).map((_, index) => (
|
||||
<PlaceholderCard
|
||||
key={`placeholder-${index}`}
|
||||
isChinese={isChinese}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</section>
|
||||
</PublicPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { marked } from 'marked'
|
||||
import { marked } from "marked";
|
||||
|
||||
interface DocArticleProps {
|
||||
content: string
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default async function DocArticle({ content }: DocArticleProps) {
|
||||
// Convert markdown to HTML
|
||||
const htmlContent = await marked(content)
|
||||
const htmlContent = await marked(content);
|
||||
|
||||
return (
|
||||
<article
|
||||
className="prose prose-slate max-w-none dark:prose-invert prose-headings:scroll-mt-24 prose-a:text-brand prose-a:no-underline hover:prose-a:underline"
|
||||
className="public-doc-prose"
|
||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,29 +1,40 @@
|
||||
import ClientTime from '@/app/components/ClientTime'
|
||||
import ClientTime from "@/app/components/ClientTime";
|
||||
|
||||
interface DocMetaPanelProps {
|
||||
description?: string
|
||||
updatedAt?: string
|
||||
tags?: string[]
|
||||
description?: string;
|
||||
updatedAt?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export default function DocMetaPanel({ description, updatedAt, tags }: DocMetaPanelProps) {
|
||||
export default function DocMetaPanel({
|
||||
description,
|
||||
updatedAt,
|
||||
tags,
|
||||
}: DocMetaPanelProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 text-sm text-brand-heading">
|
||||
{description && <p className="text-brand-heading/80">{description}</p>}
|
||||
{tags && tags.length > 0 && (
|
||||
<div className="flex flex-col gap-4 text-sm text-slate-700">
|
||||
{description ? (
|
||||
<p className="leading-6 text-text-muted">{description}</p>
|
||||
) : null}
|
||||
|
||||
{tags && tags.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className="rounded-full border border-brand-border bg-brand-surface px-3 py-1 text-xs font-medium">
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full border border-slate-900/10 bg-[#fcfbf8] px-3 py-1 text-xs font-semibold text-slate-600"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{updatedAt && (
|
||||
<p className="text-xs text-brand-heading/70" suppressHydrationWarning>
|
||||
) : null}
|
||||
|
||||
{updatedAt ? (
|
||||
<p className="text-xs text-text-subtle" suppressHydrationWarning>
|
||||
Updated <ClientTime isoString={updatedAt} />
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
82
src/components/public/PublicPageShell.tsx
Normal file
82
src/components/public/PublicPageShell.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import UnifiedNavigation from "@/components/UnifiedNavigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PublicPageShellProps = {
|
||||
children: React.ReactNode;
|
||||
mainClassName?: string;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
type PublicPageIntroProps = {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
titleClassName?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PublicPageShell({
|
||||
children,
|
||||
mainClassName,
|
||||
containerClassName,
|
||||
}: PublicPageShellProps) {
|
||||
return (
|
||||
<div className="relative min-h-screen bg-background text-text transition-colors duration-150">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.58),rgba(255,255,255,0))]"
|
||||
/>
|
||||
<div className="relative">
|
||||
<UnifiedNavigation />
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto w-full max-w-6xl px-4 pb-16 sm:px-6 sm:pb-20",
|
||||
containerClassName,
|
||||
)}
|
||||
>
|
||||
<main
|
||||
className={cn(
|
||||
"space-y-8 pt-6 sm:space-y-10 sm:pt-10",
|
||||
mainClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PublicPageIntro({
|
||||
eyebrow,
|
||||
title,
|
||||
subtitle,
|
||||
titleClassName,
|
||||
className,
|
||||
}: PublicPageIntroProps) {
|
||||
return (
|
||||
<header className={cn("space-y-4", className)}>
|
||||
{eyebrow ? (
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-text-subtle">
|
||||
{eyebrow}
|
||||
</p>
|
||||
) : null}
|
||||
<h1
|
||||
className={cn(
|
||||
"max-w-4xl text-[2.5rem] font-semibold leading-[0.92] tracking-[-0.06em] text-heading sm:text-[3.2rem]",
|
||||
titleClassName,
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle ? (
|
||||
<p className="max-w-3xl text-[1rem] leading-8 text-text-muted sm:text-[1.05rem]">
|
||||
{subtitle}
|
||||
</p>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user