feat(canvas): warm-paper theme + Tailwind v4 migration

Brings the canvas onto the warm-paper design system already shipped to
landing, marketplace, and SaaS surfaces, and migrates the build from
Tailwind v3 → v4 to match molecule-app.

Plumbing:
- swap tailwindcss v3 → v4, drop autoprefixer, add @tailwindcss/postcss
- delete tailwind.config.ts (v4 reads tokens from @theme blocks in CSS)
- globals.css: @import "tailwindcss" + @plugin "@tailwindcss/typography"
- two @theme blocks: warm-paper light defaults + always-dark surface
  tokens (bg-bg / ink-mute / line-strong) for terminal/console panels
- [data-theme="dark"] cascade overrides the warm-paper tokens for dark
- React Flow edge stroke + scrollbar + selection colour pull from
  semantic tokens so they flip with the theme

Theme infra (ported from molecule-app, identical contracts):
- lib/theme-cookie.ts: mol_theme cookie + boot script (no "use client"
  so server components can read the constants)
- lib/theme-provider.tsx: ThemeProvider + useTheme + cookie writer with
  Domain=.moleculesai.app so the preference follows the user across
  canvas/app/market/landing subdomains AND tenant subdomains
- lib/theme.ts: ColorToken union + cssVar() helper
- components/ThemeToggle.tsx: 3-way System/Light/Dark picker
- layout.tsx: SSR cookie read + nonce'd inline boot script (CSP needs
  the explicit nonce — strict-dynamic doesn't forgive an un-nonce'd
  inline sibling) + ThemeProvider wrapper + bg-surface/text-ink body

Component migration (62 files):
- Mechanical bg-zinc-* / text-zinc-* / border-zinc-* / text-white →
  semantic surface/ink/line tokens via perl negative-lookahead pass
  (preserves opacity modifiers like /80, /60)
- bg-blue-500/600 → bg-accent / bg-accent-strong
- text-red-* / amber-* / emerald-* → text-bad / warm / good
- Tinted-state banner backgrounds (bg-red-950, bg-amber-950, bg-blue-950
  etc.) intentionally left literal — they remain readable on warm-paper
  in light mode without inventing new state-soft tokens
- TerminalTab.tsx skipped — xterm renders to canvas, not DOM
- 3 unit-test assertions updated to match new token strings (credits
  pillTone, AuthGate overlay class, A2AEdge accent)

Verification:
- pnpm test: 1214/1214 pass
- pnpm tsc --noEmit: clean
- next build: ✓ Compiled successfully (8 routes)
- dev server inspection: html data-theme stamped, body uses
  bg-surface text-ink, boot script carries nonce, compiled CSS
  contains both @theme blocks + [data-theme="dark"] override

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-05-03 01:43:55 -07:00
parent d58185b8a8
commit c0eca8d0e1
75 changed files with 1888 additions and 2031 deletions

1292
canvas/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,10 +37,10 @@
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "^4.1.5", "@vitest/coverage-v8": "^4.1.5",
"autoprefixer": "^10.4.0", "@tailwindcss/postcss": "^4.0.0",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"postcss": "^8.5.13", "postcss": "^8.5.13",
"tailwindcss": "^3.4.0", "tailwindcss": "^4.0.0",
"typescript": "^5.7.0", "typescript": "^5.7.0",
"vitest": "^4.1.2" "vitest": "^4.1.2"
} }

View File

@ -1,6 +1,5 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, "@tailwindcss/postcss": {},
autoprefixer: {},
}, },
}; };

View File

@ -1,28 +1,130 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
/*
* Load order:
* 1. Tailwind core (v4) provides preflight + utility generation.
* 2. xterm overrides preflight on its own .xterm-* class names; must
* load AFTER tailwind so its specificity wins.
* 3. theme-tokens.css canvas-only motion + deploy animation vars
* (--mol-duration-*, --mol-easing-*, --mol-deploy-*). NOT colour
* tokens; the warm-paper @theme block below owns those.
* 4. settings-panel.css / org-deploy.css feature stylesheets that
* reference the variables above.
*/
@import "xterm/css/xterm.css"; @import "xterm/css/xterm.css";
/* Theme tokens MUST load before any feature stylesheet that
references them so custom properties are in scope. */
@import "../styles/theme-tokens.css"; @import "../styles/theme-tokens.css";
@import "../styles/settings-panel.css"; @import "../styles/settings-panel.css";
@import "../styles/org-deploy.css"; @import "../styles/org-deploy.css";
@tailwind base; /*
@tailwind components; * Warm-paper semantic tokens light defaults via @theme, dark
@tailwind utilities; * overrides via [data-theme="dark"]. Names are role-based
* (`bg-surface`, `text-ink`, `border-line`) not colour-based, so the
* same component classes work in either mode.
*
* Source of truth: molecule-app/app/globals.css. Keep aligned across
* surfaces (landing, market, app, canvas) so a token tweak ripples
* everywhere via a single PR per repo.
*
* Theme preference is persisted in the `mol_theme` cookie scoped to
* Domain=.moleculesai.app so the choice follows the user across
* subdomains. The inline boot script in app/layout.tsx applies it
* before paint to eliminate flash.
*/
@theme {
/* Surface — page / elevated card / sunken input / deep card */
--color-surface: #fafaf7;
--color-surface-elevated: #ffffff;
--color-surface-sunken: #f3f1ec;
--color-surface-card: #efece4;
/* Borders */
--color-line: #e6e2d8;
--color-line-soft: #efece4;
/* Text */
--color-ink: #15181c;
--color-ink-mid: #5a5e66;
--color-ink-soft: #8b8e95;
/* Brand + state */
--color-accent: #3b5bdb;
--color-accent-strong: #1a2f99;
--color-warm: #c0532b;
--color-good: #2f7a4d;
--color-bad: #b94e4a;
}
[data-theme="dark"] {
--color-surface: #0e1014;
--color-surface-elevated: #15181c;
--color-surface-sunken: #0a0b0e;
--color-surface-card: #1a1d23;
--color-line: #2a2f3a;
--color-line-soft: #1f2329;
--color-ink: #f4f1e9;
--color-ink-mid: #c8c2b4;
--color-ink-soft: #8d92a0;
/* Accents brighten slightly for AA contrast on dark backgrounds. */
--color-accent: #6883e8;
--color-accent-strong: #8aa1ee;
--color-warm: #d96f48;
--color-good: #4ca06e;
--color-bad: #d27773;
}
:root {
color-scheme: light;
}
[data-theme="dark"] {
color-scheme: dark;
}
/*
* Always-dark surface tokens. Terminals (xterm), the console modal,
* and log streams stay dark in both modes readable green-on-black
* code surfaces don't translate cleanly to a light theme. Components
* that should not light-flip use `bg-bg`, `bg-bg-elev`, `bg-bg-card`,
* `text-ink-mute`, `text-ink-dim`, `border-line-strong` instead of
* the warm-paper utilities above.
*
* Distinct names (bg-* / ink-mute / ink-dim / line-strong) so they
* don't collide with the warm-paper namespace (surface / ink /
* line). Both palettes coexist; the choice between them is per
* component, not per theme.
*/
@theme {
--color-bg: rgb(9 9 11); /* zinc-950 */
--color-bg-elev: rgb(24 24 27); /* zinc-900 */
--color-bg-card: rgb(39 39 42); /* zinc-800 */
--color-line-strong: rgb(63 63 70); /* zinc-700 */
--color-ink-mute: rgb(161 161 170); /* zinc-400 */
--color-ink-dim: rgb(113 113 122); /* zinc-500 */
--color-accent-dim: rgb(96 165 250);/* blue-400 */
--color-plasma: rgb(59 130 246); /* blue-500 */
--color-warn: rgb(251 191 36); /* amber-400 */
}
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
background: #09090b; background-color: var(--color-surface);
color: #e4e4e7; color: var(--color-ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
/* React Flow overrides for dark theme */ /* React Flow overrides for both themes. Edge stroke pulls from the
semantic line token so dark mode keeps its existing zinc-700 look
and light mode picks up the warm-paper line colour. */
.react-flow__edge-path { .react-flow__edge-path {
stroke: #3f3f46 !important; stroke: var(--color-line) !important;
stroke-width: 1.5 !important; stroke-width: 1.5 !important;
} }
@ -58,7 +160,8 @@ body {
transition: box-shadow var(--mol-duration-fast) ease; transition: box-shadow var(--mol-duration-fast) ease;
} }
/* Scrollbar styling */ /* Scrollbar styling. Track + thumb pull from the surface tokens so
they feel native to either theme. */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;
height: 6px; height: 6px;
@ -69,17 +172,17 @@ body {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #3f3f46; background: var(--color-line);
border-radius: 3px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #52525b; background: var(--color-line-strong, var(--color-ink-soft));
} }
/* Selection */ /* Selection */
::selection { ::selection {
background: rgba(59, 130, 246, 0.3); background: color-mix(in srgb, var(--color-accent) 30%, transparent);
} }
/* Panel slide animation */ /* Panel slide animation */

View File

@ -1,8 +1,14 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { headers } from "next/headers"; import { cookies, headers } from "next/headers";
import "./globals.css"; import "./globals.css";
import { AuthGate } from "@/components/AuthGate"; import { AuthGate } from "@/components/AuthGate";
import { CookieConsent } from "@/components/CookieConsent"; import { CookieConsent } from "@/components/CookieConsent";
import { ThemeProvider } from "@/lib/theme-provider";
import {
THEME_COOKIE,
readThemeCookie,
themeBootScript,
} from "@/lib/theme-cookie";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Molecule AI", title: "Molecule AI",
@ -15,7 +21,7 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
// Read the per-request CSP nonce that middleware.ts sets via the // Read the per-request CSP nonce that middleware.ts sets via the
// `x-nonce` request header. This call is load-bearing for TWO // `x-nonce` request header. This call is load-bearing for THREE
// independent reasons: // independent reasons:
// //
// 1. It opts the root layout into dynamic rendering. Without a // 1. It opts the root layout into dynamic rendering. Without a
@ -31,22 +37,56 @@ export default async function RootLayout({
// is actually read via `headers()`. The header's existence on // is actually read via `headers()`. The header's existence on
// the request isn't enough — Next.js watches for the read. // the request isn't enough — Next.js watches for the read.
// //
// Keeping the `nonce` variable unused is intentional: we don't need // 3. We need the nonce to attach to the inline theme boot script
// to pass it to any custom <Script nonce={...}> tags right now, the // below, otherwise CSP rejects it in production where
// framework takes care of its own bootstrap scripts once the read // script-src is `'self' 'nonce-{nonce}' 'strict-dynamic'`.
// happens. Destructuring via `await` + `.get()` is the minimum shape // 'strict-dynamic' propagates trust from a nonce'd script to
// Next.js recognizes as "dynamic server-side access". // scripts it inserts, but does NOT forgive an un-nonce'd
await headers(); // sibling — the boot script must carry its own nonce.
const hdrs = await headers();
const nonce = hdrs.get("x-nonce") ?? undefined;
// SSR: read the user's saved preference. For light/dark we can stamp
// data-theme on <html> here so the very first paint matches; for
// "system" we leave the attribute off and let the inline boot script
// resolve from matchMedia before paint.
const cookieStore = await cookies();
const theme = readThemeCookie(cookieStore.get(THEME_COOKIE)?.value);
const initialDataTheme = theme === "system" ? undefined : theme;
return ( return (
<html lang="en"> // suppressHydrationWarning on <html>: the inline boot script below
<body className="bg-zinc-950 text-white"> // mutates `data-theme` before React hydrates (system mode reads
// matchMedia + writes the attribute). That's the entire point of the
// script — eliminate the flash — and it's the documented escape hatch
// for "the server-rendered HTML is intentionally not what React would
// produce client-side at this exact attribute."
<html lang="en" data-theme={initialDataTheme} suppressHydrationWarning>
<head>
{/*
* Boot script: runs synchronously before the body paints, sets
* data-theme on <html> for "system" preference based on the OS
* media query. For explicit light/dark, SSR already set the
* attribute above and the script's write is a no-op.
*
* `nonce` comes from middleware's per-request CSP nonce see
* the comment block above for why CSP requires this even though
* the page also has 'strict-dynamic'.
*/}
<script
nonce={nonce}
dangerouslySetInnerHTML={{ __html: themeBootScript }}
/>
</head>
<body className="bg-surface text-ink">
<ThemeProvider initialTheme={theme}>
{/* AuthGate is a client component; it checks the session on mount {/* AuthGate is a client component; it checks the session on mount
and bounces anonymous users to the control plane's login page and bounces anonymous users to the control plane's login page
when running on a tenant subdomain. Non-SaaS hosts (localhost, when running on a tenant subdomain. Non-SaaS hosts (localhost,
vercel preview URL, apex) pass through unchanged. */} vercel preview URL, apex) pass through unchanged. */}
<AuthGate>{children}</AuthGate> <AuthGate>{children}</AuthGate>
<CookieConsent /> <CookieConsent />
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -110,15 +110,15 @@ export default function OrgsPage() {
}, []); }, []);
if (session === "loading" || (orgs === null && error === null)) { if (session === "loading" || (orgs === null && error === null)) {
return <Shell><p className="text-zinc-400">Loading</p></Shell>; return <Shell><p className="text-ink-mid">Loading</p></Shell>;
} }
if (error) { if (error) {
return ( return (
<Shell> <Shell>
<p role="alert" className="text-red-400">Error: {error}</p> <p role="alert" className="text-bad">Error: {error}</p>
<button <button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="mt-4 rounded bg-zinc-800 px-4 py-2 text-sm text-zinc-200 hover:bg-zinc-700" className="mt-4 rounded bg-surface-card px-4 py-2 text-sm text-ink hover:bg-surface-card"
> >
Retry Retry
</button> </button>
@ -136,7 +136,7 @@ export default function OrgsPage() {
<OrgRow key={o.id} org={o} /> <OrgRow key={o.id} org={o} />
))} ))}
</ul> </ul>
<div className="mt-8 border-t border-zinc-800 pt-6"> <div className="mt-8 border-t border-line pt-6">
<CreateOrgForm <CreateOrgForm
onCreated={(slug) => { onCreated={(slug) => {
// Refresh the list so the new org appears + its CTA fires. // Refresh the list so the new org appears + its CTA fires.
@ -162,11 +162,11 @@ function CheckoutBanner() {
function Shell({ children }: { children: React.ReactNode }) { function Shell({ children }: { children: React.ReactNode }) {
return ( return (
<main className="min-h-screen bg-zinc-950 text-zinc-100"> <main className="min-h-screen bg-surface text-ink">
<TermsGate> <TermsGate>
<div className="mx-auto max-w-2xl px-6 pt-20 pb-12"> <div className="mx-auto max-w-2xl px-6 pt-20 pb-12">
<h1 className="text-3xl font-bold text-white">Your organizations</h1> <h1 className="text-3xl font-bold text-ink">Your organizations</h1>
<p className="mt-2 text-zinc-400"> <p className="mt-2 text-ink-mid">
Each org is an isolated Molecule workspace. Each org is an isolated Molecule workspace.
</p> </p>
<DataResidencyNotice /> <DataResidencyNotice />
@ -184,7 +184,7 @@ function Shell({ children }: { children: React.ReactNode }) {
// region dropdown. // region dropdown.
function DataResidencyNotice() { function DataResidencyNotice() {
return ( return (
<p className="mt-3 rounded border border-zinc-800 bg-zinc-900/60 px-3 py-2 text-xs text-zinc-400"> <p className="mt-3 rounded border border-line bg-surface-sunken/60 px-3 py-2 text-xs text-ink-mid">
Workspaces run in AWS us-east-2 (Ohio, United States). EU region support is on the roadmap reach out to Workspaces run in AWS us-east-2 (Ohio, United States). EU region support is on the roadmap reach out to
{" "} {" "}
<a href="mailto:support@moleculesai.app" className="underline"> <a href="mailto:support@moleculesai.app" className="underline">
@ -197,11 +197,11 @@ function DataResidencyNotice() {
function OrgRow({ org }: { org: Org }) { function OrgRow({ org }: { org: Org }) {
return ( return (
<li className="rounded-lg border border-zinc-800 bg-zinc-900 p-4"> <li className="rounded-lg border border-line bg-surface-sunken p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<div className="font-medium text-white">{org.name}</div> <div className="font-medium text-ink">{org.name}</div>
<div className="text-sm text-zinc-400"> <div className="text-sm text-ink-mid">
{org.slug} · <StatusLabel status={org.status} /> · {org.plan || "free"} {org.slug} · <StatusLabel status={org.status} /> · {org.plan || "free"}
</div> </div>
<div className="mt-2 flex items-center gap-2"> <div className="mt-2 flex items-center gap-2">
@ -237,21 +237,21 @@ function LowCreditsBanner({ org }: { org: Org }) {
if (kind === "overage") { if (kind === "overage") {
const used = (org.overage_used_credits ?? 0).toLocaleString(); const used = (org.overage_used_credits ?? 0).toLocaleString();
return ( return (
<span className="text-xs text-amber-300"> <span className="text-xs text-warm">
overage active · {used} used overage active · {used} used
</span> </span>
); );
} }
if (kind === "out-of-credits") { if (kind === "out-of-credits") {
return ( return (
<a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-red-300 underline"> <a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-bad underline">
out of credits upgrade to keep running out of credits upgrade to keep running
</a> </a>
); );
} }
// trial-tail // trial-tail
return ( return (
<a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-amber-300 underline"> <a href={`/pricing?org=${encodeURIComponent(org.slug)}`} className="text-xs text-warm underline">
trial almost out trial almost out
</a> </a>
); );
@ -260,11 +260,11 @@ function LowCreditsBanner({ org }: { org: Org }) {
function StatusLabel({ status }: { status: OrgStatus }) { function StatusLabel({ status }: { status: OrgStatus }) {
const cls = const cls =
status === "running" status === "running"
? "text-emerald-400" ? "text-good"
: status === "awaiting_payment" : status === "awaiting_payment"
? "text-amber-400" ? "text-warm"
: status === "failed" : status === "failed"
? "text-red-400" ? "text-bad"
: "text-sky-400"; : "text-sky-400";
const label = const label =
status === "awaiting_payment" status === "awaiting_payment"
@ -283,7 +283,7 @@ function OrgCTA({ org }: { org: Org }) {
return ( return (
<a <a
href={href} href={href}
className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-500" className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-ink hover:bg-emerald-500"
> >
Open Open
</a> </a>
@ -293,7 +293,7 @@ function OrgCTA({ org }: { org: Org }) {
return ( return (
<a <a
href={`/pricing?org=${encodeURIComponent(org.slug)}`} href={`/pricing?org=${encodeURIComponent(org.slug)}`}
className="rounded bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-500" className="rounded bg-amber-600 px-4 py-2 text-sm font-medium text-ink hover:bg-amber-500"
> >
Complete payment Complete payment
</a> </a>
@ -303,21 +303,21 @@ function OrgCTA({ org }: { org: Org }) {
return ( return (
<a <a
href="mailto:support@moleculesai.app" href="mailto:support@moleculesai.app"
className="rounded bg-zinc-700 px-4 py-2 text-sm font-medium text-zinc-200 hover:bg-zinc-600" className="rounded bg-surface-card px-4 py-2 text-sm font-medium text-ink hover:bg-zinc-600"
> >
Contact support Contact support
</a> </a>
); );
} }
// provisioning / unknown — non-interactive // provisioning / unknown — non-interactive
return <span className="text-sm text-zinc-500">{org.status}</span>; return <span className="text-sm text-ink-soft">{org.status}</span>;
} }
function EmptyState({ banner }: { banner?: React.ReactNode }) { function EmptyState({ banner }: { banner?: React.ReactNode }) {
return ( return (
<Shell> <Shell>
{banner} {banner}
<p className="text-zinc-300"> <p className="text-ink-mid">
You don't have any organizations yet. Create one to get started your You don't have any organizations yet. Create one to get started your
workspace spins up automatically once billing is set up. workspace spins up automatically once billing is set up.
</p> </p>
@ -365,7 +365,7 @@ function CreateOrgForm({ onCreated }: { onCreated: (slug: string) => void }) {
return ( return (
<form onSubmit={submit} className="space-y-3"> <form onSubmit={submit} className="space-y-3">
<div> <div>
<label htmlFor="org-slug" className="block text-sm text-zinc-300">Slug (URL)</label> <label htmlFor="org-slug" className="block text-sm text-ink-mid">Slug (URL)</label>
<input <input
id="org-slug" id="org-slug"
value={slug} value={slug}
@ -374,28 +374,28 @@ function CreateOrgForm({ onCreated }: { onCreated: (slug: string) => void }) {
placeholder="acme" placeholder="acme"
required required
aria-describedby="org-slug-hint" aria-describedby="org-slug-hint"
className="mt-1 w-full rounded border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100" className="mt-1 w-full rounded border border-line bg-surface-card px-3 py-2 text-sm text-ink"
/> />
<p id="org-slug-hint" className="mt-1 text-xs text-zinc-500"> <p id="org-slug-hint" className="mt-1 text-xs text-ink-soft">
Lowercase letters, numbers, and hyphens only. Cannot be changed later. Lowercase letters, numbers, and hyphens only. Cannot be changed later.
</p> </p>
</div> </div>
<div> <div>
<label htmlFor="org-name" className="block text-sm text-zinc-300">Display name</label> <label htmlFor="org-name" className="block text-sm text-ink-mid">Display name</label>
<input <input
id="org-name" id="org-name"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="Acme Corp" placeholder="Acme Corp"
required required
className="mt-1 w-full rounded border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100" className="mt-1 w-full rounded border border-line bg-surface-card px-3 py-2 text-sm text-ink"
/> />
</div> </div>
{err && <p role="alert" className="text-sm text-red-400">{err}</p>} {err && <p role="alert" className="text-sm text-bad">{err}</p>}
<button <button
type="submit" type="submit"
disabled={submitting} disabled={submitting}
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:opacity-50" className="rounded bg-accent-strong px-4 py-2 text-sm font-medium text-ink hover:bg-accent disabled:opacity-50"
> >
{submitting ? "Creating…" : "Create organization"} {submitting ? "Creating…" : "Create organization"}
</button> </button>

View File

@ -53,10 +53,10 @@ export default function Home() {
if (hydrating) { if (hydrating) {
return ( return (
<div className="fixed inset-0 flex items-center justify-center bg-zinc-950"> <div className="fixed inset-0 flex items-center justify-center bg-surface">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<Spinner size="lg" /> <Spinner size="lg" />
<span className="text-xs text-zinc-500">Loading canvas...</span> <span className="text-xs text-ink-soft">Loading canvas...</span>
</div> </div>
</div> </div>
); );
@ -79,15 +79,15 @@ export default function Home() {
// selector that's used by other transient toasts. Don't rename // selector that's used by other transient toasts. Don't rename
// without updating that spec. // without updating that spec.
data-testid="hydration-error" data-testid="hydration-error"
className="fixed inset-0 flex flex-col items-center justify-center bg-zinc-950 text-zinc-300 gap-4 z-[9999]" className="fixed inset-0 flex flex-col items-center justify-center bg-surface text-ink-mid gap-4 z-[9999]"
> >
<p className="text-zinc-400 text-sm">{hydrationError}</p> <p className="text-ink-mid text-sm">{hydrationError}</p>
<button <button
onClick={() => { onClick={() => {
setHydrationError(null); setHydrationError(null);
window.location.reload(); window.location.reload();
}} }}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-md text-sm" className="px-4 py-2 bg-accent-strong hover:bg-accent text-ink rounded-md text-sm"
> >
Retry Retry
</button> </button>
@ -108,28 +108,28 @@ function PlatformDownDiagnostic() {
return ( return (
<div <div
role="alert" role="alert"
className="fixed inset-0 flex flex-col items-center justify-center bg-zinc-950 text-zinc-300 gap-5 z-[9999] px-6" className="fixed inset-0 flex flex-col items-center justify-center bg-surface text-ink-mid gap-5 z-[9999] px-6"
> >
<div className="text-amber-400 text-sm font-semibold uppercase tracking-wider"> <div className="text-warm text-sm font-semibold uppercase tracking-wider">
Platform infrastructure unreachable Platform infrastructure unreachable
</div> </div>
<p className="text-zinc-400 text-sm max-w-lg text-center leading-relaxed"> <p className="text-ink-mid text-sm max-w-lg text-center leading-relaxed">
The platform server returned <code className="font-mono text-amber-300">503 platform_unavailable</code>. The platform server returned <code className="font-mono text-warm">503 platform_unavailable</code>.
That means it can&apos;t reach Postgres or Redis to validate your session. That means it can&apos;t reach Postgres or Redis to validate your session.
Most common cause on a dev host: one of those services stopped. Most common cause on a dev host: one of those services stopped.
</p> </p>
<div className="bg-zinc-900/80 border border-zinc-700/50 rounded-lg px-4 py-3 max-w-lg w-full"> <div className="bg-surface-sunken/80 border border-line/50 rounded-lg px-4 py-3 max-w-lg w-full">
<div className="text-[10px] uppercase tracking-wider text-zinc-500 mb-2">Try first</div> <div className="text-[10px] uppercase tracking-wider text-ink-soft mb-2">Try first</div>
<pre className="text-[12px] text-zinc-300 font-mono whitespace-pre-wrap leading-relaxed">{`brew services start postgresql@14 <pre className="text-[12px] text-ink-mid font-mono whitespace-pre-wrap leading-relaxed">{`brew services start postgresql@14
brew services start redis`}</pre> brew services start redis`}</pre>
</div> </div>
<p className="text-[11px] text-zinc-500 max-w-lg text-center"> <p className="text-[11px] text-ink-soft max-w-lg text-center">
If both are running, check <code className="font-mono">/tmp/molecule-server.log</code> for If both are running, check <code className="font-mono">/tmp/molecule-server.log</code> for
the underlying error. If you&apos;re on hosted SaaS, this is a platform incident try again in a moment. the underlying error. If you&apos;re on hosted SaaS, this is a platform incident try again in a moment.
</p> </p>
<button <button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-md text-sm mt-2" className="px-4 py-2 bg-accent-strong hover:bg-accent text-ink rounded-md text-sm mt-2"
> >
Reload Reload
</button> </button>

View File

@ -19,17 +19,17 @@ export const metadata = {
export default function PricingPage() { export default function PricingPage() {
return ( return (
<main className="min-h-screen bg-zinc-950 text-zinc-100"> <main className="min-h-screen bg-surface text-ink">
<div className="mx-auto max-w-5xl px-6 pt-20 pb-8 text-center"> <div className="mx-auto max-w-5xl px-6 pt-20 pb-8 text-center">
<h1 className="text-5xl font-bold tracking-tight text-white md:text-6xl"> <h1 className="text-5xl font-bold tracking-tight text-ink md:text-6xl">
Pricing Pricing
</h1> </h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-zinc-300"> <p className="mx-auto mt-4 max-w-2xl text-lg text-ink-mid">
One flat price per org not per seat. Every paid tier includes the One flat price per org not per seat. Every paid tier includes the
full runtime stack. You upgrade for scale, support, and dedicated full runtime stack. You upgrade for scale, support, and dedicated
infrastructure. infrastructure.
</p> </p>
<p className="mx-auto mt-2 max-w-xl text-sm text-zinc-400"> <p className="mx-auto mt-2 max-w-xl text-sm text-ink-mid">
5-person team? You pay $29/month not $200. No seat math, ever. 5-person team? You pay $29/month not $200. No seat math, ever.
</p> </p>
</div> </div>
@ -37,42 +37,42 @@ export default function PricingPage() {
<PricingTable /> <PricingTable />
<section className="mx-auto mt-20 max-w-3xl px-6 text-center"> <section className="mx-auto mt-20 max-w-3xl px-6 text-center">
<h2 className="text-2xl font-semibold text-white">Questions?</h2> <h2 className="text-2xl font-semibold text-ink">Questions?</h2>
<p className="mt-2 text-zinc-400"> <p className="mt-2 text-ink-mid">
We publish the{" "} We publish the{" "}
<a <a
href="https://github.com/Molecule-AI/molecule-monorepo" href="https://github.com/Molecule-AI/molecule-monorepo"
className="text-blue-400 underline hover:text-blue-300" className="text-accent underline hover:text-accent"
> >
full source on GitHub full source on GitHub
</a> </a>
{" "} if something's ambiguous, file an issue or{" "} {" "} if something's ambiguous, file an issue or{" "}
<a <a
href="mailto:support@moleculesai.app" href="mailto:support@moleculesai.app"
className="text-blue-400 underline hover:text-blue-300" className="text-accent underline hover:text-accent"
> >
email support email support
</a> </a>
. .
</p> </p>
<p className="mt-6 text-sm text-zinc-500"> <p className="mt-6 text-sm text-ink-soft">
Prices shown in USD. Flat-rate per org no per-seat fees on any paid tier. Prices shown in USD. Flat-rate per org no per-seat fees on any paid tier.
Enterprise / self-hosted licensing available contact us. Enterprise / self-hosted licensing available contact us.
</p> </p>
</section> </section>
<footer className="mx-auto mt-20 max-w-5xl border-t border-zinc-800 px-6 py-6 text-center text-sm text-zinc-500"> <footer className="mx-auto mt-20 max-w-5xl border-t border-line px-6 py-6 text-center text-sm text-ink-soft">
<p> <p>
© {new Date().getFullYear()} Molecule AI, Inc. ·{" "} © {new Date().getFullYear()} Molecule AI, Inc. ·{" "}
<a href="/legal/terms" className="hover:text-zinc-300"> <a href="/legal/terms" className="hover:text-ink-mid">
Terms Terms
</a> </a>
{" "}·{" "} {" "}·{" "}
<a href="/legal/privacy" className="hover:text-zinc-300"> <a href="/legal/privacy" className="hover:text-ink-mid">
Privacy Privacy
</a> </a>
{" "}·{" "} {" "}·{" "}
<a href="/legal/dpa" className="hover:text-zinc-300"> <a href="/legal/dpa" className="hover:text-ink-mid">
DPA DPA
</a> </a>
</p> </p>

View File

@ -61,26 +61,26 @@ export function ApprovalBanner() {
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-lg bg-amber-800/40 flex items-center justify-center shrink-0 mt-0.5"> <div className="w-8 h-8 rounded-lg bg-amber-800/40 flex items-center justify-center shrink-0 mt-0.5">
<span className="text-amber-300 text-lg" aria-hidden="true"></span> <span className="text-warm text-lg" aria-hidden="true"></span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-xs text-amber-200 font-semibold">{approval.workspace_name} needs approval</div> <div className="text-xs text-amber-200 font-semibold">{approval.workspace_name} needs approval</div>
<div className="text-sm text-amber-100 mt-0.5 font-medium">{approval.action}</div> <div className="text-sm text-amber-100 mt-0.5 font-medium">{approval.action}</div>
{approval.reason && ( {approval.reason && (
<div className="text-xs text-amber-300/70 mt-1">{approval.reason}</div> <div className="text-xs text-warm/70 mt-1">{approval.reason}</div>
)} )}
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<button <button
type="button" type="button"
onClick={() => handleDecide(approval, "approved")} onClick={() => handleDecide(approval, "approved")}
className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-xs rounded-lg text-white font-medium transition-colors" className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-xs rounded-lg text-ink font-medium transition-colors"
> >
Approve Approve
</button> </button>
<button <button
type="button" type="button"
onClick={() => handleDecide(approval, "denied")} onClick={() => handleDecide(approval, "denied")}
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded-lg text-zinc-300 transition-colors" className="px-3 py-1.5 bg-surface-card hover:bg-zinc-600 text-xs rounded-lg text-ink-mid transition-colors"
> >
Deny Deny
</button> </button>

View File

@ -9,7 +9,7 @@ import type { AuditEntry, AuditResponse } from "@/types/audit";
type EventFilter = "all" | AuditEntry["event_type"]; type EventFilter = "all" | AuditEntry["event_type"];
const BADGE_COLORS: Record<AuditEntry["event_type"], { text: string; bg: string; border: string }> = { const BADGE_COLORS: Record<AuditEntry["event_type"], { text: string; bg: string; border: string }> = {
delegation: { text: "text-blue-400", bg: "bg-blue-950/40", border: "border-blue-800/40" }, delegation: { text: "text-accent", bg: "bg-blue-950/40", border: "border-blue-800/40" },
decision: { text: "text-violet-400", bg: "bg-violet-950/40", border: "border-violet-800/40" }, decision: { text: "text-violet-400", bg: "bg-violet-950/40", border: "border-violet-800/40" },
gate: { text: "text-yellow-400", bg: "bg-yellow-950/40", border: "border-yellow-800/40" }, gate: { text: "text-yellow-400", bg: "bg-yellow-950/40", border: "border-yellow-800/40" },
hitl: { text: "text-orange-400", bg: "bg-orange-950/40", border: "border-orange-800/40" }, hitl: { text: "text-orange-400", bg: "bg-orange-950/40", border: "border-orange-800/40" },
@ -127,7 +127,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-32"> <div className="flex items-center justify-center h-32">
<span className="text-xs text-zinc-500">Loading audit trail</span> <span className="text-xs text-ink-soft">Loading audit trail</span>
</div> </div>
); );
} }
@ -135,7 +135,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Filter bar */} {/* Filter bar */}
<div className="px-4 py-2.5 border-b border-zinc-800/40 flex items-center gap-1 overflow-x-auto shrink-0"> <div className="px-4 py-2.5 border-b border-line/40 flex items-center gap-1 overflow-x-auto shrink-0">
{FILTERS.map((f) => ( {FILTERS.map((f) => (
<button <button
type="button" type="button"
@ -144,8 +144,8 @@ export function AuditTrailPanel({ workspaceId }: Props) {
aria-pressed={filter === f.id} aria-pressed={filter === f.id}
className={`px-2 py-1 text-[10px] rounded-md font-medium transition-all shrink-0 ${ className={`px-2 py-1 text-[10px] rounded-md font-medium transition-all shrink-0 ${
filter === f.id filter === f.id
? "bg-zinc-700 text-zinc-100 ring-1 ring-zinc-600" ? "bg-surface-card text-ink ring-1 ring-zinc-600"
: "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/60" : "text-ink-soft hover:text-ink-mid hover:bg-surface-card/60"
}`} }`}
> >
{f.label} {f.label}
@ -155,7 +155,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
<button <button
type="button" type="button"
onClick={loadEntries} onClick={loadEntries}
className="px-2 py-1 text-[10px] bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded transition-colors shrink-0" className="px-2 py-1 text-[10px] bg-surface-card hover:bg-surface-card text-ink-mid rounded transition-colors shrink-0"
aria-label="Refresh audit trail" aria-label="Refresh audit trail"
> >
@ -164,7 +164,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
{/* Error banner */} {/* Error banner */}
{error && ( {error && (
<div className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-red-400 shrink-0"> <div className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-bad shrink-0">
{error} {error}
</div> </div>
)} )}
@ -175,8 +175,8 @@ export function AuditTrailPanel({ workspaceId }: Props) {
/* Empty state */ /* Empty state */
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center"> <div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
<span className="text-4xl text-zinc-700" aria-hidden="true"></span> <span className="text-4xl text-zinc-700" aria-hidden="true"></span>
<p className="text-sm font-medium text-zinc-400">No audit events yet</p> <p className="text-sm font-medium text-ink-mid">No audit events yet</p>
<p className="text-[11px] text-zinc-600 max-w-[200px] leading-relaxed"> <p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
Delegation, decision, gate, and human-in-the-loop events will appear here. Delegation, decision, gate, and human-in-the-loop events will appear here.
</p> </p>
</div> </div>
@ -195,7 +195,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
type="button" type="button"
onClick={loadMore} onClick={loadMore}
disabled={loadingMore} disabled={loadingMore}
className="px-4 py-2 text-[11px] bg-zinc-800 hover:bg-zinc-700 disabled:opacity-50 disabled:cursor-not-allowed text-zinc-300 rounded-lg transition-colors" className="px-4 py-2 text-[11px] bg-surface-card hover:bg-surface-card disabled:opacity-50 disabled:cursor-not-allowed text-ink-mid rounded-lg transition-colors"
> >
{loadingMore ? "Loading…" : "Load more"} {loadingMore ? "Loading…" : "Load more"}
</button> </button>
@ -203,7 +203,7 @@ export function AuditTrailPanel({ workspaceId }: Props) {
)} )}
{/* Entry count footer */} {/* Entry count footer */}
<p className="mt-3 text-center text-[9px] text-zinc-600"> <p className="mt-3 text-center text-[9px] text-ink-soft">
{entries.length} event{entries.length !== 1 ? "s" : ""} loaded {entries.length} event{entries.length !== 1 ? "s" : ""} loaded
{cursor ? " · more available" : " · all loaded"} {cursor ? " · more available" : " · all loaded"}
</p> </p>
@ -227,15 +227,15 @@ export interface AuditEntryRowProps {
*/ */
export function AuditEntryRow({ entry, now }: AuditEntryRowProps) { export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
const badge = BADGE_COLORS[entry.event_type] ?? { const badge = BADGE_COLORS[entry.event_type] ?? {
text: "text-zinc-400", text: "text-ink-mid",
bg: "bg-zinc-800/40", bg: "bg-surface-card/40",
border: "border-zinc-700/40", border: "border-line/40",
}; };
return ( return (
<div <div
role="listitem" role="listitem"
className="rounded-lg border border-zinc-800/60 bg-zinc-900/50 px-3 py-2.5 space-y-1.5" className="rounded-lg border border-line/60 bg-surface-sunken/50 px-3 py-2.5 space-y-1.5"
> >
{/* Header row: badge · actor · tamper flag · timestamp */} {/* Header row: badge · actor · tamper flag · timestamp */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -248,14 +248,14 @@ export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
</span> </span>
{/* Actor name */} {/* Actor name */}
<span className="text-[10px] text-zinc-400 truncate flex-1 min-w-0 font-mono"> <span className="text-[10px] text-ink-mid truncate flex-1 min-w-0 font-mono">
{entry.actor} {entry.actor}
</span> </span>
{/* Tamper warning — only rendered when chain is invalid */} {/* Tamper warning — only rendered when chain is invalid */}
{!entry.chain_valid && ( {!entry.chain_valid && (
<span <span
className="shrink-0 text-[11px] text-red-400 font-bold leading-none" className="shrink-0 text-[11px] text-bad font-bold leading-none"
title="Chain integrity check failed — this entry may have been tampered with" title="Chain integrity check failed — this entry may have been tampered with"
aria-label="Chain integrity warning: tampered entry" aria-label="Chain integrity warning: tampered entry"
role="img" role="img"
@ -265,13 +265,13 @@ export function AuditEntryRow({ entry, now }: AuditEntryRowProps) {
)} )}
{/* Relative timestamp */} {/* Relative timestamp */}
<span className="shrink-0 text-[9px] text-zinc-600"> <span className="shrink-0 text-[9px] text-ink-soft">
{formatAuditRelativeTime(entry.created_at, now)} {formatAuditRelativeTime(entry.created_at, now)}
</span> </span>
</div> </div>
{/* Summary text */} {/* Summary text */}
<p className="text-[11px] text-zinc-300 leading-relaxed break-words"> <p className="text-[11px] text-ink-mid leading-relaxed break-words">
{entry.summary} {entry.summary}
</p> </p>
</div> </div>

View File

@ -63,7 +63,7 @@ export function AuthGate({ children }: { children: ReactNode }) {
if (state.kind === "loading") { if (state.kind === "loading") {
// Zinc-950 backdrop matches the canvas background so the browser // Zinc-950 backdrop matches the canvas background so the browser
// never paints a white flash while the session round-trip resolves. // never paints a white flash while the session round-trip resolves.
return <div className="fixed inset-0 bg-zinc-950" aria-hidden="true" />; return <div className="fixed inset-0 bg-surface" aria-hidden="true" />;
} }
if (state.kind === "anonymous" && !state.skipRedirect) { if (state.kind === "anonymous" && !state.skipRedirect) {
// Redirect already firing from the effect above; render nothing in // Redirect already firing from the effect above; render nothing in

View File

@ -80,14 +80,14 @@ export function BatchActionBar() {
<div <div
role="toolbar" role="toolbar"
aria-label="Batch workspace actions" aria-label="Batch workspace actions"
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[200] flex items-center gap-3 px-4 py-2.5 rounded-2xl bg-zinc-900/95 border border-zinc-700/70 shadow-2xl shadow-black/50 backdrop-blur-md" className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[200] flex items-center gap-3 px-4 py-2.5 rounded-2xl bg-surface-sunken/95 border border-line/70 shadow-2xl shadow-black/50 backdrop-blur-md"
> >
{/* Selection count badge */} {/* Selection count badge */}
<span className="text-[12px] font-semibold text-zinc-100 bg-blue-600/80 px-2.5 py-0.5 rounded-full tabular-nums"> <span className="text-[12px] font-semibold text-ink bg-accent-strong/80 px-2.5 py-0.5 rounded-full tabular-nums">
{count} selected {count} selected
</span> </span>
<div className="w-px h-5 bg-zinc-700/60" aria-hidden="true" /> <div className="w-px h-5 bg-surface-card/60" aria-hidden="true" />
{/* Action buttons */} {/* Action buttons */}
<button <button
@ -104,7 +104,7 @@ export function BatchActionBar() {
type="button" type="button"
disabled={busy} disabled={busy}
onClick={() => setPending("pause")} onClick={() => setPending("pause")}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-amber-300 bg-amber-900/30 hover:bg-amber-800/50 border border-amber-700/30 hover:border-amber-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/70" className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-warm bg-amber-900/30 hover:bg-amber-800/50 border border-amber-700/30 hover:border-amber-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/70"
> >
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
Pause All Pause All
@ -114,13 +114,13 @@ export function BatchActionBar() {
type="button" type="button"
disabled={busy} disabled={busy}
onClick={() => setPending("delete")} onClick={() => setPending("delete")}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-red-300 bg-red-900/30 hover:bg-red-800/50 border border-red-700/30 hover:border-red-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/70" className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12px] font-medium text-bad bg-red-900/30 hover:bg-red-800/50 border border-red-700/30 hover:border-red-600/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/70"
> >
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
Delete All Delete All
</button> </button>
<div className="w-px h-5 bg-zinc-700/60" aria-hidden="true" /> <div className="w-px h-5 bg-surface-card/60" aria-hidden="true" />
{/* Deselect */} {/* Deselect */}
<button <button
@ -129,7 +129,7 @@ export function BatchActionBar() {
onClick={clearSelection} onClick={clearSelection}
aria-label="Clear selection" aria-label="Clear selection"
title="Clear selection (Escape)" title="Clear selection (Escape)"
className="p-1.5 rounded-lg text-[12px] text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500/70" className="p-1.5 rounded-lg text-[12px] text-ink-mid hover:text-ink hover:bg-surface-card/50 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500/70"
> >
</button> </button>

View File

@ -112,7 +112,7 @@ export function BundleDropZone() {
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
aria-label="Import bundle file" aria-label="Import bundle file"
aria-controls="bundle-file-input" aria-controls="bundle-file-input"
className="sr-only focus:not-sr-only fixed bottom-20 right-4 z-30 px-3 py-1.5 bg-zinc-900/90 border border-zinc-700/50 rounded-lg text-[10px] text-zinc-400 hover:text-zinc-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 transition-colors" className="sr-only focus:not-sr-only fixed bottom-20 right-4 z-30 px-3 py-1.5 bg-surface-sunken/90 border border-line/50 rounded-lg text-[10px] text-ink-mid hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent transition-colors"
> >
📦 Import bundle 📦 Import bundle
</button> </button>
@ -120,19 +120,19 @@ export function BundleDropZone() {
{/* Visual overlay when dragging */} {/* Visual overlay when dragging */}
{isDragging && ( {isDragging && (
<div className="fixed inset-0 z-20 flex items-center justify-center bg-blue-950/40 backdrop-blur-sm border-2 border-dashed border-blue-400/50 pointer-events-none"> <div className="fixed inset-0 z-20 flex items-center justify-center bg-blue-950/40 backdrop-blur-sm border-2 border-dashed border-blue-400/50 pointer-events-none">
<div className="bg-zinc-900/95 border border-blue-500/50 rounded-2xl px-8 py-6 shadow-2xl text-center"> <div className="bg-surface-sunken/95 border border-accent/50 rounded-2xl px-8 py-6 shadow-2xl text-center">
<div className="text-3xl mb-2" aria-hidden="true">📦</div> <div className="text-3xl mb-2" aria-hidden="true">📦</div>
<div className="text-sm font-semibold text-zinc-100">Drop Bundle to Import</div> <div className="text-sm font-semibold text-ink">Drop Bundle to Import</div>
<div className="text-xs text-zinc-500 mt-1">.bundle.json files only</div> <div className="text-xs text-ink-soft mt-1">.bundle.json files only</div>
</div> </div>
</div> </div>
)} )}
{/* Importing spinner */} {/* Importing spinner */}
{importing && ( {importing && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-zinc-900/95 border border-zinc-700/60 rounded-xl px-5 py-3 shadow-2xl flex items-center gap-3"> <div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-surface-sunken/95 border border-line/60 rounded-xl px-5 py-3 shadow-2xl flex items-center gap-3">
<div className="w-4 h-4 border-2 border-sky-400 border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-sky-400 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-zinc-200">Importing bundle...</span> <span className="text-sm text-ink">Importing bundle...</span>
</div> </div>
)} )}

View File

@ -244,11 +244,11 @@ function CanvasInner() {
<> <>
<a <a
href="#canvas-main" href="#canvas-main"
className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-zinc-900 focus:text-zinc-100 focus:rounded-lg focus:border focus:border-zinc-700" className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-surface-sunken focus:text-ink focus:rounded-lg focus:border focus:border-line"
> >
Skip to canvas Skip to canvas
</a> </a>
<main id="canvas-main" className="w-screen h-screen bg-zinc-950"> <main id="canvas-main" className="w-screen h-screen bg-surface">
<ReactFlow <ReactFlow
colorMode="dark" colorMode="dark"
nodes={nodes} nodes={nodes}
@ -276,11 +276,11 @@ function CanvasInner() {
color="#27272a" color="#27272a"
/> />
<Controls <Controls
className="!bg-zinc-900/90 !border-zinc-700/50 !rounded-lg !shadow-xl !shadow-black/20 [&>button]:!bg-zinc-800 [&>button]:!border-zinc-700/50 [&>button]:!text-zinc-400 [&>button:hover]:!bg-zinc-700 [&>button:hover]:!text-zinc-200" className="!bg-surface-sunken/90 !border-line/50 !rounded-lg !shadow-xl !shadow-black/20 [&>button]:!bg-surface-card [&>button]:!border-line/50 [&>button]:!text-ink-mid [&>button:hover]:!bg-surface-card [&>button:hover]:!text-ink"
showInteractive={false} showInteractive={false}
/> />
<MiniMap <MiniMap
className="!bg-zinc-900/90 !border-zinc-700/50 !rounded-lg !shadow-xl !shadow-black/20" className="!bg-surface-sunken/90 !border-line/50 !rounded-lg !shadow-xl !shadow-black/20"
maskColor="rgba(0, 0, 0, 0.7)" maskColor="rgba(0, 0, 0, 0.7)"
nodeColor={(node) => { nodeColor={(node) => {
// Parents show as a filled region — hierarchy visible at // Parents show as a filled region — hierarchy visible at

View File

@ -102,7 +102,7 @@ export function CommunicationOverlay() {
type="button" type="button"
onClick={() => setVisible(true)} onClick={() => setVisible(true)}
aria-label="Show communications panel" aria-label="Show communications panel"
className="fixed top-16 right-4 z-30 px-3 py-1.5 bg-zinc-900/90 border border-zinc-700/50 rounded-lg text-[10px] text-zinc-400 hover:text-zinc-200 transition-colors" className="fixed top-16 right-4 z-30 px-3 py-1.5 bg-surface-sunken/90 border border-line/50 rounded-lg text-[10px] text-ink-mid hover:text-ink transition-colors"
> >
<span aria-hidden="true"> </span>{comms.length > 0 ? `${comms.length} comms` : "Communications"} <span aria-hidden="true"> </span>{comms.length > 0 ? `${comms.length} comms` : "Communications"}
</button> </button>
@ -110,16 +110,16 @@ export function CommunicationOverlay() {
} }
return ( return (
<div className="fixed top-16 right-4 z-30 w-[320px] max-h-[400px] bg-zinc-900/95 border border-zinc-700/50 rounded-xl shadow-xl shadow-black/30 backdrop-blur-sm overflow-hidden"> <div className="fixed top-16 right-4 z-30 w-[320px] max-h-[400px] bg-surface-sunken/95 border border-line/50 rounded-xl shadow-xl shadow-black/30 backdrop-blur-sm overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/60"> <div className="flex items-center justify-between px-3 py-2 border-b border-line/60">
<div className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider"> <div className="text-[10px] font-semibold text-ink-mid uppercase tracking-wider">
<span aria-hidden="true"> </span>Communications ({comms.length}) <span aria-hidden="true"> </span>Communications ({comms.length})
</div> </div>
<button <button
type="button" type="button"
onClick={() => setVisible(false)} onClick={() => setVisible(false)}
aria-label="Close communications panel" aria-label="Close communications panel"
className="text-zinc-500 hover:text-zinc-300 text-xs" className="text-ink-soft hover:text-ink-mid text-xs"
> >
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
</button> </button>
@ -128,10 +128,10 @@ export function CommunicationOverlay() {
<div className="overflow-y-auto max-h-[350px] p-2 space-y-1"> <div className="overflow-y-auto max-h-[350px] p-2 space-y-1">
{comms.map((c) => { {comms.map((c) => {
const isSelected = selectedNodeId === c.sourceId || selectedNodeId === c.targetId; const isSelected = selectedNodeId === c.sourceId || selectedNodeId === c.targetId;
const typeColor = c.type === "a2a_send" ? "text-cyan-400" : c.type === "a2a_receive" ? "text-blue-400" : "text-amber-400"; const typeColor = c.type === "a2a_send" ? "text-cyan-400" : c.type === "a2a_receive" ? "text-accent" : "text-warm";
const typeIcon = c.type === "a2a_send" ? "↗" : c.type === "a2a_receive" ? "↙" : "◆"; const typeIcon = c.type === "a2a_send" ? "↗" : c.type === "a2a_receive" ? "↙" : "◆";
const statusIcon = c.status === "ok" ? "✓" : c.status === "error" ? "✕" : "⏱"; const statusIcon = c.status === "ok" ? "✓" : c.status === "error" ? "✕" : "⏱";
const statusColor = c.status === "ok" ? "text-emerald-400" : c.status === "error" ? "text-red-400" : "text-amber-400"; const statusColor = c.status === "ok" ? "text-good" : c.status === "error" ? "text-bad" : "text-warm";
const age = formatAge(c.timestamp); const age = formatAge(c.timestamp);
return ( return (
@ -140,31 +140,31 @@ export function CommunicationOverlay() {
className={`rounded-lg px-2.5 py-1.5 text-[9px] border transition-all ${ className={`rounded-lg px-2.5 py-1.5 text-[9px] border transition-all ${
isSelected isSelected
? "bg-blue-950/30 border-blue-800/40" ? "bg-blue-950/30 border-blue-800/40"
: "bg-zinc-800/30 border-zinc-700/20 hover:bg-zinc-800/50" : "bg-surface-card/30 border-line/20 hover:bg-surface-card/50"
}`} }`}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-1.5 min-w-0">
<span className={typeColor} aria-hidden="true">{typeIcon}</span> <span className={typeColor} aria-hidden="true">{typeIcon}</span>
<span className="sr-only">{COMM_TYPE_LABELS[c.type] ?? c.type}</span> <span className="sr-only">{COMM_TYPE_LABELS[c.type] ?? c.type}</span>
<span className="text-zinc-300 font-medium truncate"> <span className="text-ink-mid font-medium truncate">
{c.sourceName} {c.sourceName}
</span> </span>
<span className="text-zinc-400" aria-hidden="true"></span> <span className="text-ink-mid" aria-hidden="true"></span>
<span className="sr-only">to</span> <span className="sr-only">to</span>
<span className="text-zinc-300 truncate">{c.targetName}</span> <span className="text-ink-mid truncate">{c.targetName}</span>
</div> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<span className={statusColor} aria-hidden="true">{statusIcon}</span> <span className={statusColor} aria-hidden="true">{statusIcon}</span>
<span className="sr-only">{c.status}</span> <span className="sr-only">{c.status}</span>
<span className="text-zinc-400">{age}</span> <span className="text-ink-mid">{age}</span>
</div> </div>
</div> </div>
{c.summary && ( {c.summary && (
<div className="text-zinc-500 truncate mt-0.5 pl-4">{c.summary}</div> <div className="text-ink-soft truncate mt-0.5 pl-4">{c.summary}</div>
)} )}
{c.durationMs && ( {c.durationMs && (
<div className="text-zinc-400 pl-4">{c.durationMs}ms</div> <div className="text-ink-mid pl-4">{c.durationMs}ms</div>
)} )}
</div> </div>
); );

View File

@ -93,10 +93,10 @@ export function ConfirmDialog({
const confirmColors = const confirmColors =
confirmVariant === "danger" confirmVariant === "danger"
? "bg-red-600 hover:bg-red-500 text-white" ? "bg-red-600 hover:bg-red-500 text-ink"
: confirmVariant === "warning" : confirmVariant === "warning"
? "bg-amber-600 hover:bg-amber-500 text-white" ? "bg-amber-600 hover:bg-amber-500 text-ink"
: "bg-blue-600 hover:bg-blue-500 text-white"; : "bg-accent-strong hover:bg-accent text-ink";
// Render via Portal so the fixed-position dialog escapes any containing block // Render via Portal so the fixed-position dialog escapes any containing block
// (e.g. parents with transform, filter, will-change that break position:fixed). // (e.g. parents with transform, filter, will-change that break position:fixed).
@ -111,19 +111,19 @@ export function ConfirmDialog({
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="confirm-dialog-title" aria-labelledby="confirm-dialog-title"
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[380px] w-full mx-4 overflow-hidden" className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl shadow-black/50 max-w-[380px] w-full mx-4 overflow-hidden"
> >
<div className="px-5 py-4"> <div className="px-5 py-4">
<h3 id="confirm-dialog-title" className="text-sm font-semibold text-zinc-100 mb-2">{title}</h3> <h3 id="confirm-dialog-title" className="text-sm font-semibold text-ink mb-2">{title}</h3>
<p className="text-[13px] text-zinc-400 leading-relaxed">{message}</p> <p className="text-[13px] text-ink-mid leading-relaxed">{message}</p>
</div> </div>
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-zinc-800 bg-zinc-950/50"> <div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface/50">
{!singleButton && ( {!singleButton && (
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
className="px-3.5 py-1.5 text-[13px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors" className="px-3.5 py-1.5 text-[13px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>

View File

@ -95,15 +95,15 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="console-modal-title" aria-labelledby="console-modal-title"
className="relative bg-zinc-950 border border-zinc-800 rounded-xl shadow-2xl w-[min(900px,90vw)] h-[min(70vh,700px)] flex flex-col overflow-hidden" className="relative bg-surface border border-line rounded-xl shadow-2xl w-[min(900px,90vw)] h-[min(70vh,700px)] flex flex-col overflow-hidden"
> >
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-800"> <div className="flex items-center justify-between px-4 py-3 border-b border-line">
<div> <div>
<h3 id="console-modal-title" className="text-sm font-semibold text-zinc-100"> <h3 id="console-modal-title" className="text-sm font-semibold text-ink">
EC2 console output EC2 console output
</h3> </h3>
{workspaceName && ( {workspaceName && (
<div className="text-[11px] text-zinc-500 mt-0.5 truncate max-w-[600px]"> <div className="text-[11px] text-ink-soft mt-0.5 truncate max-w-[600px]">
{workspaceName} {workspaceName}
</div> </div>
)} )}
@ -113,7 +113,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
ref={closeButtonRef} ref={closeButtonRef}
onClick={onClose} onClick={onClose}
aria-label="Close" aria-label="Close"
className="text-zinc-400 hover:text-zinc-100 text-sm px-2" className="text-ink-mid hover:text-ink text-sm px-2"
> >
</button> </button>
@ -121,14 +121,14 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
<div className="flex-1 overflow-auto bg-black/80 p-4"> <div className="flex-1 overflow-auto bg-black/80 p-4">
{loading && ( {loading && (
<div className="text-[12px] text-zinc-500" data-testid="console-loading"> <div className="text-[12px] text-ink-soft" data-testid="console-loading">
Loading console output Loading console output
</div> </div>
)} )}
{!loading && error && ( {!loading && error && (
<div <div
role="alert" role="alert"
className="text-[12px] text-amber-300 bg-amber-950/30 border border-amber-900/40 rounded px-3 py-2" className="text-[12px] text-warm bg-amber-950/30 border border-amber-900/40 rounded px-3 py-2"
data-testid="console-error" data-testid="console-error"
> >
{error} {error}
@ -136,7 +136,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
)} )}
{!loading && !error && output !== null && ( {!loading && !error && output !== null && (
<pre <pre
className="text-[11px] text-zinc-300 font-mono whitespace-pre-wrap break-all leading-tight" className="text-[11px] text-ink-mid font-mono whitespace-pre-wrap break-all leading-tight"
data-testid="console-output" data-testid="console-output"
> >
{output || "(console output is empty — the instance may still be booting)"} {output || "(console output is empty — the instance may still be booting)"}
@ -144,7 +144,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
)} )}
</div> </div>
<div className="flex items-center justify-end gap-2 px-4 py-3 border-t border-zinc-800 bg-zinc-900/40"> <div className="flex items-center justify-end gap-2 px-4 py-3 border-t border-line bg-surface-sunken/40">
{output && ( {output && (
<button <button
type="button" type="button"
@ -155,7 +155,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
showToast("Copy requires HTTPS — please select and copy manually", "info"); showToast("Copy requires HTTPS — please select and copy manually", "info");
} }
}} }}
className="px-3 py-1.5 text-[11px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors" className="px-3 py-1.5 text-[11px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
> >
Copy Copy
</button> </button>
@ -163,7 +163,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="px-3 py-1.5 text-[11px] text-zinc-300 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors" className="px-3 py-1.5 text-[11px] text-ink-mid bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
> >
Close Close
</button> </button>

View File

@ -287,24 +287,24 @@ export function ContextMenu() {
role="menu" role="menu"
aria-label={`Actions for ${contextMenu.nodeData.name}`} aria-label={`Actions for ${contextMenu.nodeData.name}`}
onKeyDown={handleMenuKeyDown} onKeyDown={handleMenuKeyDown}
className="fixed z-[60] min-w-[200px] bg-zinc-950/95 backdrop-blur-xl border border-zinc-800/60 rounded-xl shadow-2xl shadow-black/60 py-1 overflow-hidden" className="fixed z-[60] min-w-[200px] bg-surface/95 backdrop-blur-xl border border-line/60 rounded-xl shadow-2xl shadow-black/60 py-1 overflow-hidden"
style={{ left: contextMenu.x, top: contextMenu.y }} style={{ left: contextMenu.x, top: contextMenu.y }}
> >
{/* Header */} {/* Header */}
<div className="px-3.5 py-2 border-b border-zinc-800/40 mb-0.5"> <div className="px-3.5 py-2 border-b border-line/40 mb-0.5">
<div className="text-[11px] font-semibold text-zinc-200 truncate">{contextMenu.nodeData.name}</div> <div className="text-[11px] font-semibold text-ink truncate">{contextMenu.nodeData.name}</div>
<div className="flex items-center gap-1.5 mt-0.5"> <div className="flex items-center gap-1.5 mt-0.5">
<div <div
aria-hidden="true" aria-hidden="true"
className={`w-1.5 h-1.5 rounded-full ${statusDotClass(contextMenu.nodeData.status)}`} className={`w-1.5 h-1.5 rounded-full ${statusDotClass(contextMenu.nodeData.status)}`}
/> />
<span className="text-[10px] text-zinc-500">{contextMenu.nodeData.status}</span> <span className="text-[10px] text-ink-soft">{contextMenu.nodeData.status}</span>
</div> </div>
</div> </div>
{items.map((item, i) => { {items.map((item, i) => {
if (item.divider) { if (item.divider) {
return <div key={i} role="separator" className="h-px bg-zinc-800/60 my-1" />; return <div key={i} role="separator" className="h-px bg-surface-card/60 my-1" />;
} }
return ( return (
<button <button
@ -316,8 +316,8 @@ export function ContextMenu() {
aria-disabled={item.disabled} aria-disabled={item.disabled}
className={`w-full px-3.5 py-1.5 flex items-center gap-2.5 text-left text-[11px] transition-colors focus:outline-none focus:ring-1 focus:ring-inset focus:ring-zinc-600 disabled:opacity-25 disabled:cursor-not-allowed ${ className={`w-full px-3.5 py-1.5 flex items-center gap-2.5 text-left text-[11px] transition-colors focus:outline-none focus:ring-1 focus:ring-inset focus:ring-zinc-600 disabled:opacity-25 disabled:cursor-not-allowed ${
item.danger item.danger
? "text-red-400 hover:bg-red-950/40 hover:text-red-300" ? "text-bad hover:bg-red-950/40 hover:text-bad"
: "text-zinc-300 hover:bg-zinc-800/40 hover:text-zinc-100" : "text-ink-mid hover:bg-surface-card/40 hover:text-ink"
}`} }`}
> >
<span aria-hidden="true" className="w-4 text-center text-[10px] shrink-0 opacity-50">{item.icon}</span> <span aria-hidden="true" className="w-4 text-center text-[10px] shrink-0 opacity-50">{item.icon}</span>

View File

@ -99,14 +99,14 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
aria-label="Conversation trace" aria-label="Conversation trace"
> >
{/* Modal panel */} {/* Modal panel */}
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl max-w-[700px] w-full max-h-[85vh] flex flex-col overflow-hidden"> <div className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl max-w-[700px] w-full max-h-[85vh] flex flex-col overflow-hidden">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-3 border-b border-zinc-800"> <div className="flex items-center justify-between px-5 py-3 border-b border-line">
<div> <div>
<Dialog.Title className="text-sm font-semibold text-zinc-100"> <Dialog.Title className="text-sm font-semibold text-ink">
Conversation Trace Conversation Trace
</Dialog.Title> </Dialog.Title>
<p className="text-[10px] text-zinc-500 mt-0.5"> <p className="text-[10px] text-ink-soft mt-0.5">
{entries.length} events across all workspaces {entries.length} events across all workspaces
</p> </p>
</div> </div>
@ -114,7 +114,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
<button <button
type="button" type="button"
aria-label="Close conversation trace" aria-label="Close conversation trace"
className="text-zinc-500 hover:text-zinc-300 text-lg px-2" className="text-ink-soft hover:text-ink-mid text-lg px-2"
> >
</button> </button>
@ -124,13 +124,13 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
{/* Timeline */} {/* Timeline */}
<div className="flex-1 overflow-y-auto px-5 py-4"> <div className="flex-1 overflow-y-auto px-5 py-4">
{loading && ( {loading && (
<div className="text-xs text-zinc-500 text-center py-8"> <div className="text-xs text-ink-soft text-center py-8">
Loading trace from all workspaces... Loading trace from all workspaces...
</div> </div>
)} )}
{!loading && entries.length === 0 && ( {!loading && entries.length === 0 && (
<div className="text-xs text-zinc-500 text-center py-8"> <div className="text-xs text-ink-soft text-center py-8">
No activity found No activity found
</div> </div>
)} )}
@ -160,28 +160,28 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
: isSend : isSend
? "bg-cyan-500" ? "bg-cyan-500"
: isReceive : isReceive
? "bg-blue-500" ? "bg-accent"
: "bg-zinc-600" : "bg-zinc-600"
}`} }`}
/> />
<div className="w-px flex-1 bg-zinc-800 min-h-[8px]" /> <div className="w-px flex-1 bg-surface-card min-h-[8px]" />
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 pb-3 min-w-0"> <div className="flex-1 pb-3 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-[9px] text-zinc-400 font-mono"> <span className="text-[9px] text-ink-mid font-mono">
{time} {time}
</span> </span>
<span <span
className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${ className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${
isError isError
? "bg-red-950/50 text-red-400" ? "bg-red-950/50 text-bad"
: isSend : isSend
? "bg-cyan-950/50 text-cyan-400" ? "bg-cyan-950/50 text-cyan-400"
: isReceive : isReceive
? "bg-blue-950/50 text-blue-400" ? "bg-blue-950/50 text-accent"
: "bg-zinc-800 text-zinc-400" : "bg-surface-card text-ink-mid"
}`} }`}
> >
{isSend {isSend
@ -191,7 +191,7 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
: entry.activity_type.toUpperCase()} : entry.activity_type.toUpperCase()}
</span> </span>
{entry.duration_ms != null && entry.duration_ms > 0 && ( {entry.duration_ms != null && entry.duration_ms > 0 && (
<span className="text-[9px] text-zinc-400"> <span className="text-[9px] text-ink-mid">
{entry.duration_ms > 1000 {entry.duration_ms > 1000
? `${Math.round(entry.duration_ms / 1000)}s` ? `${Math.round(entry.duration_ms / 1000)}s`
: `${entry.duration_ms}ms`} : `${entry.duration_ms}ms`}
@ -207,19 +207,19 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
<span className="text-cyan-400 font-medium"> <span className="text-cyan-400 font-medium">
{sourceName || wsName} {sourceName || wsName}
</span> </span>
<span className="text-zinc-400"> </span> <span className="text-ink-mid"> </span>
<span className="text-blue-400 font-medium"> <span className="text-accent font-medium">
{targetName} {targetName}
</span> </span>
</span> </span>
) : ( ) : (
<span> <span>
<span className="text-blue-400 font-medium"> <span className="text-accent font-medium">
{targetName || wsName} {targetName || wsName}
</span> </span>
{sourceName && ( {sourceName && (
<> <>
<span className="text-zinc-400"> <span className="text-ink-mid">
{" "} {" "} {" "} {" "}
</span> </span>
<span className="text-cyan-400 font-medium"> <span className="text-cyan-400 font-medium">
@ -234,40 +234,40 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
{/* Summary */} {/* Summary */}
{entry.summary && !isA2A(entry) && ( {entry.summary && !isA2A(entry) && (
<div className="text-[10px] text-zinc-400 mt-1"> <div className="text-[10px] text-ink-mid mt-1">
<span className="text-zinc-300 font-medium">{wsName}:</span>{" "} <span className="text-ink-mid font-medium">{wsName}:</span>{" "}
{entry.summary} {entry.summary}
</div> </div>
)} )}
{/* Error */} {/* Error */}
{isError && entry.error_detail && ( {isError && entry.error_detail && (
<div className="text-[10px] text-red-400/80 mt-1 truncate"> <div className="text-[10px] text-bad/80 mt-1 truncate">
{entry.error_detail.slice(0, 200)} {entry.error_detail.slice(0, 200)}
</div> </div>
)} )}
{/* Message content — show request and/or response */} {/* Message content — show request and/or response */}
{requestText && ( {requestText && (
<div className="mt-1.5 bg-zinc-950/60 border border-zinc-800/50 rounded-lg px-3 py-2 max-h-32 overflow-y-auto"> <div className="mt-1.5 bg-surface/60 border border-line/50 rounded-lg px-3 py-2 max-h-32 overflow-y-auto">
<div className="text-[8px] text-zinc-500 uppercase mb-1"> <div className="text-[8px] text-ink-soft uppercase mb-1">
{isSend ? "Task" : "Request"} {isSend ? "Task" : "Request"}
</div> </div>
<div className="text-[10px] text-zinc-300 whitespace-pre-wrap break-words leading-relaxed"> <div className="text-[10px] text-ink-mid whitespace-pre-wrap break-words leading-relaxed">
{requestText.slice(0, 2000)} {requestText.slice(0, 2000)}
{requestText.length > 2000 && ( {requestText.length > 2000 && (
<span className="text-zinc-400"> ...({requestText.length} chars)</span> <span className="text-ink-mid"> ...({requestText.length} chars)</span>
)} )}
</div> </div>
</div> </div>
)} )}
{responseText && ( {responseText && (
<div className="mt-1 bg-zinc-950/60 border border-emerald-900/30 rounded-lg px-3 py-2 max-h-32 overflow-y-auto"> <div className="mt-1 bg-surface/60 border border-emerald-900/30 rounded-lg px-3 py-2 max-h-32 overflow-y-auto">
<div className="text-[8px] text-emerald-500/60 uppercase mb-1">Response</div> <div className="text-[8px] text-good/60 uppercase mb-1">Response</div>
<div className="text-[10px] text-zinc-300 whitespace-pre-wrap break-words leading-relaxed"> <div className="text-[10px] text-ink-mid whitespace-pre-wrap break-words leading-relaxed">
{responseText.slice(0, 2000)} {responseText.slice(0, 2000)}
{responseText.length > 2000 && ( {responseText.length > 2000 && (
<span className="text-zinc-400"> ...({responseText.length} chars)</span> <span className="text-ink-mid"> ...({responseText.length} chars)</span>
)} )}
</div> </div>
</div> </div>
@ -281,11 +281,11 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
</div> </div>
{/* Footer */} {/* Footer */}
<div className="px-5 py-3 border-t border-zinc-800 bg-zinc-950/50 flex justify-end"> <div className="px-5 py-3 border-t border-line bg-surface/50 flex justify-end">
<Dialog.Close asChild> <Dialog.Close asChild>
<button <button
type="button" type="button"
className="px-4 py-1.5 text-[12px] bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition-colors" className="px-4 py-1.5 text-[12px] bg-surface-card hover:bg-surface-card text-ink-mid rounded-lg transition-colors"
> >
Close Close
</button> </button>

View File

@ -103,21 +103,21 @@ export function CookieConsent() {
aria-modal="true" aria-modal="true"
aria-labelledby="cookie-consent-title" aria-labelledby="cookie-consent-title"
aria-describedby="cookie-consent-body" aria-describedby="cookie-consent-body"
className="fixed bottom-0 left-0 right-0 z-[9999] border-t border-zinc-800 bg-zinc-950/95 backdrop-blur-sm p-4 shadow-[0_-4px_12px_rgba(0,0,0,0.4)]" className="fixed bottom-0 left-0 right-0 z-[9999] border-t border-line bg-surface/95 backdrop-blur-sm p-4 shadow-[0_-4px_12px_rgba(0,0,0,0.4)]"
> >
<div className="mx-auto flex max-w-5xl flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="mx-auto flex max-w-5xl flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="text-sm text-zinc-300"> <div className="text-sm text-ink-mid">
<p id="cookie-consent-title" className="font-medium text-zinc-100"> <p id="cookie-consent-title" className="font-medium text-ink">
Cookies &amp; your privacy Cookies &amp; your privacy
</p> </p>
<p id="cookie-consent-body" className="mt-1 text-zinc-400"> <p id="cookie-consent-body" className="mt-1 text-ink-mid">
We use strictly-necessary cookies for authentication and session We use strictly-necessary cookies for authentication and session
continuity. Accept to also allow optional functional cookies that continuity. Accept to also allow optional functional cookies that
improve your canvas experience (layout preferences, recent improve your canvas experience (layout preferences, recent
workspaces). See our{" "} workspaces). See our{" "}
<a <a
href="https://moleculesai.app/legal/privacy" href="https://moleculesai.app/legal/privacy"
className="text-blue-400 underline hover:text-blue-300" className="text-accent underline hover:text-accent"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
@ -130,14 +130,14 @@ export function CookieConsent() {
<button <button
type="button" type="button"
onClick={() => decide("rejected")} onClick={() => decide("rejected")}
className="rounded border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-zinc-200 hover:bg-zinc-800" className="rounded border border-line bg-surface-sunken px-4 py-2 text-sm text-ink hover:bg-surface-card"
> >
Necessary only Necessary only
</button> </button>
<button <button
type="button" type="button"
onClick={() => decide("accepted")} onClick={() => decide("accepted")}
className="rounded border border-blue-600 bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500" className="rounded border border-accent bg-accent-strong px-4 py-2 text-sm font-medium text-ink hover:bg-accent"
> >
Accept all Accept all
</button> </button>

View File

@ -310,7 +310,7 @@ export function CreateWorkspaceButton() {
return ( return (
<Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<button type="button" className="fixed bottom-6 right-6 z-40 px-5 py-2.5 bg-blue-600 hover:bg-blue-500 active:bg-blue-700 text-sm font-medium rounded-xl text-white shadow-lg shadow-blue-600/20 hover:shadow-xl hover:shadow-blue-500/30 transition-all duration-200 flex items-center gap-2"> <button type="button" className="fixed bottom-6 right-6 z-40 px-5 py-2.5 bg-accent-strong hover:bg-accent active:bg-accent-strong text-sm font-medium rounded-xl text-ink shadow-lg shadow-blue-600/20 hover:shadow-xl hover:shadow-blue-500/30 transition-all duration-200 flex items-center gap-2">
<svg <svg
width="14" width="14"
height="14" height="14"
@ -333,12 +333,12 @@ export function CreateWorkspaceButton() {
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm" /> <Dialog.Overlay className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm" />
<Dialog.Content <Dialog.Content
className="fixed z-50 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-zinc-900 border border-zinc-700/60 rounded-2xl shadow-2xl shadow-black/40 w-[400px] max-h-[90vh] overflow-y-auto p-6" className="fixed z-50 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-surface-sunken border border-line/60 rounded-2xl shadow-2xl shadow-black/40 w-[400px] max-h-[90vh] overflow-y-auto p-6"
> >
<Dialog.Title className="text-base font-semibold text-zinc-100 mb-1"> <Dialog.Title className="text-base font-semibold text-ink mb-1">
Create Workspace Create Workspace
</Dialog.Title> </Dialog.Title>
<p className="text-xs text-zinc-500 mb-5"> <p className="text-xs text-ink-soft mb-5">
Add a new workspace node to the canvas Add a new workspace node to the canvas
</p> </p>
@ -367,7 +367,7 @@ export function CreateWorkspaceButton() {
{/* External toggle when on, this workspace is BYO-compute: {/* External toggle when on, this workspace is BYO-compute:
no template, no model, no hermes provider fields. Backend no template, no model, no hermes provider fields. Backend
returns a copyable connection snippet via the modal. */} returns a copyable connection snippet via the modal. */}
<label className="flex items-start gap-2 rounded-lg border border-zinc-800 p-3 cursor-pointer hover:border-zinc-700 transition-colors"> <label className="flex items-start gap-2 rounded-lg border border-line p-3 cursor-pointer hover:border-line transition-colors">
<input <input
type="checkbox" type="checkbox"
checked={isExternal} checked={isExternal}
@ -375,8 +375,8 @@ export function CreateWorkspaceButton() {
className="mt-0.5" className="mt-0.5"
/> />
<div className="text-xs"> <div className="text-xs">
<div className="text-zinc-200 font-medium">External agent (bring your own compute)</div> <div className="text-ink font-medium">External agent (bring your own compute)</div>
<div className="text-zinc-500 mt-0.5"> <div className="text-ink-soft mt-0.5">
Skip the container. We&apos;ll return a workspace_id + auth token + ready-to-paste snippet so an agent running on your laptop / server / CI can register via A2A. Skip the container. We&apos;ll return a workspace_id + auth token + ready-to-paste snippet so an agent running on your laptop / server / CI can register via A2A.
</div> </div>
</div> </div>
@ -398,7 +398,7 @@ export function CreateWorkspaceButton() {
aria-label="Workspace tier" aria-label="Workspace tier"
className={`grid gap-1.5 ${isSaaS ? "grid-cols-1" : "grid-cols-4"}`} className={`grid gap-1.5 ${isSaaS ? "grid-cols-1" : "grid-cols-4"}`}
> >
<div className={`text-[11px] text-zinc-400 mb-1 ${isSaaS ? "" : "col-span-4"}`}> <div className={`text-[11px] text-ink-mid mb-1 ${isSaaS ? "" : "col-span-4"}`}>
Tier{isSaaS ? " — dedicated VM" : ""} Tier{isSaaS ? " — dedicated VM" : ""}
</div> </div>
{TIERS.map((t, idx) => ( {TIERS.map((t, idx) => (
@ -413,8 +413,8 @@ export function CreateWorkspaceButton() {
onKeyDown={(e) => handleRadioKeyDown(e, idx)} onKeyDown={(e) => handleRadioKeyDown(e, idx)}
className={`py-2 rounded-lg text-center transition-colors ${ className={`py-2 rounded-lg text-center transition-colors ${
tier === t.value tier === t.value
? "bg-blue-600/20 border border-blue-500/50 text-blue-300" ? "bg-accent-strong/20 border border-accent/50 text-accent"
: "bg-zinc-800/60 border border-zinc-700/40 text-zinc-400 hover:text-zinc-300 hover:border-zinc-600" : "bg-surface-card/60 border border-line/40 text-ink-mid hover:text-ink-mid hover:border-line"
}`} }`}
> >
<div className="text-xs font-mono font-semibold"> <div className="text-xs font-mono font-semibold">
@ -429,13 +429,13 @@ export function CreateWorkspaceButton() {
</div> </div>
<div> <div>
<label className="text-[11px] text-zinc-400 block mb-1"> <label className="text-[11px] text-ink-mid block mb-1">
Parent Workspace Parent Workspace
</label> </label>
<select <select
value={parentId} value={parentId}
onChange={(e) => setParentId(e.target.value)} onChange={(e) => setParentId(e.target.value)}
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20 transition-colors" className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
> >
<option value="">None (root level)</option> <option value="">None (root level)</option>
{workspaces.map((ws) => ( {workspaces.map((ws) => (
@ -456,7 +456,7 @@ export function CreateWorkspaceButton() {
<p className="text-[11px] font-semibold text-violet-400 uppercase tracking-wide"> <p className="text-[11px] font-semibold text-violet-400 uppercase tracking-wide">
Hermes Provider Hermes Provider
</p> </p>
<p className="text-[11px] text-zinc-500 -mt-1"> <p className="text-[11px] text-ink-soft -mt-1">
Choose the AI provider and paste your API key. The key is Choose the AI provider and paste your API key. The key is
stored as an encrypted workspace secret. stored as an encrypted workspace secret.
</p> </p>
@ -464,7 +464,7 @@ export function CreateWorkspaceButton() {
<div> <div>
<label <label
htmlFor="hermes-provider-select" htmlFor="hermes-provider-select"
className="text-[11px] text-zinc-400 block mb-1" className="text-[11px] text-ink-mid block mb-1"
> >
Provider Provider
</label> </label>
@ -473,7 +473,7 @@ export function CreateWorkspaceButton() {
value={hermesProvider} value={hermesProvider}
onChange={(e) => setHermesProvider(e.target.value)} onChange={(e) => setHermesProvider(e.target.value)}
aria-label="Hermes provider" aria-label="Hermes provider"
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors" className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors"
> >
{availableProviders.map((p) => ( {availableProviders.map((p) => (
<option key={p.id} value={p.id}> <option key={p.id} value={p.id}>
@ -486,10 +486,10 @@ export function CreateWorkspaceButton() {
<div> <div>
<label <label
htmlFor="hermes-api-key-input" htmlFor="hermes-api-key-input"
className="text-[11px] text-zinc-400 block mb-1" className="text-[11px] text-ink-mid block mb-1"
> >
API Key{" "} API Key{" "}
<span aria-hidden="true" className="text-red-400"> <span aria-hidden="true" className="text-bad">
* *
</span> </span>
<span className="sr-only"> (required)</span> <span className="sr-only"> (required)</span>
@ -502,17 +502,17 @@ export function CreateWorkspaceButton() {
placeholder="sk-…" placeholder="sk-…"
aria-label="Hermes API key" aria-label="Hermes API key"
autoComplete="off" autoComplete="off"
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono" className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono"
/> />
</div> </div>
<div> <div>
<label <label
htmlFor="hermes-model-input" htmlFor="hermes-model-input"
className="text-[11px] text-zinc-400 block mb-1" className="text-[11px] text-ink-mid block mb-1"
> >
Model{" "} Model{" "}
<span aria-hidden="true" className="text-red-400"> <span aria-hidden="true" className="text-bad">
* *
</span> </span>
<span className="sr-only"> (required)</span> <span className="sr-only"> (required)</span>
@ -527,14 +527,14 @@ export function CreateWorkspaceButton() {
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
list="hermes-model-suggestions" list="hermes-model-suggestions"
className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono" className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono"
/> />
<datalist id="hermes-model-suggestions"> <datalist id="hermes-model-suggestions">
{HERMES_PROVIDERS.find((p) => p.id === hermesProvider)?.models.map( {HERMES_PROVIDERS.find((p) => p.id === hermesProvider)?.models.map(
(m) => <option key={m} value={m} />, (m) => <option key={m} value={m} />,
)} )}
</datalist> </datalist>
<p className="text-[10px] text-zinc-500 mt-1"> <p className="text-[10px] text-ink-soft mt-1">
Slug determines which provider hermes routes to at install time. Slug determines which provider hermes routes to at install time.
</p> </p>
</div> </div>
@ -544,7 +544,7 @@ export function CreateWorkspaceButton() {
{error && ( {error && (
<div <div
role="alert" role="alert"
className="mt-4 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-red-400" className="mt-4 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-bad"
> >
{error} {error}
</div> </div>
@ -552,7 +552,7 @@ export function CreateWorkspaceButton() {
<div className="flex justify-end gap-2.5 mt-6"> <div className="flex justify-end gap-2.5 mt-6">
<Dialog.Close asChild> <Dialog.Close asChild>
<button type="button" className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-sm rounded-lg text-zinc-300 transition-colors"> <button type="button" className="px-4 py-2 bg-surface-card hover:bg-surface-card text-sm rounded-lg text-ink-mid transition-colors">
Cancel Cancel
</button> </button>
</Dialog.Close> </Dialog.Close>
@ -560,7 +560,7 @@ export function CreateWorkspaceButton() {
type="button" type="button"
onClick={handleCreate} onClick={handleCreate}
disabled={creating} disabled={creating}
className="px-5 py-2 bg-blue-600 hover:bg-blue-500 active:bg-blue-700 text-sm rounded-lg text-white disabled:opacity-50 transition-colors" className="px-5 py-2 bg-accent-strong hover:bg-accent active:bg-accent-strong text-sm rounded-lg text-ink disabled:opacity-50 transition-colors"
> >
{creating ? "Creating..." : "Create"} {creating ? "Creating..." : "Create"}
</button> </button>
@ -604,11 +604,11 @@ function InputField({
return ( return (
<div> <div>
<label htmlFor={inputId} className="text-[11px] text-zinc-400 block mb-1"> <label htmlFor={inputId} className="text-[11px] text-ink-mid block mb-1">
{label}{" "} {label}{" "}
{required && ( {required && (
<> <>
<span aria-hidden="true" className="text-red-400"> <span aria-hidden="true" className="text-bad">
* *
</span> </span>
<span className="sr-only"> (required)</span> <span className="sr-only"> (required)</span>
@ -623,10 +623,10 @@ function InputField({
placeholder={placeholder} placeholder={placeholder}
min={type === "number" ? "0" : undefined} min={type === "number" ? "0" : undefined}
step={type === "number" ? "0.01" : undefined} step={type === "number" ? "0.01" : undefined}
className={`w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20 transition-colors ${mono ? "font-mono text-xs" : ""}`} className={`w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-zinc-500 focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors ${mono ? "font-mono text-xs" : ""}`}
/> />
{helper && ( {helper && (
<p className="mt-1 text-xs text-zinc-500">{helper}</p> <p className="mt-1 text-xs text-ink-soft">{helper}</p>
)} )}
</div> </div>
); );

View File

@ -89,10 +89,10 @@ export function DeleteCascadeConfirmDialog({
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="cascade-dialog-title" aria-labelledby="cascade-dialog-title"
className="relative bg-zinc-900 border border-red-800/60 rounded-xl shadow-2xl shadow-black/50 max-w-[420px] w-full mx-4 overflow-hidden" className="relative bg-surface-sunken border border-red-800/60 rounded-xl shadow-2xl shadow-black/50 max-w-[420px] w-full mx-4 overflow-hidden"
> >
<div className="px-5 py-4 border-b border-zinc-800"> <div className="px-5 py-4 border-b border-line">
<h3 id="cascade-dialog-title" className="text-sm font-semibold text-red-400"> <h3 id="cascade-dialog-title" className="text-sm font-semibold text-bad">
Delete Workspace and Children Delete Workspace and Children
</h3> </h3>
</div> </div>
@ -101,20 +101,20 @@ export function DeleteCascadeConfirmDialog({
{/* Warning */} {/* Warning */}
<div className="flex gap-3 mb-4"> <div className="flex gap-3 mb-4">
<div className="mt-0.5 shrink-0 w-8 h-8 rounded-full bg-red-900/30 flex items-center justify-center"> <div className="mt-0.5 shrink-0 w-8 h-8 rounded-full bg-red-900/30 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-red-400" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-bad" aria-hidden="true">
<path d="M8 3L14 13H2L8 3Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/> <path d="M8 3L14 13H2L8 3Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/>
<path d="M8 7v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/> <path d="M8 7v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg> </svg>
</div> </div>
<p className="text-[13px] text-zinc-300 leading-relaxed"> <p className="text-[13px] text-ink-mid leading-relaxed">
<span className="font-medium text-red-300">"{name}"</span> has{" "} <span className="font-medium text-bad">"{name}"</span> has{" "}
<strong className="text-zinc-100">{children.length}</strong> child{" "} <strong className="text-ink">{children.length}</strong> child{" "}
{children.length === 1 ? "workspace" : "workspaces"}: {children.length === 1 ? "workspace" : "workspaces"}:
</p> </p>
</div> </div>
{/* Child list */} {/* Child list */}
<ul className="space-y-1.5 mb-4 ml-4 list-disc list-inside text-[12px] text-zinc-400 max-h-32 overflow-y-auto"> <ul className="space-y-1.5 mb-4 ml-4 list-disc list-inside text-[12px] text-ink-mid max-h-32 overflow-y-auto">
{children.map((c) => ( {children.map((c) => (
<li key={c.id} className="truncate" title={c.name}>{c.name}</li> <li key={c.id} className="truncate" title={c.name}>{c.name}</li>
))} ))}
@ -122,7 +122,7 @@ export function DeleteCascadeConfirmDialog({
{/* Cascade warning */} {/* Cascade warning */}
<div className="rounded border border-red-900/40 bg-red-950/20 px-3 py-2.5 mb-4"> <div className="rounded border border-red-900/40 bg-red-950/20 px-3 py-2.5 mb-4">
<p className="text-[12px] text-red-300/80 leading-relaxed"> <p className="text-[12px] text-bad/80 leading-relaxed">
Deleting will cascade <strong className="text-red-200">all child workspaces and their data will be permanently removed.</strong> This cannot be undone. Deleting will cascade <strong className="text-red-200">all child workspaces and their data will be permanently removed.</strong> This cannot be undone.
</p> </p>
</div> </div>
@ -133,19 +133,19 @@ export function DeleteCascadeConfirmDialog({
type="checkbox" type="checkbox"
checked={checked} checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)} onChange={(e) => onCheckedChange(e.target.checked)}
className="mt-0.5 w-4 h-4 rounded border-zinc-600 bg-zinc-800 text-red-500 focus:ring-red-500 focus:ring-offset-0 focus:ring-offset-zinc-900 cursor-pointer" className="mt-0.5 w-4 h-4 rounded border-line bg-surface-card text-bad focus:ring-red-500 focus:ring-offset-0 focus:ring-offset-zinc-900 cursor-pointer"
/> />
<span className="text-[12px] text-zinc-400 group-hover:text-zinc-300 leading-relaxed"> <span className="text-[12px] text-ink-mid group-hover:text-ink-mid leading-relaxed">
I understand this will permanently delete all listed workspaces and their data I understand this will permanently delete all listed workspaces and their data
</span> </span>
</label> </label>
</div> </div>
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-zinc-800 bg-zinc-950/50"> <div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface/50">
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
className="px-3.5 py-1.5 text-[13px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors" className="px-3.5 py-1.5 text-[13px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>
@ -155,8 +155,8 @@ export function DeleteCascadeConfirmDialog({
disabled={!checked} disabled={!checked}
className={`px-3.5 py-1.5 text-[13px] rounded-lg transition-colors className={`px-3.5 py-1.5 text-[13px] rounded-lg transition-colors
${checked ${checked
? "bg-red-600 hover:bg-red-500 text-white cursor-pointer" ? "bg-red-600 hover:bg-red-500 text-ink cursor-pointer"
: "bg-red-900/30 text-red-500/40 cursor-not-allowed" : "bg-red-900/30 text-bad/40 cursor-not-allowed"
}`} }`}
> >
Delete All Delete All

View File

@ -75,11 +75,11 @@ export function EmptyState() {
return ( return (
<div className="absolute inset-0 flex items-start justify-center pointer-events-none z-[1] overflow-y-auto py-8"> <div className="absolute inset-0 flex items-start justify-center pointer-events-none z-[1] overflow-y-auto py-8">
<div className="relative max-w-2xl w-full rounded-3xl border border-zinc-800/70 bg-zinc-950/80 backdrop-blur-xl px-8 py-8 text-center shadow-2xl shadow-black/40 pointer-events-auto mx-4"> <div className="relative max-w-2xl w-full rounded-3xl border border-line/70 bg-surface/80 backdrop-blur-xl px-8 py-8 text-center shadow-2xl shadow-black/40 pointer-events-auto mx-4">
<div className="absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-blue-500/50 to-transparent" /> <div className="absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-blue-500/50 to-transparent" />
{/* Logo */} {/* Logo */}
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-sky-500/20 via-blue-500/20 to-violet-500/20 border border-blue-500/20 flex items-center justify-center"> <div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-sky-500/20 via-blue-500/20 to-violet-500/20 border border-accent/20 flex items-center justify-center">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none"> <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<rect x="3" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" /> <rect x="3" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" />
<rect x="15" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" /> <rect x="15" y="3" width="10" height="10" rx="2" stroke="#60a5fa" strokeWidth="1.5" opacity="0.65" />
@ -91,16 +91,16 @@ export function EmptyState() {
<p className="text-[10px] font-semibold uppercase tracking-[0.28em] text-sky-400/80 mb-2"> <p className="text-[10px] font-semibold uppercase tracking-[0.28em] text-sky-400/80 mb-2">
Welcome to Molecule AI Welcome to Molecule AI
</p> </p>
<h2 className="text-xl font-semibold text-zinc-100 mb-1"> <h2 className="text-xl font-semibold text-ink mb-1">
Deploy your first agent Deploy your first agent
</h2> </h2>
<p className="text-sm text-zinc-400 mb-6 leading-relaxed"> <p className="text-sm text-ink-mid mb-6 leading-relaxed">
Pick a template to get started instantly, or create a blank workspace. Pick a template to get started instantly, or create a blank workspace.
</p> </p>
{/* Template grid */} {/* Template grid */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center gap-2 text-xs text-zinc-400 py-4"> <div className="flex items-center justify-center gap-2 text-xs text-ink-mid py-4">
<Spinner /> <Spinner />
Loading templates... Loading templates...
</div> </div>
@ -114,21 +114,21 @@ export function EmptyState() {
key={t.id} key={t.id}
onClick={() => void deploy(t)} onClick={() => void deploy(t)}
disabled={anyDeploying} disabled={anyDeploying}
className="group rounded-xl border border-zinc-800/60 bg-zinc-900/50 px-3.5 py-3 hover:border-blue-500/40 hover:bg-zinc-900/80 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-zinc-800/60 disabled:hover:bg-zinc-900/50 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70" className="group rounded-xl border border-line/60 bg-surface-sunken/50 px-3.5 py-3 hover:border-accent/40 hover:bg-surface-sunken/80 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-line/60 disabled:hover:bg-surface-sunken/50 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium text-zinc-200 group-hover:text-zinc-100 truncate"> <span className="text-sm font-medium text-ink group-hover:text-ink truncate">
{deploying === t.id ? "Deploying..." : t.name} {deploying === t.id ? "Deploying..." : t.name}
</span> </span>
<span className={`text-[8px] font-mono font-semibold px-1.5 py-0.5 rounded-md border ${tierColor}`}> <span className={`text-[8px] font-mono font-semibold px-1.5 py-0.5 rounded-md border ${tierColor}`}>
T{t.tier} T{t.tier}
</span> </span>
</div> </div>
<p className="text-[11px] text-zinc-500 line-clamp-2 leading-relaxed"> <p className="text-[11px] text-ink-soft line-clamp-2 leading-relaxed">
{t.description || "No description"} {t.description || "No description"}
</p> </p>
{t.skill_count > 0 && ( {t.skill_count > 0 && (
<p className="text-[9px] text-zinc-500 mt-1.5"> <p className="text-[9px] text-ink-soft mt-1.5">
{t.skill_count} skill{t.skill_count !== 1 ? "s" : ""} {t.skill_count} skill{t.skill_count !== 1 ? "s" : ""}
{t.model ? ` · ${t.model}` : ""} {t.model ? ` · ${t.model}` : ""}
</p> </p>
@ -144,18 +144,18 @@ export function EmptyState() {
type="button" type="button"
onClick={createBlank} onClick={createBlank}
disabled={anyDeploying} disabled={anyDeploying}
className="w-full rounded-xl border border-dashed border-zinc-700/60 bg-zinc-900/30 px-4 py-3 text-sm text-zinc-400 hover:text-zinc-200 hover:border-zinc-600 hover:bg-zinc-900/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-zinc-400 disabled:hover:border-zinc-700/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70" className="w-full rounded-xl border border-dashed border-line/60 bg-surface-sunken/30 px-4 py-3 text-sm text-ink-mid hover:text-ink hover:border-line hover:bg-surface-sunken/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-ink-mid disabled:hover:border-line/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
> >
{blankCreating ? "Creating..." : "+ Create blank workspace"} {blankCreating ? "Creating..." : "+ Create blank workspace"}
</button> </button>
{/* Org templates — instantiate a whole team in one click */} {/* Org templates — instantiate a whole team in one click */}
<div className="mt-4 pt-4 border-t border-zinc-800/50 text-left"> <div className="mt-4 pt-4 border-t border-line/50 text-left">
<OrgTemplatesSection /> <OrgTemplatesSection />
</div> </div>
{displayError && ( {displayError && (
<div role="alert" className="mt-3 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-red-400"> <div role="alert" className="mt-3 px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-bad">
{displayError} {displayError}
</div> </div>
)} )}
@ -166,13 +166,13 @@ export function EmptyState() {
{modal} {modal}
{/* Tips */} {/* Tips */}
<div className="mt-5 pt-4 border-t border-zinc-800/50"> <div className="mt-5 pt-4 border-t border-line/50">
<div className="flex items-center justify-center gap-6 text-[10px] text-zinc-400"> <div className="flex items-center justify-center gap-6 text-[10px] text-ink-mid">
<span>Drag to nest workspaces into teams</span> <span>Drag to nest workspaces into teams</span>
<span className="text-zinc-700">|</span> <span className="text-zinc-700">|</span>
<span>Right-click for actions</span> <span>Right-click for actions</span>
<span className="text-zinc-700">|</span> <span className="text-zinc-700">|</span>
<span>Press <kbd className="px-1 py-0.5 bg-zinc-800 rounded text-zinc-500 font-mono">&#8984;K</kbd> to search</span> <span>Press <kbd className="px-1 py-0.5 bg-surface-card rounded text-ink-soft font-mono">&#8984;K</kbd> to search</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -51,8 +51,8 @@ export class ErrorBoundary extends React.Component<
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return ( return (
<div className="fixed inset-0 flex items-center justify-center bg-zinc-950 z-50"> <div className="fixed inset-0 flex items-center justify-center bg-surface z-50">
<div className="max-w-md rounded-2xl border border-red-500/30 bg-zinc-900/90 px-8 py-8 text-center shadow-2xl shadow-black/40"> <div className="max-w-md rounded-2xl border border-red-500/30 bg-surface-sunken/90 px-8 py-8 text-center shadow-2xl shadow-black/40">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-red-500/10 border border-red-500/30"> <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-red-500/10 border border-red-500/30">
<svg <svg
width="24" width="24"
@ -70,20 +70,20 @@ export class ErrorBoundary extends React.Component<
<line x1="12" y1="16" x2="12.01" y2="16" /> <line x1="12" y1="16" x2="12.01" y2="16" />
</svg> </svg>
</div> </div>
<h2 className="text-lg font-semibold text-zinc-100 mb-2"> <h2 className="text-lg font-semibold text-ink mb-2">
Something went wrong Something went wrong
</h2> </h2>
<p className="text-sm text-zinc-400 mb-1"> <p className="text-sm text-ink-mid mb-1">
An unexpected error occurred while rendering the application. An unexpected error occurred while rendering the application.
</p> </p>
<p className="text-xs text-red-400/80 mb-6 font-mono break-all"> <p className="text-xs text-bad/80 mb-6 font-mono break-all">
{this.state.error?.message ?? "Unknown error"} {this.state.error?.message ?? "Unknown error"}
</p> </p>
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<button <button
type="button" type="button"
onClick={this.handleReload} onClick={this.handleReload}
className="rounded-lg bg-blue-600 hover:bg-blue-500 px-5 py-2 text-sm font-medium text-white transition-colors" className="rounded-lg bg-accent-strong hover:bg-accent px-5 py-2 text-sm font-medium text-ink transition-colors"
> >
Reload Reload
</button> </button>
@ -93,7 +93,7 @@ export class ErrorBoundary extends React.Component<
e.preventDefault(); e.preventDefault();
this.handleReport(); this.handleReport();
}} }}
className="rounded-lg border border-zinc-700 hover:border-zinc-600 px-5 py-2 text-sm font-medium text-zinc-300 hover:text-zinc-100 transition-colors" className="rounded-lg border border-line hover:border-line px-5 py-2 text-sm font-medium text-ink-mid hover:text-ink transition-colors"
> >
Report Report
</a> </a>

View File

@ -113,13 +113,13 @@ export function ExternalConnectModal({ info, onClose }: Props) {
<Dialog.Root open onOpenChange={(o) => !o && onClose()}> <Dialog.Root open onOpenChange={(o) => !o && onClose()}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/60 z-50" /> <Dialog.Overlay className="fixed inset-0 bg-black/60 z-50" />
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-[min(720px,92vw)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-zinc-900 border border-zinc-700 p-6 shadow-2xl"> <Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-[min(720px,92vw)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-surface-sunken border border-line p-6 shadow-2xl">
<Dialog.Title className="text-lg font-semibold text-white"> <Dialog.Title className="text-lg font-semibold text-ink">
Connect your external agent Connect your external agent
</Dialog.Title> </Dialog.Title>
<Dialog.Description className="mt-1 text-sm text-zinc-400"> <Dialog.Description className="mt-1 text-sm text-ink-mid">
Paste the snippet below into your agent&apos;s deployment. The Paste the snippet below into your agent&apos;s deployment. The
auth token is shown <span className="text-amber-400">only once</span> auth token is shown <span className="text-warm">only once</span>
{" "} save it somewhere safe before closing this dialog. {" "} save it somewhere safe before closing this dialog.
</Dialog.Description> </Dialog.Description>
@ -127,7 +127,7 @@ export function ExternalConnectModal({ info, onClose }: Props) {
<div <div
role="tablist" role="tablist"
aria-label="Connection snippet format" aria-label="Connection snippet format"
className="mt-4 flex gap-1 border-b border-zinc-800" className="mt-4 flex gap-1 border-b border-line"
> >
{(() => { {(() => {
// Build the tab order dynamically. Claude Code first // Build the tab order dynamically. Claude Code first
@ -150,8 +150,8 @@ export function ExternalConnectModal({ info, onClose }: Props) {
onClick={() => setTab(t)} onClick={() => setTab(t)}
className={`px-3 py-2 text-sm border-b-2 -mb-px transition-colors ${ className={`px-3 py-2 text-sm border-b-2 -mb-px transition-colors ${
tab === t tab === t
? "border-blue-500 text-white" ? "border-accent text-ink"
: "border-transparent text-zinc-500 hover:text-zinc-300" : "border-transparent text-ink-soft hover:text-ink-mid"
}`} }`}
> >
{t === "claude" {t === "claude"
@ -226,7 +226,7 @@ export function ExternalConnectModal({ info, onClose }: Props) {
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-200" className="px-4 py-2 text-sm rounded-lg bg-surface-card hover:bg-surface-card text-ink"
> >
I&apos;ve saved it close I&apos;ve saved it close
</button> </button>
@ -252,16 +252,16 @@ function SnippetBlock({
return ( return (
<div> <div>
<div className="flex items-center justify-between pb-1"> <div className="flex items-center justify-between pb-1">
<span className="text-xs text-zinc-500">{label}</span> <span className="text-xs text-ink-soft">{label}</span>
<button <button
type="button" type="button"
onClick={onCopy} onClick={onCopy}
className="text-xs px-2 py-1 rounded bg-blue-600/80 hover:bg-blue-500 text-white" className="text-xs px-2 py-1 rounded bg-accent-strong/80 hover:bg-accent text-ink"
> >
{copied ? "Copied!" : "Copy"} {copied ? "Copied!" : "Copy"}
</button> </button>
</div> </div>
<pre className="text-xs bg-zinc-950 border border-zinc-800 rounded-lg p-3 max-h-80 overflow-auto whitespace-pre-wrap break-all font-mono text-zinc-200"> <pre className="text-xs bg-surface border border-line rounded-lg p-3 max-h-80 overflow-auto whitespace-pre-wrap break-all font-mono text-ink">
{value} {value}
</pre> </pre>
</div> </div>
@ -283,9 +283,9 @@ function Field({
}) { }) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-zinc-500 w-36 shrink-0">{label}</span> <span className="text-xs text-ink-soft w-36 shrink-0">{label}</span>
<code <code
className={`flex-1 text-xs bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-zinc-200 break-all ${mono ? "font-mono" : ""}`} className={`flex-1 text-xs bg-surface border border-line rounded px-2 py-1 text-ink break-all ${mono ? "font-mono" : ""}`}
> >
{value || "(missing)"} {value || "(missing)"}
</code> </code>
@ -293,7 +293,7 @@ function Field({
type="button" type="button"
onClick={onCopy} onClick={onCopy}
disabled={!value} disabled={!value}
className="text-xs px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-200 disabled:opacity-40" className="text-xs px-2 py-1 rounded bg-surface-card hover:bg-surface-card text-ink disabled:opacity-40"
> >
{copied ? "Copied!" : "Copy"} {copied ? "Copied!" : "Copy"}
</button> </button>

View File

@ -65,7 +65,7 @@ export function Legend() {
onClick={openLegend} onClick={openLegend}
aria-label="Show legend" aria-label="Show legend"
title="Show legend" title="Show legend"
className={`fixed bottom-6 ${leftClass} z-30 flex items-center gap-1.5 rounded-full bg-zinc-900/95 border border-zinc-700/50 px-3 py-1.5 text-[11px] font-semibold text-zinc-400 uppercase tracking-wider shadow-xl shadow-black/30 backdrop-blur-sm hover:text-zinc-200 hover:border-zinc-600 transition-[left,colors] duration-200`} className={`fixed bottom-6 ${leftClass} z-30 flex items-center gap-1.5 rounded-full bg-surface-sunken/95 border border-line/50 px-3 py-1.5 text-[11px] font-semibold text-ink-mid uppercase tracking-wider shadow-xl shadow-black/30 backdrop-blur-sm hover:text-ink hover:border-line transition-[left,colors] duration-200`}
> >
<span aria-hidden="true" className="text-[10px]"></span> <span aria-hidden="true" className="text-[10px]"></span>
Legend Legend
@ -74,15 +74,15 @@ export function Legend() {
} }
return ( return (
<div className={`fixed bottom-6 ${leftClass} z-30 bg-zinc-900/95 border border-zinc-700/50 rounded-xl px-4 py-3 shadow-xl shadow-black/30 backdrop-blur-sm max-w-[280px] transition-[left] duration-200`}> <div className={`fixed bottom-6 ${leftClass} z-30 bg-surface-sunken/95 border border-line/50 rounded-xl px-4 py-3 shadow-xl shadow-black/30 backdrop-blur-sm max-w-[280px] transition-[left] duration-200`}>
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<div className="text-[11px] font-semibold text-zinc-400 uppercase tracking-wider">Legend</div> <div className="text-[11px] font-semibold text-ink-mid uppercase tracking-wider">Legend</div>
<button <button
type="button" type="button"
onClick={closeLegend} onClick={closeLegend}
aria-label="Hide legend" aria-label="Hide legend"
title="Hide legend" title="Hide legend"
className="-mt-0.5 -mr-1 px-1.5 text-[14px] leading-none text-zinc-500 hover:text-zinc-200 transition-colors" className="-mt-0.5 -mr-1 px-1.5 text-[14px] leading-none text-ink-soft hover:text-ink transition-colors"
> >
× ×
</button> </button>
@ -90,7 +90,7 @@ export function Legend() {
{/* Status */} {/* Status */}
<div className="mb-2"> <div className="mb-2">
<div className="text-[11px] text-zinc-500 font-medium mb-1">Status</div> <div className="text-[11px] text-ink-soft font-medium mb-1">Status</div>
<div className="flex flex-wrap gap-x-3 gap-y-1"> <div className="flex flex-wrap gap-x-3 gap-y-1">
{LEGEND_STATUSES.map((s) => ( {LEGEND_STATUSES.map((s) => (
<StatusItem key={s} color={STATUS_CONFIG[s].dot} label={STATUS_CONFIG[s].label} /> <StatusItem key={s} color={STATUS_CONFIG[s].dot} label={STATUS_CONFIG[s].label} />
@ -100,22 +100,22 @@ export function Legend() {
{/* Tiers */} {/* Tiers */}
<div className="mb-2"> <div className="mb-2">
<div className="text-[11px] text-zinc-500 font-medium mb-1">Tier</div> <div className="text-[11px] text-ink-soft font-medium mb-1">Tier</div>
<div className="flex flex-wrap gap-x-3 gap-y-1"> <div className="flex flex-wrap gap-x-3 gap-y-1">
<TierItem tier={1} label="Sandboxed" color="text-sky-300 bg-sky-950/40 border-sky-700/30" /> <TierItem tier={1} label="Sandboxed" color="text-sky-300 bg-sky-950/40 border-sky-700/30" />
<TierItem tier={2} label="Standard" color="text-violet-300 bg-violet-950/40 border-violet-700/30" /> <TierItem tier={2} label="Standard" color="text-violet-300 bg-violet-950/40 border-violet-700/30" />
<TierItem tier={3} label="Full Access" color="text-amber-300 bg-amber-950/40 border-amber-700/30" /> <TierItem tier={3} label="Full Access" color="text-warm bg-amber-950/40 border-amber-700/30" />
</div> </div>
</div> </div>
{/* Communication */} {/* Communication */}
<div> <div>
<div className="text-[11px] text-zinc-500 font-medium mb-1">Communication</div> <div className="text-[11px] text-ink-soft font-medium mb-1">Communication</div>
<div className="flex flex-wrap gap-x-3 gap-y-1"> <div className="flex flex-wrap gap-x-3 gap-y-1">
<CommItem icon="↗" color="text-cyan-400" label="A2A Out" /> <CommItem icon="↗" color="text-cyan-400" label="A2A Out" />
<CommItem icon="↙" color="text-blue-400" label="A2A In" /> <CommItem icon="↙" color="text-accent" label="A2A In" />
<CommItem icon="◆" color="text-amber-400" label="Task" /> <CommItem icon="◆" color="text-warm" label="Task" />
<CommItem icon="!" color="text-red-400" label="Error" /> <CommItem icon="!" color="text-bad" label="Error" />
</div> </div>
</div> </div>
</div> </div>
@ -126,7 +126,7 @@ function StatusItem({ color, label }: { color: string; label: string }) {
return ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className={`w-1.5 h-1.5 rounded-full ${color}`} /> <div className={`w-1.5 h-1.5 rounded-full ${color}`} />
<span className="text-[11px] text-zinc-400">{label}</span> <span className="text-[11px] text-ink-mid">{label}</span>
</div> </div>
); );
} }
@ -135,7 +135,7 @@ function TierItem({ tier, label, color }: { tier: number; label: string; color:
return ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className={`text-[11px] font-mono px-1 py-0.5 rounded border ${color}`}>T{tier}</span> <span className={`text-[11px] font-mono px-1 py-0.5 rounded border ${color}`}>T{tier}</span>
<span className="text-[11px] text-zinc-400">{label}</span> <span className="text-[11px] text-ink-mid">{label}</span>
</div> </div>
); );
} }
@ -144,7 +144,7 @@ function CommItem({ icon, color, label }: { icon: string; color: string; label:
return ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className={`text-[11px] ${color}`}>{icon}</span> <span className={`text-[11px] ${color}`}>{icon}</span>
<span className="text-[11px] text-zinc-400">{label}</span> <span className="text-[11px] text-ink-mid">{label}</span>
</div> </div>
); );
} }

View File

@ -54,13 +54,13 @@ function MemorySkeletonRows() {
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
<div <div
key={i} key={i}
className="rounded-lg border border-zinc-800/60 bg-zinc-900/50 px-3 py-3 animate-pulse" className="rounded-lg border border-line/60 bg-surface-sunken/50 px-3 py-3 animate-pulse"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-2 rounded bg-zinc-700/50 flex-1" /> <div className="h-2 rounded bg-surface-card/50 flex-1" />
<div className="h-2 rounded bg-zinc-700/50 w-8" /> <div className="h-2 rounded bg-surface-card/50 w-8" />
<div className="h-2 rounded bg-zinc-700/50 w-6" /> <div className="h-2 rounded bg-surface-card/50 w-6" />
<div className="h-2 rounded bg-zinc-700/50 w-10" /> <div className="h-2 rounded bg-surface-card/50 w-10" />
</div> </div>
</div> </div>
))} ))}
@ -148,7 +148,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
if (loading && entries.length === 0 && !error) { if (loading && entries.length === 0 && !error) {
return ( return (
<div className="flex items-center justify-center h-32"> <div className="flex items-center justify-center h-32">
<span className="text-xs text-zinc-500">Loading memories</span> <span className="text-xs text-ink-soft">Loading memories</span>
</div> </div>
); );
} }
@ -156,7 +156,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Scope tabs */} {/* Scope tabs */}
<div className="px-4 pt-3 pb-2 border-b border-zinc-800/40 shrink-0"> <div className="px-4 pt-3 pb-2 border-b border-line/40 shrink-0">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{SCOPES.map((scope) => ( {SCOPES.map((scope) => (
<button <button
@ -167,8 +167,8 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
className={[ className={[
"px-3 py-1 text-[11px] rounded transition-colors", "px-3 py-1 text-[11px] rounded transition-colors",
activeScope === scope activeScope === scope
? "bg-blue-600 text-white" ? "bg-accent-strong text-ink"
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200", : "bg-surface-card text-ink-mid hover:bg-surface-card hover:text-ink",
].join(" ")} ].join(" ")}
> >
{scope} {scope}
@ -178,7 +178,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
</div> </div>
{/* Search bar + namespace filter */} {/* Search bar + namespace filter */}
<div className="px-4 pt-3 pb-2 border-b border-zinc-800/40 shrink-0 space-y-2"> <div className="px-4 pt-3 pb-2 border-b border-line/40 shrink-0 space-y-2">
<div className="relative flex items-center"> <div className="relative flex items-center">
{/* Magnifying glass icon */} {/* Magnifying glass icon */}
<svg <svg
@ -186,7 +186,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
height="12" height="12"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="none" fill="none"
className="absolute left-2.5 text-zinc-500 pointer-events-none shrink-0" className="absolute left-2.5 text-ink-soft pointer-events-none shrink-0"
aria-hidden="true" aria-hidden="true"
> >
<circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" /> <circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" />
@ -198,7 +198,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Semantic search…" placeholder="Semantic search…"
aria-label="Search memories" aria-label="Search memories"
className="w-full bg-zinc-900 border border-zinc-700/60 focus:border-blue-500/60 rounded-lg pl-8 pr-7 py-1.5 text-[11px] text-zinc-200 placeholder-zinc-600 focus:outline-none transition-colors" className="w-full bg-surface-sunken border border-line/60 focus:border-accent/60 rounded-lg pl-8 pr-7 py-1.5 text-[11px] text-ink placeholder-zinc-600 focus:outline-none transition-colors"
/> />
{searchQuery && ( {searchQuery && (
<button <button
@ -208,7 +208,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
setDebouncedQuery(""); setDebouncedQuery("");
}} }}
aria-label="Clear search" aria-label="Clear search"
className="absolute right-2 text-zinc-500 hover:text-zinc-200 transition-colors text-sm leading-none" className="absolute right-2 text-ink-soft hover:text-ink transition-colors text-sm leading-none"
> >
× ×
</button> </button>
@ -217,7 +217,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
{/* Namespace filter */} {/* Namespace filter */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label htmlFor="namespace-filter" className="text-[10px] text-zinc-500 shrink-0"> <label htmlFor="namespace-filter" className="text-[10px] text-ink-soft shrink-0">
Namespace: Namespace:
</label> </label>
<input <input
@ -227,14 +227,14 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
onChange={(e) => setActiveNamespace(e.target.value)} onChange={(e) => setActiveNamespace(e.target.value)}
placeholder="all namespaces" placeholder="all namespaces"
aria-label="Filter by namespace" aria-label="Filter by namespace"
className="flex-1 bg-zinc-900 border border-zinc-700/60 focus:border-blue-500/60 rounded px-2 py-1 text-[11px] text-zinc-200 placeholder-zinc-600 focus:outline-none transition-colors min-w-0" className="flex-1 bg-surface-sunken border border-line/60 focus:border-accent/60 rounded px-2 py-1 text-[11px] text-ink placeholder-zinc-600 focus:outline-none transition-colors min-w-0"
/> />
</div> </div>
</div> </div>
{/* Toolbar */} {/* Toolbar */}
<div className="px-4 py-2.5 border-b border-zinc-800/40 flex items-center justify-between shrink-0"> <div className="px-4 py-2.5 border-b border-line/40 flex items-center justify-between shrink-0">
<span className="text-[11px] text-zinc-500"> <span className="text-[11px] text-ink-soft">
{debouncedQuery {debouncedQuery
? `${entries.length} result${entries.length !== 1 ? "s" : ""}` ? `${entries.length} result${entries.length !== 1 ? "s" : ""}`
: entries.length === 1 : entries.length === 1
@ -244,7 +244,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
<button <button
type="button" type="button"
onClick={loadEntries} onClick={loadEntries}
className="px-2 py-1 text-[11px] bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded transition-colors" className="px-2 py-1 text-[11px] bg-surface-card hover:bg-surface-card text-ink-mid rounded transition-colors"
aria-label="Refresh memories" aria-label="Refresh memories"
> >
Refresh Refresh
@ -256,7 +256,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-red-400 shrink-0" className="mx-4 mt-3 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded text-xs text-bad shrink-0"
> >
{error} {error}
</div> </div>
@ -270,10 +270,10 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
debouncedQuery ? ( debouncedQuery ? (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center"> <div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
<span className="text-4xl text-zinc-700" aria-hidden="true"></span> <span className="text-4xl text-zinc-700" aria-hidden="true"></span>
<p className="text-sm font-medium text-zinc-400"> <p className="text-sm font-medium text-ink-mid">
No memories match your search No memories match your search
</p> </p>
<p className="text-[11px] text-zinc-600 max-w-[200px] leading-relaxed"> <p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
Try a different query or{" "} Try a different query or{" "}
<button <button
type="button" type="button"
@ -281,7 +281,7 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
setSearchQuery(""); setSearchQuery("");
setDebouncedQuery(""); setDebouncedQuery("");
}} }}
className="text-blue-500 hover:text-blue-400 underline transition-colors" className="text-accent hover:text-accent underline transition-colors"
> >
clear the search clear the search
</button> </button>
@ -291,8 +291,8 @@ export function MemoryInspectorPanel({ workspaceId }: Props) {
) : ( ) : (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-center"> <div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
<span className="text-4xl text-zinc-700" aria-hidden="true"></span> <span className="text-4xl text-zinc-700" aria-hidden="true"></span>
<p className="text-sm font-medium text-zinc-400">No {activeScope} memories</p> <p className="text-sm font-medium text-ink-mid">No {activeScope} memories</p>
<p className="text-[11px] text-zinc-600 max-w-[200px] leading-relaxed"> <p className="text-[11px] text-ink-soft max-w-[200px] leading-relaxed">
{activeScope === "LOCAL" {activeScope === "LOCAL"
? "This workspace has not written any local memories yet." ? "This workspace has not written any local memories yet."
: activeScope === "TEAM" : activeScope === "TEAM"
@ -340,11 +340,11 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
const bodyId = `mem-body-${sanitizeId(entry.id)}`; const bodyId = `mem-body-${sanitizeId(entry.id)}`;
return ( return (
<div className="rounded-lg border border-zinc-800/60 bg-zinc-900/50 overflow-hidden"> <div className="rounded-lg border border-line/60 bg-surface-sunken/50 overflow-hidden">
{/* Header row */} {/* Header row */}
<button <button
type="button" type="button"
className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-zinc-800/30 transition-colors" className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-surface-card/30 transition-colors"
onClick={() => setExpanded((prev) => !prev)} onClick={() => setExpanded((prev) => !prev)}
aria-expanded={expanded} aria-expanded={expanded}
aria-controls={bodyId} aria-controls={bodyId}
@ -354,9 +354,9 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
className={[ className={[
"text-[9px] shrink-0 font-mono px-1 py-0.5 rounded", "text-[9px] shrink-0 font-mono px-1 py-0.5 rounded",
entry.scope === "LOCAL" entry.scope === "LOCAL"
? "bg-zinc-700 text-zinc-400" ? "bg-surface-card text-ink-mid"
: entry.scope === "TEAM" : entry.scope === "TEAM"
? "bg-blue-950 text-blue-400" ? "bg-blue-950 text-accent"
: "bg-violet-950 text-violet-400", : "bg-violet-950 text-violet-400",
].join(" ")} ].join(" ")}
title={`Scope: ${entry.scope}`} title={`Scope: ${entry.scope}`}
@ -365,12 +365,12 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
</span> </span>
{/* Namespace tag */} {/* Namespace tag */}
<span className="text-[9px] shrink-0 font-mono text-zinc-500 truncate max-w-[80px]" title={entry.namespace}> <span className="text-[9px] shrink-0 font-mono text-ink-soft truncate max-w-[80px]" title={entry.namespace}>
{entry.namespace} {entry.namespace}
</span> </span>
{/* Content preview */} {/* Content preview */}
<span className="flex-1 min-w-0 text-[10px] font-mono text-zinc-300 truncate text-left"> <span className="flex-1 min-w-0 text-[10px] font-mono text-ink-mid truncate text-left">
{entry.content.length > 60 ? entry.content.slice(0, 60) + "…" : entry.content} {entry.content.length > 60 ? entry.content.slice(0, 60) + "…" : entry.content}
</span> </span>
@ -380,8 +380,8 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
className={[ className={[
"text-[9px] shrink-0 font-mono tabular-nums", "text-[9px] shrink-0 font-mono tabular-nums",
entry.similarity_score >= 0.8 entry.similarity_score >= 0.8
? "text-blue-500" ? "text-accent"
: "text-zinc-400", : "text-ink-mid",
].join(" ")} ].join(" ")}
title={`Similarity: ${(entry.similarity_score * 100).toFixed(1)}%`} title={`Similarity: ${(entry.similarity_score * 100).toFixed(1)}%`}
data-testid="similarity-badge" data-testid="similarity-badge"
@ -390,10 +390,10 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
</span> </span>
)} )}
<span className="text-[9px] text-zinc-600 shrink-0"> <span className="text-[9px] text-ink-soft shrink-0">
{formatRelativeTime(entry.created_at)} {formatRelativeTime(entry.created_at)}
</span> </span>
<span className="text-[9px] text-zinc-500 shrink-0" aria-hidden="true"> <span className="text-[9px] text-ink-soft shrink-0" aria-hidden="true">
{expanded ? "▼" : "▶"} {expanded ? "▼" : "▶"}
</span> </span>
</button> </button>
@ -404,13 +404,13 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
id={bodyId} id={bodyId}
role="region" role="region"
aria-label="Memory details" aria-label="Memory details"
className="border-t border-zinc-800/50 px-3 pb-3 pt-2 space-y-2" className="border-t border-line/50 px-3 pb-3 pt-2 space-y-2"
> >
<pre className="text-[10px] font-mono text-zinc-300 bg-zinc-950 rounded p-2 overflow-x-auto max-h-48 whitespace-pre-wrap break-all"> <pre className="text-[10px] font-mono text-ink-mid bg-surface rounded p-2 overflow-x-auto max-h-48 whitespace-pre-wrap break-all">
{entry.content} {entry.content}
</pre> </pre>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[9px] text-zinc-600"> <span className="text-[9px] text-ink-soft">
Created: {new Date(entry.created_at).toLocaleString()} Created: {new Date(entry.created_at).toLocaleString()}
</span> </span>
<button <button
@ -420,7 +420,7 @@ function MemoryEntryRow({ entry, onDelete }: MemoryEntryRowProps) {
onDelete(); onDelete();
}} }}
aria-label="Delete memory" aria-label="Delete memory"
className="text-[10px] px-2 py-0.5 bg-red-950/40 hover:bg-red-900/50 border border-red-900/30 rounded text-red-400 transition-colors shrink-0" className="text-[10px] px-2 py-0.5 bg-red-950/40 hover:bg-red-900/50 border border-red-900/30 rounded text-bad transition-colors shrink-0"
> >
Delete Delete
</button> </button>

View File

@ -368,9 +368,9 @@ function ProviderPickerModal({
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="missing-keys-title" aria-labelledby="missing-keys-title"
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[480px] w-full mx-4 max-h-[80vh] overflow-auto" className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl shadow-black/50 max-w-[480px] w-full mx-4 max-h-[80vh] overflow-auto"
> >
<div className="px-5 py-4 border-b border-zinc-800"> <div className="px-5 py-4 border-b border-line">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<div <div
className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center" className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center"
@ -382,14 +382,14 @@ function ProviderPickerModal({
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" /> <circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
</svg> </svg>
</div> </div>
<h3 id="missing-keys-title" className="text-sm font-semibold text-zinc-100"> <h3 id="missing-keys-title" className="text-sm font-semibold text-ink">
{title ?? "Missing API Keys"} {title ?? "Missing API Keys"}
</h3> </h3>
</div> </div>
<p className="text-[12px] text-zinc-400 leading-relaxed"> <p className="text-[12px] text-ink-mid leading-relaxed">
{description ?? ( {description ?? (
<> <>
The <span className="text-amber-300 font-medium">{runtimeLabel}</span>{" "} The <span className="text-warm font-medium">{runtimeLabel}</span>{" "}
runtime supports multiple providers. Pick one and paste its API key. runtime supports multiple providers. Pick one and paste its API key.
</> </>
)} )}
@ -414,17 +414,17 @@ function ProviderPickerModal({
{entries.map((entry, index) => ( {entries.map((entry, index) => (
<div <div
key={entry.key} key={entry.key}
className="bg-zinc-800/50 rounded-lg px-3 py-2.5 border border-zinc-700/50" className="bg-surface-card/50 rounded-lg px-3 py-2.5 border border-line/50"
> >
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<div> <div>
<div className="text-[11px] text-zinc-300 font-medium"> <div className="text-[11px] text-ink-mid font-medium">
{getKeyLabel(entry.key)} {getKeyLabel(entry.key)}
</div> </div>
<div className="text-[9px] font-mono text-zinc-500">{entry.key}</div> <div className="text-[9px] font-mono text-ink-soft">{entry.key}</div>
</div> </div>
{entry.saved && ( {entry.saved && (
<span className="text-[9px] text-emerald-400 bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1"> <span className="text-[9px] text-good bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true"> <svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" /> <path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
@ -446,12 +446,12 @@ function ProviderPickerModal({
handleSaveKey(index); handleSaveKey(index);
} }
}} }}
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors" className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors"
/> />
<button <button
onClick={() => handleSaveKey(index)} onClick={() => handleSaveKey(index)}
disabled={!entry.value.trim() || entry.saving} disabled={!entry.value.trim() || entry.saving}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-[11px] rounded text-white disabled:opacity-30 transition-colors shrink-0" className="px-3 py-1.5 bg-accent-strong hover:bg-accent text-[11px] rounded text-ink disabled:opacity-30 transition-colors shrink-0"
> >
{entry.saving ? "..." : "Save"} {entry.saving ? "..." : "Save"}
</button> </button>
@ -459,19 +459,19 @@ function ProviderPickerModal({
)} )}
{entry.error && ( {entry.error && (
<div className="mt-1.5 text-[10px] text-red-400">{entry.error}</div> <div className="mt-1.5 text-[10px] text-bad">{entry.error}</div>
)} )}
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div className="px-5 py-3 border-t border-zinc-800 bg-zinc-950/50 flex items-center justify-between gap-2"> <div className="px-5 py-3 border-t border-line bg-surface/50 flex items-center justify-between gap-2">
<div> <div>
{onOpenSettings && ( {onOpenSettings && (
<button <button
onClick={onOpenSettings} onClick={onOpenSettings}
className="text-[11px] text-blue-400 hover:text-blue-300 transition-colors" className="text-[11px] text-accent hover:text-accent transition-colors"
> >
Open Settings Panel Open Settings Panel
</button> </button>
@ -480,7 +480,7 @@ function ProviderPickerModal({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={onCancel} onClick={onCancel}
className="px-3.5 py-1.5 text-[12px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors" className="px-3.5 py-1.5 text-[12px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
> >
Cancel Deploy Cancel Deploy
</button> </button>
@ -492,7 +492,7 @@ function ProviderPickerModal({
!selectorValue.providerId || !selectorValue.providerId ||
(showModelInput && model.trim() === "") (showModelInput && model.trim() === "")
} }
className="px-3.5 py-1.5 text-[12px] bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-40" className="px-3.5 py-1.5 text-[12px] bg-accent-strong hover:bg-accent text-ink rounded-lg transition-colors disabled:opacity-40"
> >
{allSaved ? "Deploy" : entries.length > 1 ? "Add Keys" : "Add Key"} {allSaved ? "Deploy" : entries.length > 1 ? "Add Keys" : "Add Key"}
</button> </button>
@ -640,9 +640,9 @@ function AllKeysModal({
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="missing-keys-title" aria-labelledby="missing-keys-title"
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 max-h-[80vh] overflow-auto" className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 max-h-[80vh] overflow-auto"
> >
<div className="px-5 py-4 border-b border-zinc-800"> <div className="px-5 py-4 border-b border-line">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<div <div
className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center" className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center"
@ -654,12 +654,12 @@ function AllKeysModal({
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" /> <circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
</svg> </svg>
</div> </div>
<h3 id="missing-keys-title" className="text-sm font-semibold text-zinc-100"> <h3 id="missing-keys-title" className="text-sm font-semibold text-ink">
Missing API Keys Missing API Keys
</h3> </h3>
</div> </div>
<p className="text-[12px] text-zinc-400 leading-relaxed"> <p className="text-[12px] text-ink-mid leading-relaxed">
The <span className="text-amber-300 font-medium">{runtimeLabel}</span>{" "} The <span className="text-warm font-medium">{runtimeLabel}</span>{" "}
runtime requires the following keys to be configured before deploying. runtime requires the following keys to be configured before deploying.
</p> </p>
</div> </div>
@ -668,17 +668,17 @@ function AllKeysModal({
{entries.map((entry, index) => ( {entries.map((entry, index) => (
<div <div
key={entry.key} key={entry.key}
className="bg-zinc-800/50 rounded-lg px-3 py-2.5 border border-zinc-700/50" className="bg-surface-card/50 rounded-lg px-3 py-2.5 border border-line/50"
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div> <div>
<div className="text-[11px] text-zinc-300 font-medium"> <div className="text-[11px] text-ink-mid font-medium">
{getKeyLabel(entry.key)} {getKeyLabel(entry.key)}
</div> </div>
<div className="text-[9px] font-mono text-zinc-500">{entry.key}</div> <div className="text-[9px] font-mono text-ink-soft">{entry.key}</div>
</div> </div>
{entry.saved && ( {entry.saved && (
<span className="text-[9px] text-emerald-400 bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1"> <span className="text-[9px] text-good bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none"> <svg width="8" height="8" viewBox="0 0 8 8" fill="none">
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" /> <path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
@ -700,37 +700,37 @@ function AllKeysModal({
handleSaveKey(index); handleSaveKey(index);
} }
}} }}
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors" className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors"
/> />
<button <button
type="button" type="button"
onClick={() => handleSaveKey(index)} onClick={() => handleSaveKey(index)}
disabled={!entry.value.trim() || entry.saving} disabled={!entry.value.trim() || entry.saving}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-[11px] rounded text-white disabled:opacity-30 transition-colors shrink-0" className="px-3 py-1.5 bg-accent-strong hover:bg-accent text-[11px] rounded text-ink disabled:opacity-30 transition-colors shrink-0"
> >
{entry.saving ? "..." : "Save"} {entry.saving ? "..." : "Save"}
</button> </button>
</div> </div>
)} )}
{entry.error && <div className="mt-1.5 text-[10px] text-red-400">{entry.error}</div>} {entry.error && <div className="mt-1.5 text-[10px] text-bad">{entry.error}</div>}
</div> </div>
))} ))}
{globalError && ( {globalError && (
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[11px] text-red-400"> <div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[11px] text-bad">
{globalError} {globalError}
</div> </div>
)} )}
</div> </div>
<div className="px-5 py-3 border-t border-zinc-800 bg-zinc-950/50 flex items-center justify-between gap-2"> <div className="px-5 py-3 border-t border-line bg-surface/50 flex items-center justify-between gap-2">
<div> <div>
{onOpenSettings && ( {onOpenSettings && (
<button <button
type="button" type="button"
onClick={onOpenSettings} onClick={onOpenSettings}
className="text-[11px] text-blue-400 hover:text-blue-300 transition-colors" className="text-[11px] text-accent hover:text-accent transition-colors"
> >
Open Settings Panel Open Settings Panel
</button> </button>
@ -740,7 +740,7 @@ function AllKeysModal({
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
className="px-3.5 py-1.5 text-[12px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors" className="px-3.5 py-1.5 text-[12px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
> >
Cancel Deploy Cancel Deploy
</button> </button>
@ -748,7 +748,7 @@ function AllKeysModal({
type="button" type="button"
onClick={handleAddKeysAndDeploy} onClick={handleAddKeysAndDeploy}
disabled={!allSaved || anySaving} disabled={!allSaved || anySaving}
className="px-3.5 py-1.5 text-[12px] bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-40" className="px-3.5 py-1.5 text-[12px] bg-accent-strong hover:bg-accent text-ink rounded-lg transition-colors disabled:opacity-40"
> >
{anySaving ? "Saving..." : allSaved ? "Deploy" : "Add Keys"} {anySaving ? "Saving..." : allSaved ? "Deploy" : "Add Keys"}
</button> </button>

View File

@ -132,10 +132,10 @@ export function OnboardingWizard() {
<div <div
role="complementary" role="complementary"
aria-label="Onboarding guide" aria-label="Onboarding guide"
className="fixed bottom-20 left-4 z-50 w-80 rounded-2xl border border-zinc-700/60 bg-zinc-900/95 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden" className="fixed bottom-20 left-4 z-50 w-80 rounded-2xl border border-line/60 bg-surface-sunken/95 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
> >
{/* Progress bar */} {/* Progress bar */}
<div className="h-1 bg-zinc-800"> <div className="h-1 bg-surface-card">
<div <div
className="h-full bg-gradient-to-r from-blue-500 to-sky-400 transition-all duration-500" className="h-full bg-gradient-to-r from-blue-500 to-sky-400 transition-all duration-500"
style={{ width: `${((currentStepIdx + 1) / STEPS.length) * 100}%` }} style={{ width: `${((currentStepIdx + 1) / STEPS.length) * 100}%` }}
@ -162,17 +162,17 @@ export function OnboardingWizard() {
type="button" type="button"
onClick={dismiss} onClick={dismiss}
aria-label="Skip onboarding guide" aria-label="Skip onboarding guide"
className="text-[10px] text-zinc-400 hover:text-zinc-200 transition-colors" className="text-[10px] text-ink-mid hover:text-ink transition-colors"
> >
Skip guide Skip guide
</button> </button>
</div> </div>
{/* Content */} {/* Content */}
<h3 className="text-sm font-medium text-zinc-100 mb-1"> <h3 className="text-sm font-medium text-ink mb-1">
{currentStep.title} {currentStep.title}
</h3> </h3>
<p className="text-[11px] text-zinc-400 leading-relaxed mb-3"> <p className="text-[11px] text-ink-mid leading-relaxed mb-3">
{currentStep.description} {currentStep.description}
</p> </p>
@ -181,7 +181,7 @@ export function OnboardingWizard() {
<button <button
type="button" type="button"
onClick={handleAction} onClick={handleAction}
className="flex-1 px-3 py-1.5 bg-blue-600/90 hover:bg-blue-500 rounded-lg text-[11px] font-medium text-white transition-colors" className="flex-1 px-3 py-1.5 bg-accent-strong/90 hover:bg-accent rounded-lg text-[11px] font-medium text-ink transition-colors"
> >
{step === "welcome" {step === "welcome"
? "Create Workspace" ? "Create Workspace"
@ -199,7 +199,7 @@ export function OnboardingWizard() {
if (next) setStep(next.id); if (next) setStep(next.id);
else dismiss(); else dismiss();
}} }}
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-[11px] text-zinc-400 transition-colors" className="px-3 py-1.5 bg-surface-card hover:bg-surface-card rounded-lg text-[11px] text-ink-mid transition-colors"
> >
Next Next
</button> </button>

View File

@ -240,14 +240,14 @@ export function OrgImportPreflightModal({
onClick={onCancel} onClick={onCancel}
> >
<div <div
className="w-[560px] max-h-[80vh] overflow-auto rounded-xl bg-zinc-900 border border-zinc-700 shadow-2xl" className="w-[560px] max-h-[80vh] overflow-auto rounded-xl bg-surface-sunken border border-line shadow-2xl"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<header className="px-5 py-4 border-b border-zinc-800"> <header className="px-5 py-4 border-b border-line">
<h2 id="org-preflight-title" className="text-sm font-semibold text-zinc-100"> <h2 id="org-preflight-title" className="text-sm font-semibold text-ink">
Deploy {orgName} Deploy {orgName}
</h2> </h2>
<p className="mt-0.5 text-[11px] text-zinc-500"> <p className="mt-0.5 text-[11px] text-ink-soft">
{workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}. {workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}.
Review the credentials needed before import. Review the credentials needed before import.
</p> </p>
@ -283,23 +283,23 @@ export function OrgImportPreflightModal({
/> />
)} )}
{requiredEnv.length === 0 && recommendedEnv.length === 0 && ( {requiredEnv.length === 0 && recommendedEnv.length === 0 && (
<p className="text-[12px] text-zinc-400"> <p className="text-[12px] text-ink-mid">
No additional credentials required for this template. No additional credentials required for this template.
</p> </p>
)} )}
</section> </section>
<footer className="px-5 py-3 border-t border-zinc-800 flex items-center justify-between"> <footer className="px-5 py-3 border-t border-line flex items-center justify-between">
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
className="px-3 py-1.5 text-[11px] rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-300" className="px-3 py-1.5 text-[11px] rounded bg-surface-card hover:bg-surface-card text-ink-mid"
> >
Cancel Cancel
</button> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{missingRecommended.length > 0 && canProceed && ( {missingRecommended.length > 0 && canProceed && (
<span className="text-[10px] text-amber-400/90"> <span className="text-[10px] text-warm/90">
{missingRecommended.length} recommended key {missingRecommended.length} recommended key
{missingRecommended.length === 1 ? "" : "s"} still unset {missingRecommended.length === 1 ? "" : "s"} still unset
</span> </span>
@ -308,7 +308,7 @@ export function OrgImportPreflightModal({
type="button" type="button"
onClick={onProceed} onClick={onProceed}
disabled={!canProceed} disabled={!canProceed}
className="px-4 py-1.5 text-[11px] font-semibold rounded bg-blue-600 hover:bg-blue-500 text-white disabled:bg-zinc-700 disabled:text-zinc-500 disabled:cursor-not-allowed" className="px-4 py-1.5 text-[11px] font-semibold rounded bg-accent-strong hover:bg-accent text-ink disabled:bg-surface-card disabled:text-ink-soft disabled:cursor-not-allowed"
> >
Import Import
</button> </button>
@ -346,14 +346,14 @@ function EnvList({
? "border-red-800/60 bg-red-950/20" ? "border-red-800/60 bg-red-950/20"
: "border-amber-800/50 bg-amber-950/15"; : "border-amber-800/50 bg-amber-950/15";
const headerColor = const headerColor =
tone === "required" ? "text-red-300" : "text-amber-300"; tone === "required" ? "text-bad" : "text-warm";
return ( return (
<div className={`rounded-lg border ${accent} p-3`}> <div className={`rounded-lg border ${accent} p-3`}>
<h3 className={`text-[11px] font-semibold uppercase tracking-wide ${headerColor}`}> <h3 className={`text-[11px] font-semibold uppercase tracking-wide ${headerColor}`}>
{title} {title}
</h3> </h3>
<p className="mt-0.5 mb-2 text-[10px] text-zinc-400">{subtitle}</p> <p className="mt-0.5 mb-2 text-[10px] text-ink-mid">{subtitle}</p>
<ul className="space-y-2"> <ul className="space-y-2">
{entries.map((entry) => {entries.map((entry) =>
typeof entry === "string" ? ( typeof entry === "string" ? (
@ -397,16 +397,16 @@ function StrictEnvRow({
onSave, onSave,
}: StrictEnvRowProps) { }: StrictEnvRowProps) {
return ( return (
<li className="flex items-center gap-2 rounded bg-zinc-900/70 border border-zinc-800 px-2 py-1.5"> <li className="flex items-center gap-2 rounded bg-surface-sunken/70 border border-line px-2 py-1.5">
<code <code
className={`text-[11px] font-mono flex-1 ${ className={`text-[11px] font-mono flex-1 ${
configured ? "text-zinc-500 line-through" : "text-zinc-200" configured ? "text-ink-soft line-through" : "text-ink"
}`} }`}
> >
{envKey} {envKey}
</code> </code>
{configured ? ( {configured ? (
<span className="text-[10px] text-emerald-400"> set</span> <span className="text-[10px] text-good"> set</span>
) : ( ) : (
<> <>
<input <input
@ -422,20 +422,20 @@ function StrictEnvRow({
} }
}} }}
disabled={d?.saving} disabled={d?.saving}
className="flex-1 px-2 py-1 rounded bg-zinc-800 border border-zinc-700 text-[11px] text-zinc-200 focus:outline-none focus:border-blue-500 disabled:opacity-50" className="flex-1 px-2 py-1 rounded bg-surface-card border border-line text-[11px] text-ink focus:outline-none focus:border-accent disabled:opacity-50"
/> />
<button <button
type="button" type="button"
onClick={() => onSave(envKey)} onClick={() => onSave(envKey)}
disabled={d?.saving || !d?.value.trim()} disabled={d?.saving || !d?.value.trim()}
className="px-2 py-1 text-[10px] rounded bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-40 disabled:cursor-not-allowed" className="px-2 py-1 text-[10px] rounded bg-accent-strong hover:bg-accent text-ink disabled:opacity-40 disabled:cursor-not-allowed"
> >
{d?.saving ? "…" : "Save"} {d?.saving ? "…" : "Save"}
</button> </button>
</> </>
)} )}
{d?.error && ( {d?.error && (
<span className="text-[9px] text-red-400 basis-full pl-1"> <span className="text-[9px] text-bad basis-full pl-1">
{d.error} {d.error}
</span> </span>
)} )}
@ -467,13 +467,13 @@ function AnyOfEnvGroup({
}: AnyOfEnvGroupProps) { }: AnyOfEnvGroupProps) {
const satisfiedBy = members.find((m) => configuredKeys.has(m)); const satisfiedBy = members.find((m) => configuredKeys.has(m));
return ( return (
<li className="rounded border border-zinc-800 bg-zinc-900/50 px-2.5 py-2"> <li className="rounded border border-line bg-surface-sunken/50 px-2.5 py-2">
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<span className="text-[10px] uppercase tracking-wide text-zinc-400"> <span className="text-[10px] uppercase tracking-wide text-ink-mid">
Configure any one Configure any one
</span> </span>
{satisfiedBy && ( {satisfiedBy && (
<span className="text-[10px] text-emerald-400"> <span className="text-[10px] text-good">
using <code className="font-mono">{satisfiedBy}</code> using <code className="font-mono">{satisfiedBy}</code>
</span> </span>
)} )}
@ -486,19 +486,19 @@ function AnyOfEnvGroup({
return ( return (
<li <li
key={m} key={m}
className={`flex items-center gap-2 rounded bg-zinc-900/70 border border-zinc-800 px-2 py-1 ${ className={`flex items-center gap-2 rounded bg-surface-sunken/70 border border-line px-2 py-1 ${
dimmed ? "opacity-50" : "" dimmed ? "opacity-50" : ""
}`} }`}
> >
<code <code
className={`text-[11px] font-mono flex-1 ${ className={`text-[11px] font-mono flex-1 ${
isConfigured ? "text-zinc-500 line-through" : "text-zinc-200" isConfigured ? "text-ink-soft line-through" : "text-ink"
}`} }`}
> >
{m} {m}
</code> </code>
{isConfigured ? ( {isConfigured ? (
<span className="text-[10px] text-emerald-400"> set</span> <span className="text-[10px] text-good"> set</span>
) : ( ) : (
<> <>
<input <input
@ -514,20 +514,20 @@ function AnyOfEnvGroup({
} }
}} }}
disabled={d?.saving} disabled={d?.saving}
className="flex-1 px-2 py-1 rounded bg-zinc-800 border border-zinc-700 text-[11px] text-zinc-200 focus:outline-none focus:border-blue-500 disabled:opacity-50" className="flex-1 px-2 py-1 rounded bg-surface-card border border-line text-[11px] text-ink focus:outline-none focus:border-accent disabled:opacity-50"
/> />
<button <button
type="button" type="button"
onClick={() => onSave(m)} onClick={() => onSave(m)}
disabled={d?.saving || !d?.value.trim()} disabled={d?.saving || !d?.value.trim()}
className="px-2 py-1 text-[10px] rounded bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-40 disabled:cursor-not-allowed" className="px-2 py-1 text-[10px] rounded bg-accent-strong hover:bg-accent text-ink disabled:opacity-40 disabled:cursor-not-allowed"
> >
{d?.saving ? "…" : "Save"} {d?.saving ? "…" : "Save"}
</button> </button>
</> </>
)} )}
{d?.error && ( {d?.error && (
<span className="text-[9px] text-red-400 basis-full pl-1"> <span className="text-[9px] text-bad basis-full pl-1">
{d.error} {d.error}
</span> </span>
)} )}

View File

@ -97,27 +97,27 @@ function PlanCard({
onSelect: () => void; onSelect: () => void;
}) { }) {
const ring = plan.highlighted const ring = plan.highlighted
? "border-blue-600 ring-2 ring-blue-600/30" ? "border-accent ring-2 ring-blue-600/30"
: "border-zinc-800"; : "border-line";
return ( return (
<article <article
className={`flex flex-col rounded-lg border ${ring} bg-zinc-900/40 p-6`} className={`flex flex-col rounded-lg border ${ring} bg-surface-sunken/40 p-6`}
aria-labelledby={`plan-${plan.id}-name`} aria-labelledby={`plan-${plan.id}-name`}
> >
{plan.highlighted && ( {plan.highlighted && (
<span className="mb-3 inline-block rounded-full bg-blue-600/20 px-3 py-1 text-xs font-medium text-blue-300"> <span className="mb-3 inline-block rounded-full bg-accent-strong/20 px-3 py-1 text-xs font-medium text-accent">
Most popular Most popular
</span> </span>
)} )}
<h2 id={`plan-${plan.id}-name`} className="text-xl font-semibold text-white"> <h2 id={`plan-${plan.id}-name`} className="text-xl font-semibold text-ink">
{plan.name} {plan.name}
</h2> </h2>
<p className="mt-1 text-sm text-zinc-400">{plan.tagline}</p> <p className="mt-1 text-sm text-ink-mid">{plan.tagline}</p>
<p className="mt-4 text-3xl font-bold text-white">{plan.price}</p> <p className="mt-4 text-3xl font-bold text-ink">{plan.price}</p>
<ul className="mt-6 flex-1 space-y-2 text-sm text-zinc-300"> <ul className="mt-6 flex-1 space-y-2 text-sm text-ink-mid">
{plan.features.map((f) => ( {plan.features.map((f) => (
<li key={f} className="flex items-start"> <li key={f} className="flex items-start">
<span className="mr-2 text-blue-400" aria-hidden> <span className="mr-2 text-accent" aria-hidden>
</span> </span>
{f} {f}
@ -130,8 +130,8 @@ function PlanCard({
disabled={loading} disabled={loading}
className={`mt-6 rounded-lg px-4 py-3 text-sm font-medium ${ className={`mt-6 rounded-lg px-4 py-3 text-sm font-medium ${
plan.highlighted plan.highlighted
? "bg-blue-600 text-white hover:bg-blue-500 disabled:bg-blue-900" ? "bg-accent-strong text-ink hover:bg-accent disabled:bg-blue-900"
: "border border-zinc-700 bg-zinc-900 text-zinc-100 hover:bg-zinc-800 disabled:opacity-50" : "border border-line bg-surface-sunken text-ink hover:bg-surface-card disabled:opacity-50"
}`} }`}
> >
{loading ? "Opening checkout…" : plan.ctaLabel} {loading ? "Opening checkout…" : plan.ctaLabel}

View File

@ -356,9 +356,9 @@ export function ProviderModelSelector({
<div> <div>
<label <label
htmlFor={providerSelectId} htmlFor={providerSelectId}
className="text-[10px] uppercase tracking-wide text-zinc-500 font-semibold mb-1.5 block" className="text-[10px] uppercase tracking-wide text-ink-soft font-semibold mb-1.5 block"
> >
Provider <span aria-hidden="true" className="text-red-400">*</span> Provider <span aria-hidden="true" className="text-bad">*</span>
<span className="sr-only"> (required)</span> <span className="sr-only"> (required)</span>
</label> </label>
<select <select
@ -368,7 +368,7 @@ export function ProviderModelSelector({
disabled={disabled || catalog.length === 0} disabled={disabled || catalog.length === 0}
aria-describedby={selected?.tooltip ? `${providerSelectId}-help` : undefined} aria-describedby={selected?.tooltip ? `${providerSelectId}-help` : undefined}
data-testid="provider-select" data-testid="provider-select"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors disabled:opacity-50" className="w-full bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors disabled:opacity-50"
> >
<option value="" disabled> <option value="" disabled>
select provider select provider
@ -382,13 +382,13 @@ export function ProviderModelSelector({
{selected?.tooltip && ( {selected?.tooltip && (
<p <p
id={`${providerSelectId}-help`} id={`${providerSelectId}-help`}
className="text-[9px] text-zinc-500 mt-1 leading-relaxed" className="text-[9px] text-ink-soft mt-1 leading-relaxed"
> >
{selected.tooltip} {selected.tooltip}
</p> </p>
)} )}
{selected && selected.envVars.length > 0 && ( {selected && selected.envVars.length > 0 && (
<p className="text-[9px] text-zinc-600 mt-0.5 font-mono"> <p className="text-[9px] text-ink-soft mt-0.5 font-mono">
requires: {selected.envVars.join(", ")} requires: {selected.envVars.join(", ")}
</p> </p>
)} )}
@ -397,9 +397,9 @@ export function ProviderModelSelector({
<div> <div>
<label <label
htmlFor={modelSelectId} htmlFor={modelSelectId}
className="text-[10px] uppercase tracking-wide text-zinc-500 font-semibold mb-1.5 block" className="text-[10px] uppercase tracking-wide text-ink-soft font-semibold mb-1.5 block"
> >
Model <span aria-hidden="true" className="text-red-400">*</span> Model <span aria-hidden="true" className="text-bad">*</span>
<span className="sr-only"> (required)</span> <span className="sr-only"> (required)</span>
</label> </label>
{useTextInput ? ( {useTextInput ? (
@ -420,9 +420,9 @@ export function ProviderModelSelector({
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete="off"
data-testid="model-input" data-testid="model-input"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors disabled:opacity-50" className="w-full bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors disabled:opacity-50"
/> />
<p className="text-[9px] text-zinc-500 mt-1 leading-relaxed"> <p className="text-[9px] text-ink-soft mt-1 leading-relaxed">
{selected?.wildcard {selected?.wildcard
? wildcardHelpText(selected) ? wildcardHelpText(selected)
: "Free-text model id. Make sure the provider can resolve it."} : "Free-text model id. Make sure the provider can resolve it."}
@ -437,7 +437,7 @@ export function ProviderModelSelector({
handleModelChange(selected.models[0]?.id ?? ""); handleModelChange(selected.models[0]?.id ?? "");
} }
}} }}
className="text-[9px] text-blue-400 hover:text-blue-300 mt-0.5" className="text-[9px] text-accent hover:text-accent mt-0.5"
> >
back to model list back to model list
</button> </button>
@ -460,7 +460,7 @@ export function ProviderModelSelector({
}} }}
disabled={disabled || !selected || selected.models.length === 0} disabled={disabled || !selected || selected.models.length === 0}
data-testid="model-select" data-testid="model-select"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1.5 text-[11px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 transition-colors disabled:opacity-50" className="w-full bg-surface-sunken border border-line rounded px-2 py-1.5 text-[11px] text-ink font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 transition-colors disabled:opacity-50"
> >
<option value="" disabled> <option value="" disabled>
{selected ? "— select model —" : "— select provider first —"} {selected ? "— select model —" : "— select provider first —"}

View File

@ -321,17 +321,17 @@ export function ProvisioningTimeout({
onClick={() => handleDismiss(entry.workspaceId)} onClick={() => handleDismiss(entry.workspaceId)}
aria-label="Dismiss provisioning timeout warning" aria-label="Dismiss provisioning timeout warning"
title="Dismiss — keep this workspace running without the warning" title="Dismiss — keep this workspace running without the warning"
className="shrink-0 text-amber-400/60 hover:text-amber-200 transition-colors -mr-1" className="shrink-0 text-warm/60 hover:text-amber-200 transition-colors -mr-1"
> >
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /> <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
</svg> </svg>
</button> </button>
</div> </div>
<div className="text-[11px] text-amber-300/80 leading-relaxed"> <div className="text-[11px] text-warm/80 leading-relaxed">
<span className="font-medium text-amber-200">{entry.workspaceName}</span>{" "} <span className="font-medium text-amber-200">{entry.workspaceName}</span>{" "}
has been provisioning for{" "} has been provisioning for{" "}
<span className="font-mono text-amber-300">{formatDuration(elapsed)}</span>. <span className="font-mono text-warm">{formatDuration(elapsed)}</span>.
It may have encountered an issue. It may have encountered an issue.
</div> </div>
@ -341,7 +341,7 @@ export function ProvisioningTimeout({
type="button" type="button"
onClick={() => handleRetry(entry.workspaceId)} onClick={() => handleRetry(entry.workspaceId)}
disabled={isRetrying || isCancelling || retryCooldown.has(entry.workspaceId)} disabled={isRetrying || isCancelling || retryCooldown.has(entry.workspaceId)}
className="px-3 py-1.5 bg-amber-600 hover:bg-amber-500 text-[11px] font-medium rounded-lg text-white disabled:opacity-40 transition-colors" className="px-3 py-1.5 bg-amber-600 hover:bg-amber-500 text-[11px] font-medium rounded-lg text-ink disabled:opacity-40 transition-colors"
> >
{isRetrying ? "Retrying..." : retryCooldown.has(entry.workspaceId) ? "Wait..." : "Retry"} {isRetrying ? "Retrying..." : retryCooldown.has(entry.workspaceId) ? "Wait..." : "Retry"}
</button> </button>
@ -349,14 +349,14 @@ export function ProvisioningTimeout({
type="button" type="button"
onClick={() => handleCancelRequest(entry.workspaceId)} onClick={() => handleCancelRequest(entry.workspaceId)}
disabled={isRetrying || isCancelling} disabled={isRetrying || isCancelling}
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-[11px] text-zinc-300 rounded-lg border border-zinc-600 disabled:opacity-40 transition-colors" className="px-3 py-1.5 bg-surface-card hover:bg-surface-card text-[11px] text-ink-mid rounded-lg border border-line disabled:opacity-40 transition-colors"
> >
{isCancelling ? "Cancelling..." : "Cancel"} {isCancelling ? "Cancelling..." : "Cancel"}
</button> </button>
<button <button
type="button" type="button"
onClick={() => handleViewLogs(entry.workspaceId)} onClick={() => handleViewLogs(entry.workspaceId)}
className="px-3 py-1.5 text-[11px] text-amber-400 hover:text-amber-300 transition-colors" className="px-3 py-1.5 text-[11px] text-warm hover:text-warm transition-colors"
> >
View Logs View Logs
</button> </button>
@ -371,25 +371,25 @@ export function ProvisioningTimeout({
{confirmingCancel && ( {confirmingCancel && (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<div aria-hidden="true" className="absolute inset-0 bg-black/60" onClick={() => setConfirmingCancel(null)} /> <div aria-hidden="true" className="absolute inset-0 bg-black/60" onClick={() => setConfirmingCancel(null)} />
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-5 max-w-[340px] w-full mx-4"> <div className="relative bg-surface-sunken border border-line rounded-xl shadow-2xl p-5 max-w-[340px] w-full mx-4">
<h3 className="text-sm font-semibold text-zinc-100 mb-2"> <h3 className="text-sm font-semibold text-ink mb-2">
Cancel deployment? Cancel deployment?
</h3> </h3>
<p className="text-[12px] text-zinc-400 mb-4 leading-relaxed"> <p className="text-[12px] text-ink-mid mb-4 leading-relaxed">
This will permanently remove the workspace. This action cannot be undone. This will permanently remove the workspace. This action cannot be undone.
</p> </p>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button <button
type="button" type="button"
onClick={() => setConfirmingCancel(null)} onClick={() => setConfirmingCancel(null)}
className="px-3.5 py-1.5 text-[12px] text-zinc-400 hover:text-zinc-200 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg transition-colors" className="px-3.5 py-1.5 text-[12px] text-ink-mid hover:text-ink bg-surface-card hover:bg-surface-card border border-line rounded-lg transition-colors"
> >
Keep Keep
</button> </button>
<button <button
type="button" type="button"
onClick={handleCancelConfirm} onClick={handleCancelConfirm}
className="px-3.5 py-1.5 text-[12px] bg-red-600 hover:bg-red-500 text-white rounded-lg transition-colors" className="px-3.5 py-1.5 text-[12px] bg-red-600 hover:bg-red-500 text-ink rounded-lg transition-colors"
> >
Remove Workspace Remove Workspace
</button> </button>

View File

@ -92,12 +92,12 @@ export function SearchDialog() {
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Search workspaces" aria-label="Search workspaces"
className="w-[420px] bg-zinc-950/95 backdrop-blur-xl border border-zinc-800/60 rounded-2xl shadow-2xl shadow-black/50 overflow-hidden" className="w-[420px] bg-surface/95 backdrop-blur-xl border border-line/60 rounded-2xl shadow-2xl shadow-black/50 overflow-hidden"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Search input */} {/* Search input */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-zinc-800/40"> <div className="flex items-center gap-3 px-4 py-3 border-b border-line/40">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0 text-zinc-500" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0 text-ink-soft" aria-hidden="true">
<circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1.5" /> <circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1.5" />
<path d="M11 11l3.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> <path d="M11 11l3.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg> </svg>
@ -113,9 +113,9 @@ export function SearchDialog() {
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
placeholder="Search workspaces..." placeholder="Search workspaces..."
className="flex-1 bg-transparent text-sm text-zinc-100 placeholder-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus:outline-none rounded" className="flex-1 bg-transparent text-sm text-ink placeholder-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus:outline-none rounded"
/> />
<kbd className="text-[9px] text-zinc-400 bg-zinc-800/60 px-1.5 py-0.5 rounded border border-zinc-700/40">ESC</kbd> <kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40">ESC</kbd>
</div> </div>
{/* Results */} {/* Results */}
@ -126,7 +126,7 @@ export function SearchDialog() {
className="max-h-[300px] overflow-y-auto py-1" className="max-h-[300px] overflow-y-auto py-1"
> >
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div role="status" aria-live="polite" className="px-4 py-6 text-center text-xs text-zinc-400"> <div role="status" aria-live="polite" className="px-4 py-6 text-center text-xs text-ink-mid">
{query ? "No workspaces match" : "No workspaces yet"} {query ? "No workspaces match" : "No workspaces yet"}
</div> </div>
) : ( ) : (
@ -139,7 +139,7 @@ export function SearchDialog() {
aria-selected={index === focusedIndex} aria-selected={index === focusedIndex}
onClick={() => handleSelect(node.id)} onClick={() => handleSelect(node.id)}
className={`w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors ${ className={`w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors ${
index === focusedIndex ? "bg-zinc-800/60" : "hover:bg-zinc-800/40" index === focusedIndex ? "bg-surface-card/60" : "hover:bg-surface-card/40"
}`} }`}
> >
<div <div
@ -147,13 +147,13 @@ export function SearchDialog() {
className={`w-2 h-2 rounded-full shrink-0 ${statusDotClass(node.data.status)}`} className={`w-2 h-2 rounded-full shrink-0 ${statusDotClass(node.data.status)}`}
/> />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-sm text-zinc-200 truncate">{node.data.name}</div> <div className="text-sm text-ink truncate">{node.data.name}</div>
{node.data.role && ( {node.data.role && (
<div className="text-[10px] text-zinc-500 truncate">{node.data.role}</div> <div className="text-[10px] text-ink-soft truncate">{node.data.role}</div>
)} )}
</div> </div>
<span <span
className="text-[9px] font-mono text-zinc-400" className="text-[9px] font-mono text-ink-mid"
aria-label={`Tier ${node.data.tier}`} aria-label={`Tier ${node.data.tier}`}
> >
T{node.data.tier} T{node.data.tier}
@ -164,11 +164,11 @@ export function SearchDialog() {
</div> </div>
{/* Footer */} {/* Footer */}
<div className="px-4 py-2 border-t border-zinc-800/40 flex items-center justify-between"> <div className="px-4 py-2 border-t border-line/40 flex items-center justify-between">
<span className="text-[9px] text-zinc-400">{filtered.length} workspace{filtered.length !== 1 ? "s" : ""}</span> <span className="text-[9px] text-ink-mid">{filtered.length} workspace{filtered.length !== 1 ? "s" : ""}</span>
<div className="flex gap-2"> <div className="flex gap-2">
<kbd className="text-[9px] text-zinc-400 bg-zinc-800/60 px-1.5 py-0.5 rounded border border-zinc-700/40"> navigate</kbd> <kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40"> navigate</kbd>
<kbd className="text-[9px] text-zinc-400 bg-zinc-800/60 px-1.5 py-0.5 rounded border border-zinc-700/40"> select</kbd> <kbd className="text-[9px] text-ink-mid bg-surface-card/60 px-1.5 py-0.5 rounded border border-line/40"> select</kbd>
</div> </div>
</div> </div>
</div> </div>

View File

@ -137,7 +137,7 @@ export function SidePanel() {
return ( return (
<div <div
className="fixed top-0 right-0 h-full bg-zinc-950/95 backdrop-blur-xl border-l border-zinc-800/50 flex flex-col z-50 shadow-2xl shadow-black/50 animate-in slide-in-from-right duration-200" className="fixed top-0 right-0 h-full bg-surface/95 backdrop-blur-xl border-l border-line/50 flex flex-col z-50 shadow-2xl shadow-black/50 animate-in slide-in-from-right duration-200"
style={{ width }} style={{ width }}
> >
{/* Resize handle */} {/* Resize handle */}
@ -151,26 +151,26 @@ export function SidePanel() {
tabIndex={0} tabIndex={0}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onKeyDown={onResizeKeyDown} onKeyDown={onResizeKeyDown}
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-blue-500/30 active:bg-blue-500/50 transition-colors z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-inset" className="absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-accent/30 active:bg-accent/50 transition-colors z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset"
/> />
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800/40 bg-zinc-900/30"> <div className="flex items-center justify-between px-5 py-4 border-b border-line/40 bg-surface-sunken/30">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<div className="relative"> <div className="relative">
<StatusDot status={node.data.status} size="md" /> <StatusDot status={node.data.status} size="md" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-[14px] font-semibold text-zinc-100 truncate leading-tight"> <h2 className="text-[14px] font-semibold text-ink truncate leading-tight">
{node.data.name} {node.data.name}
</h2> </h2>
<div className="flex items-center gap-2 mt-0.5"> <div className="flex items-center gap-2 mt-0.5">
{node.data.role && ( {node.data.role && (
<span className="text-[10px] text-zinc-500 truncate"> <span className="text-[10px] text-ink-soft truncate">
{node.data.role} {node.data.role}
</span> </span>
)} )}
<span className={`text-[9px] px-1.5 py-0.5 rounded-md font-mono ${ <span className={`text-[9px] px-1.5 py-0.5 rounded-md font-mono ${
isOnline ? "text-emerald-400 bg-emerald-950/30" : "text-zinc-500 bg-zinc-800/50" isOnline ? "text-good bg-emerald-950/30" : "text-ink-soft bg-surface-card/50"
}`}> }`}>
T{node.data.tier} T{node.data.tier}
</span> </span>
@ -181,7 +181,7 @@ export function SidePanel() {
type="button" type="button"
onClick={() => selectNode(null)} onClick={() => selectNode(null)}
aria-label="Close workspace panel" aria-label="Close workspace panel"
className="w-7 h-7 flex items-center justify-center rounded-lg text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800/60 transition-colors" className="w-7 h-7 flex items-center justify-center rounded-lg text-ink-soft hover:text-ink hover:bg-surface-card/60 transition-colors"
> >
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M1 1l10 10M11 1L1 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> <path d="M1 1l10 10M11 1L1 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
@ -190,7 +190,7 @@ export function SidePanel() {
</div> </div>
{/* Capability summary */} {/* Capability summary */}
<div className="px-5 py-3 border-b border-zinc-800/40 bg-zinc-900/20"> <div className="px-5 py-3 border-b border-line/40 bg-surface-sunken/20">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<MetaPill label="Tier" value={`T${node.data.tier}`} /> <MetaPill label="Tier" value={`T${node.data.tier}`} />
<MetaPill label="Runtime" value={capability.runtime || "unknown"} /> <MetaPill label="Runtime" value={capability.runtime || "unknown"} />
@ -200,13 +200,13 @@ export function SidePanel() {
</div> </div>
{/* Tabs — relative wrapper lets the fade gradient position against the scroll container */} {/* Tabs — relative wrapper lets the fade gradient position against the scroll container */}
<div className="relative border-b border-zinc-800/40"> <div className="relative border-b border-line/40">
{/* Right-edge fade: signals more tabs are hidden off-screen when the bar overflows */} {/* Right-edge fade: signals more tabs are hidden off-screen when the bar overflows */}
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-zinc-950 to-transparent z-10" aria-hidden="true" /> <div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-zinc-950 to-transparent z-10" aria-hidden="true" />
<div <div
role="tablist" role="tablist"
aria-label="Workspace panel tabs" aria-label="Workspace panel tabs"
className="flex overflow-x-auto bg-zinc-900/20 px-1" className="flex overflow-x-auto bg-surface-sunken/20 px-1"
onKeyDown={(e) => { onKeyDown={(e) => {
const idx = TABS.findIndex((t) => t.id === panelTab); const idx = TABS.findIndex((t) => t.id === panelTab);
let next: number | null = null; let next: number | null = null;
@ -230,10 +230,10 @@ export function SidePanel() {
aria-controls={`panel-${tab.id}`} aria-controls={`panel-${tab.id}`}
tabIndex={panelTab === tab.id ? 0 : -1} tabIndex={panelTab === tab.id ? 0 : -1}
onClick={() => setPanelTab(tab.id)} onClick={() => setPanelTab(tab.id)}
className={`shrink-0 px-3 py-2.5 text-[10px] font-medium tracking-wide transition-all rounded-t-lg mx-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70 ${ className={`shrink-0 px-3 py-2.5 text-[10px] font-medium tracking-wide transition-all rounded-t-lg mx-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 ${
panelTab === tab.id panelTab === tab.id
? "text-zinc-100 bg-zinc-800/40 border-b-2 border-blue-500" ? "text-ink bg-surface-card/40 border-b-2 border-accent"
: "text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800/40" : "text-ink-soft hover:text-ink hover:bg-surface-card/40"
}`} }`}
> >
<span className="mr-1 opacity-50" aria-hidden="true">{tab.icon}</span> <span className="mr-1 opacity-50" aria-hidden="true">{tab.icon}</span>
@ -264,7 +264,7 @@ export function SidePanel() {
<Tooltip text={node.data.currentTask as string}> <Tooltip text={node.data.currentTask as string}>
<div className="px-4 py-2 bg-amber-950/20 border-b border-amber-800/20 flex items-center gap-2 cursor-default"> <div className="px-4 py-2 bg-amber-950/20 border-b border-amber-800/20 flex items-center gap-2 cursor-default">
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
<span className="text-[10px] text-amber-300/90 truncate"> <span className="text-[10px] text-warm/90 truncate">
{node.data.currentTask} {node.data.currentTask}
</span> </span>
</div> </div>
@ -295,8 +295,8 @@ export function SidePanel() {
</div> </div>
{/* Footer — workspace ID */} {/* Footer — workspace ID */}
<div className="px-5 py-2 border-t border-zinc-800/40 bg-zinc-900/20"> <div className="px-5 py-2 border-t border-line/40 bg-surface-sunken/20">
<span className="text-[9px] font-mono text-zinc-500 select-all"> <span className="text-[9px] font-mono text-ink-soft select-all">
{selectedNodeId} {selectedNodeId}
</span> </span>
</div> </div>
@ -306,9 +306,9 @@ export function SidePanel() {
function MetaPill({ label, value, tone = "zinc" }: { label: string; value: string; tone?: "zinc" | "emerald" | "amber" }) { function MetaPill({ label, value, tone = "zinc" }: { label: string; value: string; tone?: "zinc" | "emerald" | "amber" }) {
const toneClasses = { const toneClasses = {
zinc: "border-zinc-700/50 bg-zinc-900/70 text-zinc-400", zinc: "border-line/50 bg-surface-sunken/70 text-ink-mid",
emerald: "border-emerald-500/20 bg-emerald-950/20 text-emerald-300", emerald: "border-emerald-500/20 bg-emerald-950/20 text-good",
amber: "border-amber-500/20 bg-amber-950/20 text-amber-300", amber: "border-amber-500/20 bg-amber-950/20 text-warm",
}[tone]; }[tone];
return ( return (

View File

@ -236,7 +236,7 @@ export function OrgTemplatesSection() {
onClick={() => setExpanded((v) => !v)} onClick={() => setExpanded((v) => !v)}
aria-expanded={expanded} aria-expanded={expanded}
aria-controls="org-templates-body" aria-controls="org-templates-body"
className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-zinc-500 hover:text-zinc-300 font-semibold transition-colors" className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-ink-soft hover:text-ink-mid font-semibold transition-colors"
> >
<span <span
aria-hidden="true" aria-hidden="true"
@ -246,7 +246,7 @@ export function OrgTemplatesSection() {
</span> </span>
Org Templates Org Templates
{orgs.length > 0 && ( {orgs.length > 0 && (
<span className="text-zinc-600 normal-case tracking-normal"> <span className="text-ink-soft normal-case tracking-normal">
({orgs.length}) ({orgs.length})
</span> </span>
)} )}
@ -255,7 +255,7 @@ export function OrgTemplatesSection() {
type="button" type="button"
onClick={loadOrgs} onClick={loadOrgs}
aria-label="Refresh org templates" aria-label="Refresh org templates"
className="text-[10px] text-zinc-500 hover:text-zinc-300" className="text-[10px] text-ink-soft hover:text-ink-mid"
> >
</button> </button>
@ -264,20 +264,20 @@ export function OrgTemplatesSection() {
{expanded && ( {expanded && (
<div id="org-templates-body" className="space-y-2"> <div id="org-templates-body" className="space-y-2">
{loading && ( {loading && (
<div role="status" aria-live="polite" className="flex items-center gap-1.5 text-[10px] text-zinc-500"> <div role="status" aria-live="polite" className="flex items-center gap-1.5 text-[10px] text-ink-soft">
<Spinner size="sm" /> <Spinner size="sm" />
Loading Loading
</div> </div>
)} )}
{!loading && orgs.length === 0 && ( {!loading && orgs.length === 0 && (
<div className="text-[10px] text-zinc-500"> <div className="text-[10px] text-ink-soft">
No org templates in <code>org-templates/</code> No org templates in <code>org-templates/</code>
</div> </div>
)} )}
{error && ( {error && (
<div className="px-2 py-1 bg-red-950/40 border border-red-800/50 rounded text-[10px] text-red-400"> <div className="px-2 py-1 bg-red-950/40 border border-red-800/50 rounded text-[10px] text-bad">
{error} {error}
</div> </div>
)} )}
@ -287,10 +287,10 @@ export function OrgTemplatesSection() {
return ( return (
<div <div
key={o.dir} key={o.dir}
className="bg-zinc-900/50 border border-zinc-800/60 rounded-xl p-3 hover:border-zinc-700/60 transition-all" className="bg-surface-sunken/50 border border-line/60 rounded-xl p-3 hover:border-line/60 transition-all"
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-[12px] font-semibold text-zinc-200 truncate"> <span className="text-[12px] font-semibold text-ink truncate">
{o.name || o.dir} {o.name || o.dir}
</span> </span>
<span className="text-[9px] font-mono text-sky-400 bg-sky-950/40 px-1.5 py-0.5 rounded-md shrink-0"> <span className="text-[9px] font-mono text-sky-400 bg-sky-950/40 px-1.5 py-0.5 rounded-md shrink-0">
@ -298,7 +298,7 @@ export function OrgTemplatesSection() {
</span> </span>
</div> </div>
{o.description && ( {o.description && (
<p className="text-[10px] text-zinc-500 mb-2.5 line-clamp-2 leading-relaxed"> <p className="text-[10px] text-ink-soft mb-2.5 line-clamp-2 leading-relaxed">
{o.description} {o.description}
</p> </p>
)} )}
@ -306,7 +306,7 @@ export function OrgTemplatesSection() {
type="button" type="button"
onClick={() => handleImport(o)} onClick={() => handleImport(o)}
disabled={isImporting} disabled={isImporting}
className="w-full px-2 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[10px] text-blue-300 font-medium transition-colors disabled:opacity-50" className="w-full px-2 py-1.5 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[10px] text-accent font-medium transition-colors disabled:opacity-50"
> >
{isImporting ? "Importing…" : "Import org"} {isImporting ? "Importing…" : "Import org"}
</button> </button>
@ -411,7 +411,7 @@ function ImportAgentButton({ onImported }: { onImported: () => void }) {
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={importing} disabled={importing}
className="w-full px-3 py-2 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[11px] text-blue-300 font-medium transition-colors disabled:opacity-50" className="w-full px-3 py-2 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[11px] text-accent font-medium transition-colors disabled:opacity-50"
> >
{importing ? "Importing..." : "Import Agent Folder"} {importing ? "Importing..." : "Import Agent Folder"}
</button> </button>
@ -476,8 +476,8 @@ export function TemplatePalette() {
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
className={`fixed top-4 left-4 z-40 w-9 h-9 flex items-center justify-center rounded-lg transition-colors ${ className={`fixed top-4 left-4 z-40 w-9 h-9 flex items-center justify-center rounded-lg transition-colors ${
open open
? "bg-blue-600 text-white" ? "bg-accent-strong text-ink"
: "bg-zinc-900/90 border border-zinc-700/50 text-zinc-400 hover:text-zinc-200 hover:border-zinc-600" : "bg-surface-sunken/90 border border-line/50 text-ink-mid hover:text-ink hover:border-line"
}`} }`}
title="Template Palette" title="Template Palette"
aria-label={open ? "Close template palette" : "Open template palette"} aria-label={open ? "Close template palette" : "Open template palette"}
@ -496,10 +496,10 @@ export function TemplatePalette() {
{/* Sidebar */} {/* Sidebar */}
{open && ( {open && (
<div className="fixed top-0 left-0 h-full w-[280px] bg-zinc-900/95 backdrop-blur-md border-r border-zinc-800/60 z-30 flex flex-col shadow-2xl shadow-black/40"> <div className="fixed top-0 left-0 h-full w-[280px] bg-surface-sunken/95 backdrop-blur-md border-r border-line/60 z-30 flex flex-col shadow-2xl shadow-black/40">
<div className="px-4 pt-14 pb-3 border-b border-zinc-800/60"> <div className="px-4 pt-14 pb-3 border-b border-line/60">
<h2 className="text-sm font-semibold text-zinc-100">Templates</h2> <h2 className="text-sm font-semibold text-ink">Templates</h2>
<p className="text-[10px] text-zinc-500 mt-0.5">Click to deploy a workspace</p> <p className="text-[10px] text-ink-soft mt-0.5">Click to deploy a workspace</p>
</div> </div>
<div className="flex-1 overflow-y-auto p-3 space-y-2"> <div className="flex-1 overflow-y-auto p-3 space-y-2">
@ -509,20 +509,20 @@ export function TemplatePalette() {
<OrgTemplatesSection /> <OrgTemplatesSection />
{loading && ( {loading && (
<div role="status" aria-live="polite" className="flex items-center justify-center gap-2 text-xs text-zinc-500 text-center py-8"> <div role="status" aria-live="polite" className="flex items-center justify-center gap-2 text-xs text-ink-soft text-center py-8">
<Spinner /> <Spinner />
Loading Loading
</div> </div>
)} )}
{!loading && templates.length === 0 && ( {!loading && templates.length === 0 && (
<div role="status" aria-live="polite" className="text-xs text-zinc-500 text-center py-8"> <div role="status" aria-live="polite" className="text-xs text-ink-soft text-center py-8">
No templates found in<br />workspace-configs-templates/ No templates found in<br />workspace-configs-templates/
</div> </div>
)} )}
{error && ( {error && (
<div className="px-3 py-1.5 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-red-400"> <div className="px-3 py-1.5 bg-red-950/40 border border-red-800/50 rounded-lg text-xs text-bad">
{error} {error}
</div> </div>
)} )}
@ -537,10 +537,10 @@ export function TemplatePalette() {
key={t.id} key={t.id}
onClick={() => void handleDeploy(t)} onClick={() => void handleDeploy(t)}
disabled={isDeploying} disabled={isDeploying}
className="w-full text-left bg-zinc-800/40 hover:bg-zinc-800/70 border border-zinc-700/40 hover:border-zinc-600/50 rounded-xl p-3 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800/40 disabled:hover:border-zinc-700/40 group focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70" className="w-full text-left bg-surface-card/40 hover:bg-surface-card/70 border border-line/40 hover:border-line/50 rounded-xl p-3 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-card/40 disabled:hover:border-line/40 group focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-[12px] font-semibold text-zinc-200 group-hover:text-zinc-100 truncate"> <span className="text-[12px] font-semibold text-ink group-hover:text-ink truncate">
{t.name} {t.name}
</span> </span>
<span className={`text-[9px] font-mono px-1.5 py-0.5 rounded-md shrink-0 ${tierCfg.color}`}> <span className={`text-[9px] font-mono px-1.5 py-0.5 rounded-md shrink-0 ${tierCfg.color}`}>
@ -549,7 +549,7 @@ export function TemplatePalette() {
</div> </div>
{t.description && ( {t.description && (
<p className="text-[10px] text-zinc-500 mb-2 line-clamp-2 leading-relaxed"> <p className="text-[10px] text-ink-soft mb-2 line-clamp-2 leading-relaxed">
{t.description} {t.description}
</p> </p>
)} )}
@ -557,12 +557,12 @@ export function TemplatePalette() {
{t.skills?.length > 0 && ( {t.skills?.length > 0 && (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{t.skills.slice(0, 3).map((s) => ( {t.skills.slice(0, 3).map((s) => (
<span key={s} className="text-[8px] text-zinc-400 bg-zinc-700/40 px-1.5 py-0.5 rounded"> <span key={s} className="text-[8px] text-ink-mid bg-surface-card/40 px-1.5 py-0.5 rounded">
{s} {s}
</span> </span>
))} ))}
{t.skills.length > 3 && ( {t.skills.length > 3 && (
<span className="text-[8px] text-zinc-500">+{t.skills.length - 3}</span> <span className="text-[8px] text-ink-soft">+{t.skills.length - 3}</span>
)} )}
</div> </div>
)} )}
@ -575,12 +575,12 @@ export function TemplatePalette() {
})} })}
</div> </div>
<div className="px-4 py-3 border-t border-zinc-800/60 space-y-3"> <div className="px-4 py-3 border-t border-line/60 space-y-3">
<ImportAgentButton onImported={loadTemplates} /> <ImportAgentButton onImported={loadTemplates} />
<button <button
type="button" type="button"
onClick={loadTemplates} onClick={loadTemplates}
className="text-[10px] text-zinc-500 hover:text-zinc-300 transition-colors block" className="text-[10px] text-ink-soft hover:text-ink-mid transition-colors block"
> >
Refresh templates Refresh templates
</button> </button>

View File

@ -77,15 +77,15 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
<> <>
{children} {children}
{status === "pending" && ( {status === "pending" && (
<div aria-hidden="true" className="fixed inset-0 z-50 flex items-center justify-center bg-zinc-950/80 backdrop-blur-sm"> <div aria-hidden="true" className="fixed inset-0 z-50 flex items-center justify-center bg-surface/80 backdrop-blur-sm">
<div <div
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="terms-dialog-title" aria-labelledby="terms-dialog-title"
className="mx-4 max-w-lg rounded-lg border border-zinc-700 bg-zinc-900 p-6 shadow-xl" className="mx-4 max-w-lg rounded-lg border border-line bg-surface-sunken p-6 shadow-xl"
> >
<h2 id="terms-dialog-title" className="text-lg font-semibold text-white">Terms &amp; conditions</h2> <h2 id="terms-dialog-title" className="text-lg font-semibold text-ink">Terms &amp; conditions</h2>
<p className="mt-3 text-sm text-zinc-300"> <p className="mt-3 text-sm text-ink-mid">
Before you create an organization, please review our{" "} Before you create an organization, please review our{" "}
<a href="/legal/terms" className="text-sky-400 underline" target="_blank" rel="noreferrer"> <a href="/legal/terms" className="text-sky-400 underline" target="_blank" rel="noreferrer">
Terms of Service Terms of Service
@ -96,16 +96,16 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
</a> </a>
. Click agree to continue. . Click agree to continue.
</p> </p>
<p className="mt-3 text-xs text-zinc-500"> <p className="mt-3 text-xs text-ink-soft">
By agreeing you acknowledge that workspace data is stored in AWS us-east-2 (Ohio, United States). By agreeing you acknowledge that workspace data is stored in AWS us-east-2 (Ohio, United States).
</p> </p>
{error && <p role="alert" className="mt-3 text-sm text-red-400">{error}</p>} {error && <p role="alert" className="mt-3 text-sm text-bad">{error}</p>}
<div className="mt-5 flex justify-end gap-2"> <div className="mt-5 flex justify-end gap-2">
<button <button
type="button" type="button"
onClick={accept} onClick={accept}
disabled={submitting} disabled={submitting}
className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-500 disabled:opacity-50" className="rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-ink hover:bg-emerald-500 disabled:opacity-50"
> >
{submitting ? "Saving…" : "I agree"} {submitting ? "Saving…" : "I agree"}
</button> </button>

View File

@ -0,0 +1,81 @@
"use client";
import { useTheme, type ThemePreference } from "@/lib/theme-provider";
const OPTIONS: { value: ThemePreference; label: string; icon: string }[] = [
// Sun: explicit light
{
value: "light",
label: "Light",
icon: "M12 3v1.5M12 19.5V21M4.22 4.22l1.06 1.06M18.72 18.72l1.06 1.06M3 12h1.5M19.5 12H21M4.22 19.78l1.06-1.06M18.72 5.28l1.06-1.06M16 12a4 4 0 11-8 0 4 4 0 018 0z",
},
// Monitor: follow OS
{
value: "system",
label: "System",
icon: "M3 5h18v11H3zM8 21h8M9 21l1-5h4l1 5",
},
// Moon: explicit dark
{
value: "dark",
label: "Dark",
icon: "M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z",
},
];
/**
* Three-way preference picker: System / Light / Dark.
*
* Highlights the user's *picked* preference, not the resolved render
* mode. So "System" stays highlighted while the screen renders dark
* (because the OS is dark) that's the user's mental model: "I told
* the app to follow my OS."
*
* Aligned with molecule-app/components/theme-toggle.tsx so the picker
* behaves identically across surfaces.
*/
export function ThemeToggle({ className = "" }: { className?: string }) {
const { theme, setTheme } = useTheme();
return (
<div
role="radiogroup"
aria-label="Theme preference"
className={`inline-flex items-center gap-0.5 rounded-md border border-line bg-surface-sunken p-0.5 ${className}`}
>
{OPTIONS.map((opt) => {
const active = theme === opt.value;
return (
<button
key={opt.value}
type="button"
role="radio"
aria-checked={active}
aria-label={opt.label}
onClick={() => setTheme(opt.value)}
className={
"flex h-6 w-6 items-center justify-center rounded transition-colors " +
(active
? "bg-surface-elevated text-ink shadow-sm"
: "text-ink-soft hover:text-ink-mid")
}
>
<svg
width={13}
height={13}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d={opt.icon} />
</svg>
</button>
);
})}
</div>
);
}

View File

@ -44,7 +44,7 @@ export function Toaster() {
? "bg-emerald-950/90 border border-emerald-700/40 text-emerald-200" ? "bg-emerald-950/90 border border-emerald-700/40 text-emerald-200"
: type === "error" : type === "error"
? "bg-red-950/90 border border-red-700/40 text-red-200" ? "bg-red-950/90 border border-red-700/40 text-red-200"
: "bg-zinc-900/90 border border-zinc-700/40 text-zinc-200" : "bg-surface-sunken/90 border border-line/40 text-ink"
}`; }`;
const pos = const pos =
@ -66,7 +66,7 @@ export function Toaster() {
type="button" type="button"
onClick={() => dismiss(toast.id)} onClick={() => dismiss(toast.id)}
aria-label="Dismiss notification" aria-label="Dismiss notification"
className="ml-1 p-1 rounded hover:bg-zinc-700/50 transition-colors opacity-70 hover:opacity-100 shrink-0" className="ml-1 p-1 rounded hover:bg-surface-card/50 transition-colors opacity-70 hover:opacity-100 shrink-0"
> >
× ×
</button> </button>
@ -94,7 +94,7 @@ export function Toaster() {
type="button" type="button"
onClick={() => dismiss(toast.id)} onClick={() => dismiss(toast.id)}
aria-label="Dismiss notification" aria-label="Dismiss notification"
className="ml-1 p-1 rounded hover:bg-zinc-700/50 transition-colors opacity-70 hover:opacity-100 shrink-0" className="ml-1 p-1 rounded hover:bg-surface-card/50 transition-colors opacity-70 hover:opacity-100 shrink-0"
> >
× ×
</button> </button>

View File

@ -7,6 +7,7 @@ import { SettingsButton } from "@/components/settings/SettingsButton";
import { settingsGearRef } from "@/components/settings/SettingsPanel"; import { settingsGearRef } from "@/components/settings/SettingsPanel";
import { ConfirmDialog } from "@/components/ConfirmDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog";
import { showToast } from "@/components/Toaster"; import { showToast } from "@/components/Toaster";
import { ThemeToggle } from "@/components/ThemeToggle";
import { statusDotClass } from "@/lib/design-tokens"; import { statusDotClass } from "@/lib/design-tokens";
export function Toolbar() { export function Toolbar() {
@ -128,13 +129,13 @@ export function Toolbar() {
return ( return (
<div <div
className="fixed top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 bg-zinc-900/80 backdrop-blur-md border border-zinc-800/60 rounded-xl px-4 py-2 shadow-xl shadow-black/20 transition-[margin-left] duration-200" className="fixed top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 bg-surface-sunken/80 backdrop-blur-md border border-line/60 rounded-xl px-4 py-2 shadow-xl shadow-black/20 transition-[margin-left] duration-200"
style={toolbarOffsetStyle} style={toolbarOffsetStyle}
> >
{/* Logo / Title */} {/* Logo / Title */}
<div className="flex items-center gap-2 pr-3 border-r border-zinc-800/60"> <div className="flex items-center gap-2 pr-3 border-r border-line/60">
<img src="/molecule-icon.png" alt="Molecule AI" className="w-5 h-5" /> <img src="/molecule-icon.png" alt="Molecule AI" className="w-5 h-5" />
<span className="text-[11px] font-semibold text-zinc-300 tracking-wide">Molecule AI</span> <span className="text-[11px] font-semibold text-ink-mid tracking-wide">Molecule AI</span>
</div> </div>
{/* Status pills + workspace total in one segment previously two {/* Status pills + workspace total in one segment previously two
@ -154,14 +155,14 @@ export function Toolbar() {
<StatusPill color={statusDotClass("failed")} count={counts.failed} label="failed" /> <StatusPill color={statusDotClass("failed")} count={counts.failed} label="failed" />
)} )}
<span className="text-zinc-700" aria-hidden="true">·</span> <span className="text-zinc-700" aria-hidden="true">·</span>
<span className="text-[10px] text-zinc-500 whitespace-nowrap"> <span className="text-[10px] text-ink-soft whitespace-nowrap">
{counts.roots} workspace{counts.roots !== 1 ? "s" : ""} {counts.roots} workspace{counts.roots !== 1 ? "s" : ""}
{counts.children > 0 && <span className="text-zinc-600"> + {counts.children} sub</span>} {counts.children > 0 && <span className="text-ink-soft"> + {counts.children} sub</span>}
</span> </span>
</div> </div>
{/* WebSocket connection status */} {/* WebSocket connection status */}
<div className="pl-3 border-l border-zinc-800/60"> <div className="pl-3 border-l border-line/60">
<WsStatusPill status={wsStatus} /> <WsStatusPill status={wsStatus} />
</div> </div>
@ -175,10 +176,10 @@ export function Toolbar() {
title={`Stop all running tasks (${counts.activeTasks} active)`} title={`Stop all running tasks (${counts.activeTasks} active)`}
aria-label={stopping ? "Stopping all running tasks" : `Stop all running tasks (${counts.activeTasks} active)`} aria-label={stopping ? "Stopping all running tasks" : `Stop all running tasks (${counts.activeTasks} active)`}
> >
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-red-400" aria-hidden="true"> <svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-bad" aria-hidden="true">
<rect x="2" y="2" width="12" height="12" rx="2" /> <rect x="2" y="2" width="12" height="12" rx="2" />
</svg> </svg>
<span className="text-[10px] text-red-300 font-medium"> <span className="text-[10px] text-bad font-medium">
{stopping ? "Stopping..." : `Stop All (${counts.activeTasks})`} {stopping ? "Stopping..." : `Stop All (${counts.activeTasks})`}
</span> </span>
</button> </button>
@ -194,10 +195,10 @@ export function Toolbar() {
title={`Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} that need to pick up config or secret changes`} title={`Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} that need to pick up config or secret changes`}
aria-label={restartingAll ? "Restarting workspaces" : `Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} pending config or secret changes`} aria-label={restartingAll ? "Restarting workspaces" : `Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} pending config or secret changes`}
> >
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-amber-400" aria-hidden="true"> <svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-warm" aria-hidden="true">
<path d="M2 8a6 6 0 1 1 1.76 4.24M2 13v-3h3" strokeLinecap="round" strokeLinejoin="round" /> <path d="M2 8a6 6 0 1 1 1.76 4.24M2 13v-3h3" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
<span className="text-[10px] text-amber-300 font-medium"> <span className="text-[10px] text-warm font-medium">
{restartingAll ? "Restarting..." : `Restart Pending (${needsRestartNodes.length})`} {restartingAll ? "Restarting..." : `Restart Pending (${needsRestartNodes.length})`}
</span> </span>
</button> </button>
@ -217,8 +218,8 @@ export function Toolbar() {
title={showA2AEdges ? "Hide A2A delegation edges" : "Show A2A delegation edges (last 60 min)"} title={showA2AEdges ? "Hide A2A delegation edges" : "Show A2A delegation edges (last 60 min)"}
className={`flex items-center justify-center w-7 h-7 border rounded-lg transition-colors ${ className={`flex items-center justify-center w-7 h-7 border rounded-lg transition-colors ${
showA2AEdges showA2AEdges
? "bg-blue-950/50 hover:bg-blue-900/50 border-blue-800/40 text-blue-300" ? "bg-blue-950/50 hover:bg-blue-900/50 border-blue-800/40 text-accent"
: "bg-zinc-800/50 hover:bg-zinc-700/50 border-zinc-700/40 text-zinc-500 hover:text-zinc-300" : "bg-surface-card/50 hover:bg-surface-card/50 border-line/40 text-ink-soft hover:text-ink-mid"
}`} }`}
> >
{/* Mesh / network icon */} {/* Mesh / network icon */}
@ -254,7 +255,7 @@ export function Toolbar() {
}} }}
aria-label="Open audit trail for selected workspace" aria-label="Open audit trail for selected workspace"
title="Audit — view ledger for the selected workspace" title="Audit — view ledger for the selected workspace"
className="flex items-center justify-center w-7 h-7 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors text-zinc-500 hover:text-zinc-300" className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid"
> >
{/* Scroll / ledger icon */} {/* Scroll / ledger icon */}
<svg <svg
@ -276,7 +277,7 @@ export function Toolbar() {
onClick={() => useCanvasStore.getState().setSearchOpen(true)} onClick={() => useCanvasStore.getState().setSearchOpen(true)}
aria-label="Search workspaces" aria-label="Search workspaces"
title="Search (⌘K)" title="Search (⌘K)"
className="flex items-center justify-center w-7 h-7 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors text-zinc-500 hover:text-zinc-300" className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid"
> >
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" /> <circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
@ -289,7 +290,7 @@ export function Toolbar() {
<button <button
type="button" type="button"
onClick={() => setHelpOpen((open) => !open)} onClick={() => setHelpOpen((open) => !open)}
className="flex items-center justify-center w-7 h-7 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors text-zinc-500 hover:text-zinc-300" className="flex items-center justify-center w-7 h-7 bg-surface-card/50 hover:bg-surface-card/50 border border-line/40 rounded-lg transition-colors text-ink-soft hover:text-ink-mid"
aria-expanded={helpOpen} aria-expanded={helpOpen}
aria-label="Open quick help" aria-label="Open quick help"
title="Help — shortcuts & quick start" title="Help — shortcuts & quick start"
@ -301,13 +302,13 @@ export function Toolbar() {
</button> </button>
{helpOpen && ( {helpOpen && (
<div className="absolute right-0 top-full mt-2 w-72 rounded-xl border border-zinc-700/60 bg-zinc-950/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-md"> <div className="absolute right-0 top-full mt-2 w-72 rounded-xl border border-line/60 bg-surface/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-md">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[0.24em] text-zinc-400">Quick start</span> <span className="text-[10px] font-semibold uppercase tracking-[0.24em] text-ink-mid">Quick start</span>
<button <button
type="button" type="button"
onClick={() => setHelpOpen(false)} onClick={() => setHelpOpen(false)}
className="text-[10px] text-zinc-600 hover:text-zinc-300 transition-colors" className="text-[10px] text-ink-soft hover:text-ink-mid transition-colors"
> >
Close Close
</button> </button>
@ -324,6 +325,9 @@ export function Toolbar() {
)} )}
</div> </div>
{/* Theme picker — System / Light / Dark */}
<ThemeToggle />
{/* Settings gear icon */} {/* Settings gear icon */}
<SettingsButton ref={settingsGearRef} /> <SettingsButton ref={settingsGearRef} />
@ -344,7 +348,7 @@ function StatusPill({ color, count, label }: { color: string; count: number; lab
return ( return (
<div className="flex items-center gap-1.5" title={`${count} ${label}`} aria-label={`${count} ${label}`}> <div className="flex items-center gap-1.5" title={`${count} ${label}`} aria-label={`${count} ${label}`}>
<div className={`w-1.5 h-1.5 rounded-full ${color}`} aria-hidden="true" /> <div className={`w-1.5 h-1.5 rounded-full ${color}`} aria-hidden="true" />
<span className="text-[10px] text-zinc-400 tabular-nums" aria-hidden="true">{count}</span> <span className="text-[10px] text-ink-mid tabular-nums" aria-hidden="true">{count}</span>
</div> </div>
); );
} }
@ -354,7 +358,7 @@ function WsStatusPill({ status }: { status: "connected" | "connecting" | "discon
return ( return (
<div className="flex items-center gap-1.5" title="Real-time updates: connected" aria-label="Real-time updates: connected"> <div className="flex items-center gap-1.5" title="Real-time updates: connected" aria-label="Real-time updates: connected">
<div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("online")}`} aria-hidden="true" /> <div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("online")}`} aria-hidden="true" />
<span className="text-[10px] text-zinc-500" aria-hidden="true">Live</span> <span className="text-[10px] text-ink-soft" aria-hidden="true">Live</span>
</div> </div>
); );
} }
@ -362,25 +366,25 @@ function WsStatusPill({ status }: { status: "connected" | "connecting" | "discon
return ( return (
<div className="flex items-center gap-1.5" title="Real-time updates: reconnecting…" aria-label="Real-time updates: reconnecting"> <div className="flex items-center gap-1.5" title="Real-time updates: reconnecting…" aria-label="Real-time updates: reconnecting">
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse" aria-hidden="true" /> <div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse" aria-hidden="true" />
<span className="text-[10px] text-zinc-500" aria-hidden="true">Reconnecting</span> <span className="text-[10px] text-ink-soft" aria-hidden="true">Reconnecting</span>
</div> </div>
); );
} }
return ( return (
<div className="flex items-center gap-1.5" title="Real-time updates: disconnected" aria-label="Real-time updates: disconnected"> <div className="flex items-center gap-1.5" title="Real-time updates: disconnected" aria-label="Real-time updates: disconnected">
<div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("failed")}`} aria-hidden="true" /> <div className={`w-1.5 h-1.5 rounded-full ${statusDotClass("failed")}`} aria-hidden="true" />
<span className="text-[10px] text-zinc-500" aria-hidden="true">Offline</span> <span className="text-[10px] text-ink-soft" aria-hidden="true">Offline</span>
</div> </div>
); );
} }
function HelpRow({ shortcut, text }: { shortcut: string; text: string }) { function HelpRow({ shortcut, text }: { shortcut: string; text: string }) {
return ( return (
<div className="flex items-start gap-3 rounded-lg border border-zinc-800/70 bg-zinc-900/45 px-3 py-2"> <div className="flex items-start gap-3 rounded-lg border border-line/70 bg-surface-sunken/45 px-3 py-2">
<span className="shrink-0 rounded-md border border-zinc-700/60 bg-zinc-950/70 px-2 py-0.5 text-[9px] font-medium uppercase tracking-[0.18em] text-zinc-400"> <span className="shrink-0 rounded-md border border-line/60 bg-surface/70 px-2 py-0.5 text-[9px] font-medium uppercase tracking-[0.18em] text-ink-mid">
{shortcut} {shortcut}
</span> </span>
<p className="text-[11px] leading-relaxed text-zinc-500">{text}</p> <p className="text-[11px] leading-relaxed text-ink-soft">{text}</p>
</div> </div>
); );
} }

View File

@ -66,10 +66,10 @@ export function Tooltip({ text, children }: Props) {
<div <div
id={tooltipId.current} id={tooltipId.current}
role="tooltip" role="tooltip"
className="fixed z-[9999] max-w-[400px] max-h-[300px] overflow-y-auto px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg shadow-2xl shadow-black/60 pointer-events-none" className="fixed z-[9999] max-w-[400px] max-h-[300px] overflow-y-auto px-3 py-2 bg-surface-card border border-line rounded-lg shadow-2xl shadow-black/60 pointer-events-none"
style={{ left: pos.x, top: Math.max(8, pos.y - 8), transform: "translateY(-100%)" }} style={{ left: pos.x, top: Math.max(8, pos.y - 8), transform: "translateY(-100%)" }}
> >
<div className="text-[11px] text-zinc-200 whitespace-pre-wrap break-words leading-relaxed"> <div className="text-[11px] text-ink whitespace-pre-wrap break-words leading-relaxed">
{text} {text}
</div> </div>
</div>, </div>,

View File

@ -36,7 +36,7 @@ function EjectIcon(props: React.SVGProps<SVGSVGElement>) {
export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>) { export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>) {
const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline; const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline;
const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-zinc-500 bg-zinc-800" }; const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-soft bg-surface-card" };
// Org-deploy context — four derived flags off one store subscription. // Org-deploy context — four derived flags off one store subscription.
// Drives the shimmer while provisioning, the dimmed/non-draggable // Drives the shimmer while provisioning, the dimmed/non-draggable
// treatment on locked descendants, and the Cancel pill on the root. // treatment on locked descendants, and the Cancel pill on the root.
@ -69,8 +69,8 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
isVisible={isSelected} isVisible={isSelected}
minWidth={hasChildren ? 360 : 210} minWidth={hasChildren ? 360 : 210}
minHeight={hasChildren ? 200 : 110} minHeight={hasChildren ? 200 : 110}
lineClassName="!border-blue-500/40" lineClassName="!border-accent/40"
handleClassName="!w-2 !h-2 !bg-blue-500 !border !border-blue-300" handleClassName="!w-2 !h-2 !bg-accent !border !border-blue-300"
/> />
<div <div
role="button" role="button"
@ -137,13 +137,13 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
${isDragTarget ${isDragTarget
? "bg-emerald-950/40 border-2 border-emerald-400/60 ring-2 ring-emerald-400/20 scale-[1.03]" ? "bg-emerald-950/40 border-2 border-emerald-400/60 ring-2 ring-emerald-400/20 scale-[1.03]"
: isBatchSelected : isBatchSelected
? "bg-zinc-900/95 border-2 border-blue-500/80 ring-2 ring-blue-500/30 shadow-lg shadow-blue-500/15" ? "bg-surface-sunken/95 border-2 border-accent/80 ring-2 ring-accent/30 shadow-lg shadow-blue-500/15"
: isSelected : isSelected
? "bg-zinc-900/95 border border-blue-500/70 ring-1 ring-blue-500/30 shadow-lg shadow-blue-500/10" ? "bg-surface-sunken/95 border border-accent/70 ring-1 ring-accent/30 shadow-lg shadow-blue-500/10"
: "bg-zinc-900/90 border border-zinc-700/80 hover:border-zinc-500/60 shadow-lg shadow-black/30 hover:shadow-xl hover:shadow-black/40" : "bg-surface-sunken/90 border border-line/80 hover:border-zinc-500/60 shadow-lg shadow-black/30 hover:shadow-xl hover:shadow-black/40"
} }
backdrop-blur-sm backdrop-blur-sm
focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70 focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950
${deploy.isActivelyProvisioning ? "mol-deploy-shimmer" : ""} ${deploy.isActivelyProvisioning ? "mol-deploy-shimmer" : ""}
${deploy.isLockedChild ? "mol-deploy-locked" : ""} ${deploy.isLockedChild ? "mol-deploy-locked" : ""}
`} `}
@ -173,7 +173,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
<div className="flex items-center justify-between gap-2 mb-1"> <div className="flex items-center justify-between gap-2 mb-1">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full shrink-0 ${statusCfg.dot} ${statusCfg.glow} shadow-sm`} /> <div className={`w-2 h-2 rounded-full shrink-0 ${statusCfg.dot} ${statusCfg.glow} shadow-sm`} />
<span className="text-[13px] font-semibold text-zinc-100 truncate leading-tight"> <span className="text-[13px] font-semibold text-ink truncate leading-tight">
{data.name} {data.name}
</span> </span>
</div> </div>
@ -213,7 +213,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
REMOTE REMOTE
</span> </span>
) : ( ) : (
<span className="text-[7px] font-mono px-1.5 py-0.5 rounded-md text-zinc-400 bg-zinc-800/60 border border-zinc-700/30"> <span className="text-[7px] font-mono px-1.5 py-0.5 rounded-md text-ink-mid bg-surface-card/60 border border-line/30">
{runtime} {runtime}
</span> </span>
)} )}
@ -226,7 +226,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
* grow arbitrarily tall, which wrecks the grid-slot layout * grow arbitrarily tall, which wrecks the grid-slot layout
* because siblings all plan for the same CHILD_DEFAULT_HEIGHT. */} * because siblings all plan for the same CHILD_DEFAULT_HEIGHT. */}
{data.role && ( {data.role && (
<div className="text-[10px] text-zinc-400 mb-1.5 leading-tight line-clamp-2">{data.role}</div> <div className="text-[10px] text-ink-mid mb-1.5 leading-tight line-clamp-2">{data.role}</div>
)} )}
{/* Skills */} {/* Skills */}
@ -237,15 +237,15 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
key={skill} key={skill}
className={`text-[10px] px-1.5 py-0.5 rounded-md border ${ className={`text-[10px] px-1.5 py-0.5 rounded-md border ${
isOnline isOnline
? "text-emerald-300/80 bg-emerald-950/30 border-emerald-800/30" ? "text-good/80 bg-emerald-950/30 border-emerald-800/30"
: "text-zinc-400 bg-zinc-800/60 border-zinc-700/40" : "text-ink-mid bg-surface-card/60 border-line/40"
}`} }`}
> >
{skill} {skill}
</span> </span>
))} ))}
{skills.length > 4 && ( {skills.length > 4 && (
<span className="text-[10px] text-zinc-500 self-center"> <span className="text-[10px] text-ink-soft self-center">
+{skills.length - 4} +{skills.length - 4}
</span> </span>
)} )}
@ -261,7 +261,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
<Tooltip text={String(data.currentTask)}> <Tooltip text={String(data.currentTask)}>
<div className="flex items-center gap-1.5 mt-1 bg-amber-950/20 px-2 py-1 rounded-md border border-amber-800/20 cursor-default"> <div className="flex items-center gap-1.5 mt-1 bg-amber-950/20 px-2 py-1 rounded-md border border-amber-800/20 cursor-default">
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
<span className="text-[10px] text-amber-300/80 truncate">{data.currentTask}</span> <span className="text-[10px] text-warm/80 truncate">{data.currentTask}</span>
</div> </div>
</Tooltip> </Tooltip>
)} )}
@ -274,7 +274,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
e.stopPropagation(); e.stopPropagation();
useCanvasStore.getState().restartWorkspace(id).catch(() => showToast("Restart failed", "error")); useCanvasStore.getState().restartWorkspace(id).catch(() => showToast("Restart failed", "error"));
}} }}
className="flex items-center gap-1.5 mt-1 w-full bg-sky-950/30 px-2 py-1 rounded-md border border-sky-800/30 hover:bg-sky-900/40 transition-colors text-left focus-visible:ring-2 focus-visible:ring-blue-500/70 focus-visible:outline-none" className="flex items-center gap-1.5 mt-1 w-full bg-sky-950/30 px-2 py-1 rounded-md border border-sky-800/30 hover:bg-sky-900/40 transition-colors text-left focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none"
> >
<span className="text-[10px]"></span> <span className="text-[10px]"></span>
<span className="text-[10px] text-sky-300/80">Restart to apply changes</span> <span className="text-[10px] text-sky-300/80">Restart to apply changes</span>
@ -285,10 +285,10 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
<div className="flex items-center justify-between mt-0.5"> <div className="flex items-center justify-between mt-0.5">
{data.status !== "online" ? ( {data.status !== "online" ? (
<div className={`text-[10px] uppercase tracking-widest font-medium ${ <div className={`text-[10px] uppercase tracking-widest font-medium ${
data.status === "failed" ? "text-red-400" : data.status === "failed" ? "text-bad" :
data.status === "degraded" ? "text-amber-300" : data.status === "degraded" ? "text-warm" :
data.status === "provisioning" ? "text-sky-400" : data.status === "provisioning" ? "text-sky-400" :
"text-zinc-500" "text-ink-soft"
}`}> }`}>
{statusCfg.label} {statusCfg.label}
</div> </div>
@ -297,7 +297,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
{data.activeTasks > 0 && ( {data.activeTasks > 0 && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" /> <div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" />
<span className="text-[10px] text-amber-300/80 tabular-nums"> <span className="text-[10px] text-warm/80 tabular-nums">
{data.activeTasks} task{data.activeTasks > 1 ? "s" : ""} {data.activeTasks} task{data.activeTasks > 1 ? "s" : ""}
</span> </span>
</div> </div>
@ -307,7 +307,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
{/* Degraded error preview */} {/* Degraded error preview */}
{data.status === "degraded" && data.lastSampleError && ( {data.status === "degraded" && data.lastSampleError && (
<div <div
className="text-[10px] text-amber-300/60 truncate mt-1 bg-amber-950/20 px-1.5 py-0.5 rounded border border-amber-800/20" className="text-[10px] text-warm/60 truncate mt-1 bg-amber-950/20 px-1.5 py-0.5 rounded border border-amber-800/20"
title={data.lastSampleError} title={data.lastSampleError}
> >
{data.lastSampleError} {data.lastSampleError}
@ -357,7 +357,7 @@ function TeamMemberChip({
}) { }) {
const { data } = node; const { data } = node;
const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline; const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline;
const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-zinc-500 bg-zinc-800" }; const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-ink-soft bg-surface-card" };
const isOnline = data.status === "online"; const isOnline = data.status === "online";
const skills = getSkillNames(data.agentCard); const skills = getSkillNames(data.agentCard);
@ -376,7 +376,7 @@ function TeamMemberChip({
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={`Select ${data.name}`} aria-label={`Select ${data.name}`}
className="group/child relative rounded-lg bg-zinc-800/60 hover:bg-zinc-700/70 border border-zinc-700/30 hover:border-zinc-600/40 overflow-hidden transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70" className="group/child relative rounded-lg bg-surface-card/60 hover:bg-surface-card/70 border border-line/30 hover:border-line/40 overflow-hidden transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/70"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onSelect(node.id); onSelect(node.id);
@ -402,7 +402,7 @@ function TeamMemberChip({
<div className="flex items-center justify-between gap-1 mb-0.5"> <div className="flex items-center justify-between gap-1 mb-0.5">
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-1.5 min-w-0">
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${statusCfg.dot}`} /> <div className={`w-1.5 h-1.5 rounded-full shrink-0 ${statusCfg.dot}`} />
<span className="text-[10px] font-semibold text-zinc-200 truncate leading-tight"> <span className="text-[10px] font-semibold text-ink truncate leading-tight">
{data.name} {data.name}
</span> </span>
</div> </div>
@ -423,7 +423,7 @@ function TeamMemberChip({
e.stopPropagation(); e.stopPropagation();
onExtract(node.id); onExtract(node.id);
}} }}
className="opacity-0 group-hover/child:opacity-100 text-zinc-500 hover:text-sky-400 transition-all focus-visible:ring-2 focus-visible:ring-blue-500/70 focus-visible:outline-none rounded" className="opacity-0 group-hover/child:opacity-100 text-ink-soft hover:text-sky-400 transition-all focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:outline-none rounded"
> >
<EjectIcon aria-hidden="true" /> <EjectIcon aria-hidden="true" />
</button> </button>
@ -432,7 +432,7 @@ function TeamMemberChip({
{/* Role */} {/* Role */}
{data.role && ( {data.role && (
<div className="text-[10px] text-zinc-500 mb-1 leading-tight truncate">{data.role}</div> <div className="text-[10px] text-ink-soft mb-1 leading-tight truncate">{data.role}</div>
)} )}
{/* Skills */} {/* Skills */}
@ -443,15 +443,15 @@ function TeamMemberChip({
key={skill} key={skill}
className={`text-[10px] px-1 py-0.5 rounded border ${ className={`text-[10px] px-1 py-0.5 rounded border ${
isOnline isOnline
? "text-emerald-300/70 bg-emerald-950/20 border-emerald-800/20" ? "text-good/70 bg-emerald-950/20 border-emerald-800/20"
: "text-zinc-500 bg-zinc-800/40 border-zinc-700/30" : "text-ink-soft bg-surface-card/40 border-line/30"
}`} }`}
> >
{skill} {skill}
</span> </span>
))} ))}
{skills.length > 3 && ( {skills.length > 3 && (
<span className="text-[10px] text-zinc-400 self-center">+{skills.length - 3}</span> <span className="text-[10px] text-ink-mid self-center">+{skills.length - 3}</span>
)} )}
</div> </div>
)} )}
@ -460,10 +460,10 @@ function TeamMemberChip({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{data.status !== "online" ? ( {data.status !== "online" ? (
<span className={`text-[10px] uppercase tracking-widest font-medium ${ <span className={`text-[10px] uppercase tracking-widest font-medium ${
data.status === "failed" ? "text-red-400" : data.status === "failed" ? "text-bad" :
data.status === "degraded" ? "text-amber-300" : data.status === "degraded" ? "text-warm" :
data.status === "provisioning" ? "text-sky-400" : data.status === "provisioning" ? "text-sky-400" :
"text-zinc-500" "text-ink-soft"
}`}> }`}>
{statusCfg.label} {statusCfg.label}
</span> </span>
@ -471,7 +471,7 @@ function TeamMemberChip({
{data.activeTasks > 0 && ( {data.activeTasks > 0 && (
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" /> <div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse" />
<span className="text-[10px] text-amber-300 tabular-nums"> <span className="text-[10px] text-warm tabular-nums">
{data.activeTasks} {data.activeTasks}
</span> </span>
</div> </div>
@ -483,15 +483,15 @@ function TeamMemberChip({
<Tooltip text={String(data.currentTask)}> <Tooltip text={String(data.currentTask)}>
<div className="flex items-center gap-1 mt-0.5 px-1.5 py-0.5 bg-amber-950/20 rounded border border-amber-800/20 cursor-default"> <div className="flex items-center gap-1 mt-0.5 px-1.5 py-0.5 bg-amber-950/20 rounded border border-amber-800/20 cursor-default">
<div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" /> <div className="w-1 h-1 rounded-full bg-amber-400 motion-safe:animate-pulse shrink-0" />
<span className="text-[10px] text-amber-300 truncate">{data.currentTask}</span> <span className="text-[10px] text-warm truncate">{data.currentTask}</span>
</div> </div>
</Tooltip> </Tooltip>
)} )}
{/* Recursive sub-children rendered inside this card */} {/* Recursive sub-children rendered inside this card */}
{hasSubChildren && depth < MAX_NESTING_DEPTH && ( {hasSubChildren && depth < MAX_NESTING_DEPTH && (
<div className="mt-1.5 pt-1.5 border-t border-zinc-700/20"> <div className="mt-1.5 pt-1.5 border-t border-line/20">
<div className="text-[10px] text-zinc-400 uppercase tracking-widest mb-1">Team</div> <div className="text-[10px] text-ink-mid uppercase tracking-widest mb-1">Team</div>
<div className={subChildren.length >= 2 ? "grid grid-cols-2 gap-1" : "space-y-1"}> <div className={subChildren.length >= 2 ? "grid grid-cols-2 gap-1" : "space-y-1"}>
{subChildren.map((sub) => ( {subChildren.map((sub) => (
<TeamMemberChip key={sub.id} node={sub} allNodes={allNodes} depth={depth + 1} onSelect={onSelect} onExtract={onExtract} /> <TeamMemberChip key={sub.id} node={sub} allNodes={allNodes} depth={depth + 1} onSelect={onSelect} onExtract={onExtract} />

View File

@ -46,16 +46,16 @@ export function WorkspaceUsage({ workspaceId }: WorkspaceUsageProps) {
return ( return (
<div <div
className="rounded-md border border-zinc-700 bg-zinc-900 p-3 space-y-2" className="rounded-md border border-line bg-surface-sunken p-3 space-y-2"
data-testid="workspace-usage" data-testid="workspace-usage"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider"> <h4 className="text-xs font-semibold text-ink-mid uppercase tracking-wider">
Usage Usage
</h4> </h4>
{!loading && metrics && ( {!loading && metrics && (
<span <span
className="text-[10px] text-zinc-600 font-mono" className="text-[10px] text-ink-soft font-mono"
data-testid="usage-period" data-testid="usage-period"
> >
{formatPeriod(metrics.period_start, metrics.period_end)} {formatPeriod(metrics.period_start, metrics.period_end)}
@ -71,7 +71,7 @@ export function WorkspaceUsage({ workspaceId }: WorkspaceUsageProps) {
<SkeletonRow /> <SkeletonRow />
</> </>
) : error ? ( ) : error ? (
<p className="text-xs text-red-400" data-testid="usage-error"> <p className="text-xs text-bad" data-testid="usage-error">
{error} {error}
</p> </p>
) : metrics ? ( ) : metrics ? (
@ -114,8 +114,8 @@ function SkeletonRow() {
className="flex justify-between items-center animate-pulse" className="flex justify-between items-center animate-pulse"
data-testid="usage-skeleton-row" data-testid="usage-skeleton-row"
> >
<div className="h-3 w-20 rounded bg-zinc-700" /> <div className="h-3 w-20 rounded bg-surface-card" />
<div className="h-3 w-16 rounded bg-zinc-700" /> <div className="h-3 w-16 rounded bg-surface-card" />
</div> </div>
); );
} }
@ -131,8 +131,8 @@ function StatRow({
}) { }) {
return ( return (
<div className="flex justify-between items-center" data-testid={testId}> <div className="flex justify-between items-center" data-testid={testId}>
<span className="text-xs text-zinc-500">{label}</span> <span className="text-xs text-ink-soft">{label}</span>
<span className="text-xs text-zinc-400 font-mono">{value}</span> <span className="text-xs text-ink-mid font-mono">{value}</span>
</div> </div>
); );
} }

View File

@ -51,7 +51,7 @@ describe("AuthGate — loading state", () => {
</AuthGate> </AuthGate>
); );
const overlay = container.querySelector(".bg-zinc-950.fixed.inset-0"); const overlay = container.querySelector(".bg-surface.fixed.inset-0");
expect(overlay).not.toBeNull(); expect(overlay).not.toBeNull();
expect(overlay?.getAttribute("aria-hidden")).toBe("true"); expect(overlay?.getAttribute("aria-hidden")).toBe("true");
}); });

View File

@ -89,7 +89,7 @@ function A2AEdgeImpl({
// The edge stroke color matches what buildA2AEdges sets on the SVG // The edge stroke color matches what buildA2AEdges sets on the SVG
// path style. Mirror it on the badge border so the visual identity // path style. Mirror it on the badge border so the visual identity
// (hot=violet vs warm=blue) carries to the clickable label. // (hot=violet vs warm=blue) carries to the clickable label.
const accent = isHot ? "border-violet-500/60" : "border-blue-500/60"; const accent = isHot ? "border-violet-500/60" : "border-accent/60";
const accentText = isHot ? "text-violet-200" : "text-blue-200"; const accentText = isHot ? "text-violet-200" : "text-blue-200";
const ariaLabel = `${count} delegation${count === 1 ? "" : "s"} from ${ const ariaLabel = `${count} delegation${count === 1 ? "" : "s"} from ${
edgeData.label?.split(" · ")[1] ?? "recent" edgeData.label?.split(" · ")[1] ?? "recent"
@ -119,7 +119,7 @@ function A2AEdgeImpl({
onClick={handleClick} onClick={handleClick}
aria-label={ariaLabel} aria-label={ariaLabel}
title="Open source workspace's activity feed" title="Open source workspace's activity feed"
className={`px-2 py-0.5 rounded-full bg-zinc-900/95 border ${accent} ${accentText} text-[10px] font-medium shadow-md shadow-black/40 backdrop-blur-sm hover:bg-zinc-800 hover:border-opacity-100 transition-colors cursor-pointer`} className={`px-2 py-0.5 rounded-full bg-surface-sunken/95 border ${accent} ${accentText} text-[10px] font-medium shadow-md shadow-black/40 backdrop-blur-sm hover:bg-surface-card hover:border-opacity-100 transition-colors cursor-pointer`}
> >
{labelText} {labelText}
</button> </button>

View File

@ -112,10 +112,10 @@ export function OrgCancelButton({ rootId, rootName, workspaceCount }: Props) {
if (confirming) { if (confirming) {
return ( return (
<div <div
className="nodrag absolute -top-10 right-0 z-20 flex items-center gap-1.5 rounded-lg bg-zinc-900/95 px-2 py-1 shadow-lg border border-red-800/60" className="nodrag absolute -top-10 right-0 z-20 flex items-center gap-1.5 rounded-lg bg-surface-sunken/95 px-2 py-1 shadow-lg border border-red-800/60"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<span className="text-[10px] text-zinc-300"> <span className="text-[10px] text-ink-mid">
Delete {workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}? Delete {workspaceCount} workspace{workspaceCount === 1 ? "" : "s"}?
</span> </span>
<button <button
@ -130,7 +130,7 @@ export function OrgCancelButton({ rootId, rootName, workspaceCount }: Props) {
type="button" type="button"
onClick={() => setConfirming(false)} onClick={() => setConfirming(false)}
disabled={submitting} disabled={submitting}
className="px-2 py-0.5 rounded bg-zinc-700/80 hover:bg-zinc-600 text-[10px] text-zinc-200" className="px-2 py-0.5 rounded bg-surface-card/80 hover:bg-zinc-600 text-[10px] text-ink"
> >
No No
</button> </button>

View File

@ -168,7 +168,10 @@ describe("A2AEdge — render", () => {
/>, />,
); );
const btn = screen.getByRole("button"); const btn = screen.getByRole("button");
expect(btn.className).toContain("border-blue-500/60"); // Warm-paper migration: blue-500 border was mapped to the semantic
// accent token; the text-blue-200 literal is intentionally retained
// because tinted-state pill text reads in both themes.
expect(btn.className).toContain("border-accent/60");
expect(btn.className).toContain("text-blue-200"); expect(btn.className).toContain("text-blue-200");
}); });

View File

@ -105,11 +105,11 @@ export function OrgTokensTab() {
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div> <div>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<h3 className="text-sm font-semibold text-zinc-200"> <h3 className="text-sm font-semibold text-ink">
Organization API Keys Organization API Keys
</h3> </h3>
</div> </div>
<p className="text-[10px] text-zinc-500 leading-relaxed"> <p className="text-[10px] text-ink-soft leading-relaxed">
Full-admin bearer tokens for this organization. Use with external Full-admin bearer tokens for this organization. Use with external
integrations, CLI tools, or AI agents that need to manage integrations, CLI tools, or AI agents that need to manage
workspaces, settings, and secrets. Each key has the same workspaces, settings, and secrets. Each key has the same
@ -126,12 +126,12 @@ export function OrgTokensTab() {
placeholder="Label (e.g. zapier, my-ci)" placeholder="Label (e.g. zapier, my-ci)"
maxLength={100} maxLength={100}
aria-label="Organization API key label" aria-label="Organization API key label"
className="flex-1 text-[11px] bg-zinc-900/60 border border-zinc-700/50 rounded px-2 py-1.5 text-zinc-200 placeholder-zinc-600" className="flex-1 text-[11px] bg-surface-sunken/60 border border-line/50 rounded px-2 py-1.5 text-ink placeholder-zinc-600"
/> />
<button <button
onClick={handleCreate} onClick={handleCreate}
disabled={creating} disabled={creating}
className="px-3 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[11px] text-blue-300 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5" className="px-3 py-1.5 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[11px] text-accent font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
> >
{creating ? ( {creating ? (
<> <>
@ -147,10 +147,10 @@ export function OrgTokensTab() {
{newToken && ( {newToken && (
<div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2"> <div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wider"> <span className="text-[10px] text-good font-semibold uppercase tracking-wider">
{newTokenName ? `New Key: ${newTokenName}` : 'New Key Created'} {newTokenName ? `New Key: ${newTokenName}` : 'New Key Created'}
</span> </span>
<span className="text-[9px] text-emerald-500/70"> <span className="text-[9px] text-good/70">
Copy now it won't be shown again Copy now it won't be shown again
</span> </span>
</div> </div>
@ -160,14 +160,14 @@ export function OrgTokensTab() {
</code> </code>
<button <button
onClick={handleCopy} onClick={handleCopy}
className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-emerald-300 transition-colors" className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-good transition-colors"
> >
{copied ? 'Copied' : 'Copy'} {copied ? 'Copied' : 'Copy'}
</button> </button>
</div> </div>
<button <button
onClick={() => setNewToken(null)} onClick={() => setNewToken(null)}
className="text-[9px] text-emerald-500/60 hover:text-emerald-400 transition-colors" className="text-[9px] text-good/60 hover:text-good transition-colors"
> >
Dismiss Dismiss
</button> </button>
@ -175,20 +175,20 @@ export function OrgTokensTab() {
)} )}
{error && ( {error && (
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-red-400"> <div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-bad">
{error} {error}
</div> </div>
)} )}
{/* Token list */} {/* Token list */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center gap-2 py-6 text-zinc-500 text-xs"> <div className="flex items-center justify-center gap-2 py-6 text-ink-soft text-xs">
<Spinner /> Loading keys... <Spinner /> Loading keys...
</div> </div>
) : tokens.length === 0 ? ( ) : tokens.length === 0 ? (
<div className="text-center py-6"> <div className="text-center py-6">
<p className="text-xs text-zinc-500">No active keys</p> <p className="text-xs text-ink-soft">No active keys</p>
<p className="text-[10px] text-zinc-600 mt-1"> <p className="text-[10px] text-ink-soft mt-1">
Create a key above to authenticate API calls to this organization. Create a key above to authenticate API calls to this organization.
</p> </p>
</div> </div>
@ -197,19 +197,19 @@ export function OrgTokensTab() {
{tokens.map((t) => ( {tokens.map((t) => (
<div <div
key={t.id} key={t.id}
className="flex items-center justify-between bg-zinc-800/40 border border-zinc-700/30 rounded-lg px-3 py-2" className="flex items-center justify-between bg-surface-card/40 border border-line/30 rounded-lg px-3 py-2"
> >
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
<code className="text-[11px] font-mono text-zinc-300 bg-zinc-900/60 px-1.5 py-0.5 rounded shrink-0"> <code className="text-[11px] font-mono text-ink-mid bg-surface-sunken/60 px-1.5 py-0.5 rounded shrink-0">
{t.prefix}... {t.prefix}...
</code> </code>
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
{t.name && ( {t.name && (
<span className="text-[11px] text-zinc-200 truncate"> <span className="text-[11px] text-ink truncate">
{t.name} {t.name}
</span> </span>
)} )}
<div className="text-[9px] text-zinc-500 space-x-3"> <div className="text-[9px] text-ink-soft space-x-3">
<span>Created {formatAge(t.created_at)}</span> <span>Created {formatAge(t.created_at)}</span>
{t.last_used_at && ( {t.last_used_at && (
<span>Last used {formatAge(t.last_used_at)}</span> <span>Last used {formatAge(t.last_used_at)}</span>
@ -219,7 +219,7 @@ export function OrgTokensTab() {
</div> </div>
<button <button
onClick={() => setRevokeTarget(t)} onClick={() => setRevokeTarget(t)}
className="text-[10px] text-red-400/70 hover:text-red-400 transition-colors px-2 py-1 shrink-0" className="text-[10px] text-bad/70 hover:text-bad transition-colors px-2 py-1 shrink-0"
> >
Revoke Revoke
</button> </button>

View File

@ -80,15 +80,15 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-sm font-semibold text-zinc-200">API Tokens</h3> <h3 className="text-sm font-semibold text-ink">API Tokens</h3>
<p className="text-[10px] text-zinc-500 mt-0.5"> <p className="text-[10px] text-ink-soft mt-0.5">
Bearer tokens for authenticating API calls to this workspace. Bearer tokens for authenticating API calls to this workspace.
</p> </p>
</div> </div>
<button <button
onClick={handleCreate} onClick={handleCreate}
disabled={creating} disabled={creating}
className="px-3 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-[11px] text-blue-300 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5" className="px-3 py-1.5 bg-accent-strong/20 hover:bg-accent-strong/30 border border-accent/30 rounded-lg text-[11px] text-accent font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
> >
{creating ? <><Spinner size="sm" /> Creating...</> : '+ New Token'} {creating ? <><Spinner size="sm" /> Creating...</> : '+ New Token'}
</button> </button>
@ -98,8 +98,8 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
{newToken && ( {newToken && (
<div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2"> <div className="bg-emerald-950/30 border border-emerald-800/40 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wider">New Token Created</span> <span className="text-[10px] text-good font-semibold uppercase tracking-wider">New Token Created</span>
<span className="text-[9px] text-emerald-500/70">Copy now it won't be shown again</span> <span className="text-[9px] text-good/70">Copy now it won't be shown again</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="flex-1 text-[11px] text-emerald-200 bg-emerald-950/50 px-2 py-1.5 rounded font-mono break-all select-all"> <code className="flex-1 text-[11px] text-emerald-200 bg-emerald-950/50 px-2 py-1.5 rounded font-mono break-all select-all">
@ -107,14 +107,14 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
</code> </code>
<button <button
onClick={handleCopy} onClick={handleCopy}
className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-emerald-300 transition-colors" className="shrink-0 px-2 py-1.5 bg-emerald-800/40 hover:bg-emerald-700/50 border border-emerald-700/40 rounded text-[10px] text-good transition-colors"
> >
{copied ? 'Copied' : 'Copy'} {copied ? 'Copied' : 'Copy'}
</button> </button>
</div> </div>
<button <button
onClick={() => setNewToken(null)} onClick={() => setNewToken(null)}
className="text-[9px] text-emerald-500/60 hover:text-emerald-400 transition-colors" className="text-[9px] text-good/60 hover:text-good transition-colors"
> >
Dismiss Dismiss
</button> </button>
@ -122,20 +122,20 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
)} )}
{error && ( {error && (
<div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-red-400"> <div className="px-3 py-2 bg-red-950/40 border border-red-800/50 rounded-lg text-[10px] text-bad">
{error} {error}
</div> </div>
)} )}
{/* Token list */} {/* Token list */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center gap-2 py-6 text-zinc-500 text-xs"> <div className="flex items-center justify-center gap-2 py-6 text-ink-soft text-xs">
<Spinner /> Loading tokens... <Spinner /> Loading tokens...
</div> </div>
) : tokens.length === 0 ? ( ) : tokens.length === 0 ? (
<div className="text-center py-6"> <div className="text-center py-6">
<p className="text-xs text-zinc-500">No active tokens</p> <p className="text-xs text-ink-soft">No active tokens</p>
<p className="text-[10px] text-zinc-600 mt-1"> <p className="text-[10px] text-ink-soft mt-1">
Create a token to authenticate API calls. Create a token to authenticate API calls.
</p> </p>
</div> </div>
@ -144,13 +144,13 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
{tokens.map((t) => ( {tokens.map((t) => (
<div <div
key={t.id} key={t.id}
className="flex items-center justify-between bg-zinc-800/40 border border-zinc-700/30 rounded-lg px-3 py-2" className="flex items-center justify-between bg-surface-card/40 border border-line/30 rounded-lg px-3 py-2"
> >
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<code className="text-[11px] font-mono text-zinc-300 bg-zinc-900/60 px-1.5 py-0.5 rounded"> <code className="text-[11px] font-mono text-ink-mid bg-surface-sunken/60 px-1.5 py-0.5 rounded">
{t.prefix}... {t.prefix}...
</code> </code>
<div className="text-[9px] text-zinc-500 space-x-3"> <div className="text-[9px] text-ink-soft space-x-3">
<span>Created {formatAge(t.created_at)}</span> <span>Created {formatAge(t.created_at)}</span>
{t.last_used_at && ( {t.last_used_at && (
<span>Last used {formatAge(t.last_used_at)}</span> <span>Last used {formatAge(t.last_used_at)}</span>
@ -159,7 +159,7 @@ export function TokensTab({ workspaceId }: TokensTabProps) {
</div> </div>
<button <button
onClick={() => setRevokeTarget(t)} onClick={() => setRevokeTarget(t)}
className="text-[10px] text-red-400/70 hover:text-red-400 transition-colors px-2 py-1" className="text-[10px] text-bad/70 hover:text-bad transition-colors px-2 py-1"
> >
Revoke Revoke
</button> </button>

View File

@ -24,18 +24,18 @@ const FILTERS: { id: FilterType; label: string; icon: string }[] = [
]; ];
const TYPE_COLORS: Record<string, { text: string; bg: string; border: string }> = { const TYPE_COLORS: Record<string, { text: string; bg: string; border: string }> = {
a2a_receive: { text: "text-blue-400", bg: "bg-blue-950/30", border: "border-blue-800/30" }, a2a_receive: { text: "text-accent", bg: "bg-blue-950/30", border: "border-blue-800/30" },
a2a_send: { text: "text-cyan-400", bg: "bg-cyan-950/30", border: "border-cyan-800/30" }, a2a_send: { text: "text-cyan-400", bg: "bg-cyan-950/30", border: "border-cyan-800/30" },
task_update: { text: "text-amber-400", bg: "bg-amber-950/30", border: "border-amber-800/30" }, task_update: { text: "text-warm", bg: "bg-amber-950/30", border: "border-amber-800/30" },
skill_promotion: { text: "text-violet-300", bg: "bg-violet-950/30", border: "border-violet-800/30" }, skill_promotion: { text: "text-violet-300", bg: "bg-violet-950/30", border: "border-violet-800/30" },
agent_log: { text: "text-zinc-400", bg: "bg-zinc-800/30", border: "border-zinc-700/30" }, agent_log: { text: "text-ink-mid", bg: "bg-surface-card/30", border: "border-line/30" },
error: { text: "text-red-400", bg: "bg-red-950/30", border: "border-red-800/30" }, error: { text: "text-bad", bg: "bg-red-950/30", border: "border-red-800/30" },
}; };
const STATUS_ICONS: Record<string, { icon: string; color: string }> = { const STATUS_ICONS: Record<string, { icon: string; color: string }> = {
ok: { icon: "✓", color: "text-emerald-400" }, ok: { icon: "✓", color: "text-good" },
error: { icon: "✕", color: "text-red-400" }, error: { icon: "✕", color: "text-bad" },
timeout: { icon: "⏱", color: "text-amber-400" }, timeout: { icon: "⏱", color: "text-warm" },
}; };
export function ActivityTab({ workspaceId }: Props) { export function ActivityTab({ workspaceId }: Props) {
@ -75,7 +75,7 @@ export function ActivityTab({ workspaceId }: Props) {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Filter bar */} {/* Filter bar */}
<div className="px-3 pt-3 pb-2 border-b border-zinc-800/40"> <div className="px-3 pt-3 pb-2 border-b border-line/40">
<div className="flex items-center gap-1 flex-wrap"> <div className="flex items-center gap-1 flex-wrap">
{FILTERS.map((f) => ( {FILTERS.map((f) => (
<button <button
@ -84,8 +84,8 @@ export function ActivityTab({ workspaceId }: Props) {
aria-pressed={filter === f.id} aria-pressed={filter === f.id}
className={`px-2 py-1 text-[11px] rounded-md font-medium transition-all ${ className={`px-2 py-1 text-[11px] rounded-md font-medium transition-all ${
filter === f.id filter === f.id
? "bg-zinc-700 text-zinc-100 ring-1 ring-zinc-600" ? "bg-surface-card text-ink ring-1 ring-zinc-600"
: "text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/60" : "text-ink-soft hover:text-ink-mid hover:bg-surface-card/60"
}`} }`}
> >
<span className="mr-0.5 opacity-60">{f.icon}</span> {f.label} <span className="mr-0.5 opacity-60">{f.icon}</span> {f.label}
@ -96,7 +96,7 @@ export function ActivityTab({ workspaceId }: Props) {
onClick={() => setAutoRefresh(!autoRefresh)} onClick={() => setAutoRefresh(!autoRefresh)}
aria-pressed={autoRefresh} aria-pressed={autoRefresh}
className={`text-[11px] px-1.5 py-0.5 rounded ${ className={`text-[11px] px-1.5 py-0.5 rounded ${
autoRefresh ? "text-emerald-400 bg-emerald-950/30" : "text-zinc-500" autoRefresh ? "text-good bg-emerald-950/30" : "text-ink-soft"
}`} }`}
title={autoRefresh ? "Auto-refresh ON" : "Auto-refresh OFF"} title={autoRefresh ? "Auto-refresh ON" : "Auto-refresh OFF"}
> >
@ -104,20 +104,20 @@ export function ActivityTab({ workspaceId }: Props) {
</button> </button>
<button <button
onClick={() => setTraceOpen(true)} onClick={() => setTraceOpen(true)}
className="px-2 py-1 bg-blue-900/40 hover:bg-blue-800/50 text-[11px] rounded text-blue-300 border border-blue-800/30" className="px-2 py-1 bg-blue-900/40 hover:bg-blue-800/50 text-[11px] rounded text-accent border border-blue-800/30"
title="View full conversation trace across all workspaces" title="View full conversation trace across all workspaces"
> >
Full Trace Full Trace
</button> </button>
<button <button
onClick={loadActivities} onClick={loadActivities}
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[11px] rounded text-zinc-300" className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[11px] rounded text-ink-mid"
> >
Refresh Refresh
</button> </button>
</div> </div>
</div> </div>
<div className="mt-1.5 text-[10px] text-zinc-500"> <div className="mt-1.5 text-[10px] text-ink-soft">
{activities.length} {filter === "all" ? "activities" : filter.replace("_", " ") + " entries"} {activities.length} {filter === "all" ? "activities" : filter.replace("_", " ") + " entries"}
</div> </div>
</div> </div>
@ -125,18 +125,18 @@ export function ActivityTab({ workspaceId }: Props) {
{/* Activity list */} {/* Activity list */}
<div className="flex-1 overflow-y-auto p-3 space-y-1.5"> <div className="flex-1 overflow-y-auto p-3 space-y-1.5">
{loading && activities.length === 0 && ( {loading && activities.length === 0 && (
<div className="text-xs text-zinc-500 text-center py-8">Loading activity...</div> <div className="text-xs text-ink-soft text-center py-8">Loading activity...</div>
)} )}
{error && ( {error && (
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{error} {error}
</div> </div>
)} )}
{!loading && !error && activities.length === 0 && ( {!loading && !error && activities.length === 0 && (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="text-zinc-600 text-xs">No activity recorded yet</div> <div className="text-ink-soft text-xs">No activity recorded yet</div>
<div className="text-zinc-700 text-[9px] mt-1"> <div className="text-zinc-700 text-[9px] mt-1">
Activity logs appear when agents communicate or perform tasks Activity logs appear when agents communicate or perform tasks
</div> </div>
@ -184,7 +184,7 @@ function ActivityRow({
className={`rounded-lg border transition-colors ${ className={`rounded-lg border transition-colors ${
isError isError
? "bg-red-950/20 border-red-900/30" ? "bg-red-950/20 border-red-900/30"
: "bg-zinc-800/60 border-zinc-700/40" : "bg-surface-card/60 border-line/40"
}`} }`}
> >
<button type="button" onClick={onToggle} className="w-full text-left px-3 py-2"> <button type="button" onClick={onToggle} className="w-full text-left px-3 py-2">
@ -195,7 +195,7 @@ function ActivityRow({
</span> </span>
{entry.method && ( {entry.method && (
<span className="text-[10px] font-mono text-zinc-300 truncate"> <span className="text-[10px] font-mono text-ink-mid truncate">
{entry.method} {entry.method}
</span> </span>
)} )}
@ -205,23 +205,23 @@ function ActivityRow({
</span> </span>
{entry.duration_ms != null && ( {entry.duration_ms != null && (
<span className="text-[8px] text-zinc-500 font-mono tabular-nums shrink-0"> <span className="text-[8px] text-ink-soft font-mono tabular-nums shrink-0">
{entry.duration_ms}ms {entry.duration_ms}ms
</span> </span>
)} )}
<span className="text-[8px] text-zinc-500 shrink-0"> <span className="text-[8px] text-ink-soft shrink-0">
{formatTime(entry.created_at)} {formatTime(entry.created_at)}
</span> </span>
<span className="text-[9px] text-zinc-500"> <span className="text-[9px] text-ink-soft">
{expanded ? "▼" : "▶"} {expanded ? "▼" : "▶"}
</span> </span>
</div> </div>
{/* Summary — replace raw IDs with workspace names */} {/* Summary — replace raw IDs with workspace names */}
{entry.summary && ( {entry.summary && (
<div className="text-[10px] text-zinc-400 mt-1 truncate"> <div className="text-[10px] text-ink-mid mt-1 truncate">
{entry.summary {entry.summary
.replace(entry.source_id || "", resolveName(entry.source_id)) .replace(entry.source_id || "", resolveName(entry.source_id))
.replace(entry.target_id || "", resolveName(entry.target_id))} .replace(entry.target_id || "", resolveName(entry.target_id))}
@ -236,9 +236,9 @@ function ActivityRow({
{resolveName(entry.source_id)} {resolveName(entry.source_id)}
</span> </span>
)} )}
<span className="text-[9px] text-zinc-500"></span> <span className="text-[9px] text-ink-soft"></span>
{entry.target_id && ( {entry.target_id && (
<span className="text-[9px] text-blue-400/80 truncate max-w-[140px]" title={entry.target_id}> <span className="text-[9px] text-accent/80 truncate max-w-[140px]" title={entry.target_id}>
{resolveName(entry.target_id)} {resolveName(entry.target_id)}
</span> </span>
)} )}
@ -247,7 +247,7 @@ function ActivityRow({
{/* Error detail */} {/* Error detail */}
{isError && entry.error_detail && ( {isError && entry.error_detail && (
<div className="text-[9px] text-red-400/80 mt-1 truncate"> <div className="text-[9px] text-bad/80 mt-1 truncate">
{entry.error_detail} {entry.error_detail}
</div> </div>
)} )}
@ -255,7 +255,7 @@ function ActivityRow({
{/* Expanded details */} {/* Expanded details */}
{expanded && ( {expanded && (
<div className="px-3 pb-3 space-y-2 border-t border-zinc-700/30 mt-1 pt-2"> <div className="px-3 pb-3 space-y-2 border-t border-line/30 mt-1 pt-2">
{entry.source_id && ( {entry.source_id && (
<Detail label="Source" value={`${resolveName(entry.source_id)} (${entry.source_id.slice(0, 8)})`} /> <Detail label="Source" value={`${resolveName(entry.source_id)} (${entry.source_id.slice(0, 8)})`} />
)} )}
@ -278,7 +278,7 @@ function ActivityRow({
{entry.response_body && ( {entry.response_body && (
<JsonBlock label="Response" data={entry.response_body} /> <JsonBlock label="Response" data={entry.response_body} />
)} )}
<div className="text-[8px] text-zinc-500 font-mono select-all"> <div className="text-[8px] text-ink-soft font-mono select-all">
ID: {entry.id} ID: {entry.id}
</div> </div>
</div> </div>
@ -298,10 +298,10 @@ function A2AErrorPreview({ label, raw }: { label: string; raw: string }) {
const hint = inferA2AErrorHint(detail); const hint = inferA2AErrorHint(detail);
return ( return (
<div> <div>
<div className="text-[8px] text-red-400/80 uppercase tracking-wider mb-1">{label} delivery failed</div> <div className="text-[8px] text-bad/80 uppercase tracking-wider mb-1">{label} delivery failed</div>
<div className="text-[10px] text-red-300 bg-red-950/30 border border-red-800/40 rounded p-2 space-y-1.5"> <div className="text-[10px] text-bad bg-red-950/30 border border-red-800/40 rounded p-2 space-y-1.5">
<div className="font-mono whitespace-pre-wrap break-words max-h-32 overflow-y-auto">{detail}</div> <div className="font-mono whitespace-pre-wrap break-words max-h-32 overflow-y-auto">{detail}</div>
<div className="text-[9px] text-red-300/70 leading-relaxed border-t border-red-800/30 pt-1.5">{hint}</div> <div className="text-[9px] text-bad/70 leading-relaxed border-t border-red-800/30 pt-1.5">{hint}</div>
</div> </div>
</div> </div>
); );
@ -326,8 +326,8 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
} }
return ( return (
<div> <div>
<div className="text-[8px] text-zinc-500 uppercase tracking-wider mb-1">{label}</div> <div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
<div className="text-[10px] text-zinc-300 bg-zinc-900/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words"> <div className="text-[10px] text-ink-mid bg-surface-sunken/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words">
{text.slice(0, 2000)} {text.slice(0, 2000)}
</div> </div>
</div> </div>
@ -369,8 +369,8 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
return ( return (
<div> <div>
<div className="text-[8px] text-zinc-500 uppercase tracking-wider mb-1">{label}</div> <div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
<div className="text-[10px] text-zinc-300 bg-zinc-900/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words"> <div className="text-[10px] text-ink-mid bg-surface-sunken/60 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words">
{text.slice(0, 2000)} {text.slice(0, 2000)}
</div> </div>
</div> </div>
@ -380,8 +380,8 @@ function MessagePreview({ label, body }: { label: string; body: Record<string, u
function Detail({ label, value, mono, error: isError }: { label: string; value: string; mono?: boolean; error?: boolean }) { function Detail({ label, value, mono, error: isError }: { label: string; value: string; mono?: boolean; error?: boolean }) {
return ( return (
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className="text-[8px] text-zinc-500 uppercase tracking-wider w-14 shrink-0 pt-0.5">{label}</span> <span className="text-[8px] text-ink-soft uppercase tracking-wider w-14 shrink-0 pt-0.5">{label}</span>
<span className={`text-[9px] break-all ${isError ? "text-red-400" : "text-zinc-300"} ${mono ? "font-mono" : ""}`}> <span className={`text-[9px] break-all ${isError ? "text-bad" : "text-ink-mid"} ${mono ? "font-mono" : ""}`}>
{value} {value}
</span> </span>
</div> </div>
@ -391,8 +391,8 @@ function Detail({ label, value, mono, error: isError }: { label: string; value:
function JsonBlock({ label, data }: { label: string; data: Record<string, unknown> }) { function JsonBlock({ label, data }: { label: string; data: Record<string, unknown> }) {
return ( return (
<div> <div>
<div className="text-[8px] text-zinc-500 uppercase tracking-wider mb-1">{label}</div> <div className="text-[8px] text-ink-soft uppercase tracking-wider mb-1">{label}</div>
<pre className="text-[9px] text-zinc-300 bg-zinc-900/80 rounded p-2 overflow-x-auto max-h-48 font-mono"> <pre className="text-[9px] text-ink-mid bg-surface-sunken/80 rounded p-2 overflow-x-auto max-h-48 font-mono">
{JSON.stringify(data, null, 2)} {JSON.stringify(data, null, 2)}
</pre> </pre>
</div> </div>

View File

@ -116,10 +116,10 @@ export function BudgetSection({ workspaceId }: Props) {
<div className="space-y-3" data-testid="budget-section"> <div className="space-y-3" data-testid="budget-section">
{/* Section header */} {/* Section header */}
<div> <div>
<h3 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider"> <h3 className="text-xs font-semibold text-ink-mid uppercase tracking-wider">
Budget Budget
</h3> </h3>
<p className="text-[11px] text-zinc-400 mt-0.5"> <p className="text-[11px] text-ink-mid mt-0.5">
Limit total message credits for this workspace Limit total message credits for this workspace
</p> </p>
</div> </div>
@ -129,7 +129,7 @@ export function BudgetSection({ workspaceId }: Props) {
<div <div
role="alert" role="alert"
data-testid="budget-exceeded-banner" data-testid="budget-exceeded-banner"
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-zinc-950 border border-amber-700/50 text-amber-400 text-xs font-medium" className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface border border-amber-700/50 text-warm text-xs font-medium"
> >
<svg <svg
width="13" width="13"
@ -158,21 +158,21 @@ export function BudgetSection({ workspaceId }: Props) {
{/* Usage stats */} {/* Usage stats */}
{loading ? ( {loading ? (
<p className="text-xs text-zinc-500" data-testid="budget-loading"> <p className="text-xs text-ink-soft" data-testid="budget-loading">
Loading Loading
</p> </p>
) : fetchError ? ( ) : fetchError ? (
<p className="text-xs text-red-400" data-testid="budget-fetch-error"> <p className="text-xs text-bad" data-testid="budget-fetch-error">
{fetchError} {fetchError}
</p> </p>
) : budget ? ( ) : budget ? (
<div className="space-y-2"> <div className="space-y-2">
{/* Stats row */} {/* Stats row */}
<div className="flex items-baseline justify-between" data-testid="budget-stats-row"> <div className="flex items-baseline justify-between" data-testid="budget-stats-row">
<span className="text-xs text-zinc-400">Credits used</span> <span className="text-xs text-ink-mid">Credits used</span>
<span className="text-xs font-mono text-zinc-300"> <span className="text-xs font-mono text-ink-mid">
<span data-testid="budget-used-value">{(budget.budget_used ?? 0).toLocaleString()}</span> <span data-testid="budget-used-value">{(budget.budget_used ?? 0).toLocaleString()}</span>
<span className="text-zinc-500 mx-1">/</span> <span className="text-ink-soft mx-1">/</span>
<span data-testid="budget-limit-value"> <span data-testid="budget-limit-value">
{budget.budget_limit != null {budget.budget_limit != null
? budget.budget_limit.toLocaleString() ? budget.budget_limit.toLocaleString()
@ -189,11 +189,11 @@ export function BudgetSection({ workspaceId }: Props) {
aria-valuenow={progressPct} aria-valuenow={progressPct}
aria-valuemin={0} aria-valuemin={0}
aria-valuemax={100} aria-valuemax={100}
className="h-1.5 w-full rounded-full bg-zinc-800 overflow-hidden" className="h-1.5 w-full rounded-full bg-surface-card overflow-hidden"
> >
<div <div
data-testid="budget-progress-fill" data-testid="budget-progress-fill"
className="h-full rounded-full bg-blue-500 transition-all duration-300" className="h-full rounded-full bg-accent transition-all duration-300"
style={{ width: `${progressPct}%` }} style={{ width: `${progressPct}%` }}
/> />
</div> </div>
@ -201,7 +201,7 @@ export function BudgetSection({ workspaceId }: Props) {
{/* Remaining credits */} {/* Remaining credits */}
{budget.budget_remaining != null && ( {budget.budget_remaining != null && (
<p className="text-[11px] text-zinc-500" data-testid="budget-remaining"> <p className="text-[11px] text-ink-soft" data-testid="budget-remaining">
{budget.budget_remaining.toLocaleString()} credits remaining {budget.budget_remaining.toLocaleString()} credits remaining
</p> </p>
)} )}
@ -212,7 +212,7 @@ export function BudgetSection({ workspaceId }: Props) {
<div className="space-y-1.5 pt-1"> <div className="space-y-1.5 pt-1">
<label <label
htmlFor={`budget-limit-input-${workspaceId}`} htmlFor={`budget-limit-input-${workspaceId}`}
className="text-[11px] text-zinc-400 block" className="text-[11px] text-ink-mid block"
> >
Budget limit (credits) Budget limit (credits)
</label> </label>
@ -225,15 +225,15 @@ export function BudgetSection({ workspaceId }: Props) {
onChange={(e) => setLimitInput(e.target.value)} onChange={(e) => setLimitInput(e.target.value)}
placeholder="e.g. 1000 — blank for unlimited" placeholder="e.g. 1000 — blank for unlimited"
data-testid="budget-limit-input" data-testid="budget-limit-input"
className="w-full bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-sm text-zinc-300 placeholder-zinc-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-colors" className="w-full bg-surface-card border border-line rounded-lg px-3 py-2 text-sm text-ink-mid placeholder-zinc-500 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-colors"
/> />
<p className="text-xs text-zinc-500">Leave blank for unlimited</p> <p className="text-xs text-ink-soft">Leave blank for unlimited</p>
{saveError && ( {saveError && (
<div <div
role="alert" role="alert"
data-testid="budget-save-error" data-testid="budget-save-error"
className="px-3 py-1.5 rounded-lg bg-red-950/40 border border-red-800/50 text-xs text-red-400" className="px-3 py-1.5 rounded-lg bg-red-950/40 border border-red-800/50 text-xs text-bad"
> >
{saveError} {saveError}
</div> </div>
@ -243,7 +243,7 @@ export function BudgetSection({ workspaceId }: Props) {
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
data-testid="budget-save-btn" data-testid="budget-save-btn"
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-500 active:bg-blue-700 rounded-lg text-xs font-medium text-white disabled:opacity-50 transition-colors" className="px-4 py-1.5 bg-accent-strong hover:bg-accent active:bg-accent-strong rounded-lg text-xs font-medium text-ink disabled:opacity-50 transition-colors"
> >
{saving ? "Saving…" : "Save"} {saving ? "Saving…" : "Save"}
</button> </button>

View File

@ -242,7 +242,7 @@ export function ChannelsTab({ workspaceId }: Props) {
if (loading) { if (loading) {
return ( return (
<div className="p-4 text-zinc-500 text-xs">Loading channels...</div> <div className="p-4 text-ink-soft text-xs">Loading channels...</div>
); );
} }
@ -250,33 +250,33 @@ export function ChannelsTab({ workspaceId }: Props) {
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-zinc-300 tracking-wide uppercase"> <h3 className="text-xs font-semibold text-ink-mid tracking-wide uppercase">
Channels Channels
</h3> </h3>
<button <button
onClick={() => setShowForm(!showForm)} onClick={() => setShowForm(!showForm)}
className="text-[10px] px-2.5 py-1 rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 transition" className="text-[10px] px-2.5 py-1 rounded bg-accent-strong/20 text-accent hover:bg-accent-strong/30 transition"
> >
{showForm ? "Cancel" : "+ Connect"} {showForm ? "Cancel" : "+ Connect"}
</button> </button>
</div> </div>
{error && ( {error && (
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{error} {error}
</div> </div>
)} )}
{/* Create form — schema-driven */} {/* Create form — schema-driven */}
{showForm && ( {showForm && (
<div className="space-y-2 p-3 bg-zinc-800/40 rounded border border-zinc-700/50"> <div className="space-y-2 p-3 bg-surface-card/40 rounded border border-line/50">
<div> <div>
<label htmlFor={platformId} className="text-[10px] text-zinc-500 block mb-1">Platform</label> <label htmlFor={platformId} className="text-[10px] text-ink-soft block mb-1">Platform</label>
<select <select
id={platformId} id={platformId}
value={formType} value={formType}
onChange={(e) => setFormType(e.target.value)} onChange={(e) => setFormType(e.target.value)}
className="w-full text-xs bg-zinc-900 border border-zinc-700 rounded px-2 py-1.5 text-zinc-300" className="w-full text-xs bg-surface-sunken border border-line rounded px-2 py-1.5 text-ink-mid"
> >
{adapters.map((a) => ( {adapters.map((a) => (
<option key={a.type} value={a.type}>{a.display_name}</option> <option key={a.type} value={a.type}>{a.display_name}</option>
@ -308,7 +308,7 @@ export function ChannelsTab({ workspaceId }: Props) {
<button <button
onClick={handleDiscover} onClick={handleDiscover}
disabled={discovering || !formValues["bot_token"]} disabled={discovering || !formValues["bot_token"]}
className="text-[10px] px-2 py-0.5 rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 transition disabled:opacity-40" className="text-[10px] px-2 py-0.5 rounded bg-accent-strong/20 text-accent hover:bg-accent-strong/30 transition disabled:opacity-40"
> >
{discovering ? "Detecting..." : "Detect Chats"} {discovering ? "Detecting..." : "Detect Chats"}
</button> </button>
@ -318,21 +318,21 @@ export function ChannelsTab({ workspaceId }: Props) {
{discoveredChats.map((chat) => ( {discoveredChats.map((chat) => (
<label <label
key={chat.chat_id} key={chat.chat_id}
className="flex items-center gap-2 px-2 py-1.5 bg-zinc-900/50 rounded border border-zinc-700/50 cursor-pointer hover:bg-zinc-800/50" className="flex items-center gap-2 px-2 py-1.5 bg-surface-sunken/50 rounded border border-line/50 cursor-pointer hover:bg-surface-card/50"
> >
<input <input
type="checkbox" type="checkbox"
checked={selectedChats.has(chat.chat_id)} checked={selectedChats.has(chat.chat_id)}
onChange={() => toggleChat(chat.chat_id)} onChange={() => toggleChat(chat.chat_id)}
className="rounded border-zinc-600" className="rounded border-line"
/> />
<span className="text-xs text-zinc-300">{chat.name || "Unknown"}</span> <span className="text-xs text-ink-mid">{chat.name || "Unknown"}</span>
<span className="text-[10px] text-zinc-500 ml-auto">{chat.type} {chat.chat_id}</span> <span className="text-[10px] text-ink-soft ml-auto">{chat.type} {chat.chat_id}</span>
</label> </label>
))} ))}
<button <button
onClick={() => setShowManualInput(!showManualInput)} onClick={() => setShowManualInput(!showManualInput)}
className="text-[10px] text-blue-400 hover:underline" className="text-[10px] text-accent hover:underline"
> >
{showManualInput ? "hide manual input" : "edit manually"} {showManualInput ? "hide manual input" : "edit manually"}
</button> </button>
@ -347,26 +347,26 @@ export function ChannelsTab({ workspaceId }: Props) {
)} )}
<div> <div>
<label htmlFor={allowedUsersId} className="text-[10px] text-zinc-500 block mb-1"> <label htmlFor={allowedUsersId} className="text-[10px] text-ink-soft block mb-1">
Allowed Users <span className="text-zinc-600">(optional, comma-separated)</span> Allowed Users <span className="text-ink-soft">(optional, comma-separated)</span>
</label> </label>
<input <input
id={allowedUsersId} id={allowedUsersId}
value={formAllowedUsers} value={formAllowedUsers}
onChange={(e) => setFormAllowedUsers(e.target.value)} onChange={(e) => setFormAllowedUsers(e.target.value)}
placeholder="123456789, 987654321" placeholder="123456789, 987654321"
className="w-full text-xs bg-zinc-900 border border-zinc-700 rounded px-2 py-1.5 text-zinc-300 placeholder-zinc-600" className="w-full text-xs bg-surface-sunken border border-line rounded px-2 py-1.5 text-ink-mid placeholder-zinc-600"
/> />
<p className="text-[11px] text-zinc-500 mt-0.5"> <p className="text-[11px] text-ink-soft mt-0.5">
Platform-specific user IDs. Leave empty to allow everyone. Platform-specific user IDs. Leave empty to allow everyone.
</p> </p>
</div> </div>
{formError && ( {formError && (
<p className="text-[10px] text-red-400">{formError}</p> <p className="text-[10px] text-bad">{formError}</p>
)} )}
<button <button
onClick={handleCreate} onClick={handleCreate}
className="w-full text-xs py-1.5 rounded bg-blue-600 hover:bg-blue-500 text-white transition" className="w-full text-xs py-1.5 rounded bg-accent-strong hover:bg-accent text-ink transition"
> >
Connect Channel Connect Channel
</button> </button>
@ -376,8 +376,8 @@ export function ChannelsTab({ workspaceId }: Props) {
{/* Channel list */} {/* Channel list */}
{channels.length === 0 && !showForm && ( {channels.length === 0 && !showForm && (
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-zinc-500 text-xs">No channels connected</p> <p className="text-ink-soft text-xs">No channels connected</p>
<p className="text-zinc-600 text-[10px] mt-1"> <p className="text-ink-soft text-[10px] mt-1">
Connect Telegram, Slack, Discord, or Lark / Feishu to chat with this agent from social platforms. Connect Telegram, Slack, Discord, or Lark / Feishu to chat with this agent from social platforms.
</p> </p>
</div> </div>
@ -386,7 +386,7 @@ export function ChannelsTab({ workspaceId }: Props) {
{channels.map((ch) => ( {channels.map((ch) => (
<div <div
key={ch.id} key={ch.id}
className="p-3 bg-zinc-800/30 rounded border border-zinc-700/40 space-y-2" className="p-3 bg-surface-card/30 rounded border border-line/40 space-y-2"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -395,10 +395,10 @@ export function ChannelsTab({ workspaceId }: Props) {
ch.enabled ? "bg-emerald-500" : "bg-zinc-600" ch.enabled ? "bg-emerald-500" : "bg-zinc-600"
}`} }`}
/> />
<span className="text-xs font-medium text-zinc-200"> <span className="text-xs font-medium text-ink">
{ch.channel_type.charAt(0).toUpperCase() + ch.channel_type.slice(1)} {ch.channel_type.charAt(0).toUpperCase() + ch.channel_type.slice(1)}
</span> </span>
<span className="text-[10px] text-zinc-500"> <span className="text-[10px] text-ink-soft">
{ch.config.chat_id || ch.config.channel_id || ""} {ch.config.chat_id || ch.config.channel_id || ""}
</span> </span>
</div> </div>
@ -406,7 +406,7 @@ export function ChannelsTab({ workspaceId }: Props) {
<button <button
onClick={() => handleTest(ch)} onClick={() => handleTest(ch)}
disabled={testing === ch.id} disabled={testing === ch.id}
className="text-[10px] px-2 py-0.5 rounded bg-zinc-700/50 text-zinc-400 hover:text-zinc-200 transition disabled:opacity-50" className="text-[10px] px-2 py-0.5 rounded bg-surface-card/50 text-ink-mid hover:text-ink transition disabled:opacity-50"
> >
{testing === ch.id ? "Sent!" : "Test"} {testing === ch.id ? "Sent!" : "Test"}
</button> </button>
@ -414,21 +414,21 @@ export function ChannelsTab({ workspaceId }: Props) {
onClick={() => handleToggle(ch)} onClick={() => handleToggle(ch)}
className={`text-[10px] px-2 py-0.5 rounded transition ${ className={`text-[10px] px-2 py-0.5 rounded transition ${
ch.enabled ch.enabled
? "bg-emerald-900/30 text-emerald-400 hover:bg-emerald-900/50" ? "bg-emerald-900/30 text-good hover:bg-emerald-900/50"
: "bg-zinc-700/50 text-zinc-500 hover:text-zinc-300" : "bg-surface-card/50 text-ink-soft hover:text-ink-mid"
}`} }`}
> >
{ch.enabled ? "On" : "Off"} {ch.enabled ? "On" : "Off"}
</button> </button>
<button <button
onClick={() => setPendingDelete(ch)} onClick={() => setPendingDelete(ch)}
className="text-[10px] px-2 py-0.5 rounded bg-red-900/20 text-red-400 hover:bg-red-900/40 transition" className="text-[10px] px-2 py-0.5 rounded bg-red-900/20 text-bad hover:bg-red-900/40 transition"
> >
Remove Remove
</button> </button>
</div> </div>
</div> </div>
<div className="flex items-center gap-4 text-[10px] text-zinc-500"> <div className="flex items-center gap-4 text-[10px] text-ink-soft">
<span>{ch.message_count} messages</span> <span>{ch.message_count} messages</span>
<span>Last: {relativeTime(ch.last_message_at)}</span> <span>Last: {relativeTime(ch.last_message_at)}</span>
{ch.allowed_users.length > 0 && ( {ch.allowed_users.length > 0 && (
@ -467,12 +467,12 @@ function SchemaField({
}) { }) {
const inputId = useId(); const inputId = useId();
const common = const common =
"w-full text-xs bg-zinc-900 border border-zinc-700 rounded px-2 py-1.5 text-zinc-300 placeholder-zinc-600"; "w-full text-xs bg-surface-sunken border border-line rounded px-2 py-1.5 text-ink-mid placeholder-zinc-600";
return ( return (
<div> <div>
<label htmlFor={inputId} className="text-[10px] text-zinc-500 block mb-1"> <label htmlFor={inputId} className="text-[10px] text-ink-soft block mb-1">
{field.label} {field.label}
{!field.required && <span className="text-zinc-600"> (optional)</span>} {!field.required && <span className="text-ink-soft"> (optional)</span>}
</label> </label>
{field.type === "textarea" ? ( {field.type === "textarea" ? (
<textarea <textarea
@ -495,7 +495,7 @@ function SchemaField({
)} )}
{renderExtras?.()} {renderExtras?.()}
{field.help && ( {field.help && (
<p className="text-[11px] text-zinc-500 mt-0.5">{field.help}</p> <p className="text-[11px] text-ink-soft mt-0.5">{field.help}</p>
)} )}
</div> </div>
); );

View File

@ -162,7 +162,7 @@ export function ChatTab({ workspaceId, data }: Props) {
{/* Sub-tab bar — role="tablist" so screen readers expose tab context */} {/* Sub-tab bar — role="tablist" so screen readers expose tab context */}
<div <div
role="tablist" role="tablist"
className="flex border-b border-zinc-800/40 bg-zinc-900/30 px-2 shrink-0" className="flex border-b border-line/40 bg-surface-sunken/30 px-2 shrink-0"
onKeyDown={(e) => { onKeyDown={(e) => {
const tabs: ChatSubTab[] = ["my-chat", "agent-comms"]; const tabs: ChatSubTab[] = ["my-chat", "agent-comms"];
const idx = tabs.indexOf(subTab); const idx = tabs.indexOf(subTab);
@ -179,8 +179,8 @@ export function ChatTab({ workspaceId, data }: Props) {
onClick={() => setSubTab("my-chat")} onClick={() => setSubTab("my-chat")}
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${ className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
subTab === "my-chat" subTab === "my-chat"
? "text-zinc-200 border-b-2 border-blue-500" ? "text-ink border-b-2 border-accent"
: "text-zinc-500 hover:text-zinc-300" : "text-ink-soft hover:text-ink-mid"
}`} }`}
> >
My Chat My Chat
@ -194,8 +194,8 @@ export function ChatTab({ workspaceId, data }: Props) {
onClick={() => setSubTab("agent-comms")} onClick={() => setSubTab("agent-comms")}
className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${ className={`px-3 py-1.5 text-[10px] font-medium transition-colors ${
subTab === "agent-comms" subTab === "agent-comms"
? "text-zinc-200 border-b-2 border-blue-500" ? "text-ink border-b-2 border-accent"
: "text-zinc-500 hover:text-zinc-300" : "text-ink-soft hover:text-ink-mid"
}`} }`}
> >
Agent Comms Agent Comms
@ -726,10 +726,10 @@ function MyChatPanel({ workspaceId, data }: Props) {
> >
{dragOver && ( {dragOver && (
<div <div
className="absolute inset-0 z-20 flex items-center justify-center bg-blue-500/10 border-2 border-dashed border-blue-400 rounded pointer-events-none" className="absolute inset-0 z-20 flex items-center justify-center bg-accent/10 border-2 border-dashed border-blue-400 rounded pointer-events-none"
aria-live="polite" aria-live="polite"
> >
<div className="bg-zinc-900/90 border border-blue-400/50 rounded-lg px-4 py-2 text-xs text-blue-200"> <div className="bg-surface-sunken/90 border border-blue-400/50 rounded-lg px-4 py-2 text-xs text-blue-200">
Drop to attach Drop to attach
</div> </div>
</div> </div>
@ -737,14 +737,14 @@ function MyChatPanel({ workspaceId, data }: Props) {
{/* Messages */} {/* Messages */}
<div className="flex-1 overflow-y-auto p-3 space-y-3"> <div className="flex-1 overflow-y-auto p-3 space-y-3">
{loading && ( {loading && (
<div className="text-xs text-zinc-500 text-center py-4">Loading chat history...</div> <div className="text-xs text-ink-soft text-center py-4">Loading chat history...</div>
)} )}
{!loading && loadError !== null && messages.length === 0 && ( {!loading && loadError !== null && messages.length === 0 && (
<div <div
role="alert" role="alert"
className="mx-2 mt-2 rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2.5" className="mx-2 mt-2 rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2.5"
> >
<p className="text-[11px] text-red-400 mb-1.5"> <p className="text-[11px] text-bad mb-1.5">
Failed to load chat history: {loadError} Failed to load chat history: {loadError}
</p> </p>
<button <button
@ -757,14 +757,14 @@ function MyChatPanel({ workspaceId, data }: Props) {
setLoading(false); setLoading(false);
}); });
}} }}
className="text-[10px] px-2 py-0.5 rounded bg-red-800/40 text-red-300 hover:bg-red-700/50 transition-colors" className="text-[10px] px-2 py-0.5 rounded bg-red-800/40 text-bad hover:bg-red-700/50 transition-colors"
> >
Retry Retry
</button> </button>
</div> </div>
)} )}
{!loading && loadError === null && messages.length === 0 && ( {!loading && loadError === null && messages.length === 0 && (
<div className="text-xs text-zinc-500 text-center py-8"> <div className="text-xs text-ink-soft text-center py-8">
No messages yet. Send a message to start chatting with this agent. No messages yet. Send a message to start chatting with this agent.
</div> </div>
)} )}
@ -773,10 +773,10 @@ function MyChatPanel({ workspaceId, data }: Props) {
<div <div
className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${ className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${
msg.role === "user" msg.role === "user"
? "bg-blue-600/30 text-blue-100 border border-blue-500/20" ? "bg-accent-strong/30 text-blue-100 border border-accent/20"
: msg.role === "system" : msg.role === "system"
? "bg-red-900/30 text-red-200 border border-red-800/30" ? "bg-red-900/30 text-red-200 border border-red-800/30"
: "bg-zinc-800/80 text-zinc-200 border border-zinc-700/30" : "bg-surface-card/80 text-ink border border-line/30"
}`} }`}
> >
{msg.content && ( {msg.content && (
@ -796,7 +796,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
))} ))}
</div> </div>
)} )}
<div className="text-[9px] text-zinc-500 mt-1"> <div className="text-[9px] text-ink-soft mt-1">
{new Date(msg.timestamp).toLocaleTimeString()} {new Date(msg.timestamp).toLocaleTimeString()}
</div> </div>
</div> </div>
@ -809,8 +809,8 @@ function MyChatPanel({ workspaceId, data }: Props) {
without locking the Send button on a stale currentTask). */} without locking the Send button on a stale currentTask). */}
{(sending || !!data.currentTask) && ( {(sending || !!data.currentTask) && (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-zinc-800/50 border border-zinc-700/30 rounded-lg px-3 py-2 max-w-[85%]"> <div className="bg-surface-card/50 border border-line/30 rounded-lg px-3 py-2 max-w-[85%]">
<div className="flex items-center gap-2 text-xs text-zinc-400"> <div className="flex items-center gap-2 text-xs text-ink-mid">
<span className="flex gap-0.5"> <span className="flex gap-0.5">
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "0ms" }} /> <span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "0ms" }} />
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "150ms" }} /> <span className="w-1.5 h-1.5 bg-zinc-500 rounded-full motion-safe:animate-bounce" style={{ animationDelay: "150ms" }} />
@ -819,10 +819,10 @@ function MyChatPanel({ workspaceId, data }: Props) {
{thinkingElapsed}s {thinkingElapsed}s
</div> </div>
{activityLog.length > 0 && ( {activityLog.length > 0 && (
<div className="mt-1.5 text-[9px] text-zinc-500 space-y-0.5"> <div className="mt-1.5 text-[9px] text-ink-soft space-y-0.5">
<div className="text-zinc-400">Processing with {runtimeDisplayName(data.runtime)}...</div> <div className="text-ink-mid">Processing with {runtimeDisplayName(data.runtime)}...</div>
{activityLog.map((line, i) => ( {activityLog.map((line, i) => (
<div key={line + i} className="pl-2 border-l border-zinc-700"> {line}</div> <div key={line + i} className="pl-2 border-l border-line"> {line}</div>
))} ))}
</div> </div>
)} )}
@ -836,11 +836,11 @@ function MyChatPanel({ workspaceId, data }: Props) {
{error && ( {error && (
<div className="px-3 py-2 bg-red-900/20 border-t border-red-800/30"> <div className="px-3 py-2 bg-red-900/20 border-t border-red-800/30">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[10px] text-red-400">{error}</span> <span className="text-[10px] text-bad">{error}</span>
{!isOnline && ( {!isOnline && (
<button <button
onClick={() => setConfirmRestart(true)} onClick={() => setConfirmRestart(true)}
className="text-[11px] px-2 py-0.5 bg-red-800/40 text-red-300 rounded hover:bg-red-700/50" className="text-[11px] px-2 py-0.5 bg-red-800/40 text-bad rounded hover:bg-red-700/50"
> >
Restart Restart
</button> </button>
@ -850,7 +850,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
)} )}
{/* Input */} {/* Input */}
<div className="p-3 border-t border-zinc-800"> <div className="p-3 border-t border-line">
{pendingFiles.length > 0 && ( {pendingFiles.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-2"> <div className="flex flex-wrap gap-1.5 mb-2">
{pendingFiles.map((f, i) => ( {pendingFiles.map((f, i) => (
@ -876,7 +876,7 @@ function MyChatPanel({ workspaceId, data }: Props) {
disabled={!agentReachable || sending || uploading} disabled={!agentReachable || sending || uploading}
aria-label="Attach file" aria-label="Attach file"
title="Attach file" title="Attach file"
className="p-2 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg text-zinc-400 hover:text-zinc-200 transition-colors shrink-0 disabled:opacity-40" className="p-2 bg-surface-card hover:bg-surface-card border border-line rounded-lg text-ink-mid hover:text-ink transition-colors shrink-0 disabled:opacity-40"
> >
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M11 6.5 7 10.5a2 2 0 1 0 2.8 2.8l4-4a3.5 3.5 0 0 0-5-5l-4.5 4.5a5 5 0 0 0 7 7l4-4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /> <path d="M11 6.5 7 10.5a2 2 0 1 0 2.8 2.8l4-4a3.5 3.5 0 0 0-5-5l-4.5 4.5a5 5 0 0 0 7 7l4-4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
@ -896,12 +896,12 @@ function MyChatPanel({ workspaceId, data }: Props) {
placeholder={agentReachable ? "Send a message... (Shift+Enter for new line, paste images to attach)" : `Agent is ${data.status}`} placeholder={agentReachable ? "Send a message... (Shift+Enter for new line, paste images to attach)" : `Agent is ${data.status}`}
disabled={!agentReachable || sending} disabled={!agentReachable || sending}
rows={1} rows={1}
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-xs text-zinc-200 placeholder-zinc-500 focus:outline-none focus:border-blue-500 resize-none disabled:opacity-50" className="flex-1 bg-surface-card border border-line rounded-lg px-3 py-2 text-xs text-ink placeholder-zinc-500 focus:outline-none focus:border-accent resize-none disabled:opacity-50"
/> />
<button <button
onClick={sendMessage} onClick={sendMessage}
disabled={(!input.trim() && pendingFiles.length === 0) || !agentReachable || sending || uploading} disabled={(!input.trim() && pendingFiles.length === 0) || !agentReachable || sending || uploading}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-xs font-medium rounded-lg text-white disabled:opacity-30 transition-colors shrink-0" className="px-4 py-2 bg-accent-strong hover:bg-accent text-xs font-medium rounded-lg text-ink disabled:opacity-30 transition-colors shrink-0"
> >
{uploading ? "Uploading…" : "Send"} {uploading ? "Uploading…" : "Send"}
</button> </button>

View File

@ -53,37 +53,37 @@ function AgentCardSection({ workspaceId }: { workspaceId: string }) {
return ( return (
<Section title="Agent Card" defaultOpen={false}> <Section title="Agent Card" defaultOpen={false}>
{loading ? ( {loading ? (
<div className="text-[10px] text-zinc-500">Loading...</div> <div className="text-[10px] text-ink-soft">Loading...</div>
) : editing ? ( ) : editing ? (
<div className="space-y-2"> <div className="space-y-2">
<textarea <textarea
aria-label="Agent card JSON editor" aria-label="Agent card JSON editor"
value={draft} onChange={(e) => setDraft(e.target.value)} value={draft} onChange={(e) => setDraft(e.target.value)}
spellCheck={false} rows={12} spellCheck={false} rows={12}
className="w-full bg-zinc-800 border border-zinc-700 rounded p-2 text-[10px] font-mono text-zinc-200 focus:outline-none focus:border-blue-500 resize-none" className="w-full bg-surface-card border border-line rounded p-2 text-[10px] font-mono text-ink focus:outline-none focus:border-accent resize-none"
/> />
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-red-400">{error}</div>} {error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-bad">{error}</div>}
<div className="flex gap-2"> <div className="flex gap-2">
<button type="button" onClick={handleSave} disabled={saving} <button type="button" onClick={handleSave} disabled={saving}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-50"> className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-50">
{saving ? "Saving..." : "Save"} {saving ? "Saving..." : "Save"}
</button> </button>
<button type="button" onClick={() => setEditing(false)} <button type="button" onClick={() => setEditing(false)}
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button> className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
</div> </div>
</div> </div>
) : ( ) : (
<div> <div>
{card ? ( {card ? (
<pre className="text-[9px] text-zinc-400 bg-zinc-800/50 rounded p-2 overflow-x-auto max-h-48 border border-zinc-700/50"> <pre className="text-[9px] text-ink-mid bg-surface-card/50 rounded p-2 overflow-x-auto max-h-48 border border-line/50">
{JSON.stringify(card, null, 2)} {JSON.stringify(card, null, 2)}
</pre> </pre>
) : ( ) : (
<div className="text-[10px] text-zinc-500">No agent card</div> <div className="text-[10px] text-ink-soft">No agent card</div>
)} )}
{success && <div className="mt-2 px-2 py-1 bg-green-900/30 border border-green-800 rounded text-[10px] text-green-400">Updated</div>} {success && <div className="mt-2 px-2 py-1 bg-green-900/30 border border-green-800 rounded text-[10px] text-good">Updated</div>}
<button type="button" onClick={() => { setDraft(JSON.stringify(card || {}, null, 2)); setEditing(true); setError(null); setSuccess(false); }} <button type="button" onClick={() => { setDraft(JSON.stringify(card || {}, null, 2)); setEditing(true); setError(null); setSuccess(false); }}
className="mt-2 text-[10px] text-blue-400 hover:text-blue-300">Edit Agent Card</button> className="mt-2 text-[10px] text-accent hover:text-accent">Edit Agent Card</button>
</div> </div>
)} )}
</Section> </Section>
@ -592,16 +592,16 @@ export function ConfigTab({ workspaceId }: Props) {
const isDirty = (rawMode ? rawDraft !== originalYaml : toYaml(config) !== originalYaml) || providerDirty; const isDirty = (rawMode ? rawDraft !== originalYaml : toYaml(config) !== originalYaml) || providerDirty;
if (loading) { if (loading) {
return <div className="p-4 text-xs text-zinc-500">Loading config...</div>; return <div className="p-4 text-xs text-ink-soft">Loading config...</div>;
} }
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Mode toggle */} {/* Mode toggle */}
<div className="flex items-center justify-between px-3 py-1.5 border-b border-zinc-800/40 bg-zinc-900/30"> <div className="flex items-center justify-between px-3 py-1.5 border-b border-line/40 bg-surface-sunken/30">
<span className="text-[10px] text-zinc-500">config.yaml</span> <span className="text-[10px] text-ink-soft">config.yaml</span>
<label className="flex items-center gap-1.5 cursor-pointer"> <label className="flex items-center gap-1.5 cursor-pointer">
<span className="text-[9px] text-zinc-500">Raw YAML</span> <span className="text-[9px] text-ink-soft">Raw YAML</span>
<input <input
type="checkbox" type="checkbox"
checked={rawMode} checked={rawMode}
@ -626,7 +626,7 @@ export function ConfigTab({ workspaceId }: Props) {
value={rawDraft} value={rawDraft}
onChange={(e) => setRawDraft(e.target.value)} onChange={(e) => setRawDraft(e.target.value)}
spellCheck={false} spellCheck={false}
className="w-full h-full min-h-[300px] bg-zinc-800 border border-zinc-700 rounded p-3 text-xs font-mono text-zinc-200 focus:outline-none focus:border-blue-500 resize-none" className="w-full h-full min-h-[300px] bg-surface-card border border-line rounded p-3 text-xs font-mono text-ink focus:outline-none focus:border-accent resize-none"
/> />
</div> </div>
) : ( ) : (
@ -634,24 +634,24 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="General"> <Section title="General">
<TextInput label="Name" value={config.name} onChange={(v) => update("name", v)} /> <TextInput label="Name" value={config.name} onChange={(v) => update("name", v)} />
<div> <div>
<label htmlFor={descriptionId} className="text-[10px] text-zinc-500 block mb-1">Description</label> <label htmlFor={descriptionId} className="text-[10px] text-ink-soft block mb-1">Description</label>
<textarea <textarea
id={descriptionId} id={descriptionId}
value={config.description} value={config.description}
onChange={(e) => update("description", e.target.value)} onChange={(e) => update("description", e.target.value)}
rows={3} rows={3}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 resize-none" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent resize-none"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<TextInput label="Version" value={config.version} onChange={(v) => update("version", v)} mono /> <TextInput label="Version" value={config.version} onChange={(v) => update("version", v)} mono />
<div> <div>
<label htmlFor={tierId} className="text-[10px] text-zinc-500 block mb-1">Tier</label> <label htmlFor={tierId} className="text-[10px] text-ink-soft block mb-1">Tier</label>
<select <select
id={tierId} id={tierId}
value={config.tier} value={config.tier}
onChange={(e) => update("tier", parseInt(e.target.value, 10))} onChange={(e) => update("tier", parseInt(e.target.value, 10))}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
> >
<option value={1}>T1 Sandboxed</option> <option value={1}>T1 Sandboxed</option>
<option value={2}>T2 Standard</option> <option value={2}>T2 Standard</option>
@ -663,12 +663,12 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="Runtime"> <Section title="Runtime">
<div> <div>
<label htmlFor={runtimeId} className="text-[10px] text-zinc-500 block mb-1">Runtime</label> <label htmlFor={runtimeId} className="text-[10px] text-ink-soft block mb-1">Runtime</label>
<select <select
id={runtimeId} id={runtimeId}
value={config.runtime || ""} value={config.runtime || ""}
onChange={(e) => update("runtime", e.target.value)} onChange={(e) => update("runtime", e.target.value)}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
> >
{runtimeOptions.map((opt) => ( {runtimeOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
@ -747,7 +747,7 @@ export function ConfigTab({ workspaceId }: Props) {
// workspace_secrets MODEL_PROVIDER override. // workspace_secrets MODEL_PROVIDER override.
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="text-[10px] text-zinc-500 block mb-1">Model</label> <label className="text-[10px] text-ink-soft block mb-1">Model</label>
<input <input
type="text" type="text"
value={currentModelId} value={currentModelId}
@ -760,13 +760,13 @@ export function ConfigTab({ workspaceId }: Props) {
); );
}} }}
placeholder="e.g. anthropic:claude-sonnet-4-6" placeholder="e.g. anthropic:claude-sonnet-4-6"
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 font-mono focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink font-mono focus:outline-none focus:border-accent"
/> />
</div> </div>
<div> <div>
<label htmlFor={`${runtimeId}-provider`} className="text-[10px] text-zinc-500 block mb-1"> <label htmlFor={`${runtimeId}-provider`} className="text-[10px] text-ink-soft block mb-1">
Provider Provider
<span className="ml-1 text-zinc-600"> <span className="ml-1 text-ink-soft">
(override leave empty to auto-derive from model slug) (override leave empty to auto-derive from model slug)
</span> </span>
</label> </label>
@ -787,7 +787,7 @@ export function ConfigTab({ workspaceId }: Props) {
} }
aria-label="LLM provider override" aria-label="LLM provider override"
data-testid="provider-input" data-testid="provider-input"
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 font-mono focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink font-mono focus:outline-none focus:border-accent"
/> />
{providerSuggestionsList.length > 0 && ( {providerSuggestionsList.length > 0 && (
<datalist id={`${runtimeId}-providers`}> <datalist id={`${runtimeId}-providers`}>
@ -800,7 +800,7 @@ export function ConfigTab({ workspaceId }: Props) {
</div> </div>
)} )}
{provider && provider !== originalProvider && ( {provider && provider !== originalProvider && (
<p className="text-[10px] text-amber-500 mt-1"> <p className="text-[10px] text-warm mt-1">
Provider change workspace will auto-restart on Save. Provider change workspace will auto-restart on Save.
</p> </p>
)} )}
@ -815,7 +815,7 @@ export function ConfigTab({ workspaceId }: Props) {
onChange={(v) => updateNested("runtime_config" as keyof ConfigData, "required_env", v)} onChange={(v) => updateNested("runtime_config" as keyof ConfigData, "required_env", v)}
placeholder="variable NAME (e.g. ANTHROPIC_API_KEY) — not the value" placeholder="variable NAME (e.g. ANTHROPIC_API_KEY) — not the value"
/> />
<p className="text-[10px] text-zinc-500 mt-1"> <p className="text-[10px] text-ink-soft mt-1">
This declares which env var <em>names</em> the workspace needs. This declares which env var <em>names</em> the workspace needs.
Set the actual values in the <strong>Secrets</strong> section Set the actual values in the <strong>Secrets</strong> section
below those are encrypted and mounted into the container at below those are encrypted and mounted into the container at
@ -823,16 +823,16 @@ export function ConfigTab({ workspaceId }: Props) {
</p> </p>
{currentModelSpec?.required_env?.length && {currentModelSpec?.required_env?.length &&
!arraysEqual(config.runtime_config?.required_env ?? [], currentModelSpec.required_env) && ( !arraysEqual(config.runtime_config?.required_env ?? [], currentModelSpec.required_env) && (
<div className="text-[10px] text-zinc-500 mt-1 flex items-center gap-2"> <div className="text-[10px] text-ink-soft mt-1 flex items-center gap-2">
<span> <span>
Template suggests{" "} Template suggests{" "}
<code className="text-zinc-400">{currentModelSpec.required_env.join(", ")}</code>{" "} <code className="text-ink-mid">{currentModelSpec.required_env.join(", ")}</code>{" "}
for <code className="text-zinc-400">{currentModelSpec.name || currentModelSpec.id}</code>. for <code className="text-ink-mid">{currentModelSpec.name || currentModelSpec.id}</code>.
</span> </span>
<button <button
type="button" type="button"
onClick={() => updateNested("runtime_config" as keyof ConfigData, "required_env", currentModelSpec.required_env)} onClick={() => updateNested("runtime_config" as keyof ConfigData, "required_env", currentModelSpec.required_env)}
className="text-blue-400 hover:text-blue-300 underline" className="text-accent hover:text-accent underline"
> >
Apply Apply
</button> </button>
@ -846,15 +846,15 @@ export function ConfigTab({ workspaceId }: Props) {
(config.runtime_config?.model || config.model || "").toLowerCase().includes("anthropic")) && ( (config.runtime_config?.model || config.model || "").toLowerCase().includes("anthropic")) && (
<Section title="Claude Settings" defaultOpen={false}> <Section title="Claude Settings" defaultOpen={false}>
<div> <div>
<label htmlFor={effortId} className="text-[10px] text-zinc-500 block mb-1"> <label htmlFor={effortId} className="text-[10px] text-ink-soft block mb-1">
Effort Effort
<span className="ml-1 text-zinc-600">(output_config.effort Opus 4.7+)</span> <span className="ml-1 text-ink-soft">(output_config.effort Opus 4.7+)</span>
</label> </label>
<select <select
id={effortId} id={effortId}
value={config.effort || ""} value={config.effort || ""}
onChange={(e) => update("effort", e.target.value)} onChange={(e) => update("effort", e.target.value)}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
data-testid="effort-select" data-testid="effort-select"
> >
<option value=""> unset (model default) </option> <option value=""> unset (model default) </option>
@ -866,9 +866,9 @@ export function ConfigTab({ workspaceId }: Props) {
</select> </select>
</div> </div>
<div> <div>
<label htmlFor={taskBudgetId} className="text-[10px] text-zinc-500 block mb-1"> <label htmlFor={taskBudgetId} className="text-[10px] text-ink-soft block mb-1">
Task Budget (tokens) Task Budget (tokens)
<span className="ml-1 text-zinc-600">(output_config.task_budget.total 0 = unset)</span> <span className="ml-1 text-ink-soft">(output_config.task_budget.total 0 = unset)</span>
</label> </label>
<input <input
id={taskBudgetId} id={taskBudgetId}
@ -878,7 +878,7 @@ export function ConfigTab({ workspaceId }: Props) {
value={config.task_budget ?? 0} value={config.task_budget ?? 0}
onChange={(e) => update("task_budget", parseInt(e.target.value, 10) || 0)} onChange={(e) => update("task_budget", parseInt(e.target.value, 10) || 0)}
placeholder="0" placeholder="0"
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 font-mono" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent font-mono"
data-testid="task-budget-input" data-testid="task-budget-input"
/> />
</div> </div>
@ -909,12 +909,12 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="Sandbox" defaultOpen={false}> <Section title="Sandbox" defaultOpen={false}>
<div> <div>
<label htmlFor={sandboxBackendId} className="text-[10px] text-zinc-500 block mb-1">Backend</label> <label htmlFor={sandboxBackendId} className="text-[10px] text-ink-soft block mb-1">Backend</label>
<select <select
id={sandboxBackendId} id={sandboxBackendId}
value={config.sandbox?.backend || "docker"} value={config.sandbox?.backend || "docker"}
onChange={(e) => updateNested("sandbox" as keyof ConfigData, "backend", e.target.value)} onChange={(e) => updateNested("sandbox" as keyof ConfigData, "backend", e.target.value)}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
> >
<option value="subprocess">subprocess</option> <option value="subprocess">subprocess</option>
<option value="docker">docker</option> <option value="docker">docker</option>
@ -937,25 +937,25 @@ export function ConfigTab({ workspaceId }: Props) {
)} )}
{error && ( {error && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">{error}</div> <div className="mx-3 mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">{error}</div>
)} )}
{!error && RUNTIMES_WITH_OWN_CONFIG.has(config.runtime || "") && ( {!error && RUNTIMES_WITH_OWN_CONFIG.has(config.runtime || "") && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-zinc-900/50 border border-zinc-700 rounded text-xs text-zinc-400"> <div className="mx-3 mb-2 px-3 py-1.5 bg-surface-sunken/50 border border-line rounded text-xs text-ink-mid">
{config.runtime === "hermes" {config.runtime === "hermes"
? "Hermes manages its own config at ~/.hermes/config.yaml on the workspace host. Edit it via the Terminal tab or the hermes CLI, not this form." ? "Hermes manages its own config at ~/.hermes/config.yaml on the workspace host. Edit it via the Terminal tab or the hermes CLI, not this form."
: "This runtime manages its own config outside the platform template."} : "This runtime manages its own config outside the platform template."}
</div> </div>
)} )}
{success && ( {success && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-green-900/30 border border-green-800 rounded text-xs text-green-400">Saved</div> <div className="mx-3 mb-2 px-3 py-1.5 bg-green-900/30 border border-green-800 rounded text-xs text-good">Saved</div>
)} )}
<div className="p-3 border-t border-zinc-800 flex gap-2"> <div className="p-3 border-t border-line flex gap-2">
<button <button
type="button" type="button"
onClick={() => handleSave(true)} onClick={() => handleSave(true)}
disabled={!isDirty || saving} disabled={!isDirty || saving}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-xs rounded text-white disabled:opacity-30 transition-colors" className="px-3 py-1.5 bg-accent-strong hover:bg-accent text-xs rounded text-ink disabled:opacity-30 transition-colors"
> >
{saving ? "Restarting..." : "Save & Restart"} {saving ? "Restarting..." : "Save & Restart"}
</button> </button>
@ -963,14 +963,14 @@ export function ConfigTab({ workspaceId }: Props) {
type="button" type="button"
onClick={() => handleSave(false)} onClick={() => handleSave(false)}
disabled={!isDirty || saving} disabled={!isDirty || saving}
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300 disabled:opacity-30 transition-colors" className="px-3 py-1.5 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid disabled:opacity-30 transition-colors"
> >
Save Save
</button> </button>
<button <button
type="button" type="button"
onClick={loadConfig} onClick={loadConfig}
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300 ml-auto" className="px-3 py-1.5 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid ml-auto"
> >
Reload Reload
</button> </button>

View File

@ -133,7 +133,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
<input <input
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
className="w-full bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm text-zinc-100 focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-sm text-ink focus:outline-none focus:border-accent"
/> />
</Field> </Field>
<Field label="Role"> <Field label="Role">
@ -141,14 +141,14 @@ export function DetailsTab({ workspaceId, data }: Props) {
value={role} value={role}
onChange={(e) => setRole(e.target.value)} onChange={(e) => setRole(e.target.value)}
placeholder="e.g. SEO Specialist" placeholder="e.g. SEO Specialist"
className="w-full bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm text-zinc-100 focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-sm text-ink focus:outline-none focus:border-accent"
/> />
</Field> </Field>
<Field label="Tier"> <Field label="Tier">
<select <select
value={tier} value={tier}
onChange={(e) => setTier(Number(e.target.value))} onChange={(e) => setTier(Number(e.target.value))}
className="w-full bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm text-zinc-100 focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-sm text-ink focus:outline-none focus:border-accent"
> >
<option value={1}>Tier 1 No privileges</option> <option value={1}>Tier 1 No privileges</option>
<option value={2}>Tier 2 Browser</option> <option value={2}>Tier 2 Browser</option>
@ -157,7 +157,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
</select> </select>
</Field> </Field>
{saveError && ( {saveError && (
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{saveError} {saveError}
</div> </div>
)} )}
@ -166,7 +166,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
type="button" type="button"
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-xs rounded text-white disabled:opacity-50" className="px-3 py-1 bg-accent-strong hover:bg-accent text-xs rounded text-ink disabled:opacity-50"
> >
{saving ? "Saving..." : "Save"} {saving ? "Saving..." : "Save"}
</button> </button>
@ -179,7 +179,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
setRole(data.role || ""); setRole(data.role || "");
setTier(data.tier); setTier(data.tier);
}} }}
className="px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300" className="px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
> >
Cancel Cancel
</button> </button>
@ -200,7 +200,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
{isRestartable && ( {isRestartable && (
<div className="pt-2"> <div className="pt-2">
{restartError && ( {restartError && (
<div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{restartError} {restartError}
</div> </div>
)} )}
@ -208,7 +208,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
type="button" type="button"
onClick={handleRestart} onClick={handleRestart}
disabled={restarting} disabled={restarting}
className="px-3 py-1 bg-green-700 hover:bg-green-600 text-xs rounded text-white disabled:opacity-50" className="px-3 py-1 bg-green-700 hover:bg-green-600 text-xs rounded text-ink disabled:opacity-50"
> >
{restarting ? "Restarting..." : data.status === "failed" ? "Retry" : "Restart"} {restarting ? "Restarting..." : data.status === "failed" ? "Retry" : "Restart"}
</button> </button>
@ -217,7 +217,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
<button <button
type="button" type="button"
onClick={() => setEditing(true)} onClick={() => setEditing(true)}
className="mt-2 px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300" className="mt-2 px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
> >
Edit Edit
</button> </button>
@ -234,17 +234,17 @@ export function DetailsTab({ workspaceId, data }: Props) {
{data.lastSampleError ? ( {data.lastSampleError ? (
<pre <pre
data-testid="details-error-log" data-testid="details-error-log"
className="text-[11px] text-red-300 font-mono whitespace-pre-wrap break-all bg-red-950/20 border border-red-900/40 rounded p-2 max-h-[240px] overflow-auto leading-tight" className="text-[11px] text-bad font-mono whitespace-pre-wrap break-all bg-red-950/20 border border-red-900/40 rounded p-2 max-h-[240px] overflow-auto leading-tight"
> >
{data.lastSampleError} {data.lastSampleError}
</pre> </pre>
) : ( ) : (
<p className="text-xs text-zinc-500">No error detail recorded.</p> <p className="text-xs text-ink-soft">No error detail recorded.</p>
)} )}
<button <button
type="button" type="button"
onClick={() => setConsoleOpen(true)} onClick={() => setConsoleOpen(true)}
className="mt-2 px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-xs rounded text-zinc-300 border border-zinc-700" className="mt-2 px-3 py-1 bg-surface-card hover:bg-surface-card text-xs rounded text-ink-mid border border-line"
> >
View console output View console output
</button> </button>
@ -263,9 +263,9 @@ export function DetailsTab({ workspaceId, data }: Props) {
<div className="space-y-1"> <div className="space-y-1">
{skills.map((s) => ( {skills.map((s) => (
<div key={s.id} className="flex items-start gap-2"> <div key={s.id} className="flex items-start gap-2">
<span className="text-xs text-blue-400 font-mono shrink-0">{s.id}</span> <span className="text-xs text-accent font-mono shrink-0">{s.id}</span>
{s.description && ( {s.description && (
<span className="text-xs text-zinc-500">{s.description}</span> <span className="text-xs text-ink-soft">{s.description}</span>
)} )}
</div> </div>
))} ))}
@ -276,13 +276,13 @@ export function DetailsTab({ workspaceId, data }: Props) {
{/* Peers */} {/* Peers */}
<Section title={`Peers (${peers.length})`}> <Section title={`Peers (${peers.length})`}>
{peersError ? ( {peersError ? (
<p className="text-xs text-red-400">{peersError}</p> <p className="text-xs text-bad">{peersError}</p>
) : peers.length === 0 && data.status !== "online" && data.status !== "degraded" ? ( ) : peers.length === 0 && data.status !== "online" && data.status !== "degraded" ? (
<p className="text-xs text-zinc-500"> <p className="text-xs text-ink-soft">
Peers are only discoverable while the workspace is online. Peers are only discoverable while the workspace is online.
</p> </p>
) : peers.length === 0 ? ( ) : peers.length === 0 ? (
<p className="text-xs text-zinc-500">No reachable peers</p> <p className="text-xs text-ink-soft">No reachable peers</p>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{peers.map((p) => ( {peers.map((p) => (
@ -290,11 +290,11 @@ export function DetailsTab({ workspaceId, data }: Props) {
key={p.id} key={p.id}
type="button" type="button"
onClick={() => selectNode(p.id)} onClick={() => selectNode(p.id)}
className="w-full flex items-center gap-2 px-2 py-1 rounded hover:bg-zinc-800 text-left" className="w-full flex items-center gap-2 px-2 py-1 rounded hover:bg-surface-card text-left"
> >
<StatusDot status={p.status} /> <StatusDot status={p.status} />
<span className="text-xs text-zinc-200">{p.name}</span> <span className="text-xs text-ink">{p.name}</span>
{p.role && <span className="text-[10px] text-zinc-500">{p.role}</span>} {p.role && <span className="text-[10px] text-ink-soft">{p.role}</span>}
</button> </button>
))} ))}
</div> </div>
@ -304,7 +304,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
{/* Delete */} {/* Delete */}
<Section title="Danger Zone"> <Section title="Danger Zone">
{deleteError && ( {deleteError && (
<div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div className="mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{deleteError} {deleteError}
</div> </div>
)} )}
@ -315,14 +315,14 @@ export function DetailsTab({ workspaceId, data }: Props) {
aria-labelledby="delete-confirm-title" aria-labelledby="delete-confirm-title"
className="space-y-2" className="space-y-2"
> >
<h3 id="delete-confirm-title" className="text-xs font-medium text-red-400"> <h3 id="delete-confirm-title" className="text-xs font-medium text-bad">
Confirm deletion Confirm deletion
</h3> </h3>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
type="button" type="button"
onClick={handleDelete} onClick={handleDelete}
className="px-3 py-1 bg-red-600 hover:bg-red-500 text-xs rounded text-white" className="px-3 py-1 bg-red-600 hover:bg-red-500 text-xs rounded text-ink"
> >
Confirm Delete Confirm Delete
</button> </button>
@ -334,7 +334,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
// Return focus to the trigger so keyboard users aren't stranded // Return focus to the trigger so keyboard users aren't stranded
deleteButtonRef.current?.focus(); deleteButtonRef.current?.focus();
}} }}
className="px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300" className="px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
> >
Cancel Cancel
</button> </button>
@ -345,7 +345,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
type="button" type="button"
ref={deleteButtonRef} ref={deleteButtonRef}
onClick={() => setConfirmDelete(true)} onClick={() => setConfirmDelete(true)}
className="px-3 py-1 bg-zinc-800 hover:bg-red-900 border border-zinc-700 hover:border-red-700 text-xs rounded text-zinc-400 hover:text-red-400 transition-colors" className="px-3 py-1 bg-surface-card hover:bg-red-900 border border-line hover:border-red-700 text-xs rounded text-ink-mid hover:text-bad transition-colors"
> >
Delete Workspace Delete Workspace
</button> </button>
@ -367,7 +367,7 @@ export function DetailsTab({ workspaceId, data }: Props) {
function Section({ title, children }: { title: string; children: React.ReactNode }) { function Section({ title, children }: { title: string; children: React.ReactNode }) {
return ( return (
<div> <div>
<h3 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider mb-2">{title}</h3> <h3 className="text-xs font-semibold text-ink-mid uppercase tracking-wider mb-2">{title}</h3>
{children} {children}
</div> </div>
); );
@ -377,7 +377,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
const fieldId = useId(); const fieldId = useId();
return ( return (
<div> <div>
<label htmlFor={fieldId} className="text-[10px] text-zinc-500 block mb-0.5">{label}</label> <label htmlFor={fieldId} className="text-[10px] text-ink-soft block mb-0.5">{label}</label>
{cloneElement(children as ReactElement<{ id?: string }>, { id: fieldId })} {cloneElement(children as ReactElement<{ id?: string }>, { id: fieldId })}
</div> </div>
); );
@ -386,8 +386,8 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) { function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return ( return (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-xs text-zinc-500">{label}</span> <span className="text-xs text-ink-soft">{label}</span>
<span className={`text-xs text-zinc-200 ${mono ? "font-mono" : ""} text-right max-w-[200px] truncate`}> <span className={`text-xs text-ink ${mono ? "font-mono" : ""} text-right max-w-[200px] truncate`}>
{value} {value}
</span> </span>
</div> </div>

View File

@ -16,12 +16,12 @@ interface EventEntry {
} }
const EVENT_COLORS: Record<string, string> = { const EVENT_COLORS: Record<string, string> = {
WORKSPACE_ONLINE: "text-green-400", WORKSPACE_ONLINE: "text-good",
WORKSPACE_OFFLINE: "text-zinc-400", WORKSPACE_OFFLINE: "text-ink-mid",
WORKSPACE_DEGRADED: "text-yellow-400", WORKSPACE_DEGRADED: "text-yellow-400",
WORKSPACE_PROVISIONING: "text-blue-400", WORKSPACE_PROVISIONING: "text-accent",
WORKSPACE_REMOVED: "text-red-400", WORKSPACE_REMOVED: "text-bad",
WORKSPACE_PROVISION_FAILED: "text-red-400", WORKSPACE_PROVISION_FAILED: "text-bad",
AGENT_CARD_UPDATED: "text-purple-400", AGENT_CARD_UPDATED: "text-purple-400",
}; };
@ -56,58 +56,58 @@ export function EventsTab({ workspaceId }: Props) {
}, [loadEvents]); }, [loadEvents]);
if (loading && events.length === 0) { if (loading && events.length === 0) {
return <div className="p-4 text-xs text-zinc-500">Loading events...</div>; return <div className="p-4 text-xs text-ink-soft">Loading events...</div>;
} }
return ( return (
<div className="p-4 space-y-2"> <div className="p-4 space-y-2">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs text-zinc-400">{events.length} events</span> <span className="text-xs text-ink-mid">{events.length} events</span>
<button <button
onClick={loadEvents} onClick={loadEvents}
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300" className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid"
> >
Refresh Refresh
</button> </button>
</div> </div>
{error && ( {error && (
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{error} {error}
</div> </div>
)} )}
{!error && events.length === 0 ? ( {!error && events.length === 0 ? (
<p className="text-xs text-zinc-500 text-center py-4">No events yet</p> <p className="text-xs text-ink-soft text-center py-4">No events yet</p>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{events.map((event) => ( {events.map((event) => (
<div key={event.id} className="bg-zinc-800 rounded border border-zinc-700"> <div key={event.id} className="bg-surface-card rounded border border-line">
<button <button
onClick={() => setExpanded(expanded === event.id ? null : event.id)} onClick={() => setExpanded(expanded === event.id ? null : event.id)}
className="w-full flex items-center gap-2 px-3 py-2 text-left" className="w-full flex items-center gap-2 px-3 py-2 text-left"
> >
<span <span
className={`text-xs font-mono ${ className={`text-xs font-mono ${
EVENT_COLORS[event.event_type] || "text-zinc-300" EVENT_COLORS[event.event_type] || "text-ink-mid"
}`} }`}
> >
{event.event_type} {event.event_type}
</span> </span>
<span className="text-[9px] text-zinc-500 ml-auto"> <span className="text-[9px] text-ink-soft ml-auto">
{formatTime(event.created_at)} {formatTime(event.created_at)}
</span> </span>
<span className="text-[10px] text-zinc-500"> <span className="text-[10px] text-ink-soft">
{expanded === event.id ? "▼" : "▶"} {expanded === event.id ? "▼" : "▶"}
</span> </span>
</button> </button>
{expanded === event.id && ( {expanded === event.id && (
<div className="px-3 pb-2"> <div className="px-3 pb-2">
<pre className="text-[10px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-40"> <pre className="text-[10px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-40">
{JSON.stringify(event.payload, null, 2)} {JSON.stringify(event.payload, null, 2)}
</pre> </pre>
<div className="mt-1 text-[9px] text-zinc-500 font-mono"> <div className="mt-1 text-[9px] text-ink-soft font-mono">
ID: {event.id} ID: {event.id}
</div> </div>
</div> </div>

View File

@ -145,7 +145,7 @@ export function FilesTab({ workspaceId }: Props) {
}; };
if (loading) { if (loading) {
return <div className="p-4 text-xs text-zinc-500">Loading files...</div>; return <div className="p-4 text-xs text-ink-soft">Loading files...</div>;
} }
return ( return (
@ -163,34 +163,34 @@ export function FilesTab({ workspaceId }: Props) {
{showDeleteAll && ( {showDeleteAll && (
<div className="mx-3 mt-2 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded space-y-1.5"> <div className="mx-3 mt-2 px-3 py-2 bg-red-950/30 border border-red-800/40 rounded space-y-1.5">
<p className="text-xs text-red-300">Delete all {files.filter((f) => !f.dir).length} files? This cannot be undone.</p> <p className="text-xs text-bad">Delete all {files.filter((f) => !f.dir).length} files? This cannot be undone.</p>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="button" onClick={() => { handleDeleteAll(); setShowDeleteAll(false); }} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-white">Delete All</button> <button type="button" onClick={() => { handleDeleteAll(); setShowDeleteAll(false); }} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-ink">Delete All</button>
<button type="button" onClick={() => setShowDeleteAll(false)} className="px-2 py-0.5 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button> <button type="button" onClick={() => setShowDeleteAll(false)} className="px-2 py-0.5 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
</div> </div>
</div> </div>
)} )}
{error && ( {error && (
<div className="mx-3 mt-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">{error}</div> <div className="mx-3 mt-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">{error}</div>
)} )}
{confirmDelete && ( {confirmDelete && (
<div className="mx-3 mt-2 px-3 py-2 bg-amber-950/30 border border-amber-800/40 rounded space-y-1.5"> <div className="mx-3 mt-2 px-3 py-2 bg-amber-950/30 border border-amber-800/40 rounded space-y-1.5">
<p className="text-xs text-amber-300">Delete <span className="font-mono">{confirmDelete}</span>{files.find((f) => f.path === confirmDelete && f.dir) ? " and all its contents" : ""}?</p> <p className="text-xs text-warm">Delete <span className="font-mono">{confirmDelete}</span>{files.find((f) => f.path === confirmDelete && f.dir) ? " and all its contents" : ""}?</p>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="button" onClick={confirmDeleteFile} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-white">Delete</button> <button type="button" onClick={confirmDeleteFile} className="px-2 py-0.5 bg-red-600 hover:bg-red-500 text-[10px] rounded text-ink">Delete</button>
<button type="button" onClick={() => setConfirmDelete(null)} className="px-2 py-0.5 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button> <button type="button" onClick={() => setConfirmDelete(null)} className="px-2 py-0.5 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
</div> </div>
</div> </div>
)} )}
<div className="flex flex-1 min-h-0"> <div className="flex flex-1 min-h-0">
{/* File tree */} {/* File tree */}
<div className="w-[180px] border-r border-zinc-800/40 overflow-y-auto shrink-0"> <div className="w-[180px] border-r border-line/40 overflow-y-auto shrink-0">
{/* New file input */} {/* New file input */}
{showNewFile && ( {showNewFile && (
<div className="px-2 py-1 border-b border-zinc-800/40"> <div className="px-2 py-1 border-b border-line/40">
<input <input
aria-label="New file path" aria-label="New file path"
value={newFileName} value={newFileName}
@ -198,13 +198,13 @@ export function FilesTab({ workspaceId }: Props) {
onKeyDown={(e) => e.key === "Enter" && createFile()} onKeyDown={(e) => e.key === "Enter" && createFile()}
placeholder="path/file.md" placeholder="path/file.md"
autoFocus autoFocus
className="w-full bg-zinc-800 border border-zinc-600 rounded px-1.5 py-0.5 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500" className="w-full bg-surface-card border border-line rounded px-1.5 py-0.5 text-[10px] text-ink font-mono focus:outline-none focus:border-accent"
/> />
</div> </div>
)} )}
{files.length === 0 ? ( {files.length === 0 ? (
<div className="px-3 py-4 text-[10px] text-zinc-600 text-center"> <div className="px-3 py-4 text-[10px] text-ink-soft text-center">
No config files yet No config files yet
</div> </div>
) : ( ) : (

View File

@ -36,7 +36,7 @@ export function FileEditor({
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="text-2xl opacity-20 mb-2">📄</div> <div className="text-2xl opacity-20 mb-2">📄</div>
<p className="text-[10px] text-zinc-600">Select a file to edit</p> <p className="text-[10px] text-ink-soft">Select a file to edit</p>
</div> </div>
</div> </div>
); );
@ -45,18 +45,18 @@ export function FileEditor({
return ( return (
<> <>
{/* File header */} {/* File header */}
<div className="flex items-center justify-between px-3 py-1.5 border-b border-zinc-800/40 bg-zinc-900/20"> <div className="flex items-center justify-between px-3 py-1.5 border-b border-line/40 bg-surface-sunken/20">
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-1.5 min-w-0">
<span className="text-[10px] opacity-50">{getIcon(selectedFile, false)}</span> <span className="text-[10px] opacity-50">{getIcon(selectedFile, false)}</span>
<span className="text-[10px] font-mono text-zinc-300 truncate">{selectedFile}</span> <span className="text-[10px] font-mono text-ink-mid truncate">{selectedFile}</span>
{isDirty && <span className="text-[9px] text-amber-400">modified</span>} {isDirty && <span className="text-[9px] text-warm">modified</span>}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{success && <span className="text-[9px] text-emerald-400">{success}</span>} {success && <span className="text-[9px] text-good">{success}</span>}
<button <button
onClick={onDownload} onClick={onDownload}
aria-label="Download file" aria-label="Download file"
className="text-[10px] text-zinc-500 hover:text-zinc-300" className="text-[10px] text-ink-soft hover:text-ink-mid"
> >
</button> </button>
@ -64,7 +64,7 @@ export function FileEditor({
<button <button
onClick={onSave} onClick={onSave}
disabled={!isDirty || saving} disabled={!isDirty || saving}
className="text-[10px] text-blue-400 hover:text-blue-300 disabled:opacity-30" className="text-[10px] text-accent hover:text-accent disabled:opacity-30"
> >
{saving ? "Saving..." : "Save"} {saving ? "Saving..." : "Save"}
</button> </button>
@ -74,7 +74,7 @@ export function FileEditor({
{/* Editor area */} {/* Editor area */}
{loadingFile ? ( {loadingFile ? (
<div className="p-4 text-xs text-zinc-500">Loading...</div> <div className="p-4 text-xs text-ink-soft">Loading...</div>
) : ( ) : (
<textarea <textarea
ref={editorRef} ref={editorRef}
@ -103,7 +103,7 @@ export function FileEditor({
} }
}} }}
spellCheck={false} spellCheck={false}
className="flex-1 w-full bg-zinc-950 p-3 text-[11px] font-mono text-zinc-200 leading-relaxed resize-none focus:outline-none" className="flex-1 w-full bg-surface p-3 text-[11px] font-mono text-ink leading-relaxed resize-none focus:outline-none"
style={{ tabSize: 2 }} style={{ tabSize: 2 }}
/> />
)} )}

View File

@ -58,20 +58,20 @@ function TreeItem({
return ( return (
<div> <div>
<div <div
className="group w-full flex items-center gap-1 px-2 py-0.5 text-left hover:bg-zinc-800/40 transition-colors cursor-pointer" className="group w-full flex items-center gap-1 px-2 py-0.5 text-left hover:bg-surface-card/40 transition-colors cursor-pointer"
style={{ paddingLeft: `${depth * 12 + 8}px` }} style={{ paddingLeft: `${depth * 12 + 8}px` }}
onClick={() => onToggleDir(node.path)} onClick={() => onToggleDir(node.path)}
> >
<span className="text-[9px] text-zinc-500 w-3">{isLoading ? "…" : expanded ? "▼" : "▶"}</span> <span className="text-[9px] text-ink-soft w-3">{isLoading ? "…" : expanded ? "▼" : "▶"}</span>
<span className="text-[10px]">📁</span> <span className="text-[10px]">📁</span>
<span className="text-[10px] text-zinc-400 flex-1">{node.name}</span> <span className="text-[10px] text-ink-mid flex-1">{node.name}</span>
<button <button
aria-label={`Delete ${node.name}`} aria-label={`Delete ${node.name}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(node.path); onDelete(node.path);
}} }}
className="text-[9px] text-red-400/0 group-hover:text-red-400/60 hover:!text-red-400 transition-colors" className="text-[9px] text-bad/0 group-hover:text-bad/60 hover:!text-bad transition-colors"
> >
</button> </button>
@ -95,7 +95,7 @@ function TreeItem({
return ( return (
<div <div
className={`group flex items-center gap-1 px-2 py-0.5 cursor-pointer transition-colors ${ className={`group flex items-center gap-1 px-2 py-0.5 cursor-pointer transition-colors ${
isSelected ? "bg-blue-900/30 text-zinc-100" : "hover:bg-zinc-800/40 text-zinc-400" isSelected ? "bg-blue-900/30 text-ink" : "hover:bg-surface-card/40 text-ink-mid"
}`} }`}
style={{ paddingLeft: `${depth * 12 + 20}px` }} style={{ paddingLeft: `${depth * 12 + 20}px` }}
onClick={() => onSelect(node.path)} onClick={() => onSelect(node.path)}
@ -108,7 +108,7 @@ function TreeItem({
e.stopPropagation(); e.stopPropagation();
onDelete(node.path); onDelete(node.path);
}} }}
className="text-[9px] text-red-400/0 group-hover:text-red-400/60 hover:!text-red-400 transition-colors" className="text-[9px] text-bad/0 group-hover:text-bad/60 hover:!text-bad transition-colors"
> >
</button> </button>

View File

@ -26,25 +26,25 @@ export function FilesToolbar({
const uploadRef = useRef<HTMLInputElement>(null); const uploadRef = useRef<HTMLInputElement>(null);
return ( return (
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/40 bg-zinc-900/30"> <div className="flex items-center justify-between px-3 py-2 border-b border-line/40 bg-surface-sunken/30">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<select <select
value={root} value={root}
onChange={(e) => setRoot(e.target.value)} onChange={(e) => setRoot(e.target.value)}
aria-label="File root directory" aria-label="File root directory"
className="text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 rounded px-1.5 py-0.5 outline-none" className="text-[10px] bg-surface-card text-ink-mid border border-line rounded px-1.5 py-0.5 outline-none"
> >
<option value="/configs">/configs</option> <option value="/configs">/configs</option>
<option value="/home">/home</option> <option value="/home">/home</option>
<option value="/workspace">/workspace</option> <option value="/workspace">/workspace</option>
<option value="/plugins">/plugins</option> <option value="/plugins">/plugins</option>
</select> </select>
<span className="text-[10px] text-zinc-500">{fileCount} files</span> <span className="text-[10px] text-ink-soft">{fileCount} files</span>
</div> </div>
<div className="flex gap-1.5"> <div className="flex gap-1.5">
{root === "/configs" && ( {root === "/configs" && (
<> <>
<button type="button" onClick={onNewFile} aria-label="Create new file" className="text-[10px] text-blue-400 hover:text-blue-300" title="Create new file"> <button type="button" onClick={onNewFile} aria-label="Create new file" className="text-[10px] text-accent hover:text-accent" title="Create new file">
+ New + New
</button> </button>
<input <input
@ -57,20 +57,20 @@ export function FilesToolbar({
className="hidden" className="hidden"
onChange={(e) => e.target.files && onUpload(e.target.files)} onChange={(e) => e.target.files && onUpload(e.target.files)}
/> />
<button type="button" onClick={() => uploadRef.current?.click()} aria-label="Upload folder" className="text-[10px] text-blue-400 hover:text-blue-300" title="Upload folder"> <button type="button" onClick={() => uploadRef.current?.click()} aria-label="Upload folder" className="text-[10px] text-accent hover:text-accent" title="Upload folder">
Upload Upload
</button> </button>
</> </>
)} )}
<button type="button" onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Download all files"> <button type="button" onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-ink-soft hover:text-ink-mid" title="Download all files">
Export Export
</button> </button>
{root === "/configs" && ( {root === "/configs" && (
<button type="button" onClick={onClearAll} aria-label="Delete all files" className="text-[10px] text-red-400/60 hover:text-red-400" title="Delete all files"> <button type="button" onClick={onClearAll} aria-label="Delete all files" className="text-[10px] text-bad/60 hover:text-bad" title="Delete all files">
Clear Clear
</button> </button>
)} )}
<button type="button" onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Refresh"> <button type="button" onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-ink-soft hover:text-ink-mid" title="Refresh">
</button> </button>
</div> </div>

View File

@ -114,13 +114,13 @@ export function MemoryTab({ workspaceId }: Props) {
}; };
if (loading) { if (loading) {
return <div className="p-4 text-xs text-zinc-500">Loading memory...</div>; return <div className="p-4 text-xs text-ink-soft">Loading memory...</div>;
} }
return ( return (
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{error && !showAdd && ( {error && !showAdd && (
<div role="alert" className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div role="alert" className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{error} {error}
</div> </div>
)} )}
@ -128,8 +128,8 @@ export function MemoryTab({ workspaceId }: Props) {
<section className="space-y-3"> <section className="space-y-3">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<div className="text-xs font-medium text-zinc-200">Awareness dashboard</div> <div className="text-xs font-medium text-ink">Awareness dashboard</div>
<p className="text-[10px] text-zinc-500"> <p className="text-[10px] text-ink-soft">
Embedded view for the local Awareness memory UI. The current workspace id is appended to the URL for workspace-scoped routing or future filtering. Embedded view for the local Awareness memory UI. The current workspace id is appended to the URL for workspace-scoped routing or future filtering.
</p> </p>
</div> </div>
@ -137,14 +137,14 @@ export function MemoryTab({ workspaceId }: Props) {
<button <button
type="button" type="button"
onClick={() => setShowAwareness((prev) => !prev)} onClick={() => setShowAwareness((prev) => !prev)}
className="shrink-0 px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-200" className="shrink-0 px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink"
> >
{showAwareness ? "Collapse" : "Expand"} {showAwareness ? "Collapse" : "Expand"}
</button> </button>
<button <button
type="button" type="button"
onClick={openAwareness} onClick={openAwareness}
className="shrink-0 px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-200" className="shrink-0 px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink"
> >
Open Open
</button> </button>
@ -153,7 +153,7 @@ export function MemoryTab({ workspaceId }: Props) {
{showAwareness ? ( {showAwareness ? (
AWARENESS_BASE_URL ? ( AWARENESS_BASE_URL ? (
<div className="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900/70 shadow-[0_0_0_1px_rgba(255,255,255,0.02)]"> <div className="overflow-hidden rounded-xl border border-line bg-surface-sunken/70 shadow-[0_0_0_1px_rgba(255,255,255,0.02)]">
<iframe <iframe
title="Awareness dashboard" title="Awareness dashboard"
src={awarenessUrl} src={awarenessUrl}
@ -162,71 +162,71 @@ export function MemoryTab({ workspaceId }: Props) {
/> />
</div> </div>
) : ( ) : (
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/40 p-4 text-xs text-zinc-500"> <div className="rounded-xl border border-dashed border-line bg-surface-sunken/40 p-4 text-xs text-ink-soft">
Set <code className="font-mono text-zinc-300">NEXT_PUBLIC_AWARENESS_URL</code> to embed the Awareness dashboard here. Set <code className="font-mono text-ink-mid">NEXT_PUBLIC_AWARENESS_URL</code> to embed the Awareness dashboard here.
</div> </div>
) )
) : ( ) : (
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 px-4 py-3 flex items-center justify-between gap-3"> <div className="rounded-xl border border-line bg-surface-sunken/50 px-4 py-3 flex items-center justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-xs text-zinc-200">Awareness dashboard is collapsed</p> <p className="text-xs text-ink">Awareness dashboard is collapsed</p>
<p className="text-[10px] text-zinc-500 truncate"> <p className="text-[10px] text-ink-soft truncate">
Workspace context stays linked through <span className="font-mono text-zinc-400">{workspaceId}</span>. Workspace context stays linked through <span className="font-mono text-ink-mid">{workspaceId}</span>.
</p> </p>
</div> </div>
<button <button
type="button" type="button"
onClick={() => setShowAwareness(true)} onClick={() => setShowAwareness(true)}
className="shrink-0 px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white" className="shrink-0 px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink"
> >
Expand Expand
</button> </button>
</div> </div>
)} )}
<div className="grid gap-2 rounded-xl border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-[10px] text-zinc-400 sm:grid-cols-3"> <div className="grid gap-2 rounded-xl border border-line bg-surface/40 px-3 py-2 text-[10px] text-ink-mid sm:grid-cols-3">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="uppercase tracking-[0.18em] text-zinc-500">Status</span> <span className="uppercase tracking-[0.18em] text-ink-soft">Status</span>
<span className="font-medium text-emerald-300">Connected</span> <span className="font-medium text-good">Connected</span>
</div> </div>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="uppercase tracking-[0.18em] text-zinc-500">Mode</span> <span className="uppercase tracking-[0.18em] text-ink-soft">Mode</span>
<span className="font-medium text-zinc-200">{awarenessStatus}</span> <span className="font-medium text-ink">{awarenessStatus}</span>
</div> </div>
<div className="flex items-center justify-between gap-2 min-w-0"> <div className="flex items-center justify-between gap-2 min-w-0">
<span className="uppercase tracking-[0.18em] text-zinc-500">Workspace</span> <span className="uppercase tracking-[0.18em] text-ink-soft">Workspace</span>
<span className="font-mono text-zinc-300 truncate">{workspaceId}</span> <span className="font-mono text-ink-mid truncate">{workspaceId}</span>
</div> </div>
</div> </div>
</section> </section>
<section className="space-y-3 border-t border-zinc-800/60 pt-4"> <section className="space-y-3 border-t border-line/60 pt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<div className="text-xs font-medium text-zinc-200">Workspace KV memory</div> <div className="text-xs font-medium text-ink">Workspace KV memory</div>
<p className="text-[10px] text-zinc-500"> <p className="text-[10px] text-ink-soft">
Native platform key-value memory for workspace <span className="font-mono text-zinc-400">{workspaceId}</span>. Native platform key-value memory for workspace <span className="font-mono text-ink-mid">{workspaceId}</span>.
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
type="button" type="button"
onClick={() => setShowAdvanced((prev) => !prev)} onClick={() => setShowAdvanced((prev) => !prev)}
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300" className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid"
> >
{showAdvanced ? "Hide Advanced" : "Advanced"} {showAdvanced ? "Hide Advanced" : "Advanced"}
</button> </button>
<button <button
type="button" type="button"
onClick={loadMemory} onClick={loadMemory}
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300" className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid"
> >
Refresh Refresh
</button> </button>
<button <button
type="button" type="button"
onClick={() => { setShowAdd(!showAdd); if (!showAdd) setShowAdvanced(true); }} onClick={() => { setShowAdd(!showAdd); if (!showAdd) setShowAdvanced(true); }}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white" className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink"
> >
+ Add + Add
</button> </button>
@ -234,13 +234,13 @@ export function MemoryTab({ workspaceId }: Props) {
</div> </div>
{showAdvanced && showAdd && ( {showAdvanced && showAdd && (
<div className="bg-zinc-800 rounded p-3 space-y-2 border border-zinc-700"> <div className="bg-surface-card rounded p-3 space-y-2 border border-line">
<input <input
value={newKey} value={newKey}
onChange={(e) => setNewKey(e.target.value)} onChange={(e) => setNewKey(e.target.value)}
placeholder="Key" placeholder="Key"
aria-label="Memory key" aria-label="Memory key"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:border-blue-500" className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
/> />
<textarea <textarea
value={newValue} value={newValue}
@ -248,21 +248,21 @@ export function MemoryTab({ workspaceId }: Props) {
placeholder='Value (JSON or plain text)' placeholder='Value (JSON or plain text)'
rows={3} rows={3}
aria-label="Memory value (JSON or plain text)" aria-label="Memory value (JSON or plain text)"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-xs font-mono text-zinc-100 focus:outline-none focus:border-blue-500 resize-none" className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-xs font-mono text-ink focus:outline-none focus:border-accent resize-none"
/> />
<input <input
value={newTTL} value={newTTL}
onChange={(e) => setNewTTL(e.target.value)} onChange={(e) => setNewTTL(e.target.value)}
placeholder="TTL in seconds (optional)" placeholder="TTL in seconds (optional)"
aria-label="TTL in seconds (optional)" aria-label="TTL in seconds (optional)"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:border-blue-500" className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent"
/> />
{error && <div role="alert" className="text-xs text-red-400">{error}</div>} {error && <div role="alert" className="text-xs text-bad">{error}</div>}
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
type="button" type="button"
onClick={handleAdd} onClick={handleAdd}
className="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-xs rounded text-white" className="px-3 py-1 bg-accent-strong hover:bg-accent text-xs rounded text-ink"
> >
Save Save
</button> </button>
@ -272,7 +272,7 @@ export function MemoryTab({ workspaceId }: Props) {
setShowAdd(false); setShowAdd(false);
setError(null); setError(null);
}} }}
className="px-3 py-1 bg-zinc-700 hover:bg-zinc-600 text-xs rounded text-zinc-300" className="px-3 py-1 bg-surface-card hover:bg-zinc-600 text-xs rounded text-ink-mid"
> >
Cancel Cancel
</button> </button>
@ -282,25 +282,25 @@ export function MemoryTab({ workspaceId }: Props) {
{showAdvanced ? ( {showAdvanced ? (
entries.length === 0 ? ( entries.length === 0 ? (
<p className="text-xs text-zinc-500 text-center py-4">No memory entries</p> <p className="text-xs text-ink-soft text-center py-4">No memory entries</p>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{entries.map((entry) => ( {entries.map((entry) => (
<div key={entry.key} className="bg-zinc-800 rounded border border-zinc-700"> <div key={entry.key} className="bg-surface-card rounded border border-line">
<button <button
type="button" type="button"
onClick={() => setExpanded(expanded === entry.key ? null : entry.key)} onClick={() => setExpanded(expanded === entry.key ? null : entry.key)}
className="w-full flex items-center justify-between px-3 py-2 text-left" className="w-full flex items-center justify-between px-3 py-2 text-left"
aria-expanded={expanded === entry.key} aria-expanded={expanded === entry.key}
> >
<span className="text-xs font-mono text-blue-400">{entry.key}</span> <span className="text-xs font-mono text-accent">{entry.key}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{entry.expires_at && ( {entry.expires_at && (
<span className="text-[9px] text-zinc-500"> <span className="text-[9px] text-ink-soft">
TTL {new Date(entry.expires_at).toLocaleString()} TTL {new Date(entry.expires_at).toLocaleString()}
</span> </span>
)} )}
<span className="text-[10px] text-zinc-500"> <span className="text-[10px] text-ink-soft">
{expanded === entry.key ? "▼" : "▶"} {expanded === entry.key ? "▼" : "▶"}
</span> </span>
</div> </div>
@ -308,17 +308,17 @@ export function MemoryTab({ workspaceId }: Props) {
{expanded === entry.key && ( {expanded === entry.key && (
<div className="px-3 pb-2 space-y-2"> <div className="px-3 pb-2 space-y-2">
<pre className="text-[10px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-40"> <pre className="text-[10px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-40">
{JSON.stringify(entry.value, null, 2)} {JSON.stringify(entry.value, null, 2)}
</pre> </pre>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[9px] text-zinc-500"> <span className="text-[9px] text-ink-soft">
Updated: {new Date(entry.updated_at).toLocaleString()} Updated: {new Date(entry.updated_at).toLocaleString()}
</span> </span>
<button <button
type="button" type="button"
onClick={() => handleDelete(entry.key)} onClick={() => handleDelete(entry.key)}
className="text-[10px] text-red-400 hover:text-red-300" className="text-[10px] text-bad hover:text-bad"
> >
Delete Delete
</button> </button>
@ -330,17 +330,17 @@ export function MemoryTab({ workspaceId }: Props) {
</div> </div>
) )
) : ( ) : (
<div className="rounded-xl border border-zinc-800 bg-zinc-950/30 px-4 py-3 flex items-center justify-between gap-3"> <div className="rounded-xl border border-line bg-surface/30 px-4 py-3 flex items-center justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-xs text-zinc-200">Advanced workspace memory is hidden</p> <p className="text-xs text-ink">Advanced workspace memory is hidden</p>
<p className="text-[10px] text-zinc-500 truncate"> <p className="text-[10px] text-ink-soft truncate">
KV entries remain available if you need the raw platform store. KV entries remain available if you need the raw platform store.
</p> </p>
</div> </div>
<button <button
type="button" type="button"
onClick={() => setShowAdvanced(true)} onClick={() => setShowAdvanced(true)}
className="shrink-0 px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white" className="shrink-0 px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink"
> >
Show Show
</button> </button>

View File

@ -180,19 +180,19 @@ export function ScheduleTab({ workspaceId }: Props) {
}; };
if (loading) { if (loading) {
return <div className="p-4 text-[10px] text-zinc-500">Loading schedules...</div>; return <div className="p-4 text-[10px] text-ink-soft">Loading schedules...</div>;
} }
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/50"> <div className="flex items-center justify-between px-3 py-2 border-b border-line/50">
<span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider"> <span className="text-[10px] font-semibold text-ink-mid uppercase tracking-wider">
Schedules Schedules
</span> </span>
<button <button
onClick={() => { resetForm(); setShowForm(true); }} onClick={() => { resetForm(); setShowForm(true); }}
className="text-[11px] px-2 py-0.5 bg-blue-600/20 text-blue-400 rounded hover:bg-blue-600/30 transition-colors" className="text-[11px] px-2 py-0.5 bg-accent-strong/20 text-accent rounded hover:bg-accent-strong/30 transition-colors"
> >
+ Add Schedule + Add Schedule
</button> </button>
@ -200,36 +200,36 @@ export function ScheduleTab({ workspaceId }: Props) {
{/* Create/Edit Form */} {/* Create/Edit Form */}
{showForm && ( {showForm && (
<div className="p-3 border-b border-zinc-800/50 bg-zinc-900/50 space-y-2"> <div className="p-3 border-b border-line/50 bg-surface-sunken/50 space-y-2">
<input <input
type="text" type="text"
aria-label="Schedule name" aria-label="Schedule name"
placeholder="Schedule name (e.g., Daily security scan)" placeholder="Schedule name (e.g., Daily security scan)"
value={formName} value={formName}
onChange={(e) => setFormName(e.target.value)} onChange={(e) => setFormName(e.target.value)}
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-200 placeholder:text-zinc-600" className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-soft"
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex-1"> <div className="flex-1">
<label htmlFor={cronId} className="text-[10px] text-zinc-500 block mb-0.5">Cron Expression</label> <label htmlFor={cronId} className="text-[10px] text-ink-soft block mb-0.5">Cron Expression</label>
<input <input
id={cronId} id={cronId}
type="text" type="text"
value={formCron} value={formCron}
onChange={(e) => setFormCron(e.target.value)} onChange={(e) => setFormCron(e.target.value)}
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-200 font-mono" className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink font-mono"
/> />
<div className="text-[10px] text-zinc-600 mt-0.5"> <div className="text-[10px] text-ink-soft mt-0.5">
{cronToHuman(formCron)} {cronToHuman(formCron)}
</div> </div>
</div> </div>
<div className="w-24"> <div className="w-24">
<label htmlFor={timezoneId} className="text-[10px] text-zinc-500 block mb-0.5">Timezone</label> <label htmlFor={timezoneId} className="text-[10px] text-ink-soft block mb-0.5">Timezone</label>
<select <select
id={timezoneId} id={timezoneId}
value={formTimezone} value={formTimezone}
onChange={(e) => setFormTimezone(e.target.value)} onChange={(e) => setFormTimezone(e.target.value)}
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-1 py-1 text-zinc-200" className="w-full text-[10px] bg-surface-card border border-line rounded px-1 py-1 text-ink"
> >
<option value="UTC">UTC</option> <option value="UTC">UTC</option>
<option value="America/New_York">US Eastern</option> <option value="America/New_York">US Eastern</option>
@ -245,44 +245,44 @@ export function ScheduleTab({ workspaceId }: Props) {
</div> </div>
</div> </div>
<div> <div>
<label htmlFor={promptId} className="text-[10px] text-zinc-500 block mb-0.5">Prompt / Task</label> <label htmlFor={promptId} className="text-[10px] text-ink-soft block mb-0.5">Prompt / Task</label>
<textarea <textarea
id={promptId} id={promptId}
value={formPrompt} value={formPrompt}
onChange={(e) => setFormPrompt(e.target.value)} onChange={(e) => setFormPrompt(e.target.value)}
placeholder="What should the agent do on this schedule?" placeholder="What should the agent do on this schedule?"
rows={3} rows={3}
className="w-full text-[10px] bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-200 placeholder:text-zinc-600 resize-y" className="w-full text-[10px] bg-surface-card border border-line rounded px-2 py-1 text-ink placeholder:text-ink-soft resize-y"
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="flex items-center gap-1.5 text-[10px] text-zinc-400 cursor-pointer"> <label className="flex items-center gap-1.5 text-[10px] text-ink-mid cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={formEnabled} checked={formEnabled}
onChange={(e) => setFormEnabled(e.target.checked)} onChange={(e) => setFormEnabled(e.target.checked)}
className="rounded border-zinc-600" className="rounded border-line"
/> />
Enabled Enabled
</label> </label>
</div> </div>
{error && <div className="text-[10px] text-red-400">{error}</div>} {error && <div className="text-[10px] text-bad">{error}</div>}
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={!formCron || !formPrompt} disabled={!formCron || !formPrompt}
className="text-[11px] px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-500 disabled:opacity-40 transition-colors" className="text-[11px] px-3 py-1 bg-accent-strong text-ink rounded hover:bg-accent disabled:opacity-40 transition-colors"
> >
{editId ? "Update" : "Create"} {editId ? "Update" : "Create"}
</button> </button>
<button <button
onClick={resetForm} onClick={resetForm}
className="text-[11px] px-3 py-1 bg-zinc-800 text-zinc-400 rounded hover:bg-zinc-700 transition-colors" className="text-[11px] px-3 py-1 bg-surface-card text-ink-mid rounded hover:bg-surface-card transition-colors"
> >
Cancel Cancel
</button> </button>
</div> </div>
<div className="text-[10px] text-zinc-600 space-y-0.5"> <div className="text-[10px] text-ink-soft space-y-0.5">
<div>Common patterns:</div> <div>Common patterns:</div>
<div className="font-mono">{"0 9 * * *"} Daily at 9:00 AM</div> <div className="font-mono">{"0 9 * * *"} Daily at 9:00 AM</div>
<div className="font-mono">{"*/30 * * * *"} Every 30 minutes</div> <div className="font-mono">{"*/30 * * * *"} Every 30 minutes</div>
@ -297,8 +297,8 @@ export function ScheduleTab({ workspaceId }: Props) {
{schedules.length === 0 && !showForm ? ( {schedules.length === 0 && !showForm ? (
<div className="p-6 text-center"> <div className="p-6 text-center">
<div className="text-2xl mb-2"></div> <div className="text-2xl mb-2"></div>
<div className="text-[10px] text-zinc-400 mb-1">No schedules yet</div> <div className="text-[10px] text-ink-mid mb-1">No schedules yet</div>
<div className="text-[9px] text-zinc-500"> <div className="text-[9px] text-ink-soft">
Add a schedule to run tasks automatically daily scans, periodic reports, standup reminders. Add a schedule to run tasks automatically daily scans, periodic reports, standup reminders.
</div> </div>
</div> </div>
@ -306,7 +306,7 @@ export function ScheduleTab({ workspaceId }: Props) {
schedules.map((sched) => ( schedules.map((sched) => (
<div <div
key={sched.id} key={sched.id}
className={`px-3 py-2 border-b border-zinc-800/30 ${ className={`px-3 py-2 border-b border-line/30 ${
!sched.enabled ? "opacity-50" : "" !sched.enabled ? "opacity-50" : ""
}`} }`}
> >
@ -324,26 +324,26 @@ export function ScheduleTab({ workspaceId }: Props) {
}`} }`}
title={sched.enabled ? "Click to disable" : "Click to enable"} title={sched.enabled ? "Click to disable" : "Click to enable"}
/> />
<span className="text-[10px] font-medium text-zinc-200 truncate"> <span className="text-[10px] font-medium text-ink truncate">
{sched.name || "Unnamed schedule"} {sched.name || "Unnamed schedule"}
</span> </span>
</div> </div>
<div className="text-[9px] text-zinc-500 mt-0.5 font-mono"> <div className="text-[9px] text-ink-soft mt-0.5 font-mono">
{cronToHuman(sched.cron_expr)} {cronToHuman(sched.cron_expr)}
{sched.timezone !== "UTC" && ( {sched.timezone !== "UTC" && (
<span className="text-zinc-600"> ({sched.timezone})</span> <span className="text-ink-soft"> ({sched.timezone})</span>
)} )}
</div> </div>
<div className="text-[9px] text-zinc-500 mt-0.5 truncate"> <div className="text-[9px] text-ink-soft mt-0.5 truncate">
{sched.prompt.slice(0, 80)}{sched.prompt.length > 80 ? "..." : ""} {sched.prompt.slice(0, 80)}{sched.prompt.length > 80 ? "..." : ""}
</div> </div>
<div className="flex items-center gap-3 mt-1 text-[8px] text-zinc-500"> <div className="flex items-center gap-3 mt-1 text-[8px] text-ink-soft">
<span>Last: {relativeTime(sched.last_run_at)}</span> <span>Last: {relativeTime(sched.last_run_at)}</span>
<span>Next: {relativeTime(sched.next_run_at)}</span> <span>Next: {relativeTime(sched.next_run_at)}</span>
<span>Runs: {sched.run_count}</span> <span>Runs: {sched.run_count}</span>
</div> </div>
{sched.last_error && ( {sched.last_error && (
<div className="text-[8px] text-red-400/70 mt-0.5 truncate"> <div className="text-[8px] text-bad/70 mt-0.5 truncate">
Error: {sched.last_error} Error: {sched.last_error}
</div> </div>
)} )}
@ -352,7 +352,7 @@ export function ScheduleTab({ workspaceId }: Props) {
<button <button
onClick={() => handleRunNow(sched)} onClick={() => handleRunNow(sched)}
aria-label={`Run schedule ${sched.name} now`} aria-label={`Run schedule ${sched.name} now`}
className="text-[11px] px-1.5 py-0.5 text-blue-400 hover:bg-blue-600/20 rounded transition-colors" className="text-[11px] px-1.5 py-0.5 text-accent hover:bg-accent-strong/20 rounded transition-colors"
title="Run now" title="Run now"
> >
@ -360,7 +360,7 @@ export function ScheduleTab({ workspaceId }: Props) {
<button <button
onClick={() => handleEdit(sched)} onClick={() => handleEdit(sched)}
aria-label={`Edit schedule ${sched.name}`} aria-label={`Edit schedule ${sched.name}`}
className="text-[11px] px-1.5 py-0.5 text-zinc-400 hover:bg-zinc-700 rounded transition-colors" className="text-[11px] px-1.5 py-0.5 text-ink-mid hover:bg-surface-card rounded transition-colors"
title="Edit" title="Edit"
> >
@ -368,7 +368,7 @@ export function ScheduleTab({ workspaceId }: Props) {
<button <button
onClick={() => setPendingDelete({ id: sched.id, name: sched.name })} onClick={() => setPendingDelete({ id: sched.id, name: sched.name })}
aria-label={`Delete schedule ${sched.name}`} aria-label={`Delete schedule ${sched.name}`}
className="text-[11px] px-1.5 py-0.5 text-red-400 hover:bg-red-600/20 rounded transition-colors" className="text-[11px] px-1.5 py-0.5 text-bad hover:bg-red-600/20 rounded transition-colors"
title="Delete" title="Delete"
> >

View File

@ -300,11 +300,11 @@ export function SkillsTab({ workspaceId, data }: Props) {
return ( return (
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{/* Plugins section */} {/* Plugins section */}
<div className="rounded-xl border border-zinc-800 bg-zinc-900/70 p-3"> <div className="rounded-xl border border-line bg-surface-sunken/70 p-3">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<div className="text-[10px] uppercase tracking-[0.22em] text-zinc-500">Plugins</div> <div className="text-[10px] uppercase tracking-[0.22em] text-ink-soft">Plugins</div>
<h3 className="mt-1 text-sm font-semibold text-zinc-100"> <h3 className="mt-1 text-sm font-semibold text-ink">
{installed.length} installed {installed.length} installed
</h3> </h3>
</div> </div>
@ -332,27 +332,27 @@ export function SkillsTab({ workspaceId, data }: Props) {
className={`flex items-center justify-between gap-2 rounded-lg border px-3 py-2 ${ className={`flex items-center justify-between gap-2 rounded-lg border px-3 py-2 ${
inert inert
? "border-amber-800/40 bg-amber-950/10 opacity-70" ? "border-amber-800/40 bg-amber-950/10 opacity-70"
: "border-zinc-800/60 bg-zinc-950/40" : "border-line/60 bg-surface/40"
}`} }`}
> >
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[11px] font-medium text-zinc-200">{p.name}</span> <span className="text-[11px] font-medium text-ink">{p.name}</span>
{p.version && <span className="text-[10px] text-zinc-600">v{p.version}</span>} {p.version && <span className="text-[10px] text-ink-soft">v{p.version}</span>}
{inert && ( {inert && (
<span className="rounded-full border border-amber-700/50 bg-amber-950/30 px-1.5 py-0.5 text-[10px] text-amber-300"> <span className="rounded-full border border-amber-700/50 bg-amber-950/30 px-1.5 py-0.5 text-[10px] text-warm">
inert on this runtime inert on this runtime
</span> </span>
)} )}
</div> </div>
{p.description && <div className="text-[10px] text-zinc-500 truncate">{p.description}</div>} {p.description && <div className="text-[10px] text-ink-soft truncate">{p.description}</div>}
{p.skills && p.skills.length > 0 && ( {p.skills && p.skills.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
{p.skills.slice(0, 4).map((s) => ( {p.skills.slice(0, 4).map((s) => (
<span key={s} className="rounded-full bg-zinc-800/60 px-1.5 py-0.5 text-[10px] text-zinc-400">{s}</span> <span key={s} className="rounded-full bg-surface-card/60 px-1.5 py-0.5 text-[10px] text-ink-mid">{s}</span>
))} ))}
{p.skills.length > 4 && ( {p.skills.length > 4 && (
<span className="text-[10px] text-zinc-600">+{p.skills.length - 4}</span> <span className="text-[10px] text-ink-soft">+{p.skills.length - 4}</span>
)} )}
</div> </div>
)} )}
@ -360,7 +360,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
<button <button
onClick={() => handleUninstall(p.name)} onClick={() => handleUninstall(p.name)}
disabled={uninstalling === p.name} disabled={uninstalling === p.name}
className="shrink-0 rounded-full border border-red-800/40 bg-red-950/20 px-2 py-0.5 text-[11px] text-red-400 hover:bg-red-900/30 disabled:opacity-30" className="shrink-0 rounded-full border border-red-800/40 bg-red-950/20 px-2 py-0.5 text-[11px] text-bad hover:bg-red-900/30 disabled:opacity-30"
> >
{uninstalling === p.name ? "..." : "Remove"} {uninstalling === p.name ? "..." : "Remove"}
</button> </button>
@ -372,11 +372,11 @@ export function SkillsTab({ workspaceId, data }: Props) {
{/* Plugin registry (expandable) */} {/* Plugin registry (expandable) */}
{showRegistry && ( {showRegistry && (
<div className="mt-3 border-t border-zinc-800/40 pt-3"> <div className="mt-3 border-t border-line/40 pt-3">
{/* Install from any source (github://, clawhub://, …) */} {/* Install from any source (github://, clawhub://, …) */}
<div className="mb-3 rounded-lg border border-zinc-800/60 bg-zinc-950/40 p-2.5"> <div className="mb-3 rounded-lg border border-line/60 bg-surface/40 p-2.5">
<div className="flex items-center justify-between gap-2 mb-1.5"> <div className="flex items-center justify-between gap-2 mb-1.5">
<div className="text-[10px] uppercase tracking-[0.2em] text-zinc-600"> <div className="text-[10px] uppercase tracking-[0.2em] text-ink-soft">
Install from source Install from source
</div> </div>
{sourceSchemes.length > 0 && ( {sourceSchemes.length > 0 && (
@ -384,7 +384,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
{sourceSchemes.map((s) => ( {sourceSchemes.map((s) => (
<span <span
key={s} key={s}
className="rounded-full border border-zinc-700/50 bg-zinc-900/50 px-1.5 py-0.5 text-[10px] text-zinc-500" className="rounded-full border border-line/50 bg-surface-sunken/50 px-1.5 py-0.5 text-[10px] text-ink-soft"
> >
{s}:// {s}://
</span> </span>
@ -403,7 +403,7 @@ export function SkillsTab({ workspaceId, data }: Props) {
}} }}
placeholder="e.g. github://owner/repo#v1.0" placeholder="e.g. github://owner/repo#v1.0"
spellCheck={false} spellCheck={false}
className="flex-1 rounded border border-zinc-700 bg-zinc-950 px-2 py-1 text-[10px] text-zinc-200 placeholder:text-zinc-600 focus:border-violet-600 focus:outline-none" className="flex-1 rounded border border-line bg-surface px-2 py-1 text-[10px] text-ink placeholder:text-ink-soft focus:border-violet-600 focus:outline-none"
/> />
<button <button
onClick={handleInstallCustom} onClick={handleInstallCustom}
@ -413,12 +413,12 @@ export function SkillsTab({ workspaceId, data }: Props) {
{installing === customSource.trim() ? "Installing..." : "Install"} {installing === customSource.trim() ? "Installing..." : "Install"}
</button> </button>
</div> </div>
<div className="mt-1 text-[10px] text-zinc-600"> <div className="mt-1 text-[10px] text-ink-soft">
Local registry plugins below; paste any scheme URL above for GitHub or other sources. Local registry plugins below; paste any scheme URL above for GitHub or other sources.
</div> </div>
</div> </div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="text-[10px] uppercase tracking-[0.2em] text-zinc-600">Available plugins</div> <div className="text-[10px] uppercase tracking-[0.2em] text-ink-soft">Available plugins</div>
{/* Retry visible whenever registry is empty including {/* Retry visible whenever registry is empty including
the loading state so a stuck fetch (Fast Refresh the loading state so a stuck fetch (Fast Refresh
stranded promise, slow server, browser quirk) has a stranded promise, slow server, browser quirk) has a
@ -445,21 +445,21 @@ export function SkillsTab({ workspaceId, data }: Props) {
)} )}
</div> </div>
{registryLoading && registry.length === 0 ? ( {registryLoading && registry.length === 0 ? (
<div className="text-[10px] text-zinc-500">Loading registry</div> <div className="text-[10px] text-ink-soft">Loading registry</div>
) : registryError ? ( ) : registryError ? (
<div className="rounded-lg border border-red-800/40 bg-red-950/20 px-2 py-1.5"> <div className="rounded-lg border border-red-800/40 bg-red-950/20 px-2 py-1.5">
<div className="text-[10px] text-red-300 font-semibold mb-0.5"> <div className="text-[10px] text-bad font-semibold mb-0.5">
Couldn't load the plugin registry Couldn't load the plugin registry
</div> </div>
<div className="text-[10px] text-red-400/80">{registryError}</div> <div className="text-[10px] text-bad/80">{registryError}</div>
<div className="mt-1 text-[10px] text-zinc-500"> <div className="mt-1 text-[10px] text-ink-soft">
Check the platform server is reachable at /plugins. The Retry button is in the header above. Check the platform server is reachable at /plugins. The Retry button is in the header above.
</div> </div>
</div> </div>
) : registry.length === 0 ? ( ) : registry.length === 0 ? (
<div className="rounded-lg border border-zinc-800/40 bg-zinc-950/40 px-2 py-1.5"> <div className="rounded-lg border border-line/40 bg-surface/40 px-2 py-1.5">
<div className="text-[10px] text-zinc-400 mb-0.5">Registry returned 0 plugins.</div> <div className="text-[10px] text-ink-mid mb-0.5">Registry returned 0 plugins.</div>
<div className="text-[10px] text-zinc-600"> <div className="text-[10px] text-ink-soft">
This usually means the platform's plugins/ directory is empty. This usually means the platform's plugins/ directory is empty.
Run scripts/clone-manifest.sh to populate it from the standalone repos. Run scripts/clone-manifest.sh to populate it from the standalone repos.
</div> </div>
@ -469,30 +469,30 @@ export function SkillsTab({ workspaceId, data }: Props) {
{registry.map((p) => { {registry.map((p) => {
const isInstalled = installedNames.has(p.name); const isInstalled = installedNames.has(p.name);
return ( return (
<div key={p.name} className="flex items-center justify-between gap-2 rounded-lg border border-zinc-800/40 bg-zinc-950/30 px-3 py-2"> <div key={p.name} className="flex items-center justify-between gap-2 rounded-lg border border-line/40 bg-surface/30 px-3 py-2">
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[11px] text-zinc-300">{p.name}</span> <span className="text-[11px] text-ink-mid">{p.name}</span>
{p.version && <span className="text-[10px] text-zinc-600">v{p.version}</span>} {p.version && <span className="text-[10px] text-ink-soft">v{p.version}</span>}
</div> </div>
{p.description && <div className="text-[10px] text-zinc-500 truncate">{p.description}</div>} {p.description && <div className="text-[10px] text-ink-soft truncate">{p.description}</div>}
{p.tags && p.tags.length > 0 && ( {p.tags && p.tags.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
{p.tags.map((t) => ( {p.tags.map((t) => (
<span key={t} className="rounded-full border border-zinc-700/40 px-1.5 py-0.5 text-[10px] text-zinc-500">{t}</span> <span key={t} className="rounded-full border border-line/40 px-1.5 py-0.5 text-[10px] text-ink-soft">{t}</span>
))} ))}
</div> </div>
)} )}
{p.runtimes && p.runtimes.length > 0 && ( {p.runtimes && p.runtimes.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
{p.runtimes.map((r) => ( {p.runtimes.map((r) => (
<span key={r} className="rounded-full border border-blue-800/40 bg-blue-950/20 px-1.5 py-0.5 text-[10px] text-blue-300">{r}</span> <span key={r} className="rounded-full border border-blue-800/40 bg-blue-950/20 px-1.5 py-0.5 text-[10px] text-accent">{r}</span>
))} ))}
</div> </div>
)} )}
</div> </div>
{isInstalled ? ( {isInstalled ? (
<span className="shrink-0 text-[10px] text-emerald-500">Installed</span> <span className="shrink-0 text-[10px] text-good">Installed</span>
) : ( ) : (
<button <button
onClick={() => handleInstall(p.name)} onClick={() => handleInstall(p.name)}
@ -512,30 +512,30 @@ export function SkillsTab({ workspaceId, data }: Props) {
</div> </div>
{/* Skills section */} {/* Skills section */}
<div className="rounded-xl border border-zinc-800 bg-zinc-900/70 p-3"> <div className="rounded-xl border border-line bg-surface-sunken/70 p-3">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<div className="text-[10px] uppercase tracking-[0.22em] text-zinc-500">Workspace skills</div> <div className="text-[10px] uppercase tracking-[0.22em] text-ink-soft">Workspace skills</div>
<h3 className="mt-1 text-sm font-semibold text-zinc-100">Installed skills</h3> <h3 className="mt-1 text-sm font-semibold text-ink">Installed skills</h3>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<MetaPill label="Count" value={String(capability.skillCount)} /> <MetaPill label="Count" value={String(capability.skillCount)} />
<MetaPill label="Runtime" value={capability.runtime || "unknown"} /> <MetaPill label="Runtime" value={capability.runtime || "unknown"} />
</div> </div>
</div> </div>
<p className="mt-2 text-[11px] leading-5 text-zinc-500"> <p className="mt-2 text-[11px] leading-5 text-ink-soft">
Live skill directory from the Agent Card updates when the workspace hot-reloads skills. Live skill directory from the Agent Card updates when the workspace hot-reloads skills.
</p> </p>
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
<button <button
onClick={() => setPanelTab("config")} onClick={() => setPanelTab("config")}
className="rounded-full border border-zinc-700 bg-zinc-950 px-3 py-1 text-[10px] text-zinc-300 hover:bg-zinc-900" className="rounded-full border border-line bg-surface px-3 py-1 text-[10px] text-ink-mid hover:bg-surface-sunken"
> >
Open Config Open Config
</button> </button>
<button <button
onClick={() => setPanelTab("files")} onClick={() => setPanelTab("files")}
className="rounded-full border border-zinc-700 bg-zinc-950 px-3 py-1 text-[10px] text-zinc-300 hover:bg-zinc-900" className="rounded-full border border-line bg-surface px-3 py-1 text-[10px] text-ink-mid hover:bg-surface-sunken"
> >
Open Files Open Files
</button> </button>
@ -550,27 +550,27 @@ export function SkillsTab({ workspaceId, data }: Props) {
)} )}
{skills.length === 0 ? ( {skills.length === 0 ? (
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/40 p-6 text-center"> <div className="rounded-xl border border-dashed border-line bg-surface-sunken/40 p-6 text-center">
<div className="text-sm text-zinc-100">No skills loaded</div> <div className="text-sm text-ink">No skills loaded</div>
<p className="mt-2 text-[11px] leading-5 text-zinc-500"> <p className="mt-2 text-[11px] leading-5 text-ink-soft">
Add skills from the Config tab, install a plugin above, or let the runtime hot-load them. Add skills from the Config tab, install a plugin above, or let the runtime hot-load them.
</p> </p>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{skills.map((skill) => ( {skills.map((skill) => (
<div key={skill.id} className="rounded-xl border border-zinc-800 bg-zinc-900/60 p-3"> <div key={skill.id} className="rounded-xl border border-line bg-surface-sunken/60 p-3">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div> <div>
<div className="text-xs font-semibold text-zinc-100">{skill.name}</div> <div className="text-xs font-semibold text-ink">{skill.name}</div>
<div className="mt-0.5 text-[10px] font-mono text-zinc-500">{skill.id}</div> <div className="mt-0.5 text-[10px] font-mono text-ink-soft">{skill.id}</div>
</div> </div>
{skill.tags.length > 0 && ( {skill.tags.length > 0 && (
<div className="flex flex-wrap justify-end gap-1.5"> <div className="flex flex-wrap justify-end gap-1.5">
{skill.tags.slice(0, 4).map((tag) => ( {skill.tags.slice(0, 4).map((tag) => (
<span <span
key={tag} key={tag}
className="rounded-full border border-zinc-700 bg-zinc-900 px-2 py-0.5 text-[9px] text-zinc-400" className="rounded-full border border-line bg-surface-sunken px-2 py-0.5 text-[9px] text-ink-mid"
> >
{tag} {tag}
</span> </span>
@ -580,17 +580,17 @@ export function SkillsTab({ workspaceId, data }: Props) {
</div> </div>
{skill.description && ( {skill.description && (
<p className="mt-2 text-[11px] leading-5 text-zinc-400">{skill.description}</p> <p className="mt-2 text-[11px] leading-5 text-ink-mid">{skill.description}</p>
)} )}
{skill.examples.length > 0 && ( {skill.examples.length > 0 && (
<div className="mt-2"> <div className="mt-2">
<div className="text-[9px] uppercase tracking-[0.2em] text-zinc-500">Examples</div> <div className="text-[9px] uppercase tracking-[0.2em] text-ink-soft">Examples</div>
<div className="mt-1 space-y-1"> <div className="mt-1 space-y-1">
{skill.examples.slice(0, 2).map((example, index) => ( {skill.examples.slice(0, 2).map((example, index) => (
<div <div
key={`${skill.id}-${index}`} key={`${skill.id}-${index}`}
className="rounded-md border border-zinc-800 bg-zinc-950/60 px-2 py-1 text-[10px] text-zinc-300" className="rounded-md border border-line bg-surface/60 px-2 py-1 text-[10px] text-ink-mid"
> >
{example} {example}
</div> </div>
@ -624,8 +624,8 @@ function extractSkills(agentCard: Record<string, unknown> | null): SkillEntry[]
function MetaPill({ label, value }: { label: string; value: string }) { function MetaPill({ label, value }: { label: string; value: string }) {
return ( return (
<span className="inline-flex items-center gap-1 rounded-full border border-zinc-700/60 bg-zinc-950/60 px-2 py-1 text-[9px] text-zinc-300"> <span className="inline-flex items-center gap-1 rounded-full border border-line/60 bg-surface/60 px-2 py-1 text-[9px] text-ink-mid">
<span className="uppercase tracking-[0.18em] text-[8px] text-zinc-500">{label}</span> <span className="uppercase tracking-[0.18em] text-[8px] text-ink-soft">{label}</span>
<span className="font-medium">{value}</span> <span className="font-medium">{value}</span>
</span> </span>
); );

View File

@ -48,20 +48,20 @@ export function TracesTab({ workspaceId }: Props) {
}, [loadTraces]); }, [loadTraces]);
if (loading) { if (loading) {
return <div className="p-4 text-xs text-zinc-500">Loading traces...</div>; return <div className="p-4 text-xs text-ink-soft">Loading traces...</div>;
} }
return ( return (
<div className="p-4 space-y-2"> <div className="p-4 space-y-2">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs text-zinc-400">{traces.length} traces</span> <span className="text-xs text-ink-mid">{traces.length} traces</span>
<button type="button" onClick={loadTraces} className="text-[10px] text-zinc-500 hover:text-zinc-300"> <button type="button" onClick={loadTraces} className="text-[10px] text-ink-soft hover:text-ink-mid">
Refresh Refresh
</button> </button>
</div> </div>
{error && ( {error && (
<div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400"> <div className="px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-bad">
{error} {error}
</div> </div>
)} )}
@ -69,70 +69,70 @@ export function TracesTab({ workspaceId }: Props) {
{traces.length === 0 && !error ? ( {traces.length === 0 && !error ? (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="text-2xl opacity-20 mb-2" aria-hidden="true">--</div> <div className="text-2xl opacity-20 mb-2" aria-hidden="true">--</div>
<p className="text-xs text-zinc-600">No traces yet</p> <p className="text-xs text-ink-soft">No traces yet</p>
<details className="mt-2 text-[10px] text-zinc-700"> <details className="mt-2 text-[10px] text-zinc-700">
<summary className="cursor-pointer text-zinc-500 hover:text-zinc-400">How to enable tracing</summary> <summary className="cursor-pointer text-ink-soft hover:text-ink-mid">How to enable tracing</summary>
<p className="mt-1"> <p className="mt-1">
Set <code className="font-mono text-zinc-400">LANGFUSE_HOST</code>, <code className="font-mono text-zinc-400">LANGFUSE_PUBLIC_KEY</code>, <code className="font-mono text-zinc-400">LANGFUSE_SECRET_KEY</code> as workspace secrets to enable tracing. Set <code className="font-mono text-ink-mid">LANGFUSE_HOST</code>, <code className="font-mono text-ink-mid">LANGFUSE_PUBLIC_KEY</code>, <code className="font-mono text-ink-mid">LANGFUSE_SECRET_KEY</code> as workspace secrets to enable tracing.
</p> </p>
</details> </details>
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{traces.map((trace) => ( {traces.map((trace) => (
<div key={trace.id} className="bg-zinc-800/40 border border-zinc-700/40 rounded-lg overflow-hidden"> <div key={trace.id} className="bg-surface-card/40 border border-line/40 rounded-lg overflow-hidden">
<button <button
onClick={() => setExpanded(expanded === trace.id ? null : trace.id)} onClick={() => setExpanded(expanded === trace.id ? null : trace.id)}
className="w-full px-3 py-2 flex items-center gap-2 text-left hover:bg-zinc-800/60 transition-colors" className="w-full px-3 py-2 flex items-center gap-2 text-left hover:bg-surface-card/60 transition-colors"
> >
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${ <div className={`w-1.5 h-1.5 rounded-full shrink-0 ${
trace.status === "ERROR" ? "bg-red-400" : "bg-emerald-400" trace.status === "ERROR" ? "bg-red-400" : "bg-emerald-400"
}`} /> }`} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-[11px] text-zinc-200 truncate">{trace.name || "trace"}</div> <div className="text-[11px] text-ink truncate">{trace.name || "trace"}</div>
<div className="text-[9px] text-zinc-500">{formatTime(trace.timestamp)}</div> <div className="text-[9px] text-ink-soft">{formatTime(trace.timestamp)}</div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
{trace.latency != null && ( {trace.latency != null && (
<span className="text-[9px] text-zinc-500 tabular-nums"> <span className="text-[9px] text-ink-soft tabular-nums">
{trace.latency > 1000 ? `${(trace.latency / 1000).toFixed(1)}s` : `${trace.latency}ms`} {trace.latency > 1000 ? `${(trace.latency / 1000).toFixed(1)}s` : `${trace.latency}ms`}
</span> </span>
)} )}
{trace.usage?.total != null && ( {trace.usage?.total != null && (
<span className="text-[9px] text-zinc-500 tabular-nums"> <span className="text-[9px] text-ink-soft tabular-nums">
{trace.usage.total} tok {trace.usage.total} tok
</span> </span>
)} )}
<span className="text-[9px] text-zinc-500"> <span className="text-[9px] text-ink-soft">
{expanded === trace.id ? "▼" : "▶"} {expanded === trace.id ? "▼" : "▶"}
</span> </span>
</div> </div>
</button> </button>
{expanded === trace.id && ( {expanded === trace.id && (
<div className="px-3 pb-2 space-y-2 border-t border-zinc-700/30"> <div className="px-3 pb-2 space-y-2 border-t border-line/30">
{trace.input && ( {trace.input && (
<div> <div>
<div className="text-[9px] text-zinc-500 uppercase tracking-wider mt-2 mb-1">Input</div> <div className="text-[9px] text-ink-soft uppercase tracking-wider mt-2 mb-1">Input</div>
<pre className="text-[9px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-32"> <pre className="text-[9px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-32">
{String(typeof trace.input === "string" ? trace.input : JSON.stringify(trace.input, null, 2))} {String(typeof trace.input === "string" ? trace.input : JSON.stringify(trace.input, null, 2))}
</pre> </pre>
</div> </div>
)} )}
{trace.output && ( {trace.output && (
<div> <div>
<div className="text-[9px] text-zinc-500 uppercase tracking-wider mb-1">Output</div> <div className="text-[9px] text-ink-soft uppercase tracking-wider mb-1">Output</div>
<pre className="text-[9px] text-zinc-300 bg-zinc-900 rounded p-2 overflow-x-auto max-h-32"> <pre className="text-[9px] text-ink-mid bg-surface-sunken rounded p-2 overflow-x-auto max-h-32">
{String(typeof trace.output === "string" ? trace.output : JSON.stringify(trace.output, null, 2))} {String(typeof trace.output === "string" ? trace.output : JSON.stringify(trace.output, null, 2))}
</pre> </pre>
</div> </div>
)} )}
{trace.totalCost != null && ( {trace.totalCost != null && (
<div className="text-[9px] text-zinc-500"> <div className="text-[9px] text-ink-soft">
Cost: ${trace.totalCost.toFixed(6)} Cost: ${trace.totalCost.toFixed(6)}
</div> </div>
)} )}
<div className="text-[8px] text-zinc-500 font-mono select-all"> <div className="text-[8px] text-ink-soft font-mono select-all">
{trace.id} {trace.id}
</div> </div>
</div> </div>

View File

@ -363,15 +363,15 @@ export function AgentCommsPanel({ workspaceId }: { workspaceId: string }) {
}, [messages]); }, [messages]);
if (loading) { if (loading) {
return <div className="text-xs text-zinc-500 text-center py-8">Loading agent communications...</div>; return <div className="text-xs text-ink-soft text-center py-8">Loading agent communications...</div>;
} }
if (messages.length === 0) { if (messages.length === 0) {
return ( return (
<div className="text-xs text-zinc-500 text-center py-8"> <div className="text-xs text-ink-soft text-center py-8">
No agent-to-agent communications yet. No agent-to-agent communications yet.
<br /> <br />
<span className="text-zinc-600">Delegations and peer messages will appear here.</span> <span className="text-ink-soft">Delegations and peer messages will appear here.</span>
</div> </div>
); );
} }
@ -500,7 +500,7 @@ function PeerTabs({
<div <div
role="tablist" role="tablist"
aria-label="Peer threads" aria-label="Peer threads"
className="flex border-b border-zinc-800/40 bg-zinc-900/30 px-2 shrink-0 overflow-x-auto" className="flex border-b border-line/40 bg-surface-sunken/30 px-2 shrink-0 overflow-x-auto"
onKeyDown={(e) => { onKeyDown={(e) => {
const idx = ids.indexOf(selectedPeerId); const idx = ids.indexOf(selectedPeerId);
if (idx < 0) return; if (idx < 0) return;
@ -552,10 +552,10 @@ function PeerTabButton({
className={`shrink-0 px-3 py-1.5 text-[10px] font-medium transition-colors whitespace-nowrap ${ className={`shrink-0 px-3 py-1.5 text-[10px] font-medium transition-colors whitespace-nowrap ${
active active
? "border-b-2 border-cyan-500 text-cyan-200" ? "border-b-2 border-cyan-500 text-cyan-200"
: "border-b-2 border-transparent text-zinc-500 hover:text-zinc-300" : "border-b-2 border-transparent text-ink-soft hover:text-ink-mid"
}`} }`}
> >
{label} <span className="text-[9px] text-zinc-500">({count})</span> {label} <span className="text-[9px] text-ink-soft">({count})</span>
</button> </button>
); );
} }
@ -567,23 +567,23 @@ function NormalMessage({ msg }: { msg: CommMessage }) {
className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${ className={`max-w-[85%] rounded-lg px-3 py-2 text-xs ${
msg.flow === "out" msg.flow === "out"
? "bg-cyan-900/30 text-cyan-100 border border-cyan-700/20" ? "bg-cyan-900/30 text-cyan-100 border border-cyan-700/20"
: "bg-zinc-800/80 text-zinc-200 border border-zinc-700/30" : "bg-surface-card/80 text-ink border border-line/30"
}`} }`}
> >
<div className="text-[9px] text-zinc-500 mb-1"> <div className="text-[9px] text-ink-soft mb-1">
{msg.flow === "out" ? `→ To ${msg.peerName}` : `← From ${msg.peerName}`} {msg.flow === "out" ? `→ To ${msg.peerName}` : `← From ${msg.peerName}`}
</div> </div>
{msg.text ? ( {msg.text ? (
<MarkdownBody className="text-zinc-300">{msg.text}</MarkdownBody> <MarkdownBody className="text-ink-mid">{msg.text}</MarkdownBody>
) : ( ) : (
<div className="text-zinc-300">(no message text)</div> <div className="text-ink-mid">(no message text)</div>
)} )}
{msg.responseText && ( {msg.responseText && (
<MarkdownBody className="mt-1.5 pt-1.5 border-t border-zinc-700/30 text-zinc-400"> <MarkdownBody className="mt-1.5 pt-1.5 border-t border-line/30 text-ink-mid">
{msg.responseText} {msg.responseText}
</MarkdownBody> </MarkdownBody>
)} )}
<div className="text-[9px] text-zinc-500 mt-1"> <div className="text-[9px] text-ink-soft mt-1">
{new Date(msg.timestamp).toLocaleTimeString()} {new Date(msg.timestamp).toLocaleTimeString()}
</div> </div>
</div> </div>
@ -648,7 +648,7 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
return ( return (
<div className={`flex ${msg.flow === "out" ? "justify-end" : "justify-start"}`}> <div className={`flex ${msg.flow === "out" ? "justify-end" : "justify-start"}`}>
<div className="max-w-[85%] rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2 text-xs"> <div className="max-w-[85%] rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2 text-xs">
<div className="flex items-center gap-1.5 text-[10px] text-red-300 font-semibold uppercase tracking-wide mb-1.5"> <div className="flex items-center gap-1.5 text-[10px] text-bad font-semibold uppercase tracking-wide mb-1.5">
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
{msg.flow === "out" {msg.flow === "out"
? `Failed to deliver to ${msg.peerName}` ? `Failed to deliver to ${msg.peerName}`
@ -656,14 +656,14 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
</div> </div>
{msg.text && ( {msg.text && (
<div className="text-[10px] text-zinc-500 mb-1.5"> <div className="text-[10px] text-ink-soft mb-1.5">
<span className="uppercase tracking-wide">Task</span> <span className="uppercase tracking-wide">Task</span>
<MarkdownBody className="text-zinc-400">{msg.text}</MarkdownBody> <MarkdownBody className="text-ink-mid">{msg.text}</MarkdownBody>
</div> </div>
)} )}
<div className="rounded bg-zinc-950/60 border border-red-900/40 px-2 py-1.5 mb-1.5"> <div className="rounded bg-surface/60 border border-red-900/40 px-2 py-1.5 mb-1.5">
<div className="text-[9px] uppercase tracking-wide text-red-400 mb-0.5"> <div className="text-[9px] uppercase tracking-wide text-bad mb-0.5">
Underlying error Underlying error
</div> </div>
<code className="text-[11px] font-mono text-red-200 whitespace-pre-wrap break-words"> <code className="text-[11px] font-mono text-red-200 whitespace-pre-wrap break-words">
@ -671,7 +671,7 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
</code> </code>
</div> </div>
<p className="text-[10px] text-zinc-400 leading-snug mb-2">{hint}</p> <p className="text-[10px] text-ink-mid leading-snug mb-2">{hint}</p>
{msg.peerId && ( {msg.peerId && (
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
@ -686,14 +686,14 @@ function ErrorMessage({ msg }: { msg: CommMessage }) {
<button <button
type="button" type="button"
onClick={handleOpen} onClick={handleOpen}
className="px-2 py-0.5 rounded bg-zinc-800 hover:bg-zinc-700 border border-zinc-700/50 text-[10px] text-zinc-300 transition-colors" className="px-2 py-0.5 rounded bg-surface-card hover:bg-surface-card border border-line/50 text-[10px] text-ink-mid transition-colors"
> >
Open {msg.peerName} Open {msg.peerName}
</button> </button>
</div> </div>
)} )}
<div className="text-[9px] text-zinc-500 mt-1.5"> <div className="text-[9px] text-ink-soft mt-1.5">
{new Date(msg.timestamp).toLocaleTimeString()} {new Date(msg.timestamp).toLocaleTimeString()}
</div> </div>
</div> </div>

View File

@ -26,14 +26,14 @@ export function PendingAttachmentPill({
onRemove: () => void; onRemove: () => void;
}) { }) {
return ( return (
<div className="flex items-center gap-1.5 rounded-md border border-zinc-700/60 bg-zinc-800/80 px-2 py-1 text-[10px] text-zinc-300 max-w-[200px]"> <div className="flex items-center gap-1.5 rounded-md border border-line/60 bg-surface-card/80 px-2 py-1 text-[10px] text-ink-mid max-w-[200px]">
<FileGlyph className="text-zinc-400 shrink-0" /> <FileGlyph className="text-ink-mid shrink-0" />
<span className="truncate" title={file.name}>{file.name}</span> <span className="truncate" title={file.name}>{file.name}</span>
<span className="text-zinc-500 shrink-0 tabular-nums">{formatSize(file.size)}</span> <span className="text-ink-soft shrink-0 tabular-nums">{formatSize(file.size)}</span>
<button <button
onClick={onRemove} onClick={onRemove}
aria-label={`Remove ${file.name}`} aria-label={`Remove ${file.name}`}
className="ml-0.5 text-zinc-500 hover:text-zinc-200 transition-colors shrink-0" className="ml-0.5 text-ink-soft hover:text-ink transition-colors shrink-0"
> >
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <svg width="10" height="10" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /> <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
@ -57,8 +57,8 @@ export function AttachmentChip({
}) { }) {
const toneClasses = const toneClasses =
tone === "user" tone === "user"
? "border-blue-400/30 bg-blue-600/20 hover:bg-blue-600/30 text-blue-100" ? "border-blue-400/30 bg-accent-strong/20 hover:bg-accent-strong/30 text-blue-100"
: "border-zinc-600/50 bg-zinc-700/40 hover:bg-zinc-600/50 text-zinc-100"; : "border-line/50 bg-surface-card/40 hover:bg-zinc-600/50 text-ink";
return ( return (
<button <button
onClick={() => onDownload(attachment)} onClick={() => onDownload(attachment)}

View File

@ -52,7 +52,7 @@ export function TextInput({ label, value, onChange, placeholder, mono }: { label
const id = `textinput-${label.toLowerCase().replace(/\s+/g, "-")}`; const id = `textinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
return ( return (
<div> <div>
<label htmlFor={id} className="text-[10px] text-zinc-500 block mb-1">{label}</label> <label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
<input <input
id={id} id={id}
type="text" type="text"
@ -60,7 +60,7 @@ export function TextInput({ label, value, onChange, placeholder, mono }: { label
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder={placeholder} placeholder={placeholder}
aria-label={label} aria-label={label}
className={`w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 ${mono ? "font-mono" : ""}`} className={`w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent ${mono ? "font-mono" : ""}`}
/> />
</div> </div>
); );
@ -70,7 +70,7 @@ export function NumberInput({ label, value, onChange, min, max }: { label: strin
const id = `numberinput-${label.toLowerCase().replace(/\s+/g, "-")}`; const id = `numberinput-${label.toLowerCase().replace(/\s+/g, "-")}`;
return ( return (
<div> <div>
<label htmlFor={id} className="text-[10px] text-zinc-500 block mb-1">{label}</label> <label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
<input <input
id={id} id={id}
type="number" type="number"
@ -79,7 +79,7 @@ export function NumberInput({ label, value, onChange, min, max }: { label: strin
min={min} min={min}
max={max} max={max}
aria-label={label} aria-label={label}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500 font-mono" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-xs text-ink focus:outline-none focus:border-accent font-mono"
/> />
</div> </div>
); );
@ -89,7 +89,7 @@ export function Toggle({ label, checked, onChange }: { label: string; checked: b
return ( return (
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} className="accent-blue-500" /> <input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} className="accent-blue-500" />
<span className="text-[10px] text-zinc-400">{label}</span> <span className="text-[10px] text-ink-mid">{label}</span>
</label> </label>
); );
} }
@ -99,12 +99,12 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
const [input, setInput] = useState(""); const [input, setInput] = useState("");
return ( return (
<div> <div>
<label htmlFor={id} className="text-[10px] text-zinc-500 block mb-1">{label}</label> <label htmlFor={id} className="text-[10px] text-ink-soft block mb-1">{label}</label>
<div className="flex flex-wrap gap-1 mb-1"> <div className="flex flex-wrap gap-1 mb-1">
{values.map((v, i) => ( {values.map((v, i) => (
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 font-mono"> <span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-surface-card border border-line rounded text-[10px] text-ink-mid font-mono">
{v} {v}
<button type="button" aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-zinc-500 hover:text-red-400">×</button> <button type="button" aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-ink-soft hover:text-bad">×</button>
</span> </span>
))} ))}
</div> </div>
@ -121,7 +121,7 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
}} }}
placeholder={placeholder || "Type and press Enter"} placeholder={placeholder || "Type and press Enter"}
aria-label={label} aria-label={label}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-[10px] text-zinc-200 focus:outline-none focus:border-blue-500 font-mono" className="w-full bg-surface-card border border-line rounded px-2 py-1 text-[10px] text-ink focus:outline-none focus:border-accent font-mono"
/> />
</div> </div>
); );
@ -130,8 +130,8 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
export function Section({ title, children, defaultOpen = true }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) { export function Section({ title, children, defaultOpen = true }: { title: string; children: React.ReactNode; defaultOpen?: boolean }) {
const [open, setOpen] = useState(defaultOpen); const [open, setOpen] = useState(defaultOpen);
return ( return (
<div className="border border-zinc-800 rounded mb-2"> <div className="border border-line rounded mb-2">
<button type="button" onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-3 py-1.5 text-[10px] text-zinc-400 hover:text-zinc-200 bg-zinc-900/50"> <button type="button" onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-3 py-1.5 text-[10px] text-ink-mid hover:text-ink bg-surface-sunken/50">
<span className="font-medium uppercase tracking-wider">{title}</span> <span className="font-medium uppercase tracking-wider">{title}</span>
<span>{open ? "▾" : "▸"}</span> <span>{open ? "▾" : "▸"}</span>
</button> </button>

View File

@ -68,7 +68,7 @@ function labelForKey(key: string): string {
function ScopeBadge({ scope }: { scope: "global" | "workspace" | "override" }) { function ScopeBadge({ scope }: { scope: "global" | "workspace" | "override" }) {
if (scope === "global") { if (scope === "global") {
return <span className="text-[8px] text-amber-400 bg-amber-900/30 px-1.5 py-0.5 rounded" title="Inherited from global secrets">Global</span>; return <span className="text-[8px] text-warm bg-amber-900/30 px-1.5 py-0.5 rounded" title="Inherited from global secrets">Global</span>;
} }
if (scope === "override") { if (scope === "override") {
return <span className="text-[8px] text-purple-400 bg-purple-900/30 px-1.5 py-0.5 rounded" title="Overrides global secret">Override</span>; return <span className="text-[8px] text-purple-400 bg-purple-900/30 px-1.5 py-0.5 rounded" title="Overrides global secret">Override</span>;
@ -96,26 +96,26 @@ function SecretRow({ label, secretKey, isSet, scope, globalMode, onSave, onDelet
const isPlaintext = secretKey === "MODEL_PROVIDER"; const isPlaintext = secretKey === "MODEL_PROVIDER";
return ( return (
<div className="bg-zinc-800/50 rounded px-3 py-2 border border-zinc-700/50"> <div className="bg-surface-card/50 rounded px-3 py-2 border border-line/50">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="min-w-0"> <div className="min-w-0">
<div className="text-[10px] text-zinc-300">{label}</div> <div className="text-[10px] text-ink-mid">{label}</div>
<div className="flex items-center gap-2 mt-0.5"> <div className="flex items-center gap-2 mt-0.5">
<span className="text-[9px] font-mono text-zinc-500">{secretKey}</span> <span className="text-[9px] font-mono text-ink-soft">{secretKey}</span>
{isSet && ( {isSet && (
<span className="text-[9px] font-mono text-zinc-500 tracking-widest" title="Value is set (encrypted)"> <span className="text-[9px] font-mono text-ink-soft tracking-widest" title="Value is set (encrypted)">
</span> </span>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
{isSet && <span className="text-[10px] text-green-500 bg-green-900/30 px-1.5 py-0.5 rounded">Set</span>} {isSet && <span className="text-[10px] text-good bg-green-900/30 px-1.5 py-0.5 rounded">Set</span>}
{scope && <ScopeBadge scope={scope} />} {scope && <ScopeBadge scope={scope} />}
{!editing && isSet && (globalMode || scope !== "global") && ( {!editing && isSet && (globalMode || scope !== "global") && (
<button type="button" onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button> <button type="button" onClick={onDelete} className="text-[11px] text-bad hover:text-bad">Remove</button>
)} )}
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300"> <button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-accent hover:text-accent">
{actionLabel()} {actionLabel()}
</button> </button>
</div> </div>
@ -126,12 +126,12 @@ function SecretRow({ label, secretKey, isSet, scope, globalMode, onSave, onDelet
value={value} onChange={(e) => setValue(e.target.value)} value={value} onChange={(e) => setValue(e.target.value)}
placeholder={isPlaintext ? "anthropic:claude-sonnet-4-6" : "sk-..."} placeholder={isPlaintext ? "anthropic:claude-sonnet-4-6" : "sk-..."}
type={isPlaintext ? "text" : "password"} autoFocus type={isPlaintext ? "text" : "password"} autoFocus
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500" className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] text-ink font-mono focus:outline-none focus:border-accent"
/> />
<button type="button" <button type="button"
onClick={() => { onSave(value); setEditing(false); setValue(""); }} onClick={() => { onSave(value); setEditing(false); setValue(""); }}
disabled={!value} disabled={!value}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30" className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-30"
>Save</button> >Save</button>
</div> </div>
)} )}
@ -156,19 +156,19 @@ function CustomSecretRow({ secretKey, scope, globalMode, onSave, onDelete }: {
<div className="py-1.5 px-2"> <div className="py-1.5 px-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="min-w-0"> <div className="min-w-0">
<span className={`text-[10px] font-mono ${globalMode ? "text-amber-400" : scope === "global" ? "text-zinc-400" : "text-blue-400"}`}> <span className={`text-[10px] font-mono ${globalMode ? "text-warm" : scope === "global" ? "text-ink-mid" : "text-accent"}`}>
{secretKey} {secretKey}
</span> </span>
<span className="text-[9px] font-mono text-zinc-500 tracking-widest ml-2"></span> <span className="text-[9px] font-mono text-ink-soft tracking-widest ml-2"></span>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<span className="text-[10px] text-green-500">Set</span> <span className="text-[10px] text-good">Set</span>
{!globalMode && <ScopeBadge scope={scope} />} {!globalMode && <ScopeBadge scope={scope} />}
{canDelete && !editing && ( {canDelete && !editing && (
<button type="button" onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button> <button type="button" onClick={onDelete} className="text-[11px] text-bad hover:text-bad">Remove</button>
)} )}
{(canDelete || showOverride) && ( {(canDelete || showOverride) && (
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300"> <button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-accent hover:text-accent">
{editing ? "Cancel" : showOverride ? "Override" : "Update"} {editing ? "Cancel" : showOverride ? "Override" : "Update"}
</button> </button>
)} )}
@ -179,12 +179,12 @@ function CustomSecretRow({ secretKey, scope, globalMode, onSave, onDelete }: {
<input <input
value={value} onChange={(e) => setValue(e.target.value)} value={value} onChange={(e) => setValue(e.target.value)}
placeholder="New value" type="password" autoFocus placeholder="New value" type="password" autoFocus
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500" className="flex-1 bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] text-ink font-mono focus:outline-none focus:border-accent"
/> />
<button type="button" <button type="button"
onClick={() => { onSave(value); setEditing(false); setValue(""); }} onClick={() => { onSave(value); setEditing(false); setValue(""); }}
disabled={!value} disabled={!value}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30" className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-30"
>Save</button> >Save</button>
</div> </div>
)} )}
@ -288,17 +288,17 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
return ( return (
<Section title="Secrets & API Keys" defaultOpen={false}> <Section title="Secrets & API Keys" defaultOpen={false}>
{loading ? ( {loading ? (
<div className="text-[10px] text-zinc-500">Loading secrets...</div> <div className="text-[10px] text-ink-soft">Loading secrets...</div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-red-400">{error}</div>} {error && <div className="px-2 py-1 bg-red-900/30 border border-red-800 rounded text-[10px] text-bad">{error}</div>}
{/* Scope toggle */} {/* Scope toggle */}
<div className="flex items-center gap-2 pb-1"> <div className="flex items-center gap-2 pb-1">
<button <button
onClick={() => setGlobalMode(false)} onClick={() => setGlobalMode(false)}
className={`text-[10px] px-2 py-0.5 rounded transition-colors ${ className={`text-[10px] px-2 py-0.5 rounded transition-colors ${
!globalMode ? "bg-blue-600/20 text-blue-300 border border-blue-500/30" : "text-zinc-500 hover:text-zinc-300" !globalMode ? "bg-accent-strong/20 text-accent border border-accent/30" : "text-ink-soft hover:text-ink-mid"
}`} }`}
> >
This Workspace This Workspace
@ -306,7 +306,7 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
<button <button
onClick={() => setGlobalMode(true)} onClick={() => setGlobalMode(true)}
className={`text-[10px] px-2 py-0.5 rounded transition-colors ${ className={`text-[10px] px-2 py-0.5 rounded transition-colors ${
globalMode ? "bg-amber-600/20 text-amber-300 border border-amber-500/30" : "text-zinc-500 hover:text-zinc-300" globalMode ? "bg-amber-600/20 text-warm border border-amber-500/30" : "text-ink-soft hover:text-ink-mid"
}`} }`}
> >
Global (All Workspaces) Global (All Workspaces)
@ -314,7 +314,7 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
</div> </div>
{globalMode && ( {globalMode && (
<div className="px-2 py-1.5 bg-amber-950/20 border border-amber-800/30 rounded text-[10px] text-amber-400/80 leading-relaxed"> <div className="px-2 py-1.5 bg-amber-950/20 border border-amber-800/30 rounded text-[10px] text-warm/80 leading-relaxed">
Global keys apply to all workspaces. Workspace-level keys override globals with the same name. Global keys apply to all workspaces. Workspace-level keys override globals with the same name.
</div> </div>
)} )}
@ -349,27 +349,27 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
{/* Add new */} {/* Add new */}
{showAdd ? ( {showAdd ? (
<div className="bg-zinc-800/50 rounded p-2 space-y-1.5 border border-zinc-700/50"> <div className="bg-surface-card/50 rounded p-2 space-y-1.5 border border-line/50">
<input value={newKey} onChange={(e) => setNewKey(e.target.value.toUpperCase())} placeholder="KEY_NAME" <input value={newKey} onChange={(e) => setNewKey(e.target.value.toUpperCase())} placeholder="KEY_NAME"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] font-mono text-zinc-100 focus:outline-none focus:border-blue-500" /> className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] font-mono text-ink focus:outline-none focus:border-accent" />
<input value={newValue} onChange={(e) => setNewValue(e.target.value)} placeholder="Value" type="password" <input value={newValue} onChange={(e) => setNewValue(e.target.value)} placeholder="Value" type="password"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 focus:outline-none focus:border-blue-500" /> className="w-full bg-surface-sunken border border-line rounded px-2 py-1 text-[10px] text-ink focus:outline-none focus:border-accent" />
<div className="flex gap-2"> <div className="flex gap-2">
<button type="button" onClick={() => { if (newKey && newValue) handleSave(newKey, newValue); }} disabled={!newKey || !newValue} <button type="button" onClick={() => { if (newKey && newValue) handleSave(newKey, newValue); }} disabled={!newKey || !newValue}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30"> className="px-2 py-1 bg-accent-strong hover:bg-accent text-[10px] rounded text-ink disabled:opacity-30">
Save{globalMode ? " (Global)" : ""} Save{globalMode ? " (Global)" : ""}
</button> </button>
<button type="button" onClick={() => { setShowAdd(false); setNewKey(""); setNewValue(""); }} <button type="button" onClick={() => { setShowAdd(false); setNewKey(""); setNewValue(""); }}
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button> className="px-2 py-1 bg-surface-card hover:bg-zinc-600 text-[10px] rounded text-ink-mid">Cancel</button>
</div> </div>
</div> </div>
) : ( ) : (
<button type="button" onClick={() => setShowAdd(true)} className="text-[10px] text-blue-400 hover:text-blue-300"> <button type="button" onClick={() => setShowAdd(true)} className="text-[10px] text-accent hover:text-accent">
+ Add {globalMode ? "Global " : ""}Variable + Add {globalMode ? "Global " : ""}Variable
</button> </button>
)} )}
<div className="text-[9px] text-zinc-500 pt-1"> <div className="text-[9px] text-ink-soft pt-1">
Values are encrypted and never exposed to the browser. Values are encrypted and never exposed to the browser.
{globalMode {globalMode
? " Global keys are shared across all workspaces. Restart workspaces to apply changes." ? " Global keys are shared across all workspaces. Restart workspaces to apply changes."

View File

@ -14,8 +14,8 @@ describe("formatCredits", () => {
}); });
describe("pillTone", () => { describe("pillTone", () => {
it("zinc for healthy balance", () => { it("neutral surface for healthy balance", () => {
expect(pillTone({ credits_balance: 5000, plan_monthly_credits: 9000 })).toContain("zinc"); expect(pillTone({ credits_balance: 5000, plan_monthly_credits: 9000 })).toContain("surface-card");
}); });
it("amber when under 10% of monthly", () => { it("amber when under 10% of monthly", () => {
expect(pillTone({ credits_balance: 500, plan_monthly_credits: 9000 })).toContain("amber"); expect(pillTone({ credits_balance: 500, plan_monthly_credits: 9000 })).toContain("amber");
@ -26,7 +26,10 @@ describe("pillTone", () => {
}); });
it("trial (monthly=0) is healthy until balance hits zero", () => { it("trial (monthly=0) is healthy until balance hits zero", () => {
// No paid plan → no ratio reference; only "0" means empty. // No paid plan → no ratio reference; only "0" means empty.
expect(pillTone({ credits_balance: 50, plan_monthly_credits: 0 })).toContain("zinc"); // Healthy pill resolves to the warm-paper neutral surface (was zinc
// pre-v4 migration); empty pill stays red because tinted state colours
// are kept literal so the warning reads in both themes.
expect(pillTone({ credits_balance: 50, plan_monthly_credits: 0 })).toContain("surface-card");
expect(pillTone({ credits_balance: 0, plan_monthly_credits: 0 })).toContain("red"); expect(pillTone({ credits_balance: 0, plan_monthly_credits: 0 })).toContain("red");
}); });
}); });

View File

@ -32,7 +32,7 @@ export function pillTone(fields: CreditsFields): string {
if (balance <= 0) return "bg-red-950 text-red-200 border-red-800"; if (balance <= 0) return "bg-red-950 text-red-200 border-red-800";
const ratio = monthly > 0 ? balance / monthly : 1; const ratio = monthly > 0 ? balance / monthly : 1;
if (ratio < 0.1) return "bg-amber-950 text-amber-200 border-amber-800"; if (ratio < 0.1) return "bg-amber-950 text-amber-200 border-amber-800";
return "bg-zinc-800 text-zinc-200 border-zinc-700"; return "bg-surface-card text-ink border-line";
} }
// bannerKind picks which (if any) banner to show under the balance // bannerKind picks which (if any) banner to show under the balance

View File

@ -12,10 +12,10 @@ export function statusDotClass(status: string): string {
} }
export const TIER_CONFIG: Record<number, { label: string; color: string; border: string }> = { export const TIER_CONFIG: Record<number, { label: string; color: string; border: string }> = {
1: { label: "T1", color: "text-zinc-500 bg-zinc-800/80", border: "text-zinc-400 border-zinc-700/60" }, 1: { label: "T1", color: "text-ink-soft bg-surface-card/80", border: "text-ink-mid border-line/60" },
2: { label: "T2", color: "text-sky-400 bg-sky-950/50", border: "text-sky-400 border-sky-500/30" }, 2: { label: "T2", color: "text-sky-400 bg-sky-950/50", border: "text-sky-400 border-sky-500/30" },
3: { label: "T3", color: "text-violet-400 bg-violet-950/50", border: "text-violet-400 border-violet-500/30" }, 3: { label: "T3", color: "text-violet-400 bg-violet-950/50", border: "text-violet-400 border-violet-500/30" },
4: { label: "T4", color: "text-amber-400 bg-amber-950/50", border: "text-amber-400 border-amber-500/30" }, 4: { label: "T4", color: "text-warm bg-amber-950/50", border: "text-warm border-amber-500/30" },
}; };
export const COMM_TYPE_LABELS: Record<string, string> = { export const COMM_TYPE_LABELS: Record<string, string> = {

View File

@ -0,0 +1,40 @@
/**
* Theme cookie constants + boot script.
*
* No "use client" pragma these are imported by both server components
* (app/layout.tsx, which calls cookies() during SSR) and client
* components (lib/theme-provider.tsx). Constants exported from a
* "use client" file get rewritten by Next.js as client-reference
* placeholders, so a server importer sees a Function instead of the
* underlying value. Keeping shared primitives here avoids that trap.
*
* Aligned with molecule-app's matching module same cookie name, same
* three-value enum so the preference follows the user across surfaces
* (app, market, landing, canvas) when the cookie is set with
* Domain=.moleculesai.app.
*/
export type ThemePreference = "system" | "light" | "dark";
export type ResolvedTheme = "light" | "dark";
export const THEME_COOKIE = "mol_theme";
export function readThemeCookie(value: string | undefined): ThemePreference {
if (value === "light" || value === "dark" || value === "system") {
return value;
}
return "system";
}
/**
* Inline boot script. Stringified verbatim by app/layout.tsx so it runs
* synchronously before the body paints preventing a flash of the wrong
* theme. Reads cookie via document.cookie regex (no parser available
* yet), falls back to matchMedia, and stamps data-theme on <html>.
*
* Must remain tiny and dependency-free runs before hydration. The
* canvas's middleware sets a strict CSP with nonce-based script-src in
* production; the layout passes the nonce on the <script> tag so this
* passes the inline-script gate.
*/
export const themeBootScript = `(()=>{try{var m=document.cookie.match(/(?:^|;\\s*)${THEME_COOKIE}=(system|light|dark)/);var p=m?m[1]:"system";var r=p==="system"?(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):p;document.documentElement.dataset.theme=r;}catch(e){}})();`;

View File

@ -0,0 +1,145 @@
"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
THEME_COOKIE,
type ResolvedTheme,
type ThemePreference,
} from "@/lib/theme-cookie";
// Re-export so callers can keep `import { THEME_COOKIE, type ThemePreference } from "@/lib/theme-provider"`
// working — but for server-component imports, prefer the underlying module
// directly to dodge the "use client" serialization wrapper.
export { THEME_COOKIE, themeBootScript } from "@/lib/theme-cookie";
export type { ThemePreference, ResolvedTheme } from "@/lib/theme-cookie";
/**
* Theme system: System / Light / Dark.
*
* `theme` what the user picked. Persisted in the `mol_theme`
* cookie so it survives reloads and (when set to a
* parent domain) follows the user across moleculesai.app
* surfaces (app, market, docs, landing, canvas).
* `resolvedTheme` the mode actually rendered. Equal to `theme` when the
* user picked light or dark; equal to the OS preference
* when they picked system.
*
* No-flash on first paint is handled by the inline `<script>` in
* app/layout.tsx, which runs before hydration and stamps data-theme on
* <html> based on cookie + matchMedia. This provider then takes over on
* mount and keeps the attribute in sync with state changes.
*/
type ThemeContextValue = {
theme: ThemePreference;
resolvedTheme: ResolvedTheme;
setTheme: (next: ThemePreference) => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
/**
* Cookie attributes:
* - `Domain=.moleculesai.app` so the preference follows the user across
* canvas.moleculesai.app, app.moleculesai.app, market.moleculesai.app,
* docs.moleculesai.app, AND tenant subdomains (acme.moleculesai.app,
* acme.staging.moleculesai.app, ...). All match `endsWith(".moleculesai.app")`.
* Skipped on localhost (browser would reject Domain= for a
* non-public-suffix host).
* - `Max-Age=1y` long-lived; users rarely change theme.
* - `SameSite=Lax` fine for a UI preference; not security-sensitive.
* - `Secure` only in production HTTPS contexts.
*/
function writeThemeCookie(value: ThemePreference): void {
if (typeof document === "undefined") return;
const isProdHost =
typeof window !== "undefined" &&
window.location.hostname.endsWith(".moleculesai.app");
const parts = [
`${THEME_COOKIE}=${value}`,
"Path=/",
"Max-Age=31536000",
"SameSite=Lax",
];
if (isProdHost) {
parts.push("Domain=.moleculesai.app");
parts.push("Secure");
}
document.cookie = parts.join("; ");
}
function applyResolvedTheme(resolved: ResolvedTheme): void {
if (typeof document === "undefined") return;
document.documentElement.dataset.theme = resolved;
}
export function ThemeProvider({
initialTheme,
children,
}: {
initialTheme: ThemePreference;
children: React.ReactNode;
}) {
const [theme, setThemeState] = useState<ThemePreference>(initialTheme);
const [systemPref, setSystemPref] = useState<ResolvedTheme>("light");
// Track OS preference when the user is on "system". Only registers a
// listener while theme === "system" so we don't pay listener cost in
// explicit modes.
useEffect(() => {
if (typeof window === "undefined") return;
const mql = window.matchMedia("(prefers-color-scheme: dark)");
setSystemPref(mql.matches ? "dark" : "light");
if (theme !== "system") return;
const onChange = (e: MediaQueryListEvent) =>
setSystemPref(e.matches ? "dark" : "light");
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
}, [theme]);
const resolvedTheme: ResolvedTheme =
theme === "system" ? systemPref : theme;
// Reflect resolvedTheme onto <html data-theme>. The inline boot script
// already did this once before hydration; this keeps it in sync after.
useEffect(() => {
applyResolvedTheme(resolvedTheme);
}, [resolvedTheme]);
const setTheme = useCallback((next: ThemePreference) => {
setThemeState(next);
writeThemeCookie(next);
}, []);
const value = useMemo<ThemeContextValue>(
() => ({ theme, resolvedTheme, setTheme }),
[theme, resolvedTheme, setTheme],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// Defaults returned when no <ThemeProvider> is in the tree. Real app
// always wraps via app/layout.tsx; this fallback exists so unit tests
// rendering components in isolation don't have to know about theme.
// setTheme is a no-op — there's no state to mutate without a provider —
// and the noopTheme reference is stable so consumers using it in deps
// arrays don't churn.
const noopTheme: ThemeContextValue = {
theme: "system",
resolvedTheme: "light",
setTheme: () => {},
};
export function useTheme(): ThemeContextValue {
return useContext(ThemeContext) ?? noopTheme;
}

43
canvas/src/lib/theme.ts Normal file
View File

@ -0,0 +1,43 @@
/**
* Theme tokens semantic, light + dark.
*
* Source of truth for colours lives in app/globals.css as CSS custom
* properties (`--color-surface`, `--color-ink`, etc.). Tailwind v4
* generates utilities (`bg-surface`, `text-ink`, ...) from those tokens
* automatically; that's the preferred consumption path.
*
* This module exports `cssVar()` for the rare case where an inline
* `style={{}}` prop or SVG fill needs a token value the returned
* `var(--color-foo)` string follows the live theme without re-renders.
*/
export type ColorToken =
// Warm-paper surface (light-flippable)
| "surface"
| "surface-elevated"
| "surface-sunken"
| "surface-card"
| "line"
| "line-soft"
| "ink"
| "ink-mid"
| "ink-soft"
| "accent"
| "accent-strong"
| "warm"
| "good"
| "bad"
// Always-dark (terminal / console / log surfaces)
| "bg"
| "bg-elev"
| "bg-card"
| "line-strong"
| "ink-mute"
| "ink-dim"
| "accent-dim"
| "plasma"
| "warn";
export function cssVar(token: ColorToken): string {
return `var(--color-${token})`;
}

View File

@ -1,12 +0,0 @@
import type { Config } from "tailwindcss";
import typography from "@tailwindcss/typography";
const config: Config = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {},
},
plugins: [typography],
};
export default config;