Merge main into release/v0.2
This commit is contained in:
commit
8d11e30d76
218
docs/ui-refactor-proposal.md
Normal file
218
docs/ui-refactor-proposal.md
Normal 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
BIN
next_output.log
Normal file
Binary file not shown.
@ -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) {
|
||||
|
||||
186
src/app/page.tsx
186
src/app/page.tsx
@ -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">
|
||||
|
||||
@ -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,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,
|
||||
|
||||
@ -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,
|
||||
|
||||
9
src/app/xworkmate/layout.tsx
Normal file
9
src/app/xworkmate/layout.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default function XWorkmateLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
|
||||
44
src/lib/home/heroVideoMedia.ts
Normal file
44
src/lib/home/heroVideoMedia.ts
Normal 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
114
update_layout.patch
Normal 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)]">
|
||||
Loading…
Reference in New Issue
Block a user