Merge main into release/v0.2

This commit is contained in:
Haitao Pan 2026-03-18 13:45:09 +08:00
commit 8d11e30d76
19 changed files with 1498 additions and 653 deletions

View File

@ -0,0 +1,218 @@
# UI 主题与风格重构方案
作为资深 Web UI 设计师和前端工程师,针对我们 SaaS 控制台当前的 Next.js + Tailwind CSS 架构,结合 WCAG 可访问性标准和跨端响应式需求,特制定此 UI 重构方案。方案旨在提升整体视觉一致性、深色模式的可访问性,并优化桌面与移动端的用户体验(特别是 iOS/Android 浏览器的触控和渲染差异)。
---
## 1. 审查现有界面
在审查当前的界面代码(如 `tailwind.config.js`, `src/app/globals.css`, `src/components/theme/``Navbar.tsx`)后,我发现了以下改进点:
* **色彩硬编码与对比度**:在部分文件(如 `designTokens.ts``Navbar.tsx` 的内联类名)中仍然存在类似 `#3467e9`, `bg-[#f6f7f9]` 的硬编码颜色,这破坏了主题切换的完整性。同时,深色模式下的次级文本(如 `text-muted` `#cbd5f5`)在深色背景(`#0f172a`)上的对比度可能无法满足 WCAG AA 级 4.5:1 的标准。
* **语义化不足**`Navbar.tsx` 中的菜单项过度使用了 `<div>` 和普通的 `<a>` 标签,缺少 `<nav>`, `<ul>`, `<li>` 结构,并且缺乏管理下拉/折叠状态的 `aria-expanded` 属性。
* **触控目标尺寸**:移动端的某些交互元素(如链接和图标按钮)没有保证至少 44px × 44px 的物理点击区域,这在 iOS/Android 设备上容易造成误触。
* **深色模式层级**:深色模式主要依赖背景色区分层级(如 `surface-muted`缺乏细微的边框Border或发光阴影Glow/Shadow来凸显浮动面板如 Dropdown 菜单)。
---
## 2. 定义设计系统 Token
我们需要收敛硬编码颜色,改用语义化的 CSS 变量Token并对深/浅色模式设定严格的对比度要求。
### 颜色 Token 规划
* **背景色**
* 浅色:纯白 `#ffffff` (Surface) 或极浅灰 `#f8fafc` (Background)。
* 深色:避免纯黑,使用 `#0f172a` (Background) 和微亮的暗灰 `#1e293b` (Surface),减轻视觉疲劳。
* **文本色**
* 主要文本 (`--color-text`):浅色模式 `#0f172a`,深色模式 `#f8fafc`
* 次要文本 (`--color-text-muted`):确保在深浅背景下对比度均大于 4.5:1。浅色推荐 `#475569`,深色推荐 `#94a3b8`
* **主色与交互色**
* `--color-primary`:统一使用高对比度的主题蓝(如 `#2563eb`),确保按钮上的白色文字(`--color-primary-foreground`)对比度达标。
### Tailwind / CSS 变量伪代码
```css
/* src/app/globals.css 补充与覆盖 */
:root {
/* 基础结构色 */
--bg-color: #f8fafc;
--surface-color: #ffffff;
/* 文本色 */
--text-color: #0f172a;
--secondary-color: #475569;
/* 主色调 */
--primary: #2563eb;
--primary-foreground: #ffffff;
/* 边框与分隔线 */
--border-color: #e2e8f0;
}
:root[data-theme="dark"],
.dark {
--bg-color: #0f172a;
--surface-color: #1e293b;
--text-color: #f8fafc;
--secondary-color: #94a3b8; /* 保证对比度 > 4.5:1 */
--primary: #3b82f6; /* 在深色背景下稍微提亮主色 */
--primary-foreground: #ffffff;
--border-color: #334155;
/* 深色模式下的特殊视觉补偿 */
--shadow-elevation: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -2px rgba(0, 0, 0, 0.5);
}
```
---
## 3. 设计排版体系
排版需兼顾多语言(中英文混合)和多端阅读体验。
* **字体选择**:保留现有的 `Geist`,同时后备配置 `Inter``Noto Sans SC` 以优化中文显示:
`font-family: var(--font-geist-sans), 'Inter', 'Noto Sans SC', sans-serif;`
* **字号范围 (Fluid Typography / 断点响应)**
* **移动端**:基础字号 16px防止 iOS Safari 输入框自动缩放),正文 `16px - 18px`,大标题约 `24px - 28px`
* **桌面端**:正文 `16px - 20px`,大标题可达 `32px - 48px`
* **行高与间距**:正文行高 `1.5``1.6`150%-160%),段落间距使用 `margin-bottom: 1.5em`。标题行高缩紧至 `1.2`
---
## 4. 构建全局样式与主题切换
当前系统已通过 Zustand (`store.ts`) 和 `ThemeProvider.tsx` 实现了基于 `localStorage``data-theme` 的切换。
* **优化防闪烁 (FOUC)**:由于在 Next.js 中客户端 Hydration 会有延迟,需要确保在 `<head>` 中注入一个同步脚本读取 `localStorage``prefers-color-scheme`,在 React 渲染前就应用 `dark` 类或 `data-theme`
* **深色模式下的层级分离**避免仅仅改变背景色。对于弹窗、Dropdown 等元素,在深色模式下增加一个极细的亮色边框:
```css
.dark .surface-elevated {
border: 1px solid rgba(255,255,255,0.1);
box-shadow: var(--shadow-elevation);
}
```
---
## 5. 响应式布局策略
* **断点定义**
* 移动端:`< 768px` (Tailwind 默认 `md` 以下)。
* 平板/桌面端:`>= 768px` (`md` 及以上)。
* **移动端 (iOS/Android) 策略**
* 隐藏复杂的侧边栏Sidebar顶部导航精简为 Logo 和汉堡菜单。
* 所有的按钮 (`button`, `a`) 必须拥有至少 `min-h-[44px] min-w-[44px]` 的触控区域(可以使用 padding 撑开)。
* **桌面端策略**
* 最大化利用横向空间,采用 Sidebar + Main Content 或全宽 Header 的多列布局。
---
## 6. 重构导航菜单 (示例)
当前 `Navbar.tsx` 使用了平铺的链接。我们需要使用语义化标签,并通过 `aria-expanded` 增强可访问性,并分离移动端的汉堡菜单和桌面端菜单。
### 菜单组件伪代码
```tsx
'use client'
import { useState } from 'react';
export default function SemanticNavbar() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
return (
<header className="sticky top-0 z-50 w-full bg-surface border-b border-border">
<nav className="max-w-7xl mx-auto px-4 min-h-[64px] flex items-center justify-between" aria-label="Main Navigation">
{/* Logo */}
<div className="flex-shrink-0">
<a href="/" className="font-bold text-text-color text-lg flex items-center min-h-[44px]">Logo</a>
</div>
{/* Desktop Menu */}
<ul className="hidden md:flex items-center gap-6">
<li>
<a href="/dashboard" className="text-secondary-color hover:text-text-color transition-colors py-2">
控制台
</a>
</li>
<li className="relative group">
{/* 父菜单如果是功能性展开按钮,使用 button */}
<button
aria-expanded={isDropdownOpen}
aria-controls="services-dropdown"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center gap-1 text-secondary-color hover:text-text-color py-2"
>
服务 <ChevronDownIcon />
</button>
{/* Dropdown 弹窗 */}
<ul
id="services-dropdown"
className={`absolute top-full left-0 mt-2 w-48 bg-surface-elevated border border-border rounded-md shadow-md ${isDropdownOpen ? 'block' : 'hidden'}`}
>
<li>
{/* 保证键盘 Tab 键能聚焦到这里 */}
<a href="/service/1" className="block px-4 py-3 text-text-color hover:bg-surface-hover">服务一</a>
</li>
</ul>
</li>
</ul>
{/* Mobile Hamburger Button */}
<div className="md:hidden">
<button
aria-controls="mobile-menu"
aria-expanded={isMobileMenuOpen}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="p-2 min-w-[44px] min-h-[44px] text-text-color"
aria-label="Toggle navigation"
>
<HamburgerIcon />
</button>
</div>
</nav>
{/* Mobile Menu Panel */}
<div
id="mobile-menu"
className={`md:hidden overflow-hidden transition-[max-height] duration-300 ease-in-out ${isMobileMenuOpen ? 'max-h-screen' : 'max-h-0'}`}
>
<ul className="px-4 pb-4 space-y-2 bg-surface">
<li>
<a href="/dashboard" className="block py-3 text-text-color">控制台</a>
</li>
{/* 移动端子菜单可以直接平铺或使用手风琴折叠 */}
<li>
<span className="block py-3 text-secondary-color font-semibold">服务</span>
<ul className="pl-4 space-y-2 border-l-2 border-border ml-2">
<li><a href="/service/1" className="block py-3 text-text-color">服务一</a></li>
</ul>
</li>
</ul>
</div>
</header>
);
}
```
---
## 7. 针对移动端和桌面端的优化差异
* **iOS/Android 浏览器差异处理**
* **iOS Safari 底部安全区**:在主内容区底部增加 `padding-bottom: env(safe-area-inset-bottom);`,避免按钮被 Home Indicator 遮挡。
* **字体抗锯齿 (Anti-aliasing)**:在 `globals.css` 中的 `body` 标签已设置 `-webkit-font-smoothing: antialiased;`,在深色模式下这能让细小的亮色文字边缘更加平滑,没有晕影。
* **长文本限制**:在博客或文档页面,容器设置 `max-w-prose` (相当于 `max-width: 65ch`),确保每行不超过 60 个字符,提升阅读体验。
* **桌面端强化**
* 可利用 Hover 状态提供丰富的反馈(如背景变色、细微的 `transform: translateY(-1px)`)。
* 在宽屏上显示侧边栏和次要信息列(多列网格布局)。
---
## 8. 可访问性测试与迭代流程
为确保重构后的界面符合标准,开发团队应在提交代码前进行以下检查:
1. **对比度扫描**:使用 Chrome DevTools 的 Lighthouse 或 WebAIM 对比度检查器,确保所有的正文/背景对比度 >= 4.5:1大号文本 >= 3.0:1。
2. **重排与缩放测试**:使用浏览器的缩放功能放大页面到 `200%``400%`。确保在 `400%` 下页面自动转为单列移动端布局,且文字不出现截断或相互重叠(开启 CSS 文本重排 `text-wrap: balance` 或避免固定高度)。
3. **键盘导航测试**:不使用鼠标,仅用 `Tab``Enter` 遍历界面。确保所有交互元素有明显的 `:focus-visible` 外框线Tailwind `focus-visible:ring-2 focus-visible:ring-primary`)。二级菜单不能在 Tab 聚焦到父元素时自动弹出(除非是用按钮触发),以避免用户被迫穿过无数个子菜单才能到达下一个主栏目。
4. **屏幕阅读器体验**:开启 VoiceOver (Mac/iOS) 或 TalkBack (Android),验证 `aria-expanded`, `aria-controls` 等状态是否能被正确播报。

BIN
next_output.log Normal file

Binary file not shown.

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
)
);
}

View File

@ -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>
)
);
}
}

View File

@ -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) {

View File

@ -21,10 +21,13 @@ import {
import useSWR from "swr";
import Footer from "../components/Footer";
import { HeroCard } from "../components/HeroCard";
import UnifiedNavigation from "../components/UnifiedNavigation";
import { useLanguage } from "../i18n/LanguageProvider";
import { translations } from "../i18n/translations";
import {
heroVideoMedia,
type HeroVideoMedia,
} from "../lib/home/heroVideoMedia";
import { useMoltbotStore } from "../lib/moltbotStore";
import { useUserStore } from "../lib/userStore";
import { cn } from "../lib/utils";
@ -182,37 +185,12 @@ export function HeroSection() {
</div>
</div>
<div className="flex flex-col gap-3 lg:pl-4">
<div className="flex flex-col gap-2 border-b border-slate-900/10 pb-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>
{isChinese ? "主要入口" : "Launch paths"}
</p>
<p className="mt-2 max-w-md text-sm leading-6 text-text-muted">
{isChinese
? "从接入、托管到观测,保留原有入口,但改成更轻的阅读节奏。"
: "Keep the same entry points, but present them with a calmer editorial rhythm."}
</p>
</div>
<span className="hidden rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-semibold text-slate-600 sm:inline-flex">
{t.heroCards.length} {isChinese ? "个入口" : "entry paths"}
</span>
</div>
<div className="space-y-3">
{t.heroCards.map((card) => {
const Icon = getIcon(card.title, PlusCircle);
return (
<HeroCard
key={card.title}
icon={Icon}
title={card.title}
description={card.description}
guide={card.guide}
/>
);
})}
</div>
<div className="lg:pl-4">
<HeroVideoShell
items={t.heroCards.map((card) => card.title)}
isChinese={isChinese}
media={heroVideoMedia}
/>
</div>
</div>
</section>
@ -498,6 +476,150 @@ type LatestBlogPost = {
date?: string;
};
function HeroVideoShell({
items,
isChinese,
media,
}: {
items: string[];
isChinese: boolean;
media: HeroVideoMedia;
}) {
const mediaTitle = isChinese ? media.title.zh : media.title.en;
const mediaDescription = isChinese
? media.description.zh
: media.description.en;
const mediaStatusLabel = isChinese
? media.statusLabel.zh
: media.statusLabel.en;
const hasVideo = Boolean(media.videoUrl);
const previewStyle = media.posterUrl
? {
backgroundImage: `linear-gradient(180deg,rgba(15,23,42,0.18),rgba(15,23,42,0.52)), url(${media.posterUrl})`,
backgroundSize: "cover",
backgroundPosition: "center",
}
: undefined;
return (
<div className="overflow-hidden rounded-[2rem] border border-slate-900/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.88),rgba(244,247,252,0.96))] shadow-[0_24px_60px_rgba(15,23,42,0.08)]">
<div className="border-b border-slate-900/10 px-5 py-4 sm:px-6">
<div className="flex items-start justify-between gap-4">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>
{isChinese ? "产品演示" : "Product demo"}
</p>
<p className="mt-2 max-w-md text-sm leading-6 text-text-muted">
{isChinese
? "这里预留为视频展示区,后续可以直接替换成产品介绍、工作流演示或 onboarding 视频。"
: "Reserved for a video showcase. You can later replace it with a product intro, workflow demo, or onboarding clip."}
</p>
</div>
<span className="hidden rounded-full border border-slate-900/10 bg-white/90 px-3 py-1 text-xs font-semibold text-slate-600 sm:inline-flex">
{isChinese ? "16:9 占位" : "16:9 shell"}
</span>
</div>
</div>
<div className="space-y-4 p-4 sm:p-5">
<div
className={cn(
"group relative aspect-video overflow-hidden rounded-[1.6rem] border border-slate-900/10",
!hasVideo &&
"bg-[radial-gradient(circle_at_top_left,rgba(51,102,255,0.16),transparent_34%),linear-gradient(135deg,#0f172a,#172033_52%,#1f2d4d)]",
)}
style={previewStyle}
>
{hasVideo ? (
<video
className="h-full w-full object-cover"
controls
playsInline
preload="metadata"
poster={media.posterUrl || undefined}
>
<source src={media.videoUrl} />
</video>
) : (
<>
<div
aria-hidden
className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(15,23,42,0.18))]"
/>
<div
aria-hidden
className="absolute left-5 top-5 h-20 w-20 rounded-full bg-[radial-gradient(circle,rgba(255,255,255,0.18),transparent_70%)] blur-2xl"
/>
<div
aria-hidden
className="absolute right-[-1.5rem] top-[-1.5rem] h-28 w-28 rounded-full border border-white/10"
/>
</>
)}
<div className="absolute inset-0 flex flex-col justify-between p-5 sm:p-6">
<div className="flex items-center justify-between gap-3">
<span className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/10 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.2em] text-white/80 backdrop-blur">
<span className="h-2 w-2 rounded-full bg-emerald-400" />
{mediaStatusLabel}
</span>
<span className="rounded-full border border-white/10 bg-black/10 px-3 py-1 text-xs font-medium text-white/70">
{media.durationLabel}
</span>
</div>
<div className="space-y-4">
{!hasVideo ? (
<button
type="button"
className="inline-flex h-16 w-16 items-center justify-center rounded-full border border-white/15 bg-white/12 text-white shadow-[0_14px_35px_rgba(15,23,42,0.25)] backdrop-blur transition group-hover:scale-[1.02]"
>
<Play className="ml-1 h-7 w-7" fill="currentColor" />
</button>
) : null}
<div className="max-w-lg space-y-2">
<p className="text-xl font-semibold tracking-[-0.03em] text-white sm:text-2xl">
{mediaTitle}
</p>
<p className="text-sm leading-6 text-white/72 sm:text-[0.95rem]">
{mediaDescription}
</p>
</div>
</div>
<div className="space-y-3">
<div className="h-1.5 overflow-hidden rounded-full bg-white/12">
<div className="h-full w-[28%] rounded-full bg-white/75" />
</div>
<div className="grid grid-cols-3 gap-2 text-[11px] font-medium text-white/60 sm:text-xs">
{media.chapters.map((chapter) => (
<span
key={chapter.en}
className="rounded-full border border-white/10 bg-white/8 px-3 py-2 text-center"
>
{isChinese ? chapter.zh : chapter.en}
</span>
))}
</div>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<span
key={item}
className="inline-flex items-center rounded-full border border-slate-900/10 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600"
>
{item}
</span>
))}
</div>
</div>
</div>
);
}
function LogoPill({ label }: { label: string }) {
return (
<span className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-3.5 py-1.5 text-xs font-semibold text-slate-700">

View File

@ -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>
);
}

View File

@ -1,6 +1,6 @@
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic";
import { XWorkmateProfileEditor } from "@/components/xworkmate/XWorkmateProfileEditor";
import {
buildSharedXWorkmateUrl,

View File

@ -1,6 +1,6 @@
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic";
import { XWorkmateProfileEditor } from "@/components/xworkmate/XWorkmateProfileEditor";
import {
buildSharedXWorkmateUrl,

View File

@ -0,0 +1,9 @@
export const dynamic = "force-dynamic";
export default function XWorkmateLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@ -2,6 +2,8 @@ import { Suspense } from "react";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic";
import { XWorkmateLoading } from "@/app/xworkmate/XWorkmateLoading";
import { XWorkmateWorkspacePage } from "@/components/xworkmate/XWorkmateWorkspacePage";
import {

View File

@ -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 }}
/>
)
);
}

View File

@ -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>
)
);
}

View 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>
);
}

View File

@ -12,12 +12,15 @@ import {
Grip,
KeyRound,
ListTodo,
Paperclip,
Puzzle,
RefreshCw,
Send,
Settings2,
Shield,
Sparkles,
UserCircle2,
Zap,
} from "lucide-react";
import { useRouter } from "next/navigation";
@ -73,7 +76,7 @@ type DetailCardProps = {
meta: string;
};
function pickCopy(isChinese: boolean, zh: string, en: string): string {
function pickCopy<T>(isChinese: boolean, zh: T, en: T): T {
return isChinese ? zh : en;
}
@ -506,6 +509,7 @@ function AssistantHome({
secondaryActionLabel,
connectionHint,
actionDisabled,
isSharedProfile,
}: {
isChinese: boolean;
tabs: SectionTab[];
@ -518,122 +522,122 @@ function AssistantHome({
secondaryActionLabel: string;
connectionHint?: string;
actionDisabled?: boolean;
isSharedProfile?: boolean;
}) {
return (
<>
<div className="rounded-[28px] border border-[color:var(--color-surface-border)] bg-white/96 px-6 py-5 shadow-[0_18px_50px_rgba(15,23,42,0.06)]">
<div className="flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold text-[var(--color-text-subtle)]">
<DesktopChip label={pickCopy(isChinese, "主页", "Home")} />
<ChevronRight className="h-4 w-4" />
<DesktopChip
label={pickCopy(isChinese, "默认任务", "Default Task")}
/>
</div>
<h1 className="mt-4 text-[20px] font-semibold tracking-[-0.03em] text-black">
{pickCopy(isChinese, "默认任务", "Default Task")}
</h1>
<p className="mt-1 text-[15px] text-[var(--color-text-subtle)]">
{pickCopy(
isChinese,
"连接 Gateway 后,当前对话会自动作为默认任务开始执行。",
"After connecting the gateway, the current conversation starts as the default task.",
)}
</p>
<div className="mt-5 flex flex-wrap gap-3">
{tabs.map((tab, index) => (
<DesktopChip
key={tab.key}
label={tab.label}
active={index === 0}
/>
))}
</div>
</div>
<div className="inline-flex h-fit items-center rounded-full border border-[color:var(--color-surface-border)] bg-white px-4 py-2 text-sm font-semibold text-black">
{connected
? `${pickCopy(isChinese, "在线", "Online")} · ${endpointLabel}`
: pickCopy(isChinese, "离线 · 未连接目标", "Offline · No target")}
</div>
</div>
</div>
const suggestions = pickCopy(
isChinese,
[
"幻灯片",
"视频生成",
"深度研究",
"文档处理",
"数据分析",
"可视化",
"金融服务",
"产品管理",
"设计",
"邮件编辑",
],
[
"Slides",
"Video Gen",
"Deep Research",
"Docs Processing",
"Data Analysis",
"Visualization",
"Finance",
"Product Management",
"Design",
"Email Edit",
]
);
<div className="mt-5 flex min-h-[540px] flex-1 rounded-[28px] border border-[color:var(--color-surface-border)] bg-[linear-gradient(180deg,#f6f9ff_0%,#f8fbff_18%,#ffffff_62%)] shadow-[0_24px_64px_rgba(15,23,42,0.05)]">
<div className="flex flex-1 items-center justify-center rounded-[28px] border border-transparent bg-[radial-gradient(circle_at_top,_rgba(51,102,255,0.08),_transparent_34%)] p-6">
<div className="w-full max-w-[600px] rounded-[24px] border border-[color:var(--color-surface-border)] bg-white/95 p-7 shadow-[0_14px_30px_rgba(15,23,42,0.08)]">
<h2 className="text-[24px] font-semibold tracking-[-0.03em] text-black">
{pickCopy(isChinese, "先连接 Gateway", "Connect Gateway First")}
</h2>
<p className="mt-3 text-[15px] leading-7 text-[var(--color-text-subtle)]">
{pickCopy(
isChinese,
"连接后可直接对话、创建任务,并在当前会话查看结果。",
"Connect first to start chatting, create tasks, and view results in the current conversation.",
)}
</p>
{connectionHint ? (
<p className="mt-3 text-sm leading-6 text-[var(--color-text-subtle)]">
{connectionHint}
</p>
) : null}
<div className="mt-6 flex flex-wrap gap-3">
return (
<div className="flex h-full flex-col p-4">
<div className="flex-1 overflow-y-auto min-h-0">
{!isSharedProfile && (
<div className="mb-6 rounded-[16px] border border-[color:var(--color-surface-border)] bg-white/96 p-5 shadow-sm">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold text-black">
{pickCopy(isChinese, "未连接 Gateway", "Gateway Disconnected")}
</h2>
<p className="mt-1 text-sm text-[var(--color-text-subtle)]">
{connectionHint || pickCopy(isChinese, "请连接 Gateway 以获取完整能力。", "Please connect Gateway for full capabilities.")}
</p>
</div>
<button
type="button"
onClick={onOpenConnections}
disabled={actionDisabled}
className="inline-flex h-11 items-center gap-2 rounded-[14px] bg-[var(--color-primary)] px-5 text-sm font-semibold text-white shadow-[0_10px_24px_rgba(51,102,255,0.28)] transition hover:bg-[var(--color-primary-hover)] disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex h-9 items-center gap-2 rounded-lg bg-[var(--color-primary)] px-4 text-sm font-semibold text-white transition hover:bg-[var(--color-primary-hover)] disabled:cursor-not-allowed disabled:opacity-60"
>
<RefreshCw className="h-4 w-4" />
{primaryActionLabel}
</button>
<button
type="button"
onClick={onOpenConnections}
disabled={actionDisabled}
className="inline-flex h-11 items-center gap-2 rounded-[14px] border border-[color:var(--color-surface-border)] bg-white px-5 text-sm font-semibold text-[var(--color-heading)] transition hover:bg-[var(--color-surface-hover)] disabled:cursor-not-allowed disabled:opacity-60"
>
<Settings2 className="h-4 w-4" />
{secondaryActionLabel}
</button>
</div>
</div>
</div>
)}
</div>
<div className="mt-5 rounded-[24px] border border-[color:var(--color-surface-border)] bg-white/96 p-4 shadow-[var(--shadow-sm)]">
<textarea
value={prompt}
onChange={(event) => onPromptChange(event.target.value)}
placeholder={pickCopy(
isChinese,
"直接描述需求:运行任务、分析日志、部署节点......",
"Describe the task directly: run jobs, inspect logs, deploy nodes...",
)}
className="min-h-[90px] w-full resize-none rounded-[18px] border border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)] px-6 py-5 text-[15px] text-[var(--color-heading)] outline-none transition placeholder:text-[var(--color-text-subtle)] focus:border-[color:var(--color-primary-border)] focus:bg-white"
/>
<div className="mt-4 flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-wrap gap-3">
<ToolbarChip label={pickCopy(isChinese, "远程", "Remote")} />
<ToolbarChip
label={pickCopy(isChinese, "默认权限", "Default Access")}
/>
<ToolbarChip label="z-ai/glm5" active />
<ToolbarChip label={pickCopy(isChinese, "问答", "Ask")} />
<ToolbarChip label={pickCopy(isChinese, "高", "High")} />
<div className="mt-auto shrink-0 mx-auto w-full max-w-4xl pt-4">
<div className="rounded-[16px] border border-[color:var(--color-surface-border)] bg-white shadow-[0_4px_24px_rgba(15,23,42,0.04)] transition-all focus-within:border-[color:var(--color-primary-border)] focus-within:ring-1 focus-within:ring-[color:var(--color-primary-border)]">
<div className="p-2 border-b border-transparent">
<button type="button" className="p-2 text-[var(--color-text-subtle)] hover:text-black transition-colors rounded-lg hover:bg-[var(--color-surface-hover)]">
<Paperclip className="h-[18px] w-[18px]" />
</button>
</div>
<button
type="button"
onClick={onOpenConnections}
disabled={actionDisabled}
className="inline-flex h-11 items-center justify-center gap-2 self-end rounded-[14px] bg-[var(--color-primary)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--color-primary-hover)] disabled:cursor-not-allowed disabled:opacity-60"
>
<RefreshCw className="h-4 w-4" />
{primaryActionLabel}
</button>
<textarea
value={prompt}
onChange={(event) => onPromptChange(event.target.value)}
placeholder={pickCopy(
isChinese,
"输入消息...",
"Enter a message..."
)}
className="w-full resize-none bg-transparent px-4 py-2 text-[15px] leading-relaxed text-[var(--color-heading)] outline-none placeholder:text-[var(--color-text-subtle)] min-h-[80px]"
/>
<div className="flex items-center justify-between p-3">
<div className="flex items-center gap-2">
<button type="button" className="inline-flex h-8 items-center gap-1.5 rounded-md border border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)] px-2.5 text-xs font-medium text-[var(--color-text-subtle)] transition hover:bg-white hover:text-black">
<Bot className="h-3.5 w-3.5" />
Agent
<ChevronRight className="h-3 w-3 rotate-90" />
</button>
<button type="button" className="inline-flex h-8 items-center gap-1.5 rounded-md bg-white px-2.5 text-xs font-medium text-black transition hover:bg-[var(--color-surface-hover)]">
<span className="flex h-4 w-4 items-center justify-center rounded bg-black text-[10px] font-bold text-white">Z</span>
GLM-5.0
<ChevronRight className="h-3 w-3 rotate-90" />
</button>
<button type="button" className="flex h-8 w-8 items-center justify-center rounded-full border border-[color:var(--color-surface-border)] bg-white text-[var(--color-text-subtle)] transition hover:text-black hover:border-[color:var(--color-text-subtle)]">
<Zap className="h-3.5 w-3.5" />
</button>
</div>
<button
type="button"
className={cn(
"flex h-8 w-8 items-center justify-center rounded-lg transition",
prompt.trim() ? "bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)]" : "bg-[var(--color-surface-muted)] text-[var(--color-text-subtle)]"
)}
>
<Send className="h-4 w-4" />
</button>
</div>
</div>
<div className="mt-4 flex flex-wrap items-center justify-center gap-2 pb-2">
{suggestions.map((suggestion) => (
<button
key={suggestion}
type="button"
className="inline-flex h-8 items-center rounded-full border border-[color:var(--color-surface-border)] bg-white px-3.5 text-[13px] text-[var(--color-text-subtle)] transition hover:border-[color:var(--color-text-subtle)] hover:text-black"
>
{suggestion}
</button>
))}
</div>
</div>
</>
</div>
);
}
@ -710,6 +714,7 @@ export function XWorkmateWorkspacePage({
const [activeSection, setActiveSection] =
useState<WorkspaceDestination>("assistant");
const [composerValue, setComposerValue] = useState("");
const [sidebarExpanded, setSidebarExpanded] = useState(true);
const setScope = useOpenClawConsoleStore((state) => state.setScope);
const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults);
@ -799,72 +804,88 @@ export function XWorkmateWorkspacePage({
<div className="relative h-full overflow-hidden bg-[linear-gradient(180deg,#f4f7fd_0%,#f6f8fb_32%,#f3f5f8_100%)] text-[var(--color-text)]">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(51,102,255,0.10),_transparent_26%),radial-gradient(circle_at_bottom_right,_rgba(15,23,42,0.05),_transparent_24%)]" />
<div className="relative flex h-full min-h-0 gap-3 p-3">
<aside className="flex w-[76px] shrink-0 flex-col rounded-[26px] border border-white/80 bg-[rgba(255,255,255,0.74)] p-3 shadow-[0_20px_40px_rgba(15,23,42,0.06)] backdrop-blur">
<div className="flex flex-col gap-2">
{primarySections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
</div>
<div className="relative flex h-full min-h-0">
{sidebarExpanded ? (
<aside className="flex w-[76px] shrink-0 flex-col border-r border-white/80 bg-[rgba(255,255,255,0.74)] p-3 shadow-[0_20px_40px_rgba(15,23,42,0.06)] backdrop-blur z-10 transition-all duration-300">
<div className="flex flex-col gap-2">
{primarySections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
</div>
<div className="my-4 h-px bg-[var(--color-divider)]" />
<div className="my-4 h-px bg-[var(--color-divider)]" />
<div className="flex flex-col gap-2">
{workspaceSections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
</div>
<div className="flex flex-col gap-2">
{workspaceSections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
</div>
<div className="my-4 h-px bg-[var(--color-divider)]" />
<div className="my-4 h-px bg-[var(--color-divider)]" />
<div className="flex flex-col gap-2">
{toolSections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
</div>
<div className="flex flex-col gap-2">
{toolSections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
</div>
<div className="mt-auto flex flex-col gap-2">
{footerSections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
<div className="mt-auto flex flex-col gap-2">
{footerSections.map((section) => (
<SidebarButton
key={section.key}
icon={section.icon}
label={section.label}
active={section.key === activeSection}
onClick={() => setActiveSection(section.key)}
/>
))}
<button
type="button"
onClick={() => setSidebarExpanded(false)}
className="mt-2 flex h-12 w-12 items-center justify-center rounded-[16px] border border-transparent bg-white/50 text-[var(--color-text-subtle)] transition hover:text-[var(--color-heading)]"
>
<ChevronsRight className="h-[18px] w-[18px] rotate-180" />
</button>
</div>
</aside>
) : (
<div className="absolute left-0 top-4 z-20 px-2">
<button
type="button"
className="mt-2 flex h-12 w-12 items-center justify-center rounded-[16px] border border-[color:var(--color-surface-border)] bg-white/85 text-[var(--color-text-subtle)] transition hover:text-[var(--color-heading)]"
onClick={() => setSidebarExpanded(true)}
className="flex h-10 w-10 items-center justify-center rounded-lg border border-white/80 bg-white/70 shadow-sm text-[var(--color-text-subtle)] transition hover:text-[var(--color-heading)] backdrop-blur"
>
<ChevronsRight className="h-[18px] w-[18px]" />
<ListTodo className="h-[18px] w-[18px]" />
</button>
</div>
</aside>
)}
<main className="flex min-h-0 flex-1 flex-col rounded-[30px] border border-white/75 bg-[rgba(255,255,255,0.54)] p-3 shadow-[0_24px_64px_rgba(15,23,42,0.07)] backdrop-blur">
<div className="min-h-0 flex-1 rounded-[28px] border border-white/80 bg-[rgba(248,250,252,0.78)] p-3">
<div className="mx-auto flex h-full max-w-[1680px] min-h-0 flex-col">
<main className={cn(
"flex min-h-0 flex-1 flex-col bg-[rgba(255,255,255,0.54)] shadow-[0_24px_64px_rgba(15,23,42,0.07)] backdrop-blur transition-all duration-300",
!sidebarExpanded && "pl-14"
)}>
<div className="min-h-0 flex-1 bg-[rgba(248,250,252,0.78)]">
<div className="flex h-full min-h-0 flex-col">
{profile ? (
<div className="mb-3 flex flex-wrap items-center gap-2 rounded-[22px] border border-[color:var(--color-surface-border)] bg-white/90 px-5 py-4 text-sm text-[var(--color-text-subtle)] shadow-[var(--shadow-sm)]">
<div className="flex flex-wrap items-center gap-2 border-b border-[color:var(--color-surface-border)] bg-white/90 px-5 py-3 text-sm text-[var(--color-text-subtle)] shadow-[var(--shadow-sm)]">
<Shield className="h-4 w-4 text-[var(--color-primary)]" />
<span>
{profile.edition === "shared_public"
@ -898,6 +919,7 @@ export function XWorkmateWorkspacePage({
secondaryActionLabel={secondaryActionLabel}
connectionHint={connectionHint}
actionDisabled={!canEditIntegrations}
isSharedProfile={profile?.profileScope === "tenant-shared"}
/>
) : (
<SectionOverview

View File

@ -0,0 +1,44 @@
export type HeroVideoMedia = {
posterUrl?: string;
videoUrl?: string;
title: {
zh: string;
en: string;
};
description: {
zh: string;
en: string;
};
statusLabel: {
zh: string;
en: string;
};
durationLabel: string;
chapters: {
zh: string;
en: string;
}[];
};
export const heroVideoMedia: HeroVideoMedia = {
posterUrl: "",
videoUrl: "",
title: {
zh: "用一段视频解释从灵感到上线的完整路径",
en: "Show the full path from idea to launch in one video",
},
description: {
zh: "建议后续放 60 到 120 秒的产品导览、集成配置流程,或真实部署 walkthrough。",
en: "Best used for a 60-120 second product tour, integration setup flow, or real deployment walkthrough.",
},
statusLabel: {
zh: "视频待接入",
en: "Video pending",
},
durationLabel: "00:00 / 02:18",
chapters: [
{ zh: "开场介绍", en: "Intro" },
{ zh: "集成配置", en: "Setup" },
{ zh: "上线演示", en: "Launch" },
],
};

114
update_layout.patch Normal file
View File

@ -0,0 +1,114 @@
--- src/components/xworkmate/XWorkmateWorkspacePage.tsx
+++ src/components/xworkmate/XWorkmateWorkspacePage.tsx
@@ -582,6 +582,7 @@
const [activeSection, setActiveSection] =
useState<WorkspaceDestination>("assistant");
const [composerValue, setComposerValue] = useState("");
+ const [sidebarExpanded, setSidebarExpanded] = useState(true);
const setScope = useOpenClawConsoleStore((state) => state.setScope);
const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults);
@@ -663,20 +664,22 @@
<div className="relative h-full overflow-hidden bg-[linear-gradient(180deg,#f4f7fd_0%,#f6f8fb_32%,#f3f5f8_100%)] text-[var(--color-text)]">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(51,102,255,0.10),_transparent_26%),radial-gradient(circle_at_bottom_right,_rgba(15,23,42,0.05),_transparent_24%)]" />
- <div className="relative flex h-full min-h-0 gap-3 p-3">
- <aside className="flex w-[76px] shrink-0 flex-col rounded-[26px] border border-white/80 bg-[rgba(255,255,255,0.74)] p-3 shadow-[0_20px_40px_rgba(15,23,42,0.06)] backdrop-blur">
- <div className="flex flex-col gap-2">
- {primarySections.map((section) => (
- <SidebarButton
- key={section.key}
- icon={section.icon}
- label={section.label}
- active={section.key === activeSection}
- onClick={() => setActiveSection(section.key)}
- />
- ))}
- </div>
-
+ <div className="relative flex h-full min-h-0">
+ {sidebarExpanded ? (
+ <aside className="flex w-[76px] shrink-0 flex-col border-r border-white/80 bg-[rgba(255,255,255,0.74)] p-3 shadow-[0_20px_40px_rgba(15,23,42,0.06)] backdrop-blur z-10 transition-all duration-300">
+ <div className="flex flex-col gap-2">
+ {primarySections.map((section) => (
+ <SidebarButton
+ key={section.key}
+ icon={section.icon}
+ label={section.label}
+ active={section.key === activeSection}
+ onClick={() => setActiveSection(section.key)}
+ />
+ ))}
+ </div>
+
+ <div className="my-4 h-px bg-[var(--color-divider)]" />
+
+ <div className="flex flex-col gap-2">
+ {workspaceSections.map((section) => (
+ <SidebarButton
+ key={section.key}
+ icon={section.icon}
+ label={section.label}
+ active={section.key === activeSection}
+ onClick={() => setActiveSection(section.key)}
+ />
+ ))}
+ </div>
+
+ <div className="my-4 h-px bg-[var(--color-divider)]" />
+
+ <div className="flex flex-col gap-2">
+ {toolSections.map((section) => (
+ <SidebarButton
+ key={section.key}
+ icon={section.icon}
+ label={section.label}
+ active={section.key === activeSection}
+ onClick={() => setActiveSection(section.key)}
+ />
+ ))}
+ </div>
+
+ <div className="mt-auto flex flex-col gap-2">
+ {footerSections.map((section) => (
+ <SidebarButton
+ key={section.key}
+ icon={section.icon}
+ label={section.label}
+ active={section.key === activeSection}
+ onClick={() => setActiveSection(section.key)}
+ />
+ ))}
+ <button
+ type="button"
+ onClick={() => setSidebarExpanded(false)}
+ className="mt-2 flex h-12 w-12 items-center justify-center rounded-[16px] border border-transparent bg-white/50 text-[var(--color-text-subtle)] transition hover:text-[var(--color-heading)]"
+ >
+ <ChevronsRight className="h-[18px] w-[18px] rotate-180" />
+ </button>
+ </div>
+ </aside>
+ ) : (
+ <div className="absolute left-0 top-4 z-20 px-2">
+ <button
+ type="button"
+ onClick={() => setSidebarExpanded(true)}
+ className="flex h-10 w-10 items-center justify-center rounded-lg border border-white/80 bg-white/70 shadow-sm text-[var(--color-text-subtle)] transition hover:text-[var(--color-heading)] backdrop-blur"
+ >
+ <ListTodo className="h-[18px] w-[18px]" />
+ </button>
+ </div>
+ )}
+
- <main className="flex min-h-0 flex-1 flex-col rounded-[30px] border border-white/75 bg-[rgba(255,255,255,0.54)] p-3 shadow-[0_24px_64px_rgba(15,23,42,0.07)] backdrop-blur">
- <div className="min-h-0 flex-1 rounded-[28px] border border-white/80 bg-[rgba(248,250,252,0.78)] p-3">
+ <main className={cn(
+ "flex min-h-0 flex-1 flex-col bg-[rgba(255,255,255,0.54)] shadow-[0_24px_64px_rgba(15,23,42,0.07)] backdrop-blur transition-all duration-300",
+ !sidebarExpanded && "pl-14"
+ )}>
+ <div className="min-h-0 flex-1 bg-[rgba(248,250,252,0.78)]">
- <div className="mx-auto flex h-full max-w-[1680px] min-h-0 flex-col">
+ <div className="flex h-full min-h-0 flex-col">
{profile ? (
- <div className="mb-3 flex flex-wrap items-center gap-2 rounded-[22px] border border-[color:var(--color-surface-border)] bg-white/90 px-5 py-4 text-sm text-[var(--color-text-subtle)] shadow-[var(--shadow-sm)]">
+ <div className="flex flex-wrap items-center gap-2 border-b border-[color:var(--color-surface-border)] bg-white/90 px-5 py-3 text-sm text-[var(--color-text-subtle)] shadow-[var(--shadow-sm)]">