feat(brand): rebrand from Mission Control to Pipeline

- Project name: openclaw-mission-control → pipeline
- DB name: mission_control → pipeline
- Package names: pipeline-frontend, pipeline-backend
- UI text: all Mission Control references → Pipeline
- New PipelineIcon component replaces old brand mark
- Landing hero, sidebar, loading, auth, settings all rebranded
- ThemeToggle and ThemeProvider components added
- uv.lock updated for pipeline-backend package rename
This commit is contained in:
null 2026-05-19 01:48:38 -05:00
parent 0f50db1e9c
commit 827d62c05e
32 changed files with 457 additions and 176 deletions

View File

@ -6,7 +6,7 @@ FRONTEND_PORT=3000
BACKEND_PORT=8000 BACKEND_PORT=8000
# --- database --- # --- database ---
POSTGRES_DB=mission_control POSTGRES_DB=pipeline
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432 POSTGRES_PORT=5432
@ -26,6 +26,6 @@ LOCAL_AUTH_TOKEN=
# --- frontend settings --- # --- frontend settings ---
# REQUIRED: Public URL used by the browser to reach the API. # REQUIRED: Public URL used by the browser to reach the API.
# Use `auto` to target the same host currently serving Mission Control on port 8000. # Use `auto` to target the same host currently serving Pipeline on port 8000.
# Example (explicit override): NEXT_PUBLIC_API_URL=https://mc.example.com # Example (explicit override): NEXT_PUBLIC_API_URL=https://mc.example.com
NEXT_PUBLIC_API_URL=auto NEXT_PUBLIC_API_URL=auto

View File

@ -4,7 +4,7 @@ LOG_FORMAT=text
LOG_USE_UTC=false LOG_USE_UTC=false
REQUEST_LOG_SLOW_MS=1000 REQUEST_LOG_SLOW_MS=1000
REQUEST_LOG_INCLUDE_HEALTH=false REQUEST_LOG_INCLUDE_HEALTH=false
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/mission_control DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/pipeline
# For remote access, set this to your UI origin (e.g. http://<server-ip>:3000 or https://mc.example.com). # For remote access, set this to your UI origin (e.g. http://<server-ip>:3000 or https://mc.example.com).
CORS_ORIGINS=http://localhost:3000 CORS_ORIGINS=http://localhost:3000
# REQUIRED for gateway provisioning/agent heartbeats. Must be reachable by gateway runtime. # REQUIRED for gateway provisioning/agent heartbeats. Must be reachable by gateway runtime.

View File

@ -12,7 +12,7 @@ REQUEST_LOG_SLOW_MS=1000
REQUEST_LOG_INCLUDE_HEALTH=false REQUEST_LOG_INCLUDE_HEALTH=false
# Local backend -> local Postgres (adjust host/port if needed) # Local backend -> local Postgres (adjust host/port if needed)
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/mission_control_test DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/pipeline_test
CORS_ORIGINS=http://localhost:3000 CORS_ORIGINS=http://localhost:3000
BASE_URL=http://localhost:8000 BASE_URL=http://localhost:8000

View File

@ -1,4 +1,4 @@
"""Application name and version constants.""" """Application name and version constants."""
APP_NAME = "mission-control" APP_NAME = "pipeline"
APP_VERSION = "0.1.0" APP_VERSION = "0.1.0"

View File

@ -8,7 +8,7 @@ profile = "black"
line_length = 100 line_length = 100
skip = [".venv", "migrations/versions"] skip = [".venv", "migrations/versions"]
[project] [project]
name = "openclaw-agency-backend" name = "pipeline-backend"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [

View File

@ -698,7 +698,25 @@ wheels = [
] ]
[[package]] [[package]]
name = "openclaw-agency-backend" name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pathspec"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
[[package]]
name = "pipeline-backend"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
@ -767,24 +785,6 @@ requires-dist = [
] ]
provides-extras = ["dev"] provides-extras = ["dev"]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pathspec"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.5.1" version = "4.5.1"

View File

@ -1,10 +1,10 @@
name: openclaw-mission-control name: pipeline
services: services:
db: db:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
POSTGRES_DB: ${POSTGRES_DB:-mission_control} POSTGRES_DB: ${POSTGRES_DB:-pipeline}
POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
volumes: volumes:
@ -37,7 +37,7 @@ services:
- ./backend/.env - ./backend/.env
environment: environment:
# Override localhost defaults for container networking # Override localhost defaults for container networking
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-mission_control} DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-pipeline}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3030} CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3030}
DB_AUTO_MIGRATE: ${DB_AUTO_MIGRATE:-true} DB_AUTO_MIGRATE: ${DB_AUTO_MIGRATE:-true}
AUTH_MODE: ${AUTH_MODE} AUTH_MODE: ${AUTH_MODE}
@ -84,7 +84,7 @@ services:
db: db:
condition: service_healthy condition: service_healthy
environment: environment:
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-mission_control} DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-pipeline}
AUTH_MODE: ${AUTH_MODE} AUTH_MODE: ${AUTH_MODE}
LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN} LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
BASE_URL: ${BASE_URL:-http://localhost:8000} BASE_URL: ${BASE_URL:-http://localhost:8000}

View File

@ -1,5 +1,5 @@
{ {
"name": "frontend", "name": "pipeline-frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@ -861,7 +861,9 @@ export default function BoardDetailPage() {
const openedTaskIdFromUrlRef = useRef<string | null>(null); const openedTaskIdFromUrlRef = useRef<string | null>(null);
const openedPanelFromUrlRef = useRef<string | null>(null); const openedPanelFromUrlRef = useRef<string | null>(null);
const [comments, setComments] = useState<TaskComment[]>([]); const [comments, setComments] = useState<TaskComment[]>([]);
const [highlightedCommentId, setHighlightedCommentId] = useState<string | null>(null); const [highlightedCommentId, setHighlightedCommentId] = useState<
string | null
>(null);
const [liveFeed, setLiveFeed] = useState<LiveFeedItem[]>([]); const [liveFeed, setLiveFeed] = useState<LiveFeedItem[]>([]);
const liveFeedRef = useRef<LiveFeedItem[]>([]); const liveFeedRef = useRef<LiveFeedItem[]>([]);
const liveFeedFlashTimersRef = useRef<Record<string, number>>({}); const liveFeedFlashTimersRef = useRef<Record<string, number>>({});
@ -2407,9 +2409,12 @@ export default function BoardDetailPage() {
currentTaskIdFromUrl !== fullTask.id || currentTaskIdFromUrl !== fullTask.id ||
currentCommentIdFromUrl !== targetCommentId currentCommentIdFromUrl !== targetCommentId
) { ) {
router.replace(buildUrlWithTaskAndComment(fullTask.id, targetCommentId), { router.replace(
scroll: false, buildUrlWithTaskAndComment(fullTask.id, targetCommentId),
}); {
scroll: false,
},
);
} }
selectedTaskIdRef.current = fullTask.id; selectedTaskIdRef.current = fullTask.id;
setSelectedTask(fullTask); setSelectedTask(fullTask);
@ -4659,9 +4664,7 @@ export default function BoardDetailPage() {
</span>{" "} </span>{" "}
to board chat. to board chat.
</li> </li>
<li> <li>Pipeline forwards it to all agents on this board.</li>
Mission Control forwards it to all agents on this board.
</li>
</ul> </ul>
</div> </div>

View File

@ -165,7 +165,7 @@ export default function EditGatewayPage() {
? `Edit gateway — ${resolvedName.trim()}` ? `Edit gateway — ${resolvedName.trim()}`
: "Edit gateway" : "Edit gateway"
} }
description="Update connection settings for this OpenClaw gateway." description="Update connection settings for this Pipeline gateway."
isAdmin={isAdmin} isAdmin={isAdmin}
adminOnlyMessage="Only organization owners and admins can edit gateways." adminOnlyMessage="Only organization owners and admins can edit gateways."
> >

View File

@ -116,7 +116,7 @@ export default function NewGatewayPage() {
forceRedirectUrl: "/gateways/new", forceRedirectUrl: "/gateways/new",
}} }}
title="Create gateway" title="Create gateway"
description="Configure an OpenClaw gateway for mission control." description="Configure a Pipeline gateway."
isAdmin={isAdmin} isAdmin={isAdmin}
adminOnlyMessage="Only organization owners and admins can create gateways." adminOnlyMessage="Only organization owners and admins can create gateways."
> >

View File

@ -95,7 +95,7 @@ export default function GatewaysPage() {
forceRedirectUrl: "/gateways", forceRedirectUrl: "/gateways",
}} }}
title="Gateways" title="Gateways"
description="Manage OpenClaw gateway connections used by boards" description="Manage Pipeline gateway connections used by boards"
headerActions={ headerActions={
isAdmin && gateways.length > 0 ? ( isAdmin && gateways.length > 0 ? (
<Link <Link
@ -125,7 +125,7 @@ export default function GatewaysPage() {
emptyState={{ emptyState={{
title: "No gateways yet", title: "No gateways yet",
description: description:
"Create your first gateway to connect boards and start managing your OpenClaw connections.", "Create your first gateway to connect boards and start managing your Pipeline connections.",
actionHref: "/gateways/new", actionHref: "/gateways/new",
actionLabel: "Create your first gateway", actionLabel: "Create your first gateway",
}} }}
@ -145,8 +145,8 @@ export default function GatewaysPage() {
title="Delete gateway?" title="Delete gateway?"
description={ description={
<> <>
This removes the gateway connection from Mission Control. Boards This removes the gateway connection from Pipeline. Boards using it
using it will need a new gateway assigned. will need a new gateway assigned.
</> </>
} }
errorMessage={deleteMutation.error?.message} errorMessage={deleteMutation.error?.message}

View File

@ -3,6 +3,29 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
color-scheme: dark;
--bg: #070b12;
--surface: #0f1724;
--surface-muted: #162133;
--surface-strong: #223047;
--border: #263246;
--border-strong: #3a4860;
--text: #edf3fb;
--text-muted: #a7b4c7;
--text-quiet: #748195;
--accent: #60a5fa;
--accent-strong: #93c5fd;
--accent-soft: rgba(96, 165, 250, 0.18);
--success: #34d399;
--warning: #fbbf24;
--danger: #f87171;
--shadow-panel:
0 18px 50px rgba(0, 0, 0, 0.35), 0 1px 0 rgba(255, 255, 255, 0.04) inset;
--shadow-card:
0 12px 34px rgba(0, 0, 0, 0.28), 0 1px 0 rgba(255, 255, 255, 0.04) inset;
}
[data-theme="light"] {
color-scheme: light; color-scheme: light;
--bg: #f8fafc; --bg: #f8fafc;
--surface: #ffffff; --surface: #ffffff;
@ -25,6 +48,10 @@
0 1px 2px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.06); 0 1px 2px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.06);
} }
[data-theme="dark"] {
color-scheme: dark;
}
body { body {
@apply font-body; @apply font-body;
background: var(--bg); background: var(--bg);
@ -121,8 +148,16 @@ body {
} }
.bg-landing-grid { .bg-landing-grid {
background-image: background-image:
linear-gradient(to right, rgba(12, 17, 29, 0.08) 1px, transparent 1px), linear-gradient(
linear-gradient(to bottom, rgba(12, 17, 29, 0.08) 1px, transparent 1px); to right,
color-mix(in srgb, var(--text) 8%, transparent) 1px,
transparent 1px
),
linear-gradient(
to bottom,
color-mix(in srgb, var(--text) 8%, transparent) 1px,
transparent 1px
);
background-size: 120px 120px; background-size: 120px 120px;
} }
} }

20
frontend/src/app/icon.svg Normal file
View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#60a5fa" />
<path
d="M18 21h15c7 0 13 6 13 13s-6 13-13 13H18"
fill="none"
stroke="#fff"
stroke-width="6"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M18 21v26M18 34h20"
fill="none"
stroke="#0f1724"
stroke-width="6"
stroke-linecap="round"
stroke-linejoin="round"
opacity=".95"
/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -69,7 +69,7 @@ function InviteContent() {
return ( return (
<div className="min-h-screen bg-app text-strong"> <div className="min-h-screen bg-app text-strong">
<header className="border-b border-[color:var(--border)] bg-white"> <header className="border-b border-[color:var(--border)] bg-[color:var(--surface)]">
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4"> <div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
<BrandMark /> <BrandMark />
</div> </div>
@ -82,7 +82,7 @@ function InviteContent() {
Organization Invite Organization Invite
</p> </p>
<h1 className="text-2xl font-semibold text-strong"> <h1 className="text-2xl font-semibold text-strong">
Join your team in OpenClaw Join your team in Pipeline
</h1> </h1>
<p className="text-sm text-muted">{helperText}</p> <p className="text-sm text-muted">{helperText}</p>
</div> </div>

View File

@ -7,10 +7,11 @@ import { DM_Serif_Display, IBM_Plex_Sans, Sora } from "next/font/google";
import { AuthProvider } from "@/components/providers/AuthProvider"; import { AuthProvider } from "@/components/providers/AuthProvider";
import { QueryProvider } from "@/components/providers/QueryProvider"; import { QueryProvider } from "@/components/providers/QueryProvider";
import { ThemeProvider } from "@/components/providers/ThemeProvider";
import { GlobalLoader } from "@/components/ui/global-loader"; import { GlobalLoader } from "@/components/ui/global-loader";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "OpenClaw Mission Control", title: "Pipeline",
description: "A calm command center for every task.", description: "A calm command center for every task.",
}; };
@ -35,18 +36,38 @@ const displayFont = DM_Serif_Display({
weight: ["400"], weight: ["400"],
}); });
const themeScript = `
(() => {
try {
const theme = localStorage.getItem("openclaw_theme") === "light" ? "light" : "dark";
const root = document.documentElement;
root.dataset.theme = theme;
root.classList.toggle("dark", theme === "dark");
root.style.colorScheme = theme;
} catch {
document.documentElement.dataset.theme = "dark";
document.documentElement.classList.add("dark");
}
})();
`;
export default function RootLayout({ children }: { children: ReactNode }) { export default function RootLayout({ children }: { children: ReactNode }) {
return ( return (
<html lang="en"> <html lang="en" data-theme="dark" className="dark" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body <body
className={`${bodyFont.variable} ${headingFont.variable} ${displayFont.variable} min-h-screen bg-app text-strong antialiased`} className={`${bodyFont.variable} ${headingFont.variable} ${displayFont.variable} min-h-screen bg-app text-strong antialiased`}
> >
<AuthProvider> <ThemeProvider>
<QueryProvider> <AuthProvider>
<GlobalLoader /> <QueryProvider>
{children} <GlobalLoader />
</QueryProvider> {children}
</AuthProvider> </QueryProvider>
</AuthProvider>
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -6,7 +6,7 @@ export default function Loading() {
> >
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="h-10 w-10 animate-spin rounded-full border-2 border-slate-200 border-t-[var(--accent)]" /> <div className="h-10 w-10 animate-spin rounded-full border-2 border-slate-200 border-t-[var(--accent)]" />
<p className="text-sm text-slate-500">Loading mission control...</p> <p className="text-sm text-slate-500">Loading pipeline...</p>
</div> </div>
</div> </div>
); );

View File

@ -118,7 +118,7 @@ export default function OnboardingPage() {
<div className="w-full max-w-2xl rounded-xl border border-slate-200 bg-white shadow-sm"> <div className="w-full max-w-2xl rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="border-b border-slate-100 px-6 py-5"> <div className="border-b border-slate-100 px-6 py-5">
<h1 className="text-2xl font-semibold tracking-tight text-slate-900"> <h1 className="text-2xl font-semibold tracking-tight text-slate-900">
Mission Control profile Pipeline profile
</h1> </h1>
<p className="mt-1 text-sm text-slate-600"> <p className="mt-1 text-sm text-slate-600">
Sign in to configure your profile and timezone. Sign in to configure your profile and timezone.
@ -141,10 +141,10 @@ export default function OnboardingPage() {
<section className="w-full max-w-2xl rounded-xl border border-slate-200 bg-white shadow-sm"> <section className="w-full max-w-2xl rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="border-b border-slate-100 px-6 py-5"> <div className="border-b border-slate-100 px-6 py-5">
<h1 className="text-2xl font-semibold tracking-tight text-slate-900"> <h1 className="text-2xl font-semibold tracking-tight text-slate-900">
Mission Control profile Pipeline profile
</h1> </h1>
<p className="mt-1 text-sm text-slate-600"> <p className="mt-1 text-sm text-slate-600">
Configure your mission control settings and preferences. Configure your Pipeline settings and preferences.
</p> </p>
</div> </div>
<div className="px-6 py-6"> <div className="px-6 py-6">

View File

@ -246,7 +246,7 @@ export default function SettingsPage() {
Delete account Delete account
</h2> </h2>
<p className="mt-1 text-sm text-rose-800"> <p className="mt-1 text-sm text-rose-800">
This permanently removes your Mission Control account and related This permanently removes your Pipeline account and related
personal data. This action cannot be undone. personal data. This action cannot be undone.
</p> </p>
<div className="mt-4"> <div className="mt-4">

View File

@ -1,15 +1,12 @@
import { PipelineIcon } from "./PipelineIcon";
export function BrandMark() { export function BrandMark() {
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="grid h-10 w-10 place-items-center rounded-lg bg-gradient-to-br from-blue-600 to-blue-700 text-xs font-semibold text-white shadow-sm"> <PipelineIcon />
<span className="font-heading tracking-[0.2em]">OC</span>
</div>
<div className="leading-tight"> <div className="leading-tight">
<div className="font-heading text-sm uppercase tracking-[0.26em] text-strong"> <div className="font-heading text-sm uppercase tracking-[0.26em] text-strong">
OPENCLAW PIPELINE
</div>
<div className="text-[11px] font-medium text-quiet">
Mission Control
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,19 @@
import { Workflow } from "lucide-react";
type PipelineIconProps = {
className?: string;
};
export function PipelineIcon({ className }: PipelineIconProps) {
return (
<div
className={
className ??
"grid h-10 w-10 place-items-center rounded-lg bg-[color:var(--accent)] text-white shadow-sm"
}
aria-hidden="true"
>
<Workflow className="h-5 w-5" strokeWidth={2.4} />
</div>
);
}

View File

@ -3,7 +3,7 @@ import { HeroKicker } from "@/components/atoms/HeroKicker";
export function HeroCopy() { export function HeroCopy() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<HeroKicker>OpenClaw Mission Control</HeroKicker> <HeroKicker>Pipeline</HeroKicker>
<div className="space-y-4"> <div className="space-y-4">
<h1 className="font-heading text-4xl font-semibold leading-tight text-strong sm:text-5xl lg:text-6xl"> <h1 className="font-heading text-4xl font-semibold leading-tight text-strong sm:text-5xl lg:text-6xl">
Command autonomous work. Command autonomous work.

View File

@ -56,39 +56,36 @@ export function DashboardSidebar() {
: systemStatus === "unknown" : systemStatus === "unknown"
? "System status unavailable" ? "System status unavailable"
: "System degraded"; : "System degraded";
const navItemClass = (active: boolean) =>
cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-[color:var(--text-muted)] transition",
active
? "bg-[color:var(--accent-soft)] font-medium text-[color:var(--accent-strong)]"
: "hover:bg-[color:var(--surface-muted)] hover:text-[color:var(--text)]",
);
return ( return (
<aside className="fixed inset-y-0 left-0 z-40 flex w-[280px] -translate-x-full flex-col border-r border-slate-200 bg-white pt-16 shadow-lg transition-transform duration-200 ease-in-out [[data-sidebar=open]_&]:translate-x-0 md:relative md:inset-auto md:z-auto md:w-[260px] md:translate-x-0 md:pt-0 md:shadow-none md:transition-none"> <aside className="fixed inset-y-0 left-0 z-40 flex w-[280px] -translate-x-full flex-col border-r border-[color:var(--border)] bg-[color:var(--surface)] pt-16 shadow-lg transition-transform duration-200 ease-in-out [[data-sidebar=open]_&]:translate-x-0 md:relative md:inset-auto md:z-auto md:w-[260px] md:translate-x-0 md:pt-0 md:shadow-none md:transition-none">
<div className="flex-1 px-3 py-4"> <div className="flex-1 px-3 py-4">
<p className="px-3 text-xs font-semibold uppercase tracking-wider text-slate-500"> <p className="px-3 text-xs font-semibold uppercase tracking-wider text-[color:var(--text-muted)]">
Navigation Navigation
</p> </p>
<nav className="mt-3 space-y-4 text-sm"> <nav className="mt-3 space-y-4 text-sm">
<div> <div>
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-400"> <p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
Overview Overview
</p> </p>
<div className="mt-1 space-y-1"> <div className="mt-1 space-y-1">
<Link <Link
href="/dashboard" href="/dashboard"
className={cn( className={navItemClass(pathname === "/dashboard")}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname === "/dashboard"
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<BarChart3 className="h-4 w-4" /> <BarChart3 className="h-4 w-4" />
Dashboard Dashboard
</Link> </Link>
<Link <Link
href="/activity" href="/activity"
className={cn( className={navItemClass(pathname.startsWith("/activity"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/activity")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<Activity className="h-4 w-4" /> <Activity className="h-4 w-4" />
Live feed Live feed
@ -97,54 +94,34 @@ export function DashboardSidebar() {
</div> </div>
<div> <div>
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-400"> <p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
Boards Boards
</p> </p>
<div className="mt-1 space-y-1"> <div className="mt-1 space-y-1">
<Link <Link
href="/board-groups" href="/board-groups"
className={cn( className={navItemClass(pathname.startsWith("/board-groups"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/board-groups")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<Folder className="h-4 w-4" /> <Folder className="h-4 w-4" />
Board groups Board groups
</Link> </Link>
<Link <Link
href="/boards" href="/boards"
className={cn( className={navItemClass(pathname.startsWith("/boards"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/boards")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<LayoutGrid className="h-4 w-4" /> <LayoutGrid className="h-4 w-4" />
Boards Boards
</Link> </Link>
<Link <Link
href="/tags" href="/tags"
className={cn( className={navItemClass(pathname.startsWith("/tags"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/tags")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<Tags className="h-4 w-4" /> <Tags className="h-4 w-4" />
Tags Tags
</Link> </Link>
<Link <Link
href="/approvals" href="/approvals"
className={cn( className={navItemClass(pathname.startsWith("/approvals"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/approvals")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<CheckCircle2 className="h-4 w-4" /> <CheckCircle2 className="h-4 w-4" />
Approvals Approvals
@ -152,11 +129,8 @@ export function DashboardSidebar() {
{isAdmin ? ( {isAdmin ? (
<Link <Link
href="/custom-fields" href="/custom-fields"
className={cn( className={navItemClass(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition", pathname.startsWith("/custom-fields"),
pathname.startsWith("/custom-fields")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)} )}
> >
<Settings className="h-4 w-4" /> <Settings className="h-4 w-4" />
@ -169,18 +143,15 @@ export function DashboardSidebar() {
<div> <div>
{isAdmin ? ( {isAdmin ? (
<> <>
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-400"> <p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
Skills Skills
</p> </p>
<div className="mt-1 space-y-1"> <div className="mt-1 space-y-1">
<Link <Link
href="/skills/marketplace" href="/skills/marketplace"
className={cn( className={navItemClass(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname === "/skills" || pathname === "/skills" ||
pathname.startsWith("/skills/marketplace") pathname.startsWith("/skills/marketplace"),
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)} )}
> >
<Store className="h-4 w-4" /> <Store className="h-4 w-4" />
@ -188,11 +159,8 @@ export function DashboardSidebar() {
</Link> </Link>
<Link <Link
href="/skills/packs" href="/skills/packs"
className={cn( className={navItemClass(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition", pathname.startsWith("/skills/packs"),
pathname.startsWith("/skills/packs")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)} )}
> >
<Boxes className="h-4 w-4" /> <Boxes className="h-4 w-4" />
@ -204,18 +172,13 @@ export function DashboardSidebar() {
</div> </div>
<div> <div>
<p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-400"> <p className="px-3 text-[11px] font-semibold uppercase tracking-wider text-[color:var(--text-quiet)]">
Administration Administration
</p> </p>
<div className="mt-1 space-y-1"> <div className="mt-1 space-y-1">
<Link <Link
href="/organization" href="/organization"
className={cn( className={navItemClass(pathname.startsWith("/organization"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/organization")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<Building2 className="h-4 w-4" /> <Building2 className="h-4 w-4" />
Organization Organization
@ -223,12 +186,7 @@ export function DashboardSidebar() {
{isAdmin ? ( {isAdmin ? (
<Link <Link
href="/gateways" href="/gateways"
className={cn( className={navItemClass(pathname.startsWith("/gateways"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/gateways")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<Network className="h-4 w-4" /> <Network className="h-4 w-4" />
Gateways Gateways
@ -237,12 +195,7 @@ export function DashboardSidebar() {
{isAdmin ? ( {isAdmin ? (
<Link <Link
href="/agents" href="/agents"
className={cn( className={navItemClass(pathname.startsWith("/agents"))}
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/agents")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100",
)}
> >
<Bot className="h-4 w-4" /> <Bot className="h-4 w-4" />
Agents Agents
@ -252,14 +205,14 @@ export function DashboardSidebar() {
</div> </div>
</nav> </nav>
</div> </div>
<div className="border-t border-slate-200 p-4"> <div className="border-t border-[color:var(--border)] p-4">
<div className="flex items-center gap-2 text-xs text-slate-500"> <div className="flex items-center gap-2 text-xs text-[color:var(--text-muted)]">
<span <span
className={cn( className={cn(
"h-2 w-2 rounded-full", "h-2 w-2 rounded-full",
systemStatus === "operational" && "bg-emerald-500", systemStatus === "operational" && "bg-emerald-500",
systemStatus === "degraded" && "bg-rose-500", systemStatus === "degraded" && "bg-rose-500",
systemStatus === "unknown" && "bg-slate-300", systemStatus === "unknown" && "bg-[color:var(--text-quiet)]",
)} )}
/> />
{statusLabel} {statusLabel}

View File

@ -34,7 +34,7 @@ export function LandingHero() {
<> <>
<section className="hero"> <section className="hero">
<div className="hero-content"> <div className="hero-content">
<div className="hero-label">OpenClaw Mission Control</div> <div className="hero-label">Pipeline</div>
<h1> <h1>
Command <span className="hero-highlight">autonomous work.</span> Command <span className="hero-highlight">autonomous work.</span>
<br /> <br />

View File

@ -100,7 +100,7 @@ export function LocalAuthLogin({ onAuthenticated }: LocalAuthLoginProps) {
Local Authentication Local Authentication
</h1> </h1>
<p className="text-sm text-muted"> <p className="text-sm text-muted">
Enter your access token to unlock Mission Control. Enter your access token to unlock Pipeline.
</p> </p>
</div> </div>
</CardHeader> </CardHeader>

View File

@ -0,0 +1,63 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { ThemeProvider } from "@/components/providers/ThemeProvider";
import { ThemeToggle } from "./ThemeToggle";
const storage = new Map<string, string>();
function renderThemeToggle() {
render(
<ThemeProvider>
<ThemeToggle />
</ThemeProvider>,
);
}
describe("ThemeToggle", () => {
beforeEach(() => {
Object.defineProperty(window, "localStorage", {
configurable: true,
value: {
clear: () => storage.clear(),
getItem: (key: string) => storage.get(key) ?? null,
removeItem: (key: string) => storage.delete(key),
setItem: (key: string, value: string) => storage.set(key, value),
},
});
});
afterEach(() => {
storage.clear();
document.documentElement.dataset.theme = "dark";
document.documentElement.classList.add("dark");
});
it("defaults to dark mode", () => {
renderThemeToggle();
expect(
screen.getByRole("button", { name: "Switch to light mode" }),
).toBeInTheDocument();
expect(document.documentElement.dataset.theme).toBe("dark");
expect(document.documentElement).toHaveClass("dark");
});
it("stores the selected light mode", async () => {
const user = userEvent.setup();
renderThemeToggle();
await user.click(
screen.getByRole("button", { name: "Switch to light mode" }),
);
expect(storage.get("openclaw_theme")).toBe("light");
expect(document.documentElement.dataset.theme).toBe("light");
expect(document.documentElement).not.toHaveClass("dark");
expect(
screen.getByRole("button", { name: "Switch to dark mode" }),
).toBeInTheDocument();
});
});

View File

@ -0,0 +1,37 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "@/components/providers/ThemeProvider";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
const isDark = theme === "dark";
const label = isDark ? "Switch to light mode" : "Switch to dark mode";
const Icon = isDark ? Sun : Moon;
return (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={label}
aria-pressed={isDark}
className="inline-flex h-9 w-9 items-center justify-center rounded-[10px] border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-sm transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-muted)] hover:text-[color:var(--text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--surface)]"
onClick={toggleTheme}
>
<Icon className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@ -58,11 +58,11 @@ export function UserMenu({
type="button" type="button"
className={cn( className={cn(
"group inline-flex h-9 items-center gap-2 rounded-[10px] bg-transparent px-1 py-1 transition", "group inline-flex h-9 items-center gap-2 rounded-[10px] bg-transparent px-1 py-1 transition",
"hover:bg-white/70", "hover:bg-[color:var(--surface-muted)]",
// Avoid the default browser focus outline (often bright blue) on click. // Avoid the default browser focus outline (often bright blue) on click.
// Keep a subtle, enterprise-looking focus ring for keyboard navigation. // Keep a subtle, enterprise-looking focus ring for keyboard navigation.
"focus:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--neutral-300,var(--border-strong))] focus-visible:ring-offset-2 focus-visible:ring-offset-white", "focus:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--neutral-300,var(--border-strong))] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--surface)]",
"data-[state=open]:bg-white", "data-[state=open]:bg-[color:var(--surface-muted)]",
className, className,
)} )}
aria-label="Open user menu" aria-label="Open user menu"
@ -93,7 +93,7 @@ export function UserMenu({
<PopoverContent <PopoverContent
align="end" align="end"
sideOffset={12} sideOffset={12}
className="w-80 overflow-hidden rounded-2xl border border-[color:var(--neutral-200,var(--border))] bg-white/95 p-0 shadow-[0_8px_32px_rgba(10,22,40,0.08)] backdrop-blur" className="w-80 overflow-hidden rounded-2xl border border-[color:var(--neutral-200,var(--border))] bg-[color:var(--surface)]/95 p-0 shadow-[0_8px_32px_rgba(0,0,0,0.18)] backdrop-blur"
> >
<div className="border-b border-[color:var(--neutral-200,var(--border))] px-4 py-3"> <div className="border-b border-[color:var(--neutral-200,var(--border))] px-4 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -133,7 +133,7 @@ export function UserMenu({
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<Link <Link
href="/boards" href="/boards"
className="flex w-full items-center justify-center gap-2 rounded-xl border border-[color:var(--neutral-300,var(--border-strong))] bg-white px-3 py-2 text-sm font-semibold text-[color:var(--neutral-800,var(--text))] transition hover:border-[color:var(--primary-navy,var(--accent-strong))] hover:bg-[color:var(--neutral-100,var(--surface-muted))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-teal,var(--accent))] focus-visible:ring-offset-2" className="flex w-full items-center justify-center gap-2 rounded-xl border border-[color:var(--neutral-300,var(--border-strong))] bg-[color:var(--surface)] px-3 py-2 text-sm font-semibold text-[color:var(--neutral-800,var(--text))] transition hover:border-[color:var(--primary-navy,var(--accent-strong))] hover:bg-[color:var(--neutral-100,var(--surface-muted))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-teal,var(--accent))] focus-visible:ring-offset-2"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >
<Trello className="h-4 w-4 text-[color:var(--neutral-700,var(--text-quiet))]" /> <Trello className="h-4 w-4 text-[color:var(--neutral-700,var(--text-quiet))]" />

View File

@ -0,0 +1,115 @@
"use client";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
type Theme = "dark" | "light";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
};
const THEME_STORAGE_KEY = "openclaw_theme";
const DEFAULT_THEME: Theme = "dark";
const ThemeContext = createContext<ThemeContextValue | null>(null);
function isTheme(value: string | null): value is Theme {
return value === "dark" || value === "light";
}
function applyTheme(theme: Theme) {
const root = document.documentElement;
root.dataset.theme = theme;
root.classList.toggle("dark", theme === "dark");
root.style.colorScheme = theme;
}
function getStoredTheme() {
try {
const storage = window.localStorage;
if (typeof storage.getItem !== "function") return null;
return storage.getItem(THEME_STORAGE_KEY);
} catch {
return null;
}
}
function storeTheme(theme: Theme) {
try {
const storage = window.localStorage;
if (typeof storage.setItem === "function") {
storage.setItem(THEME_STORAGE_KEY, theme);
}
} catch {
// The visual preference still applies even when persistence is unavailable.
}
}
function readStoredTheme(): Theme {
if (typeof window === "undefined") return DEFAULT_THEME;
const storedTheme = getStoredTheme();
return isTheme(storedTheme) ? storedTheme : DEFAULT_THEME;
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => readStoredTheme());
useEffect(() => {
applyTheme(theme);
}, [theme]);
useEffect(() => {
const handleStorage = (event: StorageEvent) => {
if (event.key !== THEME_STORAGE_KEY) return;
const nextTheme = isTheme(event.newValue)
? event.newValue
: DEFAULT_THEME;
setThemeState(nextTheme);
applyTheme(nextTheme);
};
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, []);
const setTheme = useCallback((nextTheme: Theme) => {
setThemeState(nextTheme);
applyTheme(nextTheme);
storeTheme(nextTheme);
}, []);
const toggleTheme = useCallback(() => {
setThemeState((currentTheme) => {
const nextTheme = currentTheme === "dark" ? "light" : "dark";
applyTheme(nextTheme);
storeTheme(nextTheme);
return nextTheme;
});
}, []);
const value = useMemo(
() => ({ theme, setTheme, toggleTheme }),
[setTheme, theme, toggleTheme],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}

View File

@ -66,11 +66,14 @@ export function DashboardPageLayout({
<DashboardSidebar /> <DashboardSidebar />
<main <main
ref={mainRef} ref={mainRef}
className={cn("flex-1 overflow-y-auto bg-slate-50", mainClassName)} className={cn(
"flex-1 overflow-y-auto bg-[color:var(--bg)]",
mainClassName,
)}
> >
<div <div
className={cn( className={cn(
"border-b border-slate-200 bg-white", "border-b border-[color:var(--border)] bg-[color:var(--surface)]",
stickyHeader && "sticky top-0 z-30", stickyHeader && "sticky top-0 z-30",
headerClassName, headerClassName,
)} )}
@ -79,11 +82,11 @@ export function DashboardPageLayout({
{headerActions ? ( {headerActions ? (
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div>
<h1 className="font-heading text-2xl font-semibold tracking-tight text-slate-900"> <h1 className="font-heading text-2xl font-semibold tracking-tight text-[color:var(--text)]">
{title} {title}
</h1> </h1>
{description ? ( {description ? (
<p className="mt-1 text-sm text-slate-500"> <p className="mt-1 text-sm text-[color:var(--text-muted)]">
{description} {description}
</p> </p>
) : null} ) : null}
@ -92,11 +95,13 @@ export function DashboardPageLayout({
</div> </div>
) : ( ) : (
<div> <div>
<h1 className="font-heading text-2xl font-semibold tracking-tight text-slate-900"> <h1 className="font-heading text-2xl font-semibold tracking-tight text-[color:var(--text)]">
{title} {title}
</h1> </h1>
{description ? ( {description ? (
<p className="mt-1 text-sm text-slate-500">{description}</p> <p className="mt-1 text-sm text-[color:var(--text-muted)]">
{description}
</p>
) : null} ) : null}
</div> </div>
)} )}

View File

@ -14,6 +14,7 @@ import {
} from "@/api/generated/users/users"; } from "@/api/generated/users/users";
import { BrandMark } from "@/components/atoms/BrandMark"; import { BrandMark } from "@/components/atoms/BrandMark";
import { OrgSwitcher } from "@/components/organisms/OrgSwitcher"; import { OrgSwitcher } from "@/components/organisms/OrgSwitcher";
import { ThemeToggle } from "@/components/organisms/ThemeToggle";
import { UserMenu } from "@/components/organisms/UserMenu"; import { UserMenu } from "@/components/organisms/UserMenu";
import { isOnboardingComplete } from "@/lib/onboarding"; import { isOnboardingComplete } from "@/lib/onboarding";
@ -22,7 +23,10 @@ export function DashboardShell({ children }: { children: ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
const { isSignedIn } = useAuth(); const { isSignedIn } = useAuth();
const isOnboardingPath = pathname === "/onboarding"; const isOnboardingPath = pathname === "/onboarding";
const [sidebarState, setSidebarState] = useState({ open: false, path: pathname }); const [sidebarState, setSidebarState] = useState({
open: false,
path: pathname,
});
// Close sidebar on navigation using React's "store info from previous // Close sidebar on navigation using React's "store info from previous
// renders" pattern — conditional setState during render resets immediately // renders" pattern — conditional setState during render resets immediately
// without extra commits, avoiding both set-state-in-effect and refs rules. // without extra commits, avoiding both set-state-in-effect and refs rules.
@ -86,25 +90,33 @@ export function DashboardShell({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
if (!sidebarOpen) return; if (!sidebarOpen) return;
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setSidebarState((prev) => ({ ...prev, open: false })); if (e.key === "Escape")
setSidebarState((prev) => ({ ...prev, open: false }));
}; };
document.addEventListener("keydown", onKey); document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey);
}, [sidebarOpen]); }, [sidebarOpen]);
return ( return (
<div className="min-h-screen bg-app text-strong" data-sidebar={sidebarOpen ? "open" : "closed"}> <div
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white shadow-sm"> className="min-h-screen bg-app text-strong"
data-sidebar={sidebarOpen ? "open" : "closed"}
>
<header className="sticky top-0 z-50 border-b border-[color:var(--border)] bg-[color:var(--surface)] shadow-sm">
<div className="flex items-center py-3"> <div className="flex items-center py-3">
<div className="flex items-center px-4 md:px-6 md:w-[260px]"> <div className="flex items-center px-4 md:px-6 md:w-[260px]">
{isSignedIn ? ( {isSignedIn ? (
<button <button
type="button" type="button"
className="mr-3 rounded-lg p-2 text-slate-600 hover:bg-slate-100 md:hidden" className="mr-3 rounded-lg p-2 text-[color:var(--text-muted)] hover:bg-[color:var(--surface-muted)] md:hidden"
onClick={toggleSidebar} onClick={toggleSidebar}
aria-label="Toggle navigation" aria-label="Toggle navigation"
> >
{sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />} {sidebarOpen ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</button> </button>
) : null} ) : null}
<BrandMark /> <BrandMark />
@ -119,11 +131,14 @@ export function DashboardShell({ children }: { children: ReactNode }) {
<SignedIn> <SignedIn>
<div className="ml-auto flex items-center gap-3 px-4 md:px-6"> <div className="ml-auto flex items-center gap-3 px-4 md:px-6">
<div className="hidden text-right lg:block"> <div className="hidden text-right lg:block">
<p className="text-sm font-semibold text-slate-900"> <p className="text-sm font-semibold text-[color:var(--text)]">
{displayName} {displayName}
</p> </p>
<p className="text-xs text-slate-500">Operator</p> <p className="text-xs text-[color:var(--text-muted)]">
Operator
</p>
</div> </div>
<ThemeToggle />
<UserMenu displayName={displayName} displayEmail={displayEmail} /> <UserMenu displayName={displayName} displayEmail={displayEmail} />
</div> </div>
</SignedIn> </SignedIn>
@ -140,7 +155,7 @@ export function DashboardShell({ children }: { children: ReactNode }) {
/> />
) : null} ) : null}
<div className="grid min-h-[calc(100vh-64px)] grid-cols-1 md:grid-cols-[260px_1fr] bg-slate-50"> <div className="grid min-h-[calc(100vh-64px)] grid-cols-1 bg-[color:var(--bg)] md:grid-cols-[260px_1fr]">
{children} {children}
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@ import {
isClerkEnabled, isClerkEnabled,
} from "@/auth/clerk"; } from "@/auth/clerk";
import { PipelineIcon } from "@/components/atoms/PipelineIcon";
import { UserMenu } from "@/components/organisms/UserMenu"; import { UserMenu } from "@/components/organisms/UserMenu";
export function LandingShell({ children }: { children: ReactNode }) { export function LandingShell({ children }: { children: ReactNode }) {
@ -19,13 +20,10 @@ export function LandingShell({ children }: { children: ReactNode }) {
<div className="landing-enterprise"> <div className="landing-enterprise">
<nav className="landing-nav" aria-label="Primary navigation"> <nav className="landing-nav" aria-label="Primary navigation">
<div className="nav-container"> <div className="nav-container">
<Link href="/" className="logo-section" aria-label="OpenClaw home"> <Link href="/" className="logo-section" aria-label="Pipeline home">
<div className="logo-icon" aria-hidden="true"> <PipelineIcon className="logo-icon" />
OC
</div>
<div className="logo-text"> <div className="logo-text">
<div className="logo-name">OpenClaw</div> <div className="logo-name">Pipeline</div>
<div className="logo-tagline">Mission Control</div>
</div> </div>
</Link> </Link>
@ -89,7 +87,7 @@ export function LandingShell({ children }: { children: ReactNode }) {
<footer className="landing-footer"> <footer className="landing-footer">
<div className="footer-content"> <div className="footer-content">
<div className="footer-brand"> <div className="footer-brand">
<h3>OpenClaw</h3> <h3>Pipeline</h3>
<p>A calm command center for boards, agents, and approvals.</p> <p>A calm command center for boards, agents, and approvals.</p>
<div className="footer-tagline">Realtime Execution Visibility</div> <div className="footer-tagline">Realtime Execution Visibility</div>
</div> </div>
@ -150,7 +148,7 @@ export function LandingShell({ children }: { children: ReactNode }) {
<div className="footer-bottom"> <div className="footer-bottom">
<div className="footer-copyright"> <div className="footer-copyright">
© {new Date().getFullYear()} OpenClaw. All rights reserved. © {new Date().getFullYear()} Pipeline. All rights reserved.
</div> </div>
<div className="footer-bottom-links"> <div className="footer-bottom-links">
<Link href="#capabilities">Capabilities</Link> <Link href="#capabilities">Capabilities</Link>