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:
parent
0f50db1e9c
commit
827d62c05e
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "pipeline-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
buildUrlWithTaskAndComment(fullTask.id, targetCommentId),
|
||||||
|
{
|
||||||
scroll: false,
|
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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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`}
|
||||||
>
|
>
|
||||||
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<GlobalLoader />
|
<GlobalLoader />
|
||||||
{children}
|
{children}
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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))]" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue