feat: unify site theme and session controls

This commit is contained in:
Haitao Pan 2026-03-18 16:59:06 +08:00
parent 3f4f7d9c92
commit 2831f6028b
6 changed files with 274 additions and 157 deletions

View File

@ -13,7 +13,7 @@
--app-shell-nav-offset: 5.5rem;
/* Light theme defaults */
--color-background: #f7f7f8;
--color-background: #f5f5f7;
--color-background-muted: #ececef;
--color-surface: #ffffff;
--color-surface-elevated: rgba(255, 255, 255, 0.96);
@ -27,14 +27,14 @@
--color-text-muted: #4b5563;
--color-text-subtle: #6b7280;
--color-text-inverse: #f8fbff;
--color-primary: #3366ff;
--color-primary-hover: #4d7aff;
--color-primary-muted: #f0f4ff;
--color-primary-border: #d6e0ff;
--color-primary: #4c8bf5;
--color-primary-hover: #5d97f6;
--color-primary-muted: #edf3ff;
--color-primary-border: #d7e5ff;
--color-primary-foreground: #ffffff;
--color-accent: #254edb;
--color-accent-muted: #e3e9ff;
--color-accent-foreground: #162a6b;
--color-accent: #335fd4;
--color-accent-muted: #e7edff;
--color-accent-foreground: #1b3477;
--color-success: #16a34a;
--color-success-muted: #dcfce7;
--color-success-foreground: #166534;
@ -58,15 +58,17 @@
--gradient-app-from: #fafafa;
--gradient-app-via: #f4f5f7;
--gradient-app-to: #f7f7f8;
--gradient-primary-from: #3366ff;
--gradient-primary-to: #254edb;
--gradient-primary-from: #4c8bf5;
--gradient-primary-to: #335fd4;
--shadow-sm:
0 1px 2px rgba(17, 24, 39, 0.06), 0 1px 3px rgba(17, 24, 39, 0.04);
--shadow-md: 0 10px 24px rgba(17, 24, 39, 0.08);
0 1px 2px rgba(17, 24, 39, 0.05), 0 3px 10px rgba(17, 24, 39, 0.04);
--shadow-md: 0 10px 28px rgba(17, 24, 39, 0.07);
--shadow-soft:
0 1px 2px rgba(17, 24, 39, 0.04), 0 8px 20px rgba(17, 24, 39, 0.05);
--radius-lg: 0.875rem;
--radius-xl: 1.125rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-pill: 999px;
--type-body-size: 1rem;
@ -91,11 +93,13 @@ body {
font-size: var(--type-body-size);
line-height: var(--type-body-line-height);
background-color: var(--color-background);
background-image: linear-gradient(
180deg,
rgba(255, 255, 255, 0.58),
rgba(255, 255, 255, 0)
);
background-image:
radial-gradient(
circle at top left,
rgba(76, 139, 245, 0.08),
transparent 26%
),
linear-gradient(180deg, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0));
color: var(--color-text);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
@ -157,6 +161,73 @@ button {
}
@layer components {
.tactile-button {
display: inline-flex;
min-height: 40px;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 12px;
border: 1px solid transparent;
padding: 0.625rem 1rem;
font-size: 0.875rem;
font-weight: 600;
line-height: 1;
transition:
background-color 160ms ease-in-out,
border-color 160ms ease-in-out,
color 160ms ease-in-out,
transform 160ms ease-in-out,
box-shadow 160ms ease-in-out;
}
.tactile-button:hover {
filter: brightness(1.02);
}
.tactile-button:active {
transform: scale(0.98);
}
.tactile-button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--color-focus);
}
.tactile-button-soft {
border-color: var(--color-surface-border);
background: #f1f1f3;
color: var(--color-text);
box-shadow: var(--shadow-soft);
}
.tactile-button-soft:hover {
background: #e5e5ea;
}
.tactile-button-primary {
background: var(--color-primary);
color: var(--color-primary-foreground);
box-shadow: var(--shadow-soft);
}
.tactile-button-primary:hover {
background: var(--color-primary-hover);
}
.tactile-button-subtle {
border-color: var(--color-surface-border);
background: rgba(255, 255, 255, 0.84);
color: var(--color-text-subtle);
}
.tactile-control {
border-radius: 12px;
border: 1px solid var(--color-surface-border);
background: rgba(255, 255, 255, 0.88);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.editorial-display {
font-family: var(--font-editorial-display);
letter-spacing: -0.04em;

View File

@ -33,11 +33,11 @@ import { useMoltbotStore } from "../lib/moltbotStore";
import { cn } from "../lib/utils";
const HOME_SECTION_CLASS =
"rounded-[2rem] border border-slate-900/10 bg-white/90 shadow-[0_18px_40px_rgba(15,23,42,0.05)]";
"rounded-[1rem] border border-slate-900/8 bg-white/88 shadow-[var(--shadow-soft)]";
const HOME_SECTION_LABEL_CLASS =
"text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-text-subtle";
const HOME_LIST_CARD_CLASS =
"rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] transition duration-200";
"rounded-[0.9rem] border border-slate-900/8 bg-white/82 transition duration-200";
const EMPTY_ASSISTANT_DEFAULTS: IntegrationDefaults = {
openclawUrl: "",
openclawOrigin: "",
@ -117,7 +117,7 @@ export default function HomePage() {
<div className="mobile-home-shell relative flex min-h-screen flex-col overflow-x-hidden 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.56),rgba(255,255,255,0))]"
className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.5),rgba(255,255,255,0))]"
/>
<UnifiedNavigation />
@ -128,8 +128,8 @@ export default function HomePage() {
)}
>
<div className="relative flex-1 overflow-y-auto">
<div className="relative mx-auto max-w-6xl px-4 pb-16 sm:px-6 sm:pb-20">
<main className="relative space-y-6 pt-6 sm:space-y-8 sm:pt-10">
<div className="relative w-full px-2 pb-10 sm:px-3 sm:pb-12 lg:px-4">
<main className="relative space-y-3 pt-3 sm:space-y-4 sm:pt-4">
<HeroSection />
<StatsSection />
<ShortcutsSection />
@ -165,11 +165,12 @@ export function HeroSection() {
},
);
const entry =
homepageVideoSWR.data?.resolved ?? DEFAULT_HOMEPAGE_VIDEO_SETTINGS.defaultEntry;
homepageVideoSWR.data?.resolved ??
DEFAULT_HOMEPAGE_VIDEO_SETTINGS.defaultEntry;
const presentation = resolveHomepageVideoPresentation(entry);
const heroCopy = isChinese
? {
? {
eyebrow: "AI Native Workspace",
subtitle: "从想法到上线AI 自动完成构建、部署与优化。",
demoLabel: "产品演示",
@ -182,33 +183,37 @@ export function HeroSection() {
};
return (
<section className="relative overflow-hidden rounded-[1.75rem] border border-slate-900/10 bg-[linear-gradient(180deg,#ffffff,#faf7f2)] p-3 shadow-[0_18px_40px_rgba(15,23,42,0.05)] sm:p-4 lg:p-5">
<section className="relative overflow-hidden rounded-[1rem] border border-slate-900/8 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(247,248,250,0.98))] p-2.5 shadow-[var(--shadow-soft)] sm:p-3 lg:p-3.5">
<div aria-hidden className="pointer-events-none absolute inset-0">
<div className="absolute left-[8%] top-[8%] h-[16rem] w-[16rem] rounded-full bg-[radial-gradient(circle,rgba(37,78,219,0.1),transparent_64%)] blur-3xl" />
<div className="absolute left-[30%] top-[12%] h-[14rem] w-[14rem] rounded-full bg-[radial-gradient(circle,rgba(245,211,170,0.42),transparent_66%)] blur-3xl" />
<div className="absolute right-[10%] top-[10%] h-[18rem] w-[18rem] rounded-full bg-[radial-gradient(circle,rgba(255,255,255,0.92),transparent_72%)]" />
<div className="absolute inset-x-0 top-0 h-[18rem] bg-[linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0)_72%)]" />
<div className="absolute left-[6%] top-[4%] h-[14rem] w-[14rem] rounded-full bg-[radial-gradient(circle,rgba(76,139,245,0.12),transparent_64%)] blur-3xl" />
<div className="absolute left-[28%] top-[8%] h-[12rem] w-[12rem] rounded-full bg-[radial-gradient(circle,rgba(255,255,255,0.86),transparent_66%)] blur-2xl" />
<div className="absolute inset-x-0 top-0 h-[16rem] bg-[linear-gradient(180deg,rgba(255,255,255,0.72),rgba(255,255,255,0)_72%)]" />
</div>
<div className="relative grid gap-4 lg:grid-cols-[0.98fr_1.02fr] lg:gap-5">
<div className="flex flex-col gap-3 pt-1">
<div className="overflow-hidden rounded-[1.35rem] border border-slate-900/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(243,246,251,0.96))] shadow-[0_18px_44px_rgba(15,23,42,0.07)]">
<div className="relative grid gap-3 lg:grid-cols-[0.98fr_1.02fr] lg:gap-3">
<div className="flex flex-col gap-2">
<div className="overflow-hidden rounded-[0.95rem] border border-slate-900/8 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(245,247,250,0.98))] shadow-[var(--shadow-soft)]">
<div className="border-b border-slate-900/10 px-4 py-3 sm:px-4.5">
<div className="flex items-start justify-between gap-4">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>{heroCopy.demoLabel}</p>
<p className={HOME_SECTION_LABEL_CLASS}>
{heroCopy.demoLabel}
</p>
</div>
</div>
</div>
<div className="space-y-3 p-3 sm:p-3.5">
<DemoVideoSurface presentation={presentation} isChinese={isChinese} />
<DemoVideoSurface
presentation={presentation}
isChinese={isChinese}
/>
<div className="flex flex-wrap gap-2 text-xs text-slate-500">
<a
href={entry.videoUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center rounded-full border border-slate-900/10 bg-white px-2.5 py-1 font-semibold text-slate-600 transition hover:border-slate-300 hover:text-slate-900"
className="tactile-button tactile-button-subtle px-3 text-slate-700"
>
{isChinese ? "打开原始链接" : "Open source link"}
</a>
@ -226,8 +231,8 @@ export function HeroSection() {
</div>
</div>
<div className="lg:pl-1">
<div className="overflow-hidden rounded-[1.35rem] border border-slate-900/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.94),rgba(246,248,251,0.98))] shadow-[0_18px_44px_rgba(15,23,42,0.07)]">
<div>
<div className="overflow-hidden rounded-[0.95rem] border border-slate-900/8 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(245,247,250,0.98))] shadow-[var(--shadow-soft)]">
<div className="border-b border-slate-900/10 px-4 py-3 sm:px-4.5">
<div className="flex items-start justify-between gap-4">
<div>
@ -235,7 +240,7 @@ export function HeroSection() {
{isChinese ? "X 助手" : "X Assistant"}
</p>
</div>
<span className="hidden rounded-full border border-slate-900/10 bg-white/90 px-2.5 py-0.5 text-xs font-semibold text-slate-600 sm:inline-flex">
<span className="hidden rounded-[12px] border border-slate-900/8 bg-white/84 px-3 py-1 text-xs font-semibold text-slate-600 sm:inline-flex">
{isChinese ? "对话即入口" : "Prompt-first"}
</span>
</div>
@ -323,7 +328,7 @@ export function StatsSection() {
];
return (
<section className={cn(HOME_SECTION_CLASS, "space-y-5 p-5 lg:p-7")}>
<section className={cn(HOME_SECTION_CLASS, "space-y-4 p-4 lg:p-5")}>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>
@ -335,7 +340,7 @@ export function StatsSection() {
: "Keep key numbers in the same calm visual rhythm instead of a separate heavy dashboard block."}
</p>
</div>
<span className="inline-flex w-fit items-center rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-semibold text-slate-600">
<span className="inline-flex w-fit items-center rounded-[12px] border border-slate-900/8 bg-white/82 px-3 py-1.5 text-xs font-semibold text-slate-600">
{language === "zh" ? "每小时更新" : "Updated hourly"}
</span>
</div>
@ -344,7 +349,7 @@ export function StatsSection() {
{displayStats.map((stat, index: number) => (
<div
key={index}
className="rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] px-4 py-5"
className="rounded-[0.9rem] border border-slate-900/8 bg-white/80 px-4 py-4"
>
<div className="editorial-display text-[2.2rem] leading-none text-slate-950 sm:text-[2.7rem]">
{stat.value}
@ -395,7 +400,7 @@ export function ShortcutsSection() {
}));
return (
<section className={cn(HOME_SECTION_CLASS, "space-y-4 p-5 lg:p-7")}>
<section className={cn(HOME_SECTION_CLASS, "space-y-4 p-4 lg:p-5")}>
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>{t.shortcuts.title}</p>
@ -407,19 +412,19 @@ export function ShortcutsSection() {
<div className="flex flex-wrap gap-2 text-xs font-semibold">
<button
type="button"
className="rounded-full border border-slate-900/10 bg-slate-950 px-3 py-2 text-white transition hover:bg-primary"
className="tactile-button tactile-button-primary px-4"
>
{t.shortcuts.buttons.start}
</button>
<button
type="button"
className="rounded-full border border-slate-900/10 bg-white px-3 py-2 text-slate-700 transition hover:bg-slate-50"
className="tactile-button tactile-button-soft px-4 text-slate-700"
>
{t.shortcuts.buttons.docs}
</button>
<button
type="button"
className="rounded-full border border-slate-900/10 bg-white px-3 py-2 text-slate-700 transition hover:bg-slate-50"
className="tactile-button tactile-button-soft px-4 text-slate-700"
>
{t.shortcuts.buttons.guides}
</button>
@ -435,10 +440,10 @@ export function ShortcutsSection() {
href={item.href}
className={cn(
HOME_LIST_CARD_CLASS,
"group flex items-start gap-3 p-4 hover:-translate-y-[1px] hover:bg-white",
"group flex items-start gap-3 p-4 hover:-translate-y-[1px] hover:bg-white/96",
)}
>
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-900/[0.04] text-primary">
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-[12px] bg-slate-900/[0.04] text-primary">
<Icon className="h-5 w-5" aria-hidden />
</div>
<div className="min-w-0 space-y-1">

View File

@ -1,18 +1,37 @@
'use client'
import { useLanguage } from '../i18n/LanguageProvider'
"use client";
import { useLanguage } from "../i18n/LanguageProvider";
export default function LanguageToggle() {
const { language, setLanguage } = useLanguage()
const { language, setLanguage } = useLanguage();
return (
<select
value={language}
onChange={(e) => setLanguage(e.target.value as 'en' | 'zh')}
className="bg-gray-100 text-gray-900 border border-gray-300 px-2 py-1 rounded text-sm"
>
<option value="en">English</option>
<option value="zh"></option>
</select>
)
<div className="tactile-control relative">
<select
value={language}
onChange={(event) => setLanguage(event.target.value as "en" | "zh")}
aria-label="Language switcher"
className="min-h-10 appearance-none bg-transparent py-2 pl-4 pr-10 text-sm font-medium text-text outline-none"
>
<option value="en">English</option>
<option value="zh"></option>
</select>
<span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-text-subtle">
<svg
className="h-4 w-4"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
aria-hidden="true"
>
<path
d="M5 7.5 10 12.5 15 7.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</div>
);
}
// This component provides a dropdown to toggle between English and Chinese languages.

View File

@ -1,64 +1,68 @@
'use client'
"use client";
import { useEffect, useRef, useState } from 'react'
import { useLanguage } from '../i18n/LanguageProvider'
import { translations } from '../i18n/translations'
import { useEffect, useRef, useState } from "react";
import { useLanguage } from "../i18n/LanguageProvider";
import { translations } from "../i18n/translations";
export type ReleaseChannel = 'stable' | 'beta' | 'develop'
export type ReleaseChannel = "stable" | "beta" | "develop";
type ReleaseChannelSelectorProps = {
selected: ReleaseChannel[]
onToggle: (channel: ReleaseChannel) => void
variant?: 'default' | 'compact' | 'icon'
}
selected: ReleaseChannel[];
onToggle: (channel: ReleaseChannel) => void;
variant?: "default" | "compact" | "icon";
};
const CHANNEL_ORDER: ReleaseChannel[] = ['stable', 'beta', 'develop']
const CHANNEL_ORDER: ReleaseChannel[] = ["stable", "beta", "develop"];
export default function ReleaseChannelSelector({
selected,
onToggle,
variant = 'default',
variant = "default",
}: ReleaseChannelSelectorProps) {
const { language } = useLanguage()
const labels = translations[language].nav.releaseChannels
const [open, setOpen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const isCompact = variant === 'compact'
const isIcon = variant === 'icon'
const hasPreviewSelection = selected.some((channel) => channel !== 'stable')
const { language } = useLanguage();
const labels = translations[language].nav.releaseChannels;
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const isCompact = variant === "compact";
const isIcon = variant === "icon";
const hasPreviewSelection = selected.some((channel) => channel !== "stable");
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (!containerRef.current) return
if (containerRef.current.contains(event.target as Node)) return
setOpen(false)
}
if (!containerRef.current) return;
if (containerRef.current.contains(event.target as Node)) return;
setOpen(false);
};
if (open) {
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [open])
document.removeEventListener("mousedown", handleClickOutside);
};
}, [open]);
const selectedNames = CHANNEL_ORDER.filter((channel) => selected.includes(channel)).map(
(channel) => labels[channel].name,
)
const summary = selectedNames.length > 0 ? selectedNames.join(' + ') : labels.stable.name
const selectedNames = CHANNEL_ORDER.filter((channel) =>
selected.includes(channel),
).map((channel) => labels[channel].name);
const summary =
selectedNames.length > 0 ? selectedNames.join(" + ") : labels.stable.name;
return (
<div className={`relative ${isCompact || isIcon ? 'group' : ''}`} ref={containerRef}>
<div
className={`relative ${isCompact || isIcon ? "group" : ""}`}
ref={containerRef}
>
<button
type="button"
onClick={() => setOpen((prev) => !prev)}
className={`relative flex items-center gap-2 rounded-md border border-gray-200 bg-white/80 text-sm text-gray-700 shadow-sm transition hover:border-purple-300 hover:text-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
className={`tactile-button relative border border-surface-border bg-surface text-text shadow-[var(--shadow-soft)] ${
isIcon
? 'h-9 w-9 justify-center p-0'
? "h-10 w-10 justify-center p-0"
: isCompact
? 'px-3 py-1 text-xs font-medium'
: 'w-full justify-between px-3 py-1.5 md:w-auto md:justify-start'
? "px-3 text-xs font-medium"
: "w-full justify-between md:w-auto md:justify-start"
}`}
aria-haspopup="listbox"
aria-expanded={open}
@ -66,7 +70,7 @@ export default function ReleaseChannelSelector({
>
{isIcon ? (
<svg
className="h-5 w-5 text-purple-600"
className="h-5 w-5 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -86,64 +90,81 @@ export default function ReleaseChannelSelector({
</svg>
) : (
<>
<span className={`font-medium ${isCompact ? '' : 'text-gray-700'}`}>{labels.label}</span>
<span className={`font-medium ${isCompact ? "" : "text-text"}`}>
{labels.label}
</span>
{!isCompact && (
<span className="text-xs text-gray-500">
<span className="text-xs text-text-subtle">
{labels.summaryPrefix}: {summary}
</span>
)}
<svg
className={`h-4 w-4 text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`}
className={`h-4 w-4 text-text-subtle transition-transform ${open ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</>
)}
{isIcon && hasPreviewSelection && (
<span className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-purple-500" />
<span className="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-primary" />
)}
</button>
{(isCompact || isIcon) && (
<div className="pointer-events-none absolute bottom-full left-1/2 z-40 mb-2 hidden w-56 -translate-x-1/2 rounded-md bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition group-hover:block group-hover:opacity-100 group-focus-within:block group-focus-within:opacity-100">
<div className="pointer-events-none absolute bottom-full left-1/2 z-40 mb-2 hidden w-56 -translate-x-1/2 rounded-xl bg-slate-900 px-3 py-2 text-xs text-white opacity-0 shadow-[var(--shadow-md)] transition group-hover:block group-hover:opacity-100 group-focus-within:block group-focus-within:opacity-100">
<div className="font-semibold">{labels.label}</div>
<div className="mt-1 text-gray-200">
<div className="mt-1 text-slate-200">
{labels.summaryPrefix}: {summary}
</div>
</div>
)}
{open && (
<div className="absolute right-0 z-50 mt-2 w-64 rounded-md border border-gray-200 bg-white shadow-lg">
<ul className="py-2 text-sm text-gray-700" role="listbox" aria-label={labels.label}>
<div className="absolute right-0 z-50 mt-2 w-64 overflow-hidden rounded-[14px] border border-surface-border bg-surface shadow-[var(--shadow-md)]">
<ul
className="py-2 text-sm text-text"
role="listbox"
aria-label={labels.label}
>
{CHANNEL_ORDER.map((channel) => {
const channelLabels = labels[channel]
const checked = selected.includes(channel)
const isStable = channel === 'stable'
const channelLabels = labels[channel];
const checked = selected.includes(channel);
const isStable = channel === "stable";
return (
<li key={channel}>
<label className="flex cursor-pointer items-start gap-3 px-3 py-2 hover:bg-gray-50">
<label className="flex cursor-pointer items-start gap-3 px-3 py-2.5 transition hover:bg-surface-muted">
<input
type="checkbox"
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
className="mt-1 h-4 w-4 rounded border-surface-border text-primary focus:ring-primary"
checked={checked}
onChange={() => (!isStable ? onToggle(channel) : undefined)}
onChange={() =>
!isStable ? onToggle(channel) : undefined
}
disabled={isStable}
/>
<div>
<div className="font-medium text-gray-900">{channelLabels.name}</div>
<p className="text-xs text-gray-500">{channelLabels.description}</p>
<div className="font-medium text-text">
{channelLabels.name}
</div>
<p className="text-xs text-text-subtle">
{channelLabels.description}
</p>
</div>
</label>
</li>
)
);
})}
</ul>
</div>
)}
</div>
)
);
}

View File

@ -223,9 +223,9 @@ export default function UnifiedNavigation() {
width: "calc(100% + var(--assistant-reserve-offset, 0px))",
marginRight: "calc(var(--assistant-reserve-offset, 0px) * -1)",
}}
className="sticky top-0 z-50 w-full border-b border-surface-border bg-background/95 text-text backdrop-blur transition-colors duration-150"
className="sticky top-0 z-50 w-full border-b border-surface-border/80 bg-background/92 text-text backdrop-blur-xl transition-colors duration-150"
>
<div className="flex items-center justify-between border-b border-surface-border/70 bg-background px-5 pb-3 pt-[max(0.875rem,env(safe-area-inset-top))] lg:hidden">
<div className="flex items-center justify-between border-b border-surface-border/70 bg-background px-4 pb-3 pt-[max(0.875rem,env(safe-area-inset-top))] lg:hidden">
<Link
href="/"
className="flex items-center gap-2"
@ -245,7 +245,7 @@ export default function UnifiedNavigation() {
</Link>
<button
onClick={() => setMenuOpen(!menuOpen)}
className="rounded-[1.15rem] bg-surface-muted p-3 text-text transition-colors hover:bg-surface-hover"
className="tactile-button tactile-button-soft h-10 w-10 rounded-[12px] p-0"
aria-label="Toggle menu"
>
{menuOpen ? (
@ -270,14 +270,14 @@ export default function UnifiedNavigation() {
onClick={() => {
toggleOpen();
}}
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg transition-colors whitespace-nowrap ${
className={`tactile-button min-h-9 gap-1.5 rounded-[12px] px-3 py-2 text-[13px] shadow-none whitespace-nowrap ${
active
? "bg-primary/10 text-primary"
: "text-text-muted hover:text-text hover:bg-surface-muted"
? "border border-primary/10 bg-primary/12 text-primary"
: "text-text-muted hover:bg-surface-muted hover:text-text"
}`}
>
{item.icon && <item.icon className="w-4 h-4" />}
<span className="text-[13px] tracking-tight">
<span className="tracking-tight">
{getLabel(item.label, language)}
</span>
</button>
@ -287,14 +287,14 @@ export default function UnifiedNavigation() {
<Link
key={item.key}
href={item.href}
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg transition-colors whitespace-nowrap ${
className={`tactile-button min-h-9 gap-1.5 rounded-[12px] px-3 py-2 text-[13px] shadow-none whitespace-nowrap ${
active
? "bg-primary/10 text-primary"
: "text-text-muted hover:text-text hover:bg-surface-muted"
? "border border-primary/10 bg-primary/12 text-primary"
: "text-text-muted hover:bg-surface-muted hover:text-text"
}`}
>
{item.icon && <item.icon className="w-4 h-4" />}
<span className="text-[13px] tracking-tight">
<span className="tracking-tight">
{getLabel(item.label, language)}
</span>
</Link>
@ -339,7 +339,7 @@ export default function UnifiedNavigation() {
<DropdownMenu.Trigger asChild>
<button
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-primary to-accent text-sm font-semibold text-white shadow-shadow-sm transition hover:from-primary-hover hover:to-accent focus:outline-none focus:ring-2 focus:ring-primary/60 focus:ring-offset-2 focus:ring-offset-background outline-none ring-offset-background"
className="tactile-button tactile-button-primary h-10 w-10 rounded-[12px] p-0 text-sm outline-none"
aria-label="User account menu"
>
{accountInitial}
@ -410,7 +410,7 @@ export default function UnifiedNavigation() {
/>
<Link
href="/register"
className="rounded-md border border-surface-border px-3 py-1 text-primary transition hover:border-primary/40 hover:bg-surface-muted"
className="tactile-button tactile-button-soft min-h-10 px-4 text-primary"
>
{nav.account.register}
</Link>
@ -460,7 +460,7 @@ export default function UnifiedNavigation() {
<button
type="button"
onClick={() => setLanguage(language === "zh" ? "en" : "zh")}
className="inline-flex h-10 min-w-10 items-center justify-center rounded-full border border-surface-border bg-surface-muted/75 px-3 text-xs font-semibold uppercase tracking-[0.18em] text-text shadow-sm transition hover:bg-surface-hover"
className="tactile-button tactile-button-soft h-10 min-w-10 px-3 text-xs uppercase tracking-[0.18em]"
aria-label={
isChinese ? "切换到英文" : "Switch language to Chinese"
}
@ -469,7 +469,7 @@ export default function UnifiedNavigation() {
</button>
<button
onClick={() => setMenuOpen(false)}
className="rounded-full border border-surface-border bg-surface-muted/75 p-2.5 text-text shadow-sm transition-colors hover:bg-surface-hover"
className="tactile-button tactile-button-soft h-10 w-10 p-0"
aria-label={isChinese ? "关闭菜单" : "Close menu"}
>
<X className="h-5 w-5" />
@ -519,7 +519,7 @@ export default function UnifiedNavigation() {
{mobileQuickLinks.length > 0 ? (
<div className="pointer-events-none absolute right-0 top-[60%] flex -translate-y-1/2 justify-end min-[390px]:top-[59%] min-[430px]:top-[58%]">
<div className="pointer-events-auto w-[min(10.75rem,45vw)] rounded-[1.75rem] bg-surface-muted/82 p-4 shadow-[0_18px_40px_rgba(15,23,42,0.08)]">
<div className="pointer-events-auto w-[min(10.75rem,45vw)] rounded-[14px] border border-surface-border/70 bg-surface-muted/84 p-4 shadow-[var(--shadow-md)]">
<div className="space-y-2.5">
{mobileQuickLinks.map((item) =>
item.key === "chat" ? (
@ -574,7 +574,7 @@ export default function UnifiedNavigation() {
<Link
href={primaryAccountAction.href}
onClick={() => setMenuOpen(false)}
className="inline-flex min-h-[3.1rem] min-w-[7rem] items-center justify-center rounded-full bg-surface-muted px-6 text-[1.05rem] font-semibold text-text shadow-sm transition hover:bg-surface-hover"
className="tactile-button tactile-button-soft min-h-[3.1rem] min-w-[7rem] px-6 text-[1.05rem]"
>
{typeof primaryAccountAction.label === "function"
? primaryAccountAction.label(language)

View File

@ -89,8 +89,7 @@ type PersistedPairingRequiredLookup = {
expired: boolean;
};
const PAIRING_REQUIRED_SESSION_STORAGE_KEY =
"openclaw:pairing-required-state";
const PAIRING_REQUIRED_SESSION_STORAGE_KEY = "openclaw:pairing-required-state";
const PAIRING_REQUIRED_STATE_TTL_MS = 1000 * 60 * 60 * 12;
const PAIRING_REQUIRED_GUEST_TTL_MS = 1000 * 60 * 60;
@ -1159,7 +1158,7 @@ export function OpenClawAssistantPane({
}
const containerClassName = cn(
"flex h-full min-h-0 flex-col overflow-hidden rounded-[var(--radius-xl)] border border-[color:var(--color-surface-border)] bg-[var(--color-surface-elevated)] shadow-[var(--shadow-md)]",
"flex h-full min-h-0 flex-col overflow-hidden rounded-[14px] border border-[color:var(--color-surface-border)] bg-[var(--color-surface-elevated)] shadow-[var(--shadow-md)]",
compact ? "rounded-none border-0 shadow-none" : "",
);
@ -1184,7 +1183,7 @@ export function OpenClawAssistantPane({
)}
>
{!minimalPage ? (
<div className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)] px-2.5 py-1 text-xs font-medium text-[var(--color-text-subtle)]">
<div className="inline-flex min-h-10 items-center gap-2 rounded-[12px] border border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)] px-3 py-2 text-xs font-medium text-[var(--color-text-subtle)] shadow-[var(--shadow-soft)]">
<span
className={cn(
"h-2.5 w-2.5 rounded-full",
@ -1227,7 +1226,7 @@ export function OpenClawAssistantPane({
setSelectedSessionKey("");
void connectGateway("", event.target.value, { force: true });
}}
className="w-full rounded-full border border-[color:var(--color-surface-border)] bg-[var(--color-surface)] px-3 py-1.5 text-sm text-[var(--color-text)] outline-none transition focus:border-[color:var(--color-primary)]"
className="tactile-control min-h-10 w-full px-3 py-2 text-sm text-[var(--color-text)] outline-none transition focus:border-[color:var(--color-primary)]"
>
<option value="">{copy.mainAgent}</option>
{agents.map((agent) => (
@ -1243,7 +1242,7 @@ export function OpenClawAssistantPane({
)}
{minimalPage ? (
<div className="ml-auto inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] bg-[var(--color-primary-muted)] px-3 py-1.5 text-xs font-semibold text-[var(--color-heading)]">
<div className="ml-auto inline-flex min-h-10 items-center gap-2 rounded-[12px] border border-[color:var(--color-surface-border)] bg-[var(--color-primary-muted)] px-3 py-2 text-xs font-semibold text-[var(--color-heading)] shadow-[var(--shadow-soft)]">
<span
className={cn(
"h-2.5 w-2.5 rounded-full",
@ -1262,10 +1261,10 @@ export function OpenClawAssistantPane({
<>
<button
type="button"
onClick={() => {
void connectGateway(undefined, undefined, { force: true });
}}
className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text)] transition hover:border-[color:var(--color-primary-border)] hover:bg-[var(--color-surface-muted)]"
onClick={() => {
void connectGateway(undefined, undefined, { force: true });
}}
className="tactile-button tactile-button-soft px-3 text-xs text-[var(--color-text)]"
title={copy.reconnect}
>
{connectionState === "connecting" ? (
@ -1279,7 +1278,7 @@ export function OpenClawAssistantPane({
<button
type="button"
onClick={() => router.push("/panel/api")}
className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-primary-border)] bg-[var(--color-primary-muted)] px-3 py-1.5 text-xs font-semibold text-[var(--color-primary)] transition hover:opacity-90"
className="tactile-button tactile-button-primary border border-[color:var(--color-primary-border)] px-3 text-xs text-[var(--color-primary-foreground)]"
title={copy.integrations}
>
<Settings2 className="h-3.5 w-3.5" />
@ -1305,10 +1304,12 @@ export function OpenClawAssistantPane({
type="button"
onClick={() => {
setSelectedSessionKey(session.key);
void connectGateway(session.key, undefined, { force: true });
void connectGateway(session.key, undefined, {
force: true,
});
}}
className={cn(
"inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs transition",
"inline-flex min-h-9 items-center gap-2 rounded-[12px] border px-3 py-1.5 text-xs transition",
session.key === selectedSessionKey
? "border-[color:var(--color-primary)] bg-[var(--color-primary-muted)] text-[var(--color-primary)]"
: "border-[color:var(--color-surface-border)] bg-[var(--color-surface)] text-[var(--color-text-subtle)] hover:border-[color:var(--color-primary-border)]",
@ -1363,7 +1364,7 @@ export function OpenClawAssistantPane({
<button
type="button"
onClick={() => router.push("/panel/api")}
className="inline-flex items-center gap-2 rounded-full bg-[var(--color-primary)] px-3.5 py-2 text-sm font-semibold text-[var(--color-primary-foreground)]"
className="tactile-button tactile-button-primary px-4 text-sm"
>
{copy.openIntegrations}
<ChevronRight className="h-4 w-4" />
@ -1392,7 +1393,7 @@ export function OpenClawAssistantPane({
setComposerValue(action);
textareaRef.current?.focus();
}}
className="rounded-full border border-[color:var(--color-surface-border)] bg-[var(--color-surface)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-subtle)] transition hover:border-[color:var(--color-primary-border)] hover:text-[var(--color-primary)]"
className="tactile-button tactile-button-subtle min-h-9 px-3 text-xs font-medium text-[var(--color-text-subtle)] hover:text-[var(--color-primary)]"
>
{action}
</button>
@ -1473,7 +1474,7 @@ export function OpenClawAssistantPane({
type="button"
onClick={() => setAssistantMode(option.value)}
className={cn(
"rounded-full px-2.5 py-1 text-xs font-semibold transition",
"min-h-9 rounded-[12px] px-3 py-1.5 text-xs font-semibold transition",
assistantMode === option.value
? "bg-[var(--color-primary)] text-[var(--color-primary-foreground)]"
: "border border-[color:var(--color-surface-border)] text-[var(--color-text-subtle)] hover:border-[color:var(--color-primary-border)]",
@ -1483,7 +1484,7 @@ export function OpenClawAssistantPane({
</button>
))}
<div className="ml-auto flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] bg-[var(--color-surface)] px-2.5 py-1 text-xs text-[var(--color-text-subtle)]">
<div className="ml-auto flex min-h-9 items-center gap-2 rounded-[12px] border border-[color:var(--color-surface-border)] bg-[var(--color-surface)] px-3 py-1.5 text-xs text-[var(--color-text-subtle)]">
<BrainCircuit className="h-3.5 w-3.5" />
<select
value={thinking}
@ -1506,7 +1507,7 @@ export function OpenClawAssistantPane({
{attachments.map((attachment) => (
<div
key={attachment.id}
className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)] px-2.5 py-1 text-xs text-[var(--color-text)]"
className="inline-flex min-h-8 items-center gap-2 rounded-[12px] border border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)] px-3 py-1 text-xs text-[var(--color-text)]"
>
{attachment.type === "image" ? (
<Camera className="h-3.5 w-3.5" />
@ -1540,14 +1541,14 @@ export function OpenClawAssistantPane({
<button
type="button"
onClick={() => router.push("/login")}
className="inline-flex items-center gap-2 rounded-full bg-[var(--color-primary)] px-3 py-1.5 text-xs font-semibold text-[var(--color-primary-foreground)] transition hover:opacity-90"
className="tactile-button tactile-button-primary px-3 text-xs"
>
{copy.login}
</button>
<button
type="button"
onClick={() => router.push("/register")}
className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] bg-[var(--color-surface)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text)] transition hover:border-[color:var(--color-primary-border)] hover:bg-[var(--color-surface-muted)]"
className="tactile-button tactile-button-soft px-3 text-xs text-[var(--color-text)]"
>
{copy.register}
</button>
@ -1586,7 +1587,7 @@ export function OpenClawAssistantPane({
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] px-2.5 py-1 text-xs font-semibold text-[var(--color-text)] transition hover:border-[color:var(--color-primary-border)] hover:bg-[var(--color-surface-muted)]"
className="tactile-button tactile-button-soft min-h-9 px-3 text-xs text-[var(--color-text)]"
>
<Paperclip className="h-3.5 w-3.5" />
{copy.attachment}
@ -1598,7 +1599,7 @@ export function OpenClawAssistantPane({
void capturePage();
}}
disabled={isCapturing}
className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] px-2.5 py-1 text-xs font-semibold text-[var(--color-text)] transition hover:border-[color:var(--color-primary-border)] hover:bg-[var(--color-surface-muted)] disabled:cursor-not-allowed disabled:opacity-60"
className="tactile-button tactile-button-soft min-h-9 px-3 text-xs text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-60"
>
{isCapturing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
@ -1629,7 +1630,7 @@ export function OpenClawAssistantPane({
isSending ||
(!composerValue.trim() && attachments.length === 0)
}
className="inline-flex items-center gap-2 rounded-full bg-[var(--color-primary)] px-3.5 py-1.5 text-sm font-semibold text-[var(--color-primary-foreground)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
className="tactile-button tactile-button-primary px-4 text-sm disabled:cursor-not-allowed disabled:opacity-60"
>
{isSending ? (
<Loader2 className="h-4 w-4 animate-spin" />