Standardize Foundry frontend colors with semantic design tokens (#241)

Extract hardcoded colors from 15+ component files into a centralized
token system (tokens.ts + shared-styles.ts) so all UI colors flow
through FoundryTokens. This eliminates 160+ scattered color values
and makes light mode a single-file change in the future.

- Add FoundryTokens interface with dark/light variants
- Add shared style helpers (buttons, cards, inputs, badges)
- Bridge CSS custom properties for styles.css theme support
- Add useFoundryTokens() hook and ColorMode context
- Migrate all mock-layout/* and mock-onboarding components

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nicholas Kissel 2026-03-11 20:52:06 -07:00 committed by GitHub
parent ed6e6f6fa5
commit f09b9090bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 887 additions and 523 deletions

View file

@ -1,6 +1,13 @@
import { createDarkTheme, type Theme } from "baseui";
import { createContext, useContext } from "react";
import { createDarkTheme, createLightTheme, type Theme } from "baseui";
import { useStyletron } from "baseui";
import { getFoundryTokens, type FoundryTokens } from "../styles/tokens";
export const appTheme: Theme = createDarkTheme({
const STORAGE_KEY = "sandbox-agent-foundry:color-mode";
export type ColorMode = "dark" | "light";
export const darkTheme: Theme = createDarkTheme({
colors: {
primary: "#e4e4e7", // zinc-200
accent: "#ff4f00", // orange accent (inspector)
@ -16,3 +23,60 @@ export const appTheme: Theme = createDarkTheme({
borderTransparent: "rgba(255, 255, 255, 0.07)", // inspector --border-2
},
});
export const lightTheme: Theme = createLightTheme({
colors: {
primary: "#27272a", // zinc-800
accent: "#ff4f00", // orange accent (inspector)
backgroundPrimary: "#ffffff",
backgroundSecondary: "#f4f4f5", // zinc-100
backgroundTertiary: "#fafafa", // zinc-50
backgroundInversePrimary: "#18181b",
contentPrimary: "#09090b", // zinc-950
contentSecondary: "#52525b", // zinc-600
contentTertiary: "#a1a1aa", // zinc-400
contentInversePrimary: "#ffffff",
borderOpaque: "rgba(0, 0, 0, 0.10)",
borderTransparent: "rgba(0, 0, 0, 0.06)",
},
});
/** Kept for backwards compat — defaults to dark */
export const appTheme = darkTheme;
export interface ColorModeContext {
colorMode: ColorMode;
setColorMode: (mode: ColorMode) => void;
}
export const ColorModeCtx = createContext<ColorModeContext>({
colorMode: "dark",
setColorMode: () => {},
});
export function useColorMode() {
return useContext(ColorModeCtx);
}
export function useFoundryTokens(): FoundryTokens {
const [, theme] = useStyletron();
return getFoundryTokens(theme);
}
export function getStoredColorMode(): ColorMode {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") return stored;
} catch {
// ignore
}
return "dark";
}
export function storeColorMode(mode: ColorMode) {
try {
localStorage.setItem(STORAGE_KEY, mode);
} catch {
// ignore
}
}

View file

@ -3,6 +3,7 @@ import { useNavigate } from "@tanstack/react-router";
import { useStyletron } from "baseui";
import { PanelLeft, PanelRight } from "lucide-react";
import { useFoundryTokens } from "../app/theme";
import { DiffContent } from "./mock-layout/diff-content";
import { MessageList } from "./mock-layout/message-list";
@ -95,6 +96,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
rightSidebarCollapsed?: boolean;
onToggleRightSidebar?: () => void;
}) {
const t = useFoundryTokens();
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
const [editValue, setEditValue] = useState("");
@ -471,13 +473,13 @@ const TranscriptPanel = memo(function TranscriptPanel({
minHeight: 0,
display: "flex",
flexDirection: "column",
backgroundColor: "#09090b",
backgroundColor: t.surfacePrimary,
overflow: "hidden",
borderTopLeftRadius: "12px",
borderTopRightRadius: rightSidebarCollapsed ? "12px" : 0,
borderBottomLeftRadius: "24px",
borderBottomRightRadius: rightSidebarCollapsed ? "24px" : 0,
border: "1px solid rgba(255, 255, 255, 0.10)",
border: `1px solid ${t.borderDefault}`,
}}
>
<TabStrip
@ -534,8 +536,8 @@ const TranscriptPanel = memo(function TranscriptPanel({
border: 0,
borderRadius: "999px",
padding: "10px 18px",
background: "rgba(255, 255, 255, 0.12)",
color: "#e4e4e7",
background: t.borderMedium,
color: t.textPrimary,
cursor: "pointer",
fontWeight: 600,
}}
@ -676,6 +678,7 @@ const RightRail = memo(function RightRail({
onToggleSidebar?: () => void;
}) {
const [css] = useStyletron();
const t = useFoundryTokens();
const railRef = useRef<HTMLDivElement>(null);
const [terminalHeight, setTerminalHeight] = useState(() => {
if (typeof window === "undefined") {
@ -745,7 +748,7 @@ const RightRail = memo(function RightRail({
flex: 1,
display: "flex",
flexDirection: "column",
backgroundColor: "#090607",
backgroundColor: t.surfacePrimary,
})}
>
<div
@ -777,8 +780,8 @@ const RightRail = memo(function RightRail({
flexShrink: 0,
cursor: "ns-resize",
position: "relative",
backgroundColor: "#050505",
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
backgroundColor: t.surfacePrimary,
borderRight: `1px solid ${t.borderDefault}`,
":before": {
content: '""',
position: "absolute",
@ -788,7 +791,7 @@ const RightRail = memo(function RightRail({
height: "4px",
borderRadius: "999px",
transform: "translate(-50%, -50%)",
backgroundColor: "rgba(255, 255, 255, 0.14)",
backgroundColor: t.borderMedium,
},
})}
/>
@ -796,11 +799,11 @@ const RightRail = memo(function RightRail({
className={css({
height: `${terminalHeight}px`,
minHeight: `${RIGHT_RAIL_MIN_SECTION_HEIGHT}px`,
backgroundColor: "#080506",
backgroundColor: t.surfacePrimary,
overflow: "hidden",
borderBottomRightRadius: "12px",
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
borderBottom: "1px solid rgba(255, 255, 255, 0.10)",
borderRight: `1px solid ${t.borderDefault}`,
borderBottom: `1px solid ${t.borderDefault}`,
})}
>
<TerminalPane workspaceId={workspaceId} taskId={task.id} />
@ -819,17 +822,18 @@ function MockWorkspaceOrgBar() {
const navigate = useNavigate();
const snapshot = useMockAppSnapshot();
const organization = activeMockOrganization(snapshot);
const t = useFoundryTokens();
if (!organization) {
return null;
}
const buttonStyle = {
border: "1px solid rgba(255,255,255,0.12)",
border: `1px solid ${t.borderMedium}`,
borderRadius: "999px",
padding: "8px 12px",
background: "rgba(255,255,255,0.03)",
color: "rgba(255,255,255,0.86)",
background: t.interactiveSubtle,
color: t.textPrimary,
cursor: "pointer",
fontSize: "13px",
fontWeight: 600,
@ -843,13 +847,13 @@ function MockWorkspaceOrgBar() {
justifyContent: "space-between",
gap: "16px",
padding: "12px 20px",
borderBottom: "1px solid rgba(255,255,255,0.08)",
background: "#101010",
borderBottom: `1px solid ${t.borderSubtle}`,
background: t.surfaceSecondary,
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
<strong style={{ fontSize: "14px", fontWeight: 600 }}>{organization.settings.displayName}</strong>
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.6)" }}>{organization.settings.primaryDomain}</span>
<span style={{ fontSize: "12px", color: t.textMuted }}>{organization.settings.primaryDomain}</span>
</div>
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
<button
@ -892,6 +896,7 @@ function MockWorkspaceOrgBar() {
export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: MockLayoutProps) {
const [css] = useStyletron();
const t = useFoundryTokens();
const navigate = useNavigate();
const taskWorkbenchClient = useMemo(() => getTaskWorkbenchClient(workspaceId), [workspaceId]);
const viewModel = useSyncExternalStore(
@ -1287,11 +1292,11 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#71717a",
color: t.textTertiary,
position: "relative",
zIndex: 9999,
flexShrink: 0,
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
});
const sidebarTransition = "width 200ms ease";
@ -1341,7 +1346,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
</div>
<div style={contentFrameStyle}>
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
<SPanel $style={{ backgroundColor: "#09090b", flex: 1, minWidth: 0 }}>
<SPanel $style={{ backgroundColor: t.surfacePrimary, flex: 1, minWidth: 0 }}>
{!leftSidebarOpen || !rightSidebarOpen ? (
<div style={{ display: "flex", alignItems: "center", padding: "8px 8px 0 8px" }}>
{leftSidebarOpen ? null : (
@ -1391,8 +1396,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
border: 0,
borderRadius: "999px",
padding: "10px 18px",
background: viewModel.repos.length > 0 ? "rgba(255, 255, 255, 0.12)" : "#444",
color: "#e4e4e7",
background: viewModel.repos.length > 0 ? t.borderMedium : t.textTertiary,
color: t.textPrimary,
cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed",
fontWeight: 600,
}}

View file

@ -2,10 +2,12 @@ import { memo, useEffect, useState } from "react";
import { useStyletron } from "baseui";
import { LabelXSmall } from "baseui/typography";
import { useFoundryTokens } from "../../app/theme";
import { formatMessageTimestamp, type HistoryEvent } from "./view-model";
export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: { events: HistoryEvent[]; onSelect: (event: HistoryEvent) => void }) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const [open, setOpen] = useState(false);
const [activeEventId, setActiveEventId] = useState<string | null>(events[events.length - 1]?.id ?? null);
@ -42,10 +44,10 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
})}
>
<div className={css({ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "6px" })}>
<LabelXSmall color={theme.colors.contentTertiary} $style={{ letterSpacing: "0.08em", textTransform: "uppercase" }}>
<LabelXSmall color={t.textTertiary} $style={{ letterSpacing: "0.08em", textTransform: "uppercase" }}>
Task Events
</LabelXSmall>
<LabelXSmall color={theme.colors.contentTertiary}>{events.length}</LabelXSmall>
<LabelXSmall color={t.textTertiary}>{events.length}</LabelXSmall>
</div>
<div className={css({ display: "flex", flexDirection: "column", gap: "6px" })}>
{events.map((event) => {
@ -70,12 +72,12 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
padding: "9px 10px",
borderRadius: "12px",
cursor: "pointer",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.08)" : "transparent",
color: isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary,
backgroundColor: isActive ? t.borderSubtle : "transparent",
color: isActive ? t.textPrimary : t.textSecondary,
transition: "background 160ms ease, color 160ms ease",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.08)",
color: theme.colors.contentPrimary,
backgroundColor: t.borderSubtle,
color: t.textPrimary,
},
})}
>
@ -91,9 +93,9 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
>
{event.preview}
</div>
<LabelXSmall color={theme.colors.contentTertiary}>{event.sessionName}</LabelXSmall>
<LabelXSmall color={t.textTertiary}>{event.sessionName}</LabelXSmall>
</div>
<LabelXSmall color={theme.colors.contentTertiary}>{formatMessageTimestamp(event.createdAtMs)}</LabelXSmall>
<LabelXSmall color={t.textTertiary}>{formatMessageTimestamp(event.createdAtMs)}</LabelXSmall>
</button>
);
})}
@ -119,7 +121,7 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
className={css({
height: "3px",
borderRadius: "999px",
backgroundColor: isActive ? "#ff4f00" : "rgba(255, 255, 255, 0.22)",
backgroundColor: isActive ? t.accent : t.textMuted,
opacity: isActive ? 1 : 0.75,
transition: "background 160ms ease, opacity 160ms ease",
})}

View file

@ -4,6 +4,7 @@ import { useStyletron } from "baseui";
import { LabelSmall, LabelXSmall } from "baseui/typography";
import { Copy } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { HistoryMinimap } from "./history-minimap";
import { SpinnerDot } from "./ui";
import { buildDisplayMessages, formatMessageDuration, formatMessageTimestamp, type AgentTab, type HistoryEvent, type Message } from "./view-model";
@ -19,7 +20,8 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
copiedMessageId: string | null;
onCopyMessage: (message: Message) => void;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const isUser = message.sender === "client";
const isCopied = copiedMessageId === message.id;
const messageTimestamp = formatMessageTimestamp(message.createdAtMs);
@ -47,8 +49,8 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
...(isUser
? {
padding: "12px 16px",
backgroundColor: "rgba(255, 255, 255, 0.10)",
color: "#e4e4e7",
backgroundColor: t.borderDefault,
color: t.textPrimary,
borderTopLeftRadius: "18px",
borderTopRightRadius: "18px",
borderBottomLeftRadius: "18px",
@ -57,7 +59,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
: {
backgroundColor: "transparent",
border: "none",
color: "#e4e4e7",
color: t.textPrimary,
borderRadius: "0",
padding: "0",
}),
@ -86,7 +88,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
})}
>
{displayFooter ? (
<LabelXSmall color={theme.colors.contentTertiary} $style={{ fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em" }}>
<LabelXSmall color={t.textTertiary} $style={{ fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em" }}>
{displayFooter}
</LabelXSmall>
) : null}
@ -106,9 +108,9 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
gap: "5px",
fontSize: "11px",
cursor: "pointer",
color: isCopied ? theme.colors.contentPrimary : theme.colors.contentSecondary,
color: isCopied ? t.textPrimary : t.textSecondary,
transition: "color 160ms ease",
":hover": { color: theme.colors.contentPrimary },
":hover": { color: t.textPrimary },
})}
>
<Copy size={11} />
@ -138,7 +140,8 @@ export const MessageList = memo(function MessageList({
onCopyMessage: (message: Message) => void;
thinkingTimerLabel: string | null;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const messages = useMemo(() => buildDisplayMessages(tab), [tab]);
const messagesById = useMemo(() => new Map(messages.map((message) => [message.id, message])), [messages]);
const transcriptEntries = useMemo<TranscriptEntry[]>(
@ -183,7 +186,7 @@ export const MessageList = memo(function MessageList({
display: "flex",
alignItems: "center",
gap: "8px",
color: "#ff4f00",
color: t.accent,
fontSize: "11px",
fontFamily: '"IBM Plex Mono", monospace',
letterSpacing: "0.01em",
@ -221,7 +224,7 @@ export const MessageList = memo(function MessageList({
gap: "8px",
})}
>
<LabelSmall color={theme.colors.contentTertiary}>
<LabelSmall color={t.textTertiary}>
{!tab.created ? "Choose an agent and model, then send your first message" : "No messages yet in this session"}
</LabelSmall>
</div>
@ -241,15 +244,15 @@ export const MessageList = memo(function MessageList({
renderThinkingState={() => (
<div className={transcriptClassNames.thinkingRow}>
<SpinnerDot size={12} />
<LabelXSmall color="#ff4f00" $style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<LabelXSmall color={t.accent} $style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span>Agent is thinking</span>
{thinkingTimerLabel ? (
<span
className={css({
padding: "2px 7px",
borderRadius: "999px",
backgroundColor: "rgba(255, 79, 0, 0.12)",
border: "1px solid rgba(255, 79, 0, 0.2)",
backgroundColor: t.accentSubtle,
border: `1px solid rgba(255, 79, 0, 0.2)`,
fontFamily: '"IBM Plex Mono", monospace',
fontSize: "10px",
letterSpacing: "0.04em",

View file

@ -3,6 +3,7 @@ import { useStyletron } from "baseui";
import { StatefulPopover, PLACEMENT } from "baseui/popover";
import { ChevronDown, ChevronUp, Star } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { AgentIcon } from "./ui";
import { MODEL_GROUPS, modelLabel, providerAgent, type ModelId } from "./view-model";
@ -19,7 +20,8 @@ const ModelPickerContent = memo(function ModelPickerContent({
onSetDefault: (id: ModelId) => void;
close: () => void;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const [hoveredId, setHoveredId] = useState<ModelId | null>(null);
return (
@ -31,7 +33,7 @@ const ModelPickerContent = memo(function ModelPickerContent({
padding: "6px 12px",
fontSize: "10px",
fontWeight: 700,
color: theme.colors.contentTertiary,
color: t.textTertiary,
textTransform: "uppercase",
letterSpacing: "0.05em",
})}
@ -61,21 +63,21 @@ const ModelPickerContent = memo(function ModelPickerContent({
cursor: "pointer",
fontSize: "12px",
fontWeight: isActive ? 600 : 400,
color: isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary,
color: isActive ? t.textPrimary : t.textSecondary,
borderRadius: "6px",
marginLeft: "4px",
marginRight: "4px",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.08)" },
":hover": { backgroundColor: t.borderSubtle },
})}
>
<AgentIcon agent={agent} size={12} />
<span className={css({ flex: 1 })}>{model.label}</span>
{isDefault ? <Star size={11} fill="#ff4f00" color="#ff4f00" /> : null}
{isDefault ? <Star size={11} fill={t.accent} color={t.accent} /> : null}
{!isDefault && isHovered ? (
<Star
size={11}
color={theme.colors.contentTertiary}
className={css({ cursor: "pointer", ":hover": { color: "#ff4f00" } })}
color={t.textTertiary}
className={css({ cursor: "pointer", ":hover": { color: t.accent } })}
onClick={(event) => {
event.stopPropagation();
onSetDefault(model.id);
@ -102,7 +104,8 @@ export const ModelPicker = memo(function ModelPicker({
onChange: (id: ModelId) => void;
onSetDefault: (id: ModelId) => void;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const [isOpen, setIsOpen] = useState(false);
return (
@ -121,8 +124,8 @@ export const ModelPicker = memo(function ModelPicker({
borderTopRightRadius: "10px",
borderBottomLeftRadius: "10px",
borderBottomRightRadius: "10px",
border: "1px solid rgba(255, 255, 255, 0.10)",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
border: `1px solid ${t.borderDefault}`,
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px ${t.interactiveSubtle}`,
zIndex: 100,
},
},
@ -150,10 +153,10 @@ export const ModelPicker = memo(function ModelPicker({
borderRadius: "6px",
fontSize: "12px",
fontWeight: 500,
color: theme.colors.contentSecondary,
backgroundColor: "rgba(255, 255, 255, 0.10)",
border: "1px solid rgba(255, 255, 255, 0.14)",
":hover": { color: theme.colors.contentPrimary, backgroundColor: "rgba(255, 255, 255, 0.14)" },
color: t.textSecondary,
backgroundColor: t.borderDefault,
border: `1px solid ${t.borderMedium}`,
":hover": { color: t.textPrimary, backgroundColor: t.borderMedium },
})}
>
{modelLabel(value)}

View file

@ -3,6 +3,7 @@ import { useStyletron } from "baseui";
import { ChatComposer, type ChatComposerClassNames } from "@sandbox-agent/react";
import { FileCode, SendHorizonal, Square, X } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { ModelPicker } from "./model-picker";
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT } from "./ui";
import { fileName, type LineAttachment, type ModelId } from "./view-model";
@ -36,16 +37,17 @@ export const PromptComposer = memo(function PromptComposer({
onChangeModel: (model: ModelId) => void;
onSetDefaultModel: (model: ModelId) => void;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const composerClassNames: Partial<ChatComposerClassNames> = {
form: css({
position: "relative",
backgroundColor: "rgba(255, 255, 255, 0.06)",
border: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: t.interactiveHover,
border: `1px solid ${t.borderDefault}`,
borderRadius: "12px",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`,
transition: "border-color 200ms ease",
":focus-within": { borderColor: "rgba(255, 255, 255, 0.15)" },
":focus-within": { borderColor: t.borderMedium },
display: "flex",
flexDirection: "column",
}),
@ -57,7 +59,7 @@ export const PromptComposer = memo(function PromptComposer({
background: "transparent",
border: "none",
borderRadius: "12px 12px 0 0",
color: theme.colors.contentPrimary,
color: t.textPrimary,
fontSize: "13px",
fontFamily: "inherit",
resize: "none",
@ -66,7 +68,7 @@ export const PromptComposer = memo(function PromptComposer({
maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT + 40}px`,
boxSizing: "border-box",
overflowY: "hidden",
"::placeholder": { color: theme.colors.contentSecondary },
"::placeholder": { color: t.textSecondary },
}),
submit: css({
appearance: "none",
@ -87,11 +89,11 @@ export const PromptComposer = memo(function PromptComposer({
justifyContent: "center",
lineHeight: 0,
fontSize: 0,
color: theme.colors.contentPrimary,
color: t.textPrimary,
transition: "background 200ms ease",
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
backgroundColor: isRunning ? t.interactiveHover : t.borderMedium,
":hover": {
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.20)",
backgroundColor: isRunning ? t.borderMedium : "rgba(255, 255, 255, 0.20)",
},
":disabled": {
cursor: "not-allowed",
@ -105,7 +107,7 @@ export const PromptComposer = memo(function PromptComposer({
width: "100%",
height: "100%",
lineHeight: 0,
color: isRunning ? theme.colors.contentPrimary : "#ffffff",
color: isRunning ? t.textPrimary : t.textPrimary,
}),
};
@ -131,11 +133,11 @@ export const PromptComposer = memo(function PromptComposer({
gap: "4px",
padding: "2px 8px",
borderRadius: "4px",
backgroundColor: "rgba(255, 255, 255, 0.06)",
border: "1px solid rgba(255, 255, 255, 0.14)",
backgroundColor: t.interactiveHover,
border: `1px solid ${t.borderMedium}`,
fontSize: "11px",
fontFamily: '"IBM Plex Mono", monospace',
color: theme.colors.contentSecondary,
color: t.textSecondary,
})}
>
<FileCode size={11} />

View file

@ -3,6 +3,7 @@ import { useStyletron } from "baseui";
import { LabelSmall } from "baseui/typography";
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest, PanelRight } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
import { type FileTreeNode, type Task, diffTabId } from "./view-model";
@ -19,7 +20,8 @@ const FileTree = memo(function FileTree({
onFileContextMenu: (event: MouseEvent, path: string) => void;
changedPaths: Set<string>;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
return (
@ -56,8 +58,8 @@ const FileTree = memo(function FileTree({
cursor: "pointer",
fontSize: "12px",
fontFamily: '"IBM Plex Mono", monospace',
color: isChanged ? theme.colors.contentPrimary : theme.colors.contentTertiary,
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)" },
color: isChanged ? t.textPrimary : t.textTertiary,
":hover": { backgroundColor: t.interactiveHover },
})}
>
{node.isDir ? (
@ -72,7 +74,7 @@ const FileTree = memo(function FileTree({
<FolderOpen size={13} />
</>
) : (
<FileCode size={13} color={isChanged ? theme.colors.contentPrimary : undefined} style={{ marginLeft: "16px" }} />
<FileCode size={13} color={isChanged ? t.textPrimary : undefined} style={{ marginLeft: "16px" }} />
)}
<span>{node.name}</span>
</div>
@ -103,7 +105,8 @@ export const RightSidebar = memo(function RightSidebar({
onPublishPr: () => void;
onToggleSidebar?: () => void;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const [rightTab, setRightTab] = useState<"changes" | "files">("changes");
const contextMenu = useContextMenu();
const changedPaths = useMemo(() => new Set(task.fileChanges.map((file) => file.path)), [task.fileChanges]);
@ -147,8 +150,8 @@ export const RightSidebar = memo(function RightSidebar({
);
return (
<SPanel $style={{ backgroundColor: "#09090b", minWidth: 0 }}>
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", overflow: "hidden" }}>
<SPanel $style={{ backgroundColor: t.surfacePrimary, minWidth: 0 }}>
<PanelHeaderBar $style={{ backgroundColor: t.surfaceSecondary, borderBottom: "none", overflow: "hidden" }}>
<div ref={headerRef} className={css({ display: "flex", alignItems: "center", flex: 1, minWidth: 0, justifyContent: "flex-end", gap: "2px" })}>
{!isTerminal ? (
<div className={css({ display: "flex", alignItems: "center", gap: "2px", flexShrink: 1, minWidth: 0 })}>
@ -178,10 +181,10 @@ export const RightSidebar = memo(function RightSidebar({
lineHeight: 1,
whiteSpace: "nowrap",
flexShrink: 0,
color: theme.colors.contentSecondary,
color: t.textSecondary,
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
})}
>
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
@ -205,10 +208,10 @@ export const RightSidebar = memo(function RightSidebar({
lineHeight: 1,
whiteSpace: "nowrap",
flexShrink: 0,
color: theme.colors.contentSecondary,
color: t.textSecondary,
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
})}
>
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />
@ -233,10 +236,10 @@ export const RightSidebar = memo(function RightSidebar({
lineHeight: 1,
whiteSpace: "nowrap",
flexShrink: 0,
color: theme.colors.contentSecondary,
color: t.textSecondary,
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
})}
>
<Archive size={12} style={{ flexShrink: 0 }} />
@ -256,13 +259,13 @@ export const RightSidebar = memo(function RightSidebar({
width: "26px",
height: "26px",
borderRadius: "6px",
color: "#71717a",
color: t.textTertiary,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
})}
>
<PanelRight size={14} />
@ -277,8 +280,8 @@ export const RightSidebar = memo(function RightSidebar({
minHeight: 0,
display: "flex",
flexDirection: "column",
borderTop: "1px solid rgba(255, 255, 255, 0.10)",
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
borderTop: `1px solid ${t.borderDefault}`,
borderRight: `1px solid ${t.borderDefault}`,
borderTopRightRadius: "12px",
overflow: "hidden",
}}
@ -288,8 +291,8 @@ export const RightSidebar = memo(function RightSidebar({
display: "flex",
alignItems: "stretch",
gap: "4px",
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: "#09090b",
borderBottom: `1px solid ${t.borderDefault}`,
backgroundColor: t.surfacePrimary,
height: "41px",
minHeight: "41px",
flexShrink: 0,
@ -318,12 +321,12 @@ export const RightSidebar = memo(function RightSidebar({
fontWeight: 500,
lineHeight: 1,
whiteSpace: "nowrap",
color: rightTab === "changes" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "transparent",
color: rightTab === "changes" ? t.textPrimary : t.textSecondary,
backgroundColor: rightTab === "changes" ? t.interactiveHover : "transparent",
transitionProperty: "color, background-color",
transitionDuration: "200ms",
transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
":hover": { color: t.textPrimary, backgroundColor: rightTab === "changes" ? t.interactiveHover : t.interactiveSubtle },
})}
>
Changes
@ -336,8 +339,8 @@ export const RightSidebar = memo(function RightSidebar({
minWidth: "16px",
height: "16px",
padding: "0 5px",
background: "#3f3f46",
color: "#a1a1aa",
background: t.surfaceElevated,
color: t.textSecondary,
fontSize: "9px",
fontWeight: 700,
borderRadius: "8px",
@ -367,12 +370,12 @@ export const RightSidebar = memo(function RightSidebar({
fontWeight: 500,
lineHeight: 1,
whiteSpace: "nowrap",
color: rightTab === "files" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "transparent",
color: rightTab === "files" ? t.textPrimary : t.textSecondary,
backgroundColor: rightTab === "files" ? t.interactiveHover : "transparent",
transitionProperty: "color, background-color",
transitionDuration: "200ms",
transitionTimingFunction: "ease",
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
":hover": { color: t.textPrimary, backgroundColor: rightTab === "files" ? t.interactiveHover : t.interactiveSubtle },
})}
>
All Files
@ -384,13 +387,13 @@ export const RightSidebar = memo(function RightSidebar({
<div className={css({ padding: "10px 14px", display: "flex", flexDirection: "column", gap: "2px" })}>
{task.fileChanges.length === 0 ? (
<div className={css({ padding: "20px 0", textAlign: "center" })}>
<LabelSmall color={theme.colors.contentTertiary}>No changes yet</LabelSmall>
<LabelSmall color={t.textTertiary}>No changes yet</LabelSmall>
</div>
) : null}
{task.fileChanges.map((file) => {
const isActive = activeTabId === diffTabId(file.path);
const TypeIcon = file.type === "A" ? FilePlus : file.type === "D" ? FileX : FileCode;
const iconColor = file.type === "A" ? "#7ee787" : file.type === "D" ? "#ffa198" : theme.colors.contentTertiary;
const iconColor = file.type === "A" ? t.statusSuccess : file.type === "D" ? t.statusError : t.textTertiary;
return (
<div
key={file.path}
@ -402,9 +405,9 @@ export const RightSidebar = memo(function RightSidebar({
gap: "8px",
padding: "6px 10px",
borderRadius: "6px",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
backgroundColor: isActive ? t.interactiveHover : "transparent",
cursor: "pointer",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)" },
":hover": { backgroundColor: t.interactiveHover },
})}
>
<TypeIcon size={14} color={iconColor} style={{ flexShrink: 0 }} />
@ -414,7 +417,7 @@ export const RightSidebar = memo(function RightSidebar({
minWidth: 0,
fontFamily: '"IBM Plex Mono", monospace',
fontSize: "12px",
color: isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary,
color: isActive ? t.textPrimary : t.textSecondary,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
@ -432,8 +435,8 @@ export const RightSidebar = memo(function RightSidebar({
fontFamily: '"IBM Plex Mono", monospace',
})}
>
<span className={css({ color: "#7ee787" })}>+{file.added}</span>
<span className={css({ color: "#ffa198" })}>-{file.removed}</span>
<span className={css({ color: t.statusSuccess })}>+{file.added}</span>
<span className={css({ color: t.statusError })}>-{file.removed}</span>
<span className={css({ color: iconColor, fontWeight: 600, width: "10px", textAlign: "center" })}>{file.type}</span>
</div>
</div>
@ -446,7 +449,7 @@ export const RightSidebar = memo(function RightSidebar({
<FileTree nodes={task.fileTree} depth={0} onSelectFile={onOpenDiff} onFileContextMenu={openFileMenu} changedPaths={changedPaths} />
) : (
<div className={css({ padding: "20px 0", textAlign: "center" })}>
<LabelSmall color={theme.colors.contentTertiary}>No files yet</LabelSmall>
<LabelSmall color={t.textTertiary}>No files yet</LabelSmall>
</div>
)}
</div>

View file

@ -21,6 +21,8 @@ import {
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
import { activeMockOrganization, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../../lib/mock-app";
import { useFoundryTokens } from "../../app/theme";
import type { FoundryTokens } from "../../styles/tokens";
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
@ -65,7 +67,8 @@ export const Sidebar = memo(function Sidebar({
onReorderProjects: (fromIndex: number, toIndex: number) => void;
onToggleSidebar?: () => void;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const contextMenu = useContextMenu();
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({});
const dragIndexRef = useRef<number | null>(null);
@ -106,13 +109,13 @@ export const Sidebar = memo(function Sidebar({
width: "26px",
height: "26px",
borderRadius: "6px",
color: "#71717a",
color: t.textTertiary,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
})}
>
<PanelLeft size={14} />
@ -122,7 +125,7 @@ export const Sidebar = memo(function Sidebar({
) : null}
<PanelHeaderBar $style={{ backgroundColor: "transparent", borderBottom: "none" }}>
<LabelSmall
color={theme.colors.contentPrimary}
color={t.textPrimary}
$style={{ fontWeight: 500, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px", lineHeight: 1 }}
>
<ListChecks size={14} />
@ -140,13 +143,13 @@ export const Sidebar = memo(function Sidebar({
width: "26px",
height: "26px",
borderRadius: "6px",
color: "#71717a",
color: t.textTertiary,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
})}
>
<PanelLeft size={14} />
@ -172,8 +175,8 @@ export const Sidebar = memo(function Sidebar({
width: "26px",
height: "26px",
borderRadius: "8px",
backgroundColor: newTaskRepos.length > 0 ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.06)",
color: "#e4e4e7",
backgroundColor: newTaskRepos.length > 0 ? t.borderMedium : t.interactiveHover,
color: t.textPrimary,
cursor: newTaskRepos.length > 0 ? "pointer" : "not-allowed",
display: "flex",
alignItems: "center",
@ -188,7 +191,7 @@ export const Sidebar = memo(function Sidebar({
</div>
</PanelHeaderBar>
<div className={css({ padding: "0 8px 8px", display: "flex", flexDirection: "column", gap: "6px" })}>
<LabelXSmall color={theme.colors.contentTertiary} $style={{ textTransform: "uppercase", letterSpacing: "0.04em" }}>
<LabelXSmall color={t.textTertiary} $style={{ textTransform: "uppercase", letterSpacing: "0.04em" }}>
Repo
</LabelXSmall>
<select
@ -200,9 +203,9 @@ export const Sidebar = memo(function Sidebar({
className={css({
width: "100%",
borderRadius: "8px",
border: "1px solid rgba(255, 255, 255, 0.10)",
backgroundColor: "rgba(255, 255, 255, 0.05)",
color: "#f4f4f5",
border: `1px solid ${t.borderDefault}`,
backgroundColor: t.interactiveHover,
color: t.textPrimary,
fontSize: "12px",
padding: "8px 10px",
outline: "none",
@ -258,7 +261,7 @@ export const Sidebar = memo(function Sidebar({
display: "flex",
flexDirection: "column",
gap: "4px",
borderTop: isDragOver ? "2px solid #ff4f00" : "2px solid transparent",
borderTop: isDragOver ? `2px solid ${t.accent}` : "2px solid transparent",
transition: "border-color 150ms ease",
})}
>
@ -294,7 +297,7 @@ export const Sidebar = memo(function Sidebar({
fontSize: "9px",
fontWeight: 700,
lineHeight: 1,
color: "#fff",
color: t.textOnAccent,
backgroundColor: projectIconColor(project.label),
})}
data-project-icon
@ -302,15 +305,11 @@ export const Sidebar = memo(function Sidebar({
{projectInitial(project.label)}
</span>
<span className={css({ position: "absolute", inset: 0, display: "none", alignItems: "center", justifyContent: "center" })} data-chevron>
{isCollapsed ? (
<ChevronDown size={12} color={theme.colors.contentTertiary} />
) : (
<ChevronUp size={12} color={theme.colors.contentTertiary} />
)}
{isCollapsed ? <ChevronDown size={12} color={t.textTertiary} /> : <ChevronUp size={12} color={t.textTertiary} />}
</span>
</div>
<LabelSmall
color={theme.colors.contentSecondary}
color={t.textSecondary}
$style={{
fontSize: "11px",
fontWeight: 700,
@ -324,7 +323,7 @@ export const Sidebar = memo(function Sidebar({
{project.label}
</LabelSmall>
</div>
{isCollapsed ? <LabelXSmall color={theme.colors.contentTertiary}>{formatRelativeAge(project.updatedAtMs)}</LabelXSmall> : null}
{isCollapsed ? <LabelXSmall color={t.textTertiary}>{formatRelativeAge(project.updatedAtMs)}</LabelXSmall> : null}
</div>
{!isCollapsed &&
@ -353,11 +352,11 @@ export const Sidebar = memo(function Sidebar({
padding: "8px 12px",
borderRadius: "8px",
border: "1px solid transparent",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
backgroundColor: isActive ? t.interactiveHover : "transparent",
cursor: "pointer",
transition: "all 200ms ease",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
backgroundColor: t.interactiveHover,
},
})}
>
@ -384,27 +383,27 @@ export const Sidebar = memo(function Sidebar({
minWidth: 0,
flexShrink: 1,
}}
color={hasUnread ? "#ffffff" : theme.colors.contentSecondary}
color={hasUnread ? t.textPrimary : t.textSecondary}
>
{task.title}
</LabelSmall>
{task.pullRequest != null ? (
<span className={css({ display: "inline-flex", alignItems: "center", gap: "4px", flexShrink: 0 })}>
<LabelXSmall color={theme.colors.contentSecondary} $style={{ fontWeight: 600 }}>
<LabelXSmall color={t.textSecondary} $style={{ fontWeight: 600 }}>
#{task.pullRequest.number}
</LabelXSmall>
{task.pullRequest.status === "draft" ? <CloudUpload size={11} color="#ff4f00" /> : null}
{task.pullRequest.status === "draft" ? <CloudUpload size={11} color={t.accent} /> : null}
</span>
) : (
<GitPullRequestDraft size={11} color={theme.colors.contentTertiary} />
<GitPullRequestDraft size={11} color={t.textTertiary} />
)}
{hasDiffs ? (
<div className={css({ display: "flex", gap: "4px", flexShrink: 0, marginLeft: "auto" })}>
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
<span className={css({ fontSize: "11px", color: t.statusSuccess })}>+{totalAdded}</span>
<span className={css({ fontSize: "11px", color: t.statusError })}>-{totalRemoved}</span>
</div>
) : null}
<LabelXSmall color={theme.colors.contentTertiary} $style={{ flexShrink: 0, marginLeft: hasDiffs ? undefined : "auto" }}>
<LabelXSmall color={t.textTertiary} $style={{ flexShrink: 0, marginLeft: hasDiffs ? undefined : "auto" }}>
{formatRelativeAge(task.updatedAtMs)}
</LabelXSmall>
</div>
@ -422,7 +421,7 @@ export const Sidebar = memo(function Sidebar({
);
});
const menuButtonStyle = (highlight: boolean) =>
const menuButtonStyle = (highlight: boolean, tokens: FoundryTokens) =>
({
display: "flex",
alignItems: "center",
@ -431,8 +430,8 @@ const menuButtonStyle = (highlight: boolean) =>
padding: "8px 12px",
borderRadius: "6px",
border: "none",
background: highlight ? "rgba(255, 255, 255, 0.06)" : "transparent",
color: "rgba(255, 255, 255, 0.75)",
background: highlight ? tokens.interactiveHover : "transparent",
color: tokens.textSecondary,
cursor: "pointer",
fontSize: "13px",
fontWeight: 400 as const,
@ -442,6 +441,7 @@ const menuButtonStyle = (highlight: boolean) =>
function SidebarFooter() {
const [css] = useStyletron();
const t = useFoundryTokens();
const navigate = useNavigate();
const client = useMockAppClient();
const snapshot = useMockAppSnapshot();
@ -547,9 +547,9 @@ function SidebarFooter() {
const popoverStyle = css({
borderRadius: "10px",
border: "1px solid rgba(255, 255, 255, 0.10)",
backgroundColor: "#18181b",
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
border: `1px solid ${t.borderDefault}`,
backgroundColor: t.surfaceElevated,
boxShadow: `${t.shadow}, 0 0 0 1px ${t.interactiveSubtle}`,
padding: "4px",
display: "flex",
flexDirection: "column",
@ -591,11 +591,11 @@ function SidebarFooter() {
type="button"
onClick={() => setWorkspaceFlyoutOpen((prev) => !prev)}
className={css({
...menuButtonStyle(workspaceFlyoutOpen),
...menuButtonStyle(workspaceFlyoutOpen, t),
fontWeight: 500,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
color: "#ffffff",
backgroundColor: t.interactiveHover,
color: t.textPrimary,
},
})}
>
@ -610,7 +610,7 @@ function SidebarFooter() {
justifyContent: "center",
fontSize: "9px",
fontWeight: 700,
color: "#ffffff",
color: t.textOnAccent,
flexShrink: 0,
})}
>
@ -619,7 +619,7 @@ function SidebarFooter() {
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
{organization.settings.displayName}
</span>
<ChevronRight size={12} className={css({ flexShrink: 0, color: "rgba(255, 255, 255, 0.35)" })} />
<ChevronRight size={12} className={css({ flexShrink: 0, color: t.textMuted })} />
</button>
</div>
) : null}
@ -663,12 +663,12 @@ function SidebarFooter() {
}
}}
className={css({
...menuButtonStyle(isActive),
...menuButtonStyle(isActive, t),
fontWeight: isActive ? 600 : 400,
color: isActive ? "#ffffff" : "rgba(255, 255, 255, 0.65)",
color: isActive ? t.textPrimary : t.textTertiary,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
color: "#ffffff",
backgroundColor: t.interactiveHover,
color: t.textPrimary,
},
})}
>
@ -683,7 +683,7 @@ function SidebarFooter() {
justifyContent: "center",
fontSize: "9px",
fontWeight: 700,
color: "#ffffff",
color: t.textOnAccent,
flexShrink: 0,
})}
>
@ -707,11 +707,11 @@ function SidebarFooter() {
type="button"
onClick={item.onClick}
className={css({
...menuButtonStyle(false),
color: item.danger ? "#ffa198" : "rgba(255, 255, 255, 0.75)",
...menuButtonStyle(false, t),
color: item.danger ? t.statusError : t.textSecondary,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
color: item.danger ? "#ff6b6b" : "#ffffff",
backgroundColor: t.interactiveHover,
color: item.danger ? t.statusError : t.textPrimary,
},
})}
>
@ -740,13 +740,13 @@ function SidebarFooter() {
height: "28px",
borderRadius: "6px",
border: "none",
background: open ? "rgba(255, 255, 255, 0.06)" : "transparent",
color: open ? "#ffffff" : "#71717a",
background: open ? t.interactiveHover : "transparent",
color: open ? t.textPrimary : t.textTertiary,
cursor: "pointer",
transition: "all 160ms ease",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
color: "#a1a1aa",
backgroundColor: t.interactiveHover,
color: t.textSecondary,
},
})}
>

View file

@ -3,6 +3,7 @@ import { useStyletron } from "baseui";
import { LabelXSmall } from "baseui/typography";
import { FileCode, Plus, X } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { ContextMenuOverlay, TabAvatar, useContextMenu } from "./ui";
import { diffTabId, fileName, type Task } from "./view-model";
@ -39,7 +40,8 @@ export const TabStrip = memo(function TabStrip({
onAddTab: () => void;
sidebarCollapsed?: boolean;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const isDesktop = !!import.meta.env.VITE_DESKTOP;
const contextMenu = useContextMenu();
@ -53,9 +55,9 @@ export const TabStrip = memo(function TabStrip({
className={css({
display: "flex",
alignItems: "stretch",
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
borderBottom: `1px solid ${t.borderDefault}`,
gap: "4px",
backgroundColor: "#09090b",
backgroundColor: t.surfacePrimary,
paddingLeft: sidebarCollapsed ? "14px" : "6px",
height: "41px",
minHeight: "41px",
@ -97,11 +99,11 @@ export const TabStrip = memo(function TabStrip({
marginTop: "6px",
marginBottom: "6px",
borderRadius: "8px",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
backgroundColor: isActive ? t.interactiveHover : "transparent",
cursor: "pointer",
transition: "color 200ms ease, background-color 200ms ease",
flexShrink: 0,
":hover": { color: "#e4e4e7", backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
":hover": { color: t.textPrimary, backgroundColor: isActive ? t.interactiveHover : t.interactiveSubtle },
})}
>
<div
@ -144,19 +146,19 @@ export const TabStrip = memo(function TabStrip({
maxWidth: "180px",
fontSize: "11px",
fontWeight: 600,
color: theme.colors.contentPrimary,
borderBottom: "1px solid rgba(255, 255, 255, 0.3)",
color: t.textPrimary,
borderBottom: `1px solid ${t.borderFocus}`,
})}
/>
) : (
<LabelXSmall color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} $style={{ fontWeight: 500 }}>
<LabelXSmall color={isActive ? t.textPrimary : t.textSecondary} $style={{ fontWeight: 500 }}>
{tab.sessionName}
</LabelXSmall>
)}
{task.tabs.length > 1 ? (
<X
size={11}
color={theme.colors.contentTertiary}
color={t.textTertiary}
data-tab-close
className={css({ cursor: "pointer", opacity: 0 })}
onClick={(event) => {
@ -190,23 +192,20 @@ export const TabStrip = memo(function TabStrip({
marginTop: "6px",
marginBottom: "6px",
borderRadius: "8px",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
backgroundColor: isActive ? t.interactiveHover : "transparent",
cursor: "pointer",
transition: "color 200ms ease, background-color 200ms ease",
flexShrink: 0,
":hover": { color: "#e4e4e7", backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
":hover": { color: t.textPrimary, backgroundColor: isActive ? t.interactiveHover : t.interactiveSubtle },
})}
>
<FileCode size={12} color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} />
<LabelXSmall
color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary}
$style={{ fontWeight: 500, fontFamily: '"IBM Plex Mono", monospace' }}
>
<FileCode size={12} color={isActive ? t.textPrimary : t.textSecondary} />
<LabelXSmall color={isActive ? t.textPrimary : t.textSecondary} $style={{ fontWeight: 500, fontFamily: '"IBM Plex Mono", monospace' }}>
{fileName(path)}
</LabelXSmall>
<X
size={11}
color={theme.colors.contentTertiary}
color={t.textTertiary}
data-tab-close
className={css({ cursor: "pointer", opacity: 0 })}
onClick={(event) => {
@ -230,7 +229,7 @@ export const TabStrip = memo(function TabStrip({
flexShrink: 0,
})}
>
<Plus size={14} color={theme.colors.contentTertiary} />
<Plus size={14} color={t.textTertiary} />
</div>
</div>
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}

View file

@ -2,6 +2,7 @@ import type { SandboxProcessRecord } from "@sandbox-agent/foundry-client";
import { ProcessTerminal } from "@sandbox-agent/react";
import { useQuery } from "@tanstack/react-query";
import { useStyletron } from "baseui";
import { useFoundryTokens } from "../../app/theme";
import { ChevronDown, Loader2, RefreshCw, Skull, SquareTerminal, Trash2, X } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { SandboxAgent } from "sandbox-agent";
@ -62,6 +63,7 @@ function formatProcessTabTitle(process: Pick<SandboxProcessRecord, "command" | "
export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
const [css] = useStyletron();
const t = useFoundryTokens();
const [activeTabId, setActiveTabId] = useState<string>(PROCESSES_TAB_ID);
const [processTabs, setProcessTabs] = useState<ProcessTab[]>([]);
const [selectedProcessId, setSelectedProcessId] = useState<string | null>(null);
@ -388,7 +390,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
alignItems: "center",
justifyContent: "center",
padding: "24px",
backgroundColor: "#080506",
backgroundColor: t.surfacePrimary,
});
const emptyCopyClassName = css({
@ -396,7 +398,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
display: "flex",
flexDirection: "column",
gap: "10px",
color: "rgba(255, 255, 255, 0.72)",
color: t.textSecondary,
fontSize: "12px",
lineHeight: 1.6,
textAlign: "center",
@ -412,13 +414,13 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
gap: "6px",
padding: "6px 10px",
borderRadius: "8px",
border: "1px solid rgba(255, 255, 255, 0.1)",
color: "#f4f4f5",
border: `1px solid ${t.borderDefault}`,
color: t.textPrimary,
cursor: "pointer",
fontSize: "11px",
fontWeight: 600,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.06)",
backgroundColor: t.interactiveHover,
},
":disabled": {
opacity: 0.45,
@ -445,7 +447,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
minHeight: 0,
display: "grid",
gridTemplateRows: "auto minmax(0, 1fr)",
backgroundColor: "#080506",
backgroundColor: t.surfacePrimary,
})}
>
<div
@ -454,7 +456,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
flexDirection: "column",
gap: "12px",
padding: "14px 14px 12px",
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
borderBottom: `1px solid ${t.borderSubtle}`,
})}
>
<div
@ -472,8 +474,8 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
gap: "2px",
})}
>
<strong className={css({ fontSize: "12px", color: "#f5f5f5" })}>Processes</strong>
<span className={css({ fontSize: "11px", color: "rgba(255, 255, 255, 0.56)" })}>
<strong className={css({ fontSize: "12px", color: t.textPrimary })}>Processes</strong>
<span className={css({ fontSize: "11px", color: t.textMuted })}>
Process lifecycle goes through the actor. Terminal transport goes straight to the sandbox.
</span>
</div>
@ -499,10 +501,10 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
<input
className={css({
width: "100%",
border: "1px solid rgba(255, 255, 255, 0.1)",
border: `1px solid ${t.borderDefault}`,
borderRadius: "8px",
backgroundColor: "#0d0a0b",
color: "#f4f4f5",
backgroundColor: t.surfaceTertiary,
color: t.textPrimary,
fontSize: "12px",
padding: "9px 10px",
})}
@ -516,10 +518,10 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
<input
className={css({
width: "100%",
border: "1px solid rgba(255, 255, 255, 0.1)",
border: `1px solid ${t.borderDefault}`,
borderRadius: "8px",
backgroundColor: "#0d0a0b",
color: "#f4f4f5",
backgroundColor: t.surfaceTertiary,
color: t.textPrimary,
fontSize: "12px",
padding: "9px 10px",
})}
@ -535,10 +537,10 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
width: "100%",
minHeight: "56px",
resize: "none",
border: "1px solid rgba(255, 255, 255, 0.1)",
border: `1px solid ${t.borderDefault}`,
borderRadius: "8px",
backgroundColor: "#0d0a0b",
color: "#f4f4f5",
backgroundColor: t.surfaceTertiary,
color: t.textPrimary,
fontSize: "12px",
padding: "9px 10px",
gridColumn: "1 / -1",
@ -552,7 +554,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
/>
</div>
<div className={css({ display: "flex", alignItems: "center", gap: "14px", fontSize: "11px", color: "rgba(255, 255, 255, 0.68)" })}>
<div className={css({ display: "flex", alignItems: "center", gap: "14px", fontSize: "11px", color: t.textSecondary })}>
<label className={css({ display: "flex", alignItems: "center", gap: "6px" })}>
<input
type="checkbox"
@ -584,7 +586,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
</button>
</div>
{createError ? <div className={css({ fontSize: "11px", color: "#fda4af" })}>{createError}</div> : null}
{createError ? <div className={css({ fontSize: "11px", color: t.statusError })}>{createError}</div> : null}
</div>
<div
@ -598,11 +600,11 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
className={css({
minHeight: 0,
overflowY: "auto",
borderRight: "1px solid rgba(255, 255, 255, 0.08)",
borderRight: `1px solid ${t.borderSubtle}`,
})}
>
{processes.length === 0 ? (
<div className={css({ padding: "16px", fontSize: "12px", color: "rgba(255,255,255,0.56)" })}>No processes yet.</div>
<div className={css({ padding: "16px", fontSize: "12px", color: t.textMuted })}>No processes yet.</div>
) : (
processes.map((process) => {
const isSelected = selectedProcessId === process.id;
@ -633,8 +635,8 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
gap: "8px",
padding: "12px 14px",
cursor: "pointer",
backgroundColor: isSelected ? "rgba(255, 255, 255, 0.06)" : "transparent",
borderBottom: "1px solid rgba(255, 255, 255, 0.06)",
backgroundColor: isSelected ? t.interactiveHover : "transparent",
borderBottom: `1px solid ${t.borderSubtle}`,
outline: "none",
":focus-visible": {
boxShadow: "inset 0 0 0 1px rgba(249, 115, 22, 0.85)",
@ -647,11 +649,11 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
width: "8px",
height: "8px",
borderRadius: "999px",
backgroundColor: process.status === "running" ? "#4ade80" : "#71717a",
backgroundColor: process.status === "running" ? t.statusSuccess : t.textTertiary,
flexShrink: 0,
})}
/>
<span className={css({ fontSize: "12px", color: "#f4f4f5", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
<span className={css({ fontSize: "12px", color: t.textPrimary, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
{formatCommandSummary(process)}
</span>
</div>
@ -662,7 +664,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
justifyContent: "space-between",
gap: "10px",
fontSize: "10px",
color: "rgba(255,255,255,0.5)",
color: t.textMuted,
})}
>
<span>{process.pid ? `PID ${process.pid}` : "PID ?"}</span>
@ -739,14 +741,14 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
flexDirection: "column",
gap: "8px",
padding: "14px",
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
borderBottom: `1px solid ${t.borderSubtle}`,
})}
>
<div className={css({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px" })}>
<strong className={css({ fontSize: "12px", color: "#f4f4f5" })}>{formatCommandSummary(selectedProcess)}</strong>
<span className={css({ fontSize: "10px", color: "rgba(255,255,255,0.56)" })}>{selectedProcess.status}</span>
<strong className={css({ fontSize: "12px", color: t.textPrimary })}>{formatCommandSummary(selectedProcess)}</strong>
<span className={css({ fontSize: "10px", color: t.textMuted })}>{selectedProcess.status}</span>
</div>
<div className={css({ display: "flex", flexWrap: "wrap", gap: "10px", fontSize: "10px", color: "rgba(255,255,255,0.5)" })}>
<div className={css({ display: "flex", flexWrap: "wrap", gap: "10px", fontSize: "10px", color: t.textMuted })}>
<span>{selectedProcess.pid ? `PID ${selectedProcess.pid}` : "PID ?"}</span>
<span>{selectedProcess.id}</span>
{selectedProcess.exitCode != null ? <span>exit={selectedProcess.exitCode}</span> : null}
@ -759,16 +761,16 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
alignItems: "center",
justifyContent: "space-between",
padding: "10px 14px",
borderBottom: "1px solid rgba(255,255,255,0.08)",
borderBottom: `1px solid ${t.borderSubtle}`,
})}
>
<span className={css({ fontSize: "11px", color: "rgba(255,255,255,0.68)" })}>Logs</span>
<span className={css({ fontSize: "11px", color: t.textSecondary })}>Logs</span>
<button type="button" className={smallButtonClassName} onClick={() => void refreshLogs()} disabled={logsLoading}>
{logsLoading ? <Loader2 size={11} className={css({ animation: "hf-spin 0.8s linear infinite" })} /> : <RefreshCw size={11} />}
Refresh
</button>
</div>
{logsError ? <div className={css({ padding: "14px", fontSize: "11px", color: "#fda4af" })}>{logsError}</div> : null}
{logsError ? <div className={css({ padding: "14px", fontSize: "11px", color: t.statusError })}>{logsError}</div> : null}
<pre
className={css({
flex: 1,
@ -778,7 +780,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
overflow: "auto",
fontSize: "11px",
lineHeight: 1.6,
color: "#d4d4d8",
color: t.textSecondary,
fontFamily: '"IBM Plex Mono", monospace',
})}
>
@ -827,7 +829,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
}
return (
<div className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", backgroundColor: "#080506" })}>
<div className={css({ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", backgroundColor: t.surfacePrimary })}>
<div
className={css({
display: "flex",
@ -835,9 +837,9 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
justifyContent: "space-between",
gap: "10px",
padding: "10px 14px",
borderBottom: "1px solid rgba(255,255,255,0.08)",
borderBottom: `1px solid ${t.borderSubtle}`,
fontSize: "11px",
color: "rgba(255,255,255,0.56)",
color: t.textMuted,
})}
>
<span>{formatCommandSummary(activeTerminalProcess)}</span>
@ -854,7 +856,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
minHeight: 0,
border: "none",
borderRadius: 0,
background: "#080506",
background: t.surfacePrimary,
}}
terminalStyle={{
minHeight: 0,
@ -910,7 +912,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
minHeight: 0,
display: "flex",
flexDirection: "column",
backgroundColor: "#080506",
backgroundColor: t.surfacePrimary,
overflow: "hidden",
})}
>
@ -921,9 +923,9 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
gap: "8px",
minHeight: "38px",
padding: "0 10px",
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
backgroundColor: "#090607",
color: "rgba(255, 255, 255, 0.72)",
borderBottom: `1px solid ${t.borderSubtle}`,
backgroundColor: t.surfaceTertiary,
color: t.textSecondary,
fontSize: "12px",
fontWeight: 600,
})}
@ -943,7 +945,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
justifyContent: "center",
width: "20px",
height: "20px",
color: "rgba(255, 255, 255, 0.56)",
color: t.textMuted,
})}
>
<ChevronDown size={14} />
@ -963,7 +965,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
alignItems: "center",
height: "100%",
padding: "0 10px",
color: activeTabId === PROCESSES_TAB_ID ? "#f5f5f5" : "rgba(255, 255, 255, 0.65)",
color: activeTabId === PROCESSES_TAB_ID ? t.textPrimary : t.textMuted,
cursor: "pointer",
":after":
activeTabId === PROCESSES_TAB_ID
@ -975,7 +977,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
bottom: 0,
height: "2px",
borderRadius: "999px",
backgroundColor: "#f5f5f5",
backgroundColor: t.textPrimary,
}
: undefined,
})}
@ -1008,7 +1010,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
gap: "6px",
height: "100%",
padding: "0 10px",
color: activeTabId === tab.id ? "#f5f5f5" : "rgba(255, 255, 255, 0.65)",
color: activeTabId === tab.id ? t.textPrimary : t.textMuted,
cursor: "pointer",
":after":
activeTabId === tab.id
@ -1020,7 +1022,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
bottom: 0,
height: "2px",
borderRadius: "999px",
backgroundColor: "#f5f5f5",
backgroundColor: t.textPrimary,
}
: undefined,
})}
@ -1044,7 +1046,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
width: "18px",
height: "18px",
marginRight: "4px",
color: "rgba(255, 255, 255, 0.42)",
color: t.textMuted,
cursor: "pointer",
})}
>
@ -1071,7 +1073,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
width: "28px",
height: "100%",
marginLeft: "2px",
color: "rgba(255, 255, 255, 0.72)",
color: t.textSecondary,
fontSize: "18px",
lineHeight: 1,
cursor: "pointer",

View file

@ -3,6 +3,7 @@ import { useStyletron } from "baseui";
import { LabelSmall } from "baseui/typography";
import { Clock, MailOpen, PanelLeft, PanelRight } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { PanelHeaderBar } from "./ui";
import { type AgentTab, type Task } from "./view-model";
@ -39,12 +40,13 @@ export const TranscriptHeader = memo(function TranscriptHeader({
rightSidebarCollapsed?: boolean;
onToggleRightSidebar?: () => void;
}) {
const [css, theme] = useStyletron();
const [css] = useStyletron();
const t = useFoundryTokens();
const isDesktop = !!import.meta.env.VITE_DESKTOP;
const needsTrafficLightInset = isDesktop && sidebarCollapsed;
return (
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", paddingLeft: needsTrafficLightInset ? "74px" : "14px" }}>
<PanelHeaderBar $style={{ backgroundColor: t.surfaceSecondary, borderBottom: "none", paddingLeft: needsTrafficLightInset ? "74px" : "14px" }}>
{sidebarCollapsed && onToggleSidebar ? (
<div
className={css({
@ -55,9 +57,9 @@ export const TranscriptHeader = memo(function TranscriptHeader({
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#71717a",
color: t.textTertiary,
flexShrink: 0,
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
})}
onClick={onToggleSidebar}
onMouseEnter={onSidebarPeekStart}
@ -89,8 +91,8 @@ export const TranscriptHeader = memo(function TranscriptHeader({
outline: "none",
fontWeight: 500,
fontSize: "14px",
color: theme.colors.contentPrimary,
borderBottom: "1px solid rgba(255, 255, 255, 0.3)",
color: t.textPrimary,
borderBottom: `1px solid ${t.borderFocus}`,
minWidth: "80px",
maxWidth: "300px",
})}
@ -98,7 +100,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
) : (
<LabelSmall
title="Rename"
color={theme.colors.contentPrimary}
color={t.textPrimary}
$style={{ fontWeight: 400, whiteSpace: "nowrap", cursor: "pointer", ":hover": { textDecoration: "underline" } }}
onClick={() => onStartEditingField("title", task.title)}
>
@ -127,9 +129,9 @@ export const TranscriptHeader = memo(function TranscriptHeader({
outline: "none",
padding: "2px 8px",
borderRadius: "999px",
border: "1px solid rgba(255, 255, 255, 0.3)",
backgroundColor: "rgba(255, 255, 255, 0.03)",
color: "#e4e4e7",
border: `1px solid ${t.borderFocus}`,
backgroundColor: t.interactiveSubtle,
color: t.textPrimary,
fontSize: "11px",
whiteSpace: "nowrap",
fontFamily: '"IBM Plex Mono", monospace',
@ -143,14 +145,14 @@ export const TranscriptHeader = memo(function TranscriptHeader({
className={css({
padding: "2px 8px",
borderRadius: "999px",
border: "1px solid rgba(255, 255, 255, 0.14)",
backgroundColor: "rgba(255, 255, 255, 0.03)",
color: "#e4e4e7",
border: `1px solid ${t.borderMedium}`,
backgroundColor: t.interactiveSubtle,
color: t.textPrimary,
fontSize: "11px",
whiteSpace: "nowrap",
fontFamily: '"IBM Plex Mono", monospace',
cursor: "pointer",
":hover": { borderColor: "rgba(255, 255, 255, 0.3)" },
":hover": { borderColor: t.borderFocus },
})}
>
{task.branch}
@ -165,12 +167,12 @@ export const TranscriptHeader = memo(function TranscriptHeader({
gap: "5px",
padding: "3px 10px",
borderRadius: "6px",
backgroundColor: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.08)",
backgroundColor: t.interactiveHover,
border: `1px solid ${t.borderSubtle}`,
fontSize: "11px",
fontWeight: 500,
lineHeight: 1,
color: theme.colors.contentSecondary,
color: t.textSecondary,
whiteSpace: "nowrap",
})}
>
@ -195,10 +197,10 @@ export const TranscriptHeader = memo(function TranscriptHeader({
fontSize: "11px",
fontWeight: 500,
lineHeight: 1,
color: theme.colors.contentSecondary,
color: t.textSecondary,
cursor: "pointer",
transition: "all 200ms ease",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
":hover": { backgroundColor: t.interactiveHover, color: t.textPrimary },
})}
>
<MailOpen size={12} style={{ flexShrink: 0 }} />{" "}
@ -215,9 +217,9 @@ export const TranscriptHeader = memo(function TranscriptHeader({
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#71717a",
color: t.textTertiary,
flexShrink: 0,
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
":hover": { color: t.textSecondary, backgroundColor: t.interactiveHover },
})}
onClick={onToggleRightSidebar}
>

View file

@ -2,6 +2,8 @@ import { memo, useCallback, useEffect, useState, type MouseEvent } from "react";
import { styled, useStyletron } from "baseui";
import { GitPullRequest, GitPullRequestDraft } from "lucide-react";
import { useFoundryTokens } from "../../app/theme";
import { getFoundryTokens } from "../../styles/tokens";
import type { AgentKind, AgentTab } from "./view-model";
export interface ContextMenuItem {
@ -43,6 +45,7 @@ export const ContextMenuOverlay = memo(function ContextMenuOverlay({
onClose: () => void;
}) {
const [css] = useStyletron();
const t = useFoundryTokens();
return (
<div
@ -51,12 +54,12 @@ export const ContextMenuOverlay = memo(function ContextMenuOverlay({
zIndex: 9999,
top: `${menu.y}px`,
left: `${menu.x}px`,
backgroundColor: "#1a1a1d",
border: "1px solid rgba(255, 255, 255, 0.18)",
backgroundColor: t.surfaceElevated,
border: `1px solid ${t.borderMedium}`,
borderRadius: "8px",
padding: "4px 0",
minWidth: "160px",
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.6)",
boxShadow: t.shadow,
})}
>
{menu.items.map((item, index) => (
@ -69,9 +72,9 @@ export const ContextMenuOverlay = memo(function ContextMenuOverlay({
className={css({
padding: "8px 14px",
fontSize: "12px",
color: "#e4e4e7",
color: t.textPrimary,
cursor: "pointer",
":hover": { backgroundColor: "rgba(255, 255, 255, 0.08)" },
":hover": { backgroundColor: t.interactiveHover },
})}
>
{item.label}
@ -82,14 +85,16 @@ export const ContextMenuOverlay = memo(function ContextMenuOverlay({
});
export const SpinnerDot = memo(function SpinnerDot({ size = 10 }: { size?: number }) {
const t = useFoundryTokens();
return (
<div
style={{
width: size,
height: size,
borderRadius: "50%",
border: "2px solid rgba(255, 79, 0, 0.25)",
borderTopColor: "#ff4f00",
border: `2px solid ${t.accentSubtle}`,
borderTopColor: t.accent,
animation: "hf-spin 0.8s linear infinite",
flexShrink: 0,
}}
@ -98,13 +103,15 @@ export const SpinnerDot = memo(function SpinnerDot({ size = 10 }: { size?: numbe
});
export const UnreadDot = memo(function UnreadDot() {
const t = useFoundryTokens();
return (
<div
style={{
width: 7,
height: 7,
borderRadius: "50%",
backgroundColor: "#ff4f00",
backgroundColor: t.accent,
flexShrink: 0,
}}
/>
@ -112,10 +119,12 @@ export const UnreadDot = memo(function UnreadDot() {
});
export const TaskIndicator = memo(function TaskIndicator({ isRunning, hasUnread, isDraft }: { isRunning: boolean; hasUnread: boolean; isDraft: boolean }) {
const t = useFoundryTokens();
if (isRunning) return <SpinnerDot size={8} />;
if (hasUnread) return <UnreadDot />;
if (isDraft) return <GitPullRequestDraft size={12} color="#a1a1aa" />;
return <GitPullRequest size={12} color="#7ee787" />;
if (isDraft) return <GitPullRequestDraft size={12} color={t.textSecondary} />;
return <GitPullRequest size={12} color={t.statusSuccess} />;
});
const ClaudeIcon = memo(function ClaudeIcon({ size = 14 }: { size?: number }) {
@ -130,21 +139,25 @@ const ClaudeIcon = memo(function ClaudeIcon({ size = 14 }: { size?: number }) {
});
const OpenAIIcon = memo(function OpenAIIcon({ size = 14 }: { size?: number }) {
const t = useFoundryTokens();
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364l2.0153-1.1639a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
fill="#ffffff"
fill={t.textPrimary}
/>
</svg>
);
});
const CursorIcon = memo(function CursorIcon({ size = 14 }: { size?: number }) {
const t = useFoundryTokens();
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
<rect x="3" y="3" width="18" height="18" rx="4" stroke="#A1A1AA" strokeWidth="1.5" />
<path d="M8 12h8M12 8v8" stroke="#A1A1AA" strokeWidth="1.5" strokeLinecap="round" />
<rect x="3" y="3" width="18" height="18" rx="4" stroke={t.textSecondary} strokeWidth="1.5" />
<path d="M8 12h8M12 8v8" stroke={t.textSecondary} strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
});
@ -166,21 +179,27 @@ export const TabAvatar = memo(function TabAvatar({ tab }: { tab: AgentTab }) {
return <AgentIcon agent={tab.agent} size={13} />;
});
export const Shell = styled("div", ({ $theme }) => ({
export const Shell = styled("div", ({ $theme }) => {
const t = getFoundryTokens($theme);
return {
display: "flex",
height: "100dvh",
backgroundColor: $theme.colors.backgroundSecondary,
backgroundColor: t.surfaceSecondary,
overflow: "hidden",
}));
};
});
export const SPanel = styled("section", ({ $theme }) => ({
export const SPanel = styled("section", ({ $theme }) => {
const t = getFoundryTokens($theme);
return {
minHeight: 0,
flex: 1,
display: "flex",
flexDirection: "column" as const,
backgroundColor: $theme.colors.backgroundSecondary,
backgroundColor: t.surfaceSecondary,
overflow: "hidden",
}));
};
});
export const ScrollBody = styled("div", () => ({
minHeight: 0,
@ -195,16 +214,19 @@ export const HEADER_HEIGHT = "42px";
export const PROMPT_TEXTAREA_MIN_HEIGHT = 56;
export const PROMPT_TEXTAREA_MAX_HEIGHT = 100;
export const PanelHeaderBar = styled("div", ({ $theme }) => ({
export const PanelHeaderBar = styled("div", ({ $theme }) => {
const t = getFoundryTokens($theme);
return {
display: "flex",
alignItems: "center",
minHeight: HEADER_HEIGHT,
maxHeight: HEADER_HEIGHT,
padding: "0 14px",
borderBottom: `1px solid ${$theme.colors.borderOpaque}`,
backgroundColor: $theme.colors.backgroundTertiary,
borderBottom: `1px solid ${t.borderDefault}`,
backgroundColor: t.surfaceTertiary,
gap: "8px",
flexShrink: 0,
position: "relative" as const,
zIndex: 9999,
}));
};
});

View file

@ -1,9 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { type FoundryBillingPlanId, type FoundryOrganization, type FoundryOrganizationMember, type FoundryUser } from "@sandbox-agent/foundry-shared";
import { useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Clock, CreditCard, FileText, Github, LogOut, Settings, Users } from "lucide-react";
import { ArrowLeft, Clock, CreditCard, FileText, Github, LogOut, Moon, Settings, Sun, Users } from "lucide-react";
import { activeMockUser, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../lib/mock-app";
import { isMockFrontendClient } from "../lib/env";
import { useColorMode, useFoundryTokens } from "../app/theme";
import type { FoundryTokens } from "../styles/tokens";
import { appSurfaceStyle, primaryButtonStyle, secondaryButtonStyle, subtleButtonStyle, cardStyle, badgeStyle, inputStyle } from "../styles/shared-styles";
const dateFormatter = new Intl.DateTimeFormat("en-US", {
month: "short",
@ -49,17 +52,6 @@ const taskHourPackages = [
{ hours: 1000, price: 120 },
];
function appSurfaceStyle(): React.CSSProperties {
return {
minHeight: "100dvh",
display: "flex",
flexDirection: "column",
background: "#09090b",
color: "#ffffff",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
};
}
function DesktopDragRegion() {
const isDesktop = !!import.meta.env.VITE_DESKTOP;
const onDragMouseDown = useCallback((event: React.PointerEvent) => {
@ -100,75 +92,6 @@ function DesktopDragRegion() {
);
}
function primaryButtonStyle(): React.CSSProperties {
return {
border: 0,
borderRadius: "6px",
padding: "6px 12px",
background: "#ffffff",
color: "#09090b",
fontWeight: 500,
fontSize: "12px",
cursor: "pointer",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
lineHeight: 1.4,
};
}
function secondaryButtonStyle(): React.CSSProperties {
return {
border: "1px solid rgba(255, 255, 255, 0.10)",
borderRadius: "6px",
padding: "5px 11px",
background: "rgba(255, 255, 255, 0.03)",
color: "#d4d4d8",
fontWeight: 500,
fontSize: "12px",
cursor: "pointer",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
lineHeight: 1.4,
};
}
function subtleButtonStyle(): React.CSSProperties {
return {
border: 0,
borderRadius: "6px",
padding: "6px 10px",
background: "rgba(255, 255, 255, 0.05)",
color: "#a1a1aa",
fontWeight: 500,
fontSize: "12px",
cursor: "pointer",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
lineHeight: 1.4,
};
}
function cardStyle(): React.CSSProperties {
return {
background: "#0f0f11",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "8px",
};
}
function badgeStyle(background: string, color = "#a1a1aa"): React.CSSProperties {
return {
display: "inline-flex",
alignItems: "center",
gap: "4px",
padding: "2px 6px",
borderRadius: "4px",
background,
color,
fontSize: "10px",
fontWeight: 500,
letterSpacing: "0.01em",
lineHeight: 1.4,
};
}
function formatDate(value: string | null): string {
if (!value) {
return "N/A";
@ -192,42 +115,44 @@ function checkoutPath(organization: FoundryOrganization, planId: FoundryBillingP
return `/organizations/${organization.id}/checkout/${planId}`;
}
function statusBadge(organization: FoundryOrganization) {
function statusBadge(t: FoundryTokens, organization: FoundryOrganization) {
if (organization.kind === "personal") {
return <span style={badgeStyle("rgba(24, 140, 255, 0.18)", "#b9d8ff")}>Personal workspace</span>;
return <span style={badgeStyle(t, "rgba(24, 140, 255, 0.18)", "#b9d8ff")}>Personal workspace</span>;
}
return <span style={badgeStyle("rgba(255, 79, 0, 0.16)", "#ffd6c7")}>GitHub organization</span>;
return <span style={badgeStyle(t, "rgba(255, 79, 0, 0.16)", "#ffd6c7")}>GitHub organization</span>;
}
function githubBadge(organization: FoundryOrganization) {
function githubBadge(t: FoundryTokens, organization: FoundryOrganization) {
if (organization.github.installationStatus === "connected") {
return <span style={badgeStyle("rgba(46, 160, 67, 0.16)", "#b7f0c3")}>GitHub connected</span>;
return <span style={badgeStyle(t, "rgba(46, 160, 67, 0.16)", "#b7f0c3")}>GitHub connected</span>;
}
if (organization.github.installationStatus === "reconnect_required") {
return <span style={badgeStyle("rgba(255, 193, 7, 0.18)", "#ffe6a6")}>Reconnect required</span>;
return <span style={badgeStyle(t, "rgba(255, 193, 7, 0.18)", "#ffe6a6")}>Reconnect required</span>;
}
return <span style={badgeStyle("rgba(255, 255, 255, 0.08)")}>Install GitHub App</span>;
return <span style={badgeStyle(t, t.borderSubtle)}>Install GitHub App</span>;
}
function StatCard({ label, value, caption }: { label: string; value: string; caption: string }) {
const t = useFoundryTokens();
return (
<div
style={{
...cardStyle(),
...cardStyle(t),
padding: "14px 16px",
display: "flex",
flexDirection: "column",
gap: "6px",
}}
>
<div style={{ fontSize: "10px", color: "#71717a", textTransform: "uppercase", letterSpacing: "0.04em" }}>{label}</div>
<div style={{ fontSize: "10px", color: t.textTertiary, textTransform: "uppercase", letterSpacing: "0.04em" }}>{label}</div>
<div style={{ fontSize: "16px", fontWeight: 600 }}>{value}</div>
<div style={{ fontSize: "11px", color: "#71717a", lineHeight: 1.5 }}>{caption}</div>
<div style={{ fontSize: "11px", color: t.textTertiary, lineHeight: 1.5 }}>{caption}</div>
</div>
);
}
function MemberRow({ member }: { member: FoundryOrganizationMember }) {
const t = useFoundryTokens();
return (
<div
style={{
@ -235,18 +160,19 @@ function MemberRow({ member }: { member: FoundryOrganizationMember }) {
gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) 100px",
gap: "10px",
padding: "8px 0",
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
borderTop: `1px solid ${t.borderSubtle}`,
alignItems: "center",
}}
>
<div>
<div style={{ fontWeight: 500, fontSize: "12px" }}>{member.name}</div>
<div style={{ color: "#a1a1aa", fontSize: "11px" }}>{member.email}</div>
<div style={{ color: t.textSecondary, fontSize: "11px" }}>{member.email}</div>
</div>
<div style={{ color: "#a1a1aa", fontSize: "12px", textTransform: "capitalize" }}>{member.role}</div>
<div style={{ color: t.textSecondary, fontSize: "12px", textTransform: "capitalize" }}>{member.role}</div>
<div>
<span
style={badgeStyle(
t,
member.state === "active" ? "rgba(46, 160, 67, 0.16)" : "rgba(255, 193, 7, 0.18)",
member.state === "active" ? "#b7f0c3" : "#ffe6a6",
)}
@ -261,6 +187,7 @@ function MemberRow({ member }: { member: FoundryOrganizationMember }) {
export function MockSignInPage() {
const client = useMockAppClient();
const navigate = useNavigate();
const t = useFoundryTokens();
return (
<div
@ -271,9 +198,9 @@ export function MockSignInPage() {
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "#09090b",
background: t.surfacePrimary,
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
color: "#ffffff",
color: t.textPrimary,
}}
>
<DesktopDragRegion />
@ -302,7 +229,7 @@ export function MockSignInPage() {
style={{
fontSize: "20px",
fontWeight: 600,
color: "#ffffff",
color: t.textPrimary,
margin: "0 0 8px 0",
letterSpacing: "-0.01em",
}}
@ -314,7 +241,7 @@ export function MockSignInPage() {
style={{
fontSize: "13px",
fontWeight: 400,
color: "#71717a",
color: t.textTertiary,
margin: "0 0 32px 0",
lineHeight: 1.5,
}}
@ -341,8 +268,8 @@ export function MockSignInPage() {
width: "100%",
height: "44px",
padding: "0 20px",
background: "#ffffff",
color: "#09090b",
background: t.textPrimary,
color: t.textOnPrimary,
border: "none",
borderRadius: "8px",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
@ -363,7 +290,7 @@ export function MockSignInPage() {
style={{
marginTop: "32px",
fontSize: "13px",
color: "#52525b",
color: t.textTertiary,
textDecoration: "none",
}}
>
@ -379,6 +306,7 @@ export function MockOrganizationSelectorPage() {
const snapshot = useMockAppSnapshot();
const organizations: FoundryOrganization[] = eligibleOrganizations(snapshot);
const navigate = useNavigate();
const t = useFoundryTokens();
return (
<div
@ -389,9 +317,9 @@ export function MockOrganizationSelectorPage() {
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "#09090b",
background: t.surfacePrimary,
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
color: "#ffffff",
color: t.textPrimary,
}}
>
<DesktopDragRegion />
@ -416,7 +344,7 @@ export function MockOrganizationSelectorPage() {
<rect x="19.25" y="18.25" width="91.5" height="91.5" rx="25.75" stroke="#F0F0F0" strokeWidth="8.5" />
</svg>
<h1 style={{ fontSize: "20px", fontWeight: 600, margin: "0 0 6px 0", letterSpacing: "-0.01em" }}>Select a workspace</h1>
<p style={{ fontSize: "13px", color: "#71717a", margin: 0 }}>Choose where you want to work.</p>
<p style={{ fontSize: "13px", color: t.textTertiary, margin: 0 }}>Choose where you want to work.</p>
</div>
{/* Workspace list */}
@ -425,7 +353,7 @@ export function MockOrganizationSelectorPage() {
display: "flex",
flexDirection: "column",
borderRadius: "12px",
border: "1px solid rgba(255, 255, 255, 0.08)",
border: `1px solid ${t.borderSubtle}`,
overflow: "hidden",
}}
>
@ -444,20 +372,20 @@ export function MockOrganizationSelectorPage() {
alignItems: "center",
gap: "14px",
padding: "16px 18px",
background: "#0f0f11",
background: t.surfaceSecondary,
border: "none",
borderTop: index > 0 ? "1px solid rgba(255, 255, 255, 0.06)" : "none",
color: "#ffffff",
borderTop: index > 0 ? `1px solid ${t.borderSubtle}` : "none",
color: t.textPrimary,
cursor: "pointer",
textAlign: "left",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
transition: "background 150ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255, 255, 255, 0.04)";
e.currentTarget.style.background = t.interactiveSubtle;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "#0f0f11";
e.currentTarget.style.background = t.surfaceSecondary;
}}
>
{/* Avatar */}
@ -481,7 +409,7 @@ export function MockOrganizationSelectorPage() {
{/* Info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: "14px", fontWeight: 500, lineHeight: 1.3 }}>{organization.settings.displayName}</div>
<div style={{ fontSize: "12px", color: "#71717a", lineHeight: 1.3, marginTop: "1px" }}>
<div style={{ fontSize: "12px", color: t.textTertiary, lineHeight: 1.3, marginTop: "1px" }}>
{organization.kind === "personal" ? "Personal" : "Organization"} · {planCatalog[organization.billing.planId]!.label} ·{" "}
{organization.members.length} member{organization.members.length !== 1 ? "s" : ""}
</div>
@ -493,7 +421,7 @@ export function MockOrganizationSelectorPage() {
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#52525b"
stroke={t.textTertiary}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
@ -518,7 +446,7 @@ export function MockOrganizationSelectorPage() {
style={{
background: "none",
border: "none",
color: "#52525b",
color: t.textTertiary,
fontSize: "13px",
cursor: "pointer",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
@ -536,6 +464,7 @@ export function MockOrganizationSelectorPage() {
type SettingsSection = "settings" | "members" | "billing" | "docs";
function SettingsNavItem({ icon, label, active, onClick }: { icon: React.ReactNode; label: string; active: boolean; onClick: () => void }) {
const t = useFoundryTokens();
return (
<button
type="button"
@ -548,8 +477,8 @@ function SettingsNavItem({ icon, label, active, onClick }: { icon: React.ReactNo
padding: "5px 10px",
borderRadius: "6px",
border: "none",
background: active ? "rgba(255, 255, 255, 0.06)" : "transparent",
color: active ? "#ffffff" : "rgba(255, 255, 255, 0.50)",
background: active ? t.interactiveHover : "transparent",
color: active ? t.textPrimary : t.textMuted,
cursor: "pointer",
fontSize: "12px",
fontWeight: active ? 500 : 400,
@ -559,7 +488,7 @@ function SettingsNavItem({ icon, label, active, onClick }: { icon: React.ReactNo
lineHeight: 1.4,
}}
onMouseEnter={(event) => {
if (!active) event.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.04)";
if (!active) event.currentTarget.style.backgroundColor = t.interactiveSubtle;
}}
onMouseLeave={(event) => {
if (!active) event.currentTarget.style.backgroundColor = "transparent";
@ -572,16 +501,18 @@ function SettingsNavItem({ icon, label, active, onClick }: { icon: React.ReactNo
}
function SettingsContentSection({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
const t = useFoundryTokens();
return (
<div>
<h2 style={{ margin: "0 0 2px", fontSize: "13px", fontWeight: 600, color: "#e4e4e7" }}>{title}</h2>
{description ? <p style={{ margin: "0 0 12px", fontSize: "11px", color: "rgba(255, 255, 255, 0.40)", lineHeight: 1.5 }}>{description}</p> : null}
<h2 style={{ margin: "0 0 2px", fontSize: "13px", fontWeight: 600, color: t.textPrimary }}>{title}</h2>
{description ? <p style={{ margin: "0 0 12px", fontSize: "11px", color: t.textMuted, lineHeight: 1.5 }}>{description}</p> : null}
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>{children}</div>
</div>
);
}
function SettingsRow({ label, description, action }: { label: string; description?: string; action?: React.ReactNode }) {
const t = useFoundryTokens();
return (
<div
style={{
@ -591,13 +522,13 @@ function SettingsRow({ label, description, action }: { label: string; descriptio
gap: "12px",
padding: "10px 12px",
borderRadius: "6px",
border: "1px solid rgba(255, 255, 255, 0.06)",
background: "rgba(255, 255, 255, 0.02)",
border: `1px solid ${t.borderSubtle}`,
background: t.interactiveSubtle,
}}
>
<div>
<div style={{ fontSize: "12px", fontWeight: 500 }}>{label}</div>
{description ? <div style={{ fontSize: "11px", color: "rgba(255, 255, 255, 0.40)", marginTop: "1px" }}>{description}</div> : null}
{description ? <div style={{ fontSize: "11px", color: t.textMuted, marginTop: "1px" }}>{description}</div> : null}
</div>
{action ?? null}
</div>
@ -619,6 +550,7 @@ function SettingsLayout({
const snapshot = useMockAppSnapshot();
const user = activeMockUser(snapshot);
const navigate = useNavigate();
const t = useFoundryTokens();
const navSections: Array<{ section: SettingsSection; icon: React.ReactNode; label: string }> = [
{ section: "settings", icon: <Settings size={13} />, label: "Settings" },
@ -628,7 +560,7 @@ function SettingsLayout({
];
return (
<div style={appSurfaceStyle()}>
<div style={appSurfaceStyle(t)}>
<DesktopDragRegion />
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
{/* Left nav */}
@ -636,7 +568,7 @@ function SettingsLayout({
style={{
width: "200px",
flexShrink: 0,
borderRight: "1px solid rgba(255, 255, 255, 0.06)",
borderRight: `1px solid ${t.borderSubtle}`,
padding: "44px 10px 16px",
display: "flex",
flexDirection: "column",
@ -654,7 +586,7 @@ function SettingsLayout({
})();
}}
style={{
...subtleButtonStyle(),
...subtleButtonStyle(t),
display: "flex",
alignItems: "center",
gap: "5px",
@ -669,7 +601,7 @@ function SettingsLayout({
{/* User header */}
<div style={{ padding: "2px 10px 12px", display: "flex", flexDirection: "column", gap: "1px" }}>
<span style={{ fontSize: "12px", fontWeight: 600 }}>{user?.name ?? "User"}</span>
<span style={{ fontSize: "10px", color: "rgba(255, 255, 255, 0.38)" }}>
<span style={{ fontSize: "10px", color: t.textMuted }}>
{planCatalog[organization.billing.planId]?.label ?? "Free"} Plan · {user?.email ?? ""}
</span>
</div>
@ -705,6 +637,7 @@ function SettingsLayout({
export function MockOrganizationSettingsPage({ organization }: { organization: FoundryOrganization }) {
const client = useMockAppClient();
const navigate = useNavigate();
const t = useFoundryTokens();
const [section, setSection] = useState<SettingsSection>("settings");
const [displayName, setDisplayName] = useState(organization.settings.displayName);
const [slug, setSlug] = useState(organization.settings.slug);
@ -726,17 +659,17 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
<SettingsContentSection title="Organization Profile">
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>Display name</span>
<input value={displayName} onChange={(event) => setDisplayName(event.target.value)} style={inputStyle()} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Display name</span>
<input value={displayName} onChange={(event) => setDisplayName(event.target.value)} style={inputStyle(t)} />
</label>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}>
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>Slug</span>
<input value={slug} onChange={(event) => setSlug(event.target.value)} style={inputStyle()} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Slug</span>
<input value={slug} onChange={(event) => setSlug(event.target.value)} style={inputStyle(t)} />
</label>
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>Primary domain</span>
<input value={primaryDomain} onChange={(event) => setPrimaryDomain(event.target.value)} style={inputStyle()} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Primary domain</span>
<input value={primaryDomain} onChange={(event) => setPrimaryDomain(event.target.value)} style={inputStyle(t)} />
</label>
</div>
<div>
@ -750,23 +683,25 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
primaryDomain,
})
}
style={primaryButtonStyle()}
style={primaryButtonStyle(t)}
>
Save changes
</button>
</div>
</SettingsContentSection>
<AppearanceSection />
<SettingsContentSection
title="GitHub"
description={`Connected as ${organization.github.connectedAccount}. ${organization.github.importedRepoCount} repos imported.`}
>
<SettingsRow label="Installation status" description={`Last sync: ${organization.github.lastSyncLabel}`} action={githubBadge(organization)} />
<SettingsRow label="Installation status" description={`Last sync: ${organization.github.lastSyncLabel}`} action={githubBadge(t, organization)} />
<div style={{ display: "flex", gap: "8px" }}>
<button type="button" onClick={() => void client.reconnectGithub(organization.id)} style={secondaryButtonStyle()}>
<button type="button" onClick={() => void client.reconnectGithub(organization.id)} style={secondaryButtonStyle(t)}>
Reconnect GitHub
</button>
<button type="button" onClick={() => void client.triggerGithubSync(organization.id)} style={subtleButtonStyle()}>
<button type="button" onClick={() => void client.triggerGithubSync(organization.id)} style={subtleButtonStyle(t)}>
Sync repos
</button>
</div>
@ -777,7 +712,7 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
label="Sandbox Agent connection"
description="Manage your Sandbox Agent integration and API keys."
action={
<button type="button" onClick={() => window.open("https://sandbox-agent.dev", "_blank", "noopener,noreferrer")} style={secondaryButtonStyle()}>
<button type="button" onClick={() => window.open("https://sandbox-agent.dev", "_blank", "noopener,noreferrer")} style={secondaryButtonStyle(t)}>
Configure
</button>
}
@ -792,9 +727,9 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
<button
type="button"
style={{
...secondaryButtonStyle(),
...secondaryButtonStyle(t),
borderColor: "rgba(255, 110, 110, 0.24)",
color: "#ffa198",
color: t.statusError,
}}
>
Delete
@ -809,7 +744,7 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
<div>
<h1 style={{ margin: "0 0 2px", fontSize: "15px", fontWeight: 600 }}>Members</h1>
<p style={{ margin: 0, fontSize: "11px", color: "rgba(255, 255, 255, 0.40)" }}>
<p style={{ margin: 0, fontSize: "11px", color: t.textMuted }}>
{organization.members.length} member{organization.members.length !== 1 ? "s" : ""}
</p>
</div>
@ -823,14 +758,14 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
{!organization.billing.stripeCustomerId.trim() ? (
<div
style={{
...cardStyle(),
...cardStyle(t),
padding: "20px",
border: "1px solid rgba(99, 102, 241, 0.3)",
background: "linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)",
}}
>
<div style={{ fontSize: "14px", fontWeight: 600, marginBottom: "4px" }}>Invite your team</div>
<div style={{ fontSize: "11px", color: "#a1a1aa", lineHeight: 1.6, marginBottom: "14px" }}>
<div style={{ fontSize: "11px", color: t.textSecondary, lineHeight: 1.6, marginBottom: "14px" }}>
Upgrade to Pro to add team members and unlock collaboration features:
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }}>
@ -842,11 +777,11 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
].map((feature) => (
<div key={feature} style={{ display: "flex", alignItems: "flex-start", gap: "8px" }}>
<span style={{ color: "#6366f1", fontSize: "14px", lineHeight: 1.2, flexShrink: 0 }}>+</span>
<span style={{ fontSize: "11px", color: "#d4d4d8", lineHeight: 1.5 }}>{feature}</span>
<span style={{ fontSize: "11px", color: t.textSecondary, lineHeight: 1.5 }}>{feature}</span>
</div>
))}
</div>
<button type="button" onClick={() => void navigate({ to: checkoutPath(organization, "team") })} style={primaryButtonStyle()}>
<button type="button" onClick={() => void navigate({ to: checkoutPath(organization, "team") })} style={primaryButtonStyle(t)}>
Upgrade to Pro $25/mo per seat
</button>
</div>
@ -858,13 +793,13 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
<div>
<h1 style={{ margin: "0 0 2px", fontSize: "15px", fontWeight: 600 }}>Docs</h1>
<p style={{ margin: 0, fontSize: "11px", color: "rgba(255, 255, 255, 0.40)" }}>Documentation and resources.</p>
<p style={{ margin: 0, fontSize: "11px", color: t.textMuted }}>Documentation and resources.</p>
</div>
<SettingsRow
label="Sandbox Agent Documentation"
description="Learn about Sandbox Agent features, APIs, and integrations."
action={
<button type="button" onClick={() => window.open("https://sandbox-agent.dev", "_blank", "noopener,noreferrer")} style={secondaryButtonStyle()}>
<button type="button" onClick={() => window.open("https://sandbox-agent.dev", "_blank", "noopener,noreferrer")} style={secondaryButtonStyle(t)}>
Open docs
</button>
}
@ -878,6 +813,7 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
export function MockOrganizationBillingPage({ organization }: { organization: FoundryOrganization }) {
const client = useMockAppClient();
const navigate = useNavigate();
const t = useFoundryTokens();
const hasStripeCustomer = organization.billing.stripeCustomerId.trim().length > 0;
const effectivePlanId: FoundryBillingPlanId = hasStripeCustomer ? organization.billing.planId : "free";
const currentPlan = planCatalog[effectivePlanId]!;
@ -894,7 +830,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
<div>
<h1 style={{ margin: "0 0 2px", fontSize: "15px", fontWeight: 600 }}>Billing & Invoices</h1>
<p style={{ margin: 0, fontSize: "11px", color: "rgba(255, 255, 255, 0.40)" }}>Manage your plan, task hours, and invoices.</p>
<p style={{ margin: 0, fontSize: "11px", color: t.textMuted }}>Manage your plan, task hours, and invoices.</p>
</div>
{/* Overview stats */}
@ -909,17 +845,17 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
</div>
{/* Task hours usage bar */}
<div style={{ ...cardStyle(), padding: "16px" }}>
<div style={{ ...cardStyle(t), padding: "16px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "10px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<Clock size={13} style={{ color: "#a1a1aa" }} />
<Clock size={13} style={{ color: t.textSecondary }} />
<span style={{ fontSize: "12px", fontWeight: 600 }}>Task Hours</span>
</div>
<span style={{ fontSize: "11px", color: "#a1a1aa" }}>
<span style={{ fontSize: "11px", color: t.textSecondary }}>
{taskHoursUsed.toFixed(1)} / {taskHoursIncluded}h used
</span>
</div>
<div style={{ height: "6px", borderRadius: "3px", backgroundColor: "rgba(255, 255, 255, 0.08)", overflow: "hidden" }}>
<div style={{ height: "6px", borderRadius: "3px", backgroundColor: t.borderSubtle, overflow: "hidden" }}>
<div
style={{
height: "100%",
@ -931,8 +867,8 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
/>
</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: "6px" }}>
<span style={{ fontSize: "10px", color: "#71717a" }}>Metered by the minute</span>
<span style={{ fontSize: "10px", color: "#71717a" }}>$0.12 / task hour overage</span>
<span style={{ fontSize: "10px", color: t.textTertiary }}>Metered by the minute</span>
<span style={{ fontSize: "10px", color: t.textTertiary }}>$0.12 / task hour overage</span>
</div>
</div>
@ -940,7 +876,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
{isFree ? (
<div
style={{
...cardStyle(),
...cardStyle(t),
padding: "18px",
border: "1px solid rgba(99, 102, 241, 0.3)",
background: "linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)",
@ -949,7 +885,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "16px" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<div style={{ fontSize: "14px", fontWeight: 600 }}>Upgrade to Pro</div>
<div style={{ fontSize: "11px", color: "#a1a1aa", lineHeight: 1.5 }}>
<div style={{ fontSize: "11px", color: t.textSecondary, lineHeight: 1.5 }}>
Get 200 task hours per month, plus the ability to purchase additional hours in bulk. Currently limited to {currentPlan.taskHours} hours on the
Free plan.
</div>
@ -957,7 +893,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
<button
type="button"
onClick={() => void navigate({ to: checkoutPath(organization, "team") })}
style={{ ...primaryButtonStyle(), whiteSpace: "nowrap", flexShrink: 0 }}
style={{ ...primaryButtonStyle(t), whiteSpace: "nowrap", flexShrink: 0 }}
>
Upgrade $25/mo
</button>
@ -976,7 +912,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
<div
key={pkg.hours}
style={{
...cardStyle(),
...cardStyle(t),
padding: "14px",
display: "flex",
flexDirection: "column",
@ -985,15 +921,15 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
transition: "border-color 150ms ease",
}}
onMouseEnter={(event) => {
(event.currentTarget as HTMLDivElement).style.borderColor = "rgba(255, 255, 255, 0.20)";
(event.currentTarget as HTMLDivElement).style.borderColor = t.borderMedium;
}}
onMouseLeave={(event) => {
(event.currentTarget as HTMLDivElement).style.borderColor = "rgba(255, 255, 255, 0.08)";
(event.currentTarget as HTMLDivElement).style.borderColor = t.borderSubtle;
}}
>
<div style={{ fontSize: "16px", fontWeight: 700 }}>{pkg.hours}h</div>
<div style={{ fontSize: "11px", color: "#a1a1aa" }}>${((pkg.price / pkg.hours) * 60).toFixed(1)}¢/min</div>
<button type="button" style={{ ...secondaryButtonStyle(), width: "100%", textAlign: "center", marginTop: "auto" }}>
<div style={{ fontSize: "11px", color: t.textSecondary }}>${((pkg.price / pkg.hours) * 60).toFixed(1)}¢/min</div>
<button type="button" style={{ ...secondaryButtonStyle(t), width: "100%", textAlign: "center", marginTop: "auto" }}>
${pkg.price}
</button>
</div>
@ -1011,16 +947,16 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
onClick={() =>
void (isMockFrontendClient ? navigate({ to: checkoutPath(organization, effectivePlanId) }) : client.openBillingPortal(organization.id))
}
style={secondaryButtonStyle()}
style={secondaryButtonStyle(t)}
>
{isMockFrontendClient ? "Open hosted checkout mock" : "Manage in Stripe"}
</button>
{organization.billing.status === "scheduled_cancel" ? (
<button type="button" onClick={() => void client.resumeSubscription(organization.id)} style={primaryButtonStyle()}>
<button type="button" onClick={() => void client.resumeSubscription(organization.id)} style={primaryButtonStyle(t)}>
Resume subscription
</button>
) : (
<button type="button" onClick={() => void client.cancelScheduledRenewal(organization.id)} style={subtleButtonStyle()}>
<button type="button" onClick={() => void client.cancelScheduledRenewal(organization.id)} style={subtleButtonStyle(t)}>
Cancel at period end
</button>
)}
@ -1031,7 +967,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
{/* Invoices */}
<SettingsContentSection title="Invoices" description="Recent billing activity.">
{organization.billing.invoices.length === 0 ? (
<div style={{ color: "#a1a1aa", fontSize: "11px" }}>No invoices yet.</div>
<div style={{ color: t.textSecondary, fontSize: "11px" }}>No invoices yet.</div>
) : (
<div style={{ display: "flex", flexDirection: "column" }}>
{organization.billing.invoices.map((invoice) => (
@ -1043,17 +979,18 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
gap: "10px",
alignItems: "center",
padding: "8px 0",
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
borderTop: `1px solid ${t.borderSubtle}`,
}}
>
<div>
<div style={{ fontSize: "12px", fontWeight: 500 }}>{invoice.label}</div>
<div style={{ fontSize: "10px", color: "#a1a1aa" }}>{invoice.issuedAt}</div>
<div style={{ fontSize: "10px", color: t.textSecondary }}>{invoice.issuedAt}</div>
</div>
<div style={{ fontSize: "12px", fontWeight: 500 }}>${invoice.amountUsd}</div>
<div>
<span
style={badgeStyle(
t,
invoice.status === "paid" ? "rgba(46, 160, 67, 0.16)" : "rgba(255, 193, 7, 0.18)",
invoice.status === "paid" ? "#b7f0c3" : "#ffe6a6",
)}
@ -1074,6 +1011,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
export function MockHostedCheckoutPage({ organization, planId }: { organization: FoundryOrganization; planId: FoundryBillingPlanId }) {
const client = useMockAppClient();
const navigate = useNavigate();
const t = useFoundryTokens();
const plan = planCatalog[planId]!;
return (
@ -1081,7 +1019,7 @@ export function MockHostedCheckoutPage({ organization, planId }: { organization:
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
<div>
<h1 style={{ margin: "0 0 2px", fontSize: "15px", fontWeight: 600 }}>Checkout {plan.label}</h1>
<p style={{ margin: 0, fontSize: "11px", color: "rgba(255, 255, 255, 0.40)" }}>Complete payment to activate the {plan.label} plan.</p>
<p style={{ margin: 0, fontSize: "11px", color: t.textMuted }}>Complete payment to activate the {plan.label} plan.</p>
</div>
<SettingsContentSection title="Order summary" description={`${organization.settings.displayName}${plan.label} plan.`}>
@ -1095,12 +1033,12 @@ export function MockHostedCheckoutPage({ organization, planId }: { organization:
<SettingsContentSection title="Card details">
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>Cardholder</span>
<input value={organization.settings.displayName} readOnly style={inputStyle()} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Cardholder</span>
<input value={organization.settings.displayName} readOnly style={inputStyle(t)} />
</label>
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>Card number</span>
<input value="4242 4242 4242 4242" readOnly style={inputStyle()} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Card number</span>
<input value="4242 4242 4242 4242" readOnly style={inputStyle(t)} />
</label>
<div style={{ display: "flex", gap: "8px" }}>
<button
@ -1113,11 +1051,11 @@ export function MockHostedCheckoutPage({ organization, planId }: { organization:
}
})();
}}
style={primaryButtonStyle()}
style={primaryButtonStyle(t)}
>
{isMockFrontendClient ? "Complete checkout" : "Continue to Stripe"}
</button>
<button type="button" onClick={() => void navigate({ to: billingPath(organization) })} style={subtleButtonStyle()}>
<button type="button" onClick={() => void navigate({ to: billingPath(organization) })} style={subtleButtonStyle(t)}>
Cancel
</button>
</div>
@ -1128,6 +1066,7 @@ export function MockHostedCheckoutPage({ organization, planId }: { organization:
}
function CheckoutLine({ label, value }: { label: string; value: string }) {
const t = useFoundryTokens();
return (
<div
style={{
@ -1136,10 +1075,10 @@ function CheckoutLine({ label, value }: { label: string; value: string }) {
justifyContent: "space-between",
gap: "10px",
padding: "7px 0",
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
borderTop: `1px solid ${t.borderSubtle}`,
}}
>
<div style={{ color: "#a1a1aa", fontSize: "11px" }}>{label}</div>
<div style={{ color: t.textSecondary, fontSize: "11px" }}>{label}</div>
<div style={{ fontSize: "12px", fontWeight: 500 }}>{value}</div>
</div>
);
@ -1150,6 +1089,7 @@ export function MockAccountSettingsPage() {
const snapshot = useMockAppSnapshot();
const user = activeMockUser(snapshot);
const navigate = useNavigate();
const t = useFoundryTokens();
const [name, setName] = useState(user?.name ?? "");
const [email, setEmail] = useState(user?.email ?? "");
@ -1159,7 +1099,7 @@ export function MockAccountSettingsPage() {
}, [user?.name, user?.email]);
return (
<div style={appSurfaceStyle()}>
<div style={appSurfaceStyle(t)}>
<DesktopDragRegion />
<div style={{ display: "flex", flex: 1, minHeight: 0 }}>
{/* Left nav */}
@ -1167,7 +1107,7 @@ export function MockAccountSettingsPage() {
style={{
width: "200px",
flexShrink: 0,
borderRight: "1px solid rgba(255, 255, 255, 0.06)",
borderRight: `1px solid ${t.borderSubtle}`,
padding: "44px 10px 16px",
display: "flex",
flexDirection: "column",
@ -1179,7 +1119,7 @@ export function MockAccountSettingsPage() {
type="button"
onClick={() => void navigate({ to: "/" })}
style={{
...subtleButtonStyle(),
...subtleButtonStyle(t),
display: "flex",
alignItems: "center",
gap: "5px",
@ -1193,7 +1133,7 @@ export function MockAccountSettingsPage() {
<div style={{ padding: "2px 10px 12px", display: "flex", flexDirection: "column", gap: "1px" }}>
<span style={{ fontSize: "12px", fontWeight: 600 }}>{user?.name ?? "User"}</span>
<span style={{ fontSize: "10px", color: "rgba(255, 255, 255, 0.38)" }}>{user?.email ?? ""}</span>
<span style={{ fontSize: "10px", color: t.textMuted }}>{user?.email ?? ""}</span>
</div>
<SettingsNavItem icon={<Settings size={13} />} label="General" active onClick={() => {}} />
@ -1205,24 +1145,24 @@ export function MockAccountSettingsPage() {
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
<div>
<h1 style={{ margin: "0 0 2px", fontSize: "15px", fontWeight: 600 }}>Account</h1>
<p style={{ margin: 0, fontSize: "11px", color: "rgba(255, 255, 255, 0.40)" }}>Manage your personal account settings.</p>
<p style={{ margin: 0, fontSize: "11px", color: t.textMuted }}>Manage your personal account settings.</p>
</div>
<SettingsContentSection title="Profile">
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>Display name</span>
<input value={name} onChange={(e) => setName(e.target.value)} style={inputStyle()} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Display name</span>
<input value={name} onChange={(e) => setName(e.target.value)} style={inputStyle(t)} />
</label>
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>Email</span>
<input value={email} onChange={(e) => setEmail(e.target.value)} style={inputStyle()} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>Email</span>
<input value={email} onChange={(e) => setEmail(e.target.value)} style={inputStyle(t)} />
</label>
<label style={{ display: "grid", gap: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 500, color: "rgba(255, 255, 255, 0.55)" }}>GitHub</span>
<input value={`@${user?.githubLogin ?? ""}`} readOnly style={{ ...inputStyle(), color: "rgba(255, 255, 255, 0.40)" }} />
<span style={{ fontSize: "11px", fontWeight: 500, color: t.textMuted }}>GitHub</span>
<input value={`@${user?.githubLogin ?? ""}`} readOnly style={{ ...inputStyle(t), color: t.textMuted }} />
</label>
<div>
<button type="button" style={primaryButtonStyle()}>
<button type="button" style={primaryButtonStyle(t)}>
Save changes
</button>
</div>
@ -1242,7 +1182,7 @@ export function MockAccountSettingsPage() {
await navigate({ to: "/signin" });
})();
}}
style={{ ...secondaryButtonStyle(), display: "inline-flex", alignItems: "center", gap: "6px" }}
style={{ ...secondaryButtonStyle(t), display: "inline-flex", alignItems: "center", gap: "6px" }}
>
<LogOut size={12} />
Sign out
@ -1258,9 +1198,9 @@ export function MockAccountSettingsPage() {
<button
type="button"
style={{
...secondaryButtonStyle(),
...secondaryButtonStyle(t),
borderColor: "rgba(255, 110, 110, 0.24)",
color: "#ffa198",
color: t.statusError,
whiteSpace: "nowrap",
flexShrink: 0,
}}
@ -1278,17 +1218,53 @@ export function MockAccountSettingsPage() {
);
}
function inputStyle(): React.CSSProperties {
return {
width: "100%",
borderRadius: "6px",
border: "1px solid rgba(255, 255, 255, 0.10)",
background: "rgba(255, 255, 255, 0.04)",
color: "#ffffff",
padding: "6px 10px",
fontSize: "12px",
fontFamily: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
outline: "none",
lineHeight: 1.5,
};
function AppearanceSection() {
const { colorMode, setColorMode } = useColorMode();
const t = useFoundryTokens();
const isDark = colorMode === "dark";
return (
<SettingsContentSection title="Appearance" description="Customize how Foundry looks.">
<SettingsRow
label="Light mode"
description={isDark ? "Currently using dark mode." : "Currently using light mode."}
action={
<button
type="button"
onClick={() => setColorMode(isDark ? "light" : "dark")}
style={{
position: "relative",
width: "36px",
height: "20px",
borderRadius: "10px",
border: "1px solid rgba(128, 128, 128, 0.3)",
background: isDark ? t.borderDefault : t.accent,
cursor: "pointer",
padding: 0,
transition: "background 0.2s",
flexShrink: 0,
}}
>
<div
style={{
position: "absolute",
top: "2px",
left: isDark ? "2px" : "16px",
width: "14px",
height: "14px",
borderRadius: "50%",
background: isDark ? t.textTertiary : "#ffffff",
transition: "left 0.2s, background 0.2s",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{isDark ? <Moon size={8} /> : <Sun size={8} color={t.accent} />}
</div>
</button>
}
/>
</SettingsContentSection>
);
}

View file

@ -1,4 +1,4 @@
import { StrictMode } from "react";
import { StrictMode, useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import { BaseProvider } from "baseui";
import { RouterProvider } from "@tanstack/react-router";
@ -6,7 +6,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Client as Styletron } from "styletron-engine-atomic";
import { Provider as StyletronProvider } from "styletron-react";
import { router } from "./app/router";
import { appTheme } from "./app/theme";
import { ColorModeCtx, darkTheme, getStoredColorMode, lightTheme, storeColorMode, type ColorMode } from "./app/theme";
import { applyCssTokens, getFoundryTokens } from "./styles/tokens";
import "./styles.css";
const queryClient = new QueryClient({
@ -20,14 +21,48 @@ const queryClient = new QueryClient({
const styletronEngine = new Styletron();
createRoot(document.getElementById("root")!).render(
<StrictMode>
function App() {
const [colorMode, setColorModeState] = useState<ColorMode>(getStoredColorMode);
const colorModeCtx = useMemo(
() => ({
colorMode,
setColorMode: (mode: ColorMode) => {
storeColorMode(mode);
setColorModeState(mode);
},
}),
[colorMode],
);
const theme = colorMode === "dark" ? darkTheme : lightTheme;
const tokens = getFoundryTokens(theme);
// Sync CSS custom properties and root element styles with color mode
useEffect(() => {
applyCssTokens(tokens);
document.documentElement.style.colorScheme = colorMode;
document.documentElement.style.background = tokens.surfacePrimary;
document.documentElement.style.color = tokens.textPrimary;
document.body.style.background = tokens.surfacePrimary;
document.body.style.color = tokens.textPrimary;
}, [colorMode, tokens]);
return (
<ColorModeCtx.Provider value={colorModeCtx}>
<StyletronProvider value={styletronEngine}>
<BaseProvider theme={appTheme}>
<BaseProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</BaseProvider>
</StyletronProvider>
</ColorModeCtx.Provider>
);
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -3,8 +3,8 @@
:root {
color-scheme: dark;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
background: #000000;
color: #ffffff;
background: var(--f-surface-primary, #000000);
color: var(--f-text-primary, #ffffff);
}
html,
@ -15,8 +15,8 @@ body,
body {
margin: 0;
background: #000000;
color: #ffffff;
background: var(--f-surface-primary, #000000);
color: var(--f-text-primary, #ffffff);
overflow: hidden;
}
@ -61,12 +61,12 @@ pre {
align-items: center;
gap: 8px;
padding: 8px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
background: #111111;
border-bottom: 1px solid var(--f-border-default, rgba(255, 255, 255, 0.12));
background: var(--f-surface-secondary, #111111);
}
.mock-diff-path {
color: #fafafa;
color: var(--f-text-primary, #fafafa);
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 13px;
font-weight: 600;
@ -80,11 +80,11 @@ pre {
}
.mock-diff-added {
color: #7ee787;
color: var(--f-status-success, #7ee787);
}
.mock-diff-removed {
color: #ffa198;
color: var(--f-status-error, #ffa198);
}
.mock-diff-body {
@ -109,11 +109,11 @@ pre {
.mock-diff-row[data-kind="hunk"] {
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
border-bottom: 1px solid var(--f-border-default, rgba(255, 255, 255, 0.12));
}
.mock-diff-row[data-kind="hunk"]:not(:first-child) {
border-top: 1px solid rgba(255, 255, 255, 0.12);
border-top: 1px solid var(--f-border-default, rgba(255, 255, 255, 0.12));
}
.mock-diff-gutter {
@ -129,7 +129,7 @@ pre {
.mock-diff-line-number {
display: block;
color: #71717a;
color: var(--f-text-tertiary, #71717a);
opacity: 0.5;
}
@ -137,7 +137,7 @@ pre {
flex: 1;
padding: 0 10px;
overflow: hidden;
color: #a1a1aa;
color: var(--f-text-secondary, #a1a1aa);
font-size: 12px;
font-weight: 400;
line-height: 20px;
@ -146,15 +146,15 @@ pre {
}
.mock-diff-row[data-kind="add"] .mock-diff-line-text {
color: #7ee787;
color: var(--f-status-success, #7ee787);
}
.mock-diff-row[data-kind="remove"] .mock-diff-line-text {
color: #ffa198;
color: var(--f-status-error, #ffa198);
}
.mock-diff-row[data-kind="hunk"] .mock-diff-line-text {
color: #71717a;
color: var(--f-text-tertiary, #71717a);
}
.mock-diff-row[data-kind="hunk"] .mock-diff-line-text {
@ -171,7 +171,7 @@ pre {
padding: 0;
border: 0;
background: transparent;
color: #ff4f00;
color: var(--f-accent, #ff4f00);
cursor: pointer;
opacity: 0;
pointer-events: none;
@ -185,7 +185,7 @@ pre {
.mock-diff-row:not([data-kind="hunk"]):hover .mock-diff-attach-button {
opacity: 1;
pointer-events: auto;
background: rgba(255, 79, 0, 0.1);
background: var(--f-accent-subtle, rgba(255, 79, 0, 0.1));
}
.mock-diff-row:not([data-kind="hunk"]):hover .mock-diff-line-number {
@ -198,7 +198,7 @@ pre {
}
.mock-diff-empty-copy {
color: #71717a;
color: var(--f-text-tertiary, #71717a);
font-size: 14px;
line-height: 1.4;
}

View file

@ -0,0 +1,105 @@
import type { CSSProperties } from "react";
import type { FoundryTokens } from "./tokens";
const fontFamily = "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif";
export function appSurfaceStyle(t: FoundryTokens): CSSProperties {
return {
minHeight: "100dvh",
display: "flex",
flexDirection: "column",
background: t.surfacePrimary,
color: t.textPrimary,
fontFamily,
};
}
export function primaryButtonStyle(t: FoundryTokens): CSSProperties {
return {
border: 0,
borderRadius: "6px",
padding: "6px 12px",
background: t.textPrimary,
color: t.textOnPrimary,
fontWeight: 500,
fontSize: "12px",
cursor: "pointer",
fontFamily,
lineHeight: 1.4,
};
}
export function secondaryButtonStyle(t: FoundryTokens): CSSProperties {
return {
border: `1px solid ${t.borderDefault}`,
borderRadius: "6px",
padding: "5px 11px",
background: t.interactiveSubtle,
color: t.textSecondary,
fontWeight: 500,
fontSize: "12px",
cursor: "pointer",
fontFamily,
lineHeight: 1.4,
};
}
export function subtleButtonStyle(t: FoundryTokens): CSSProperties {
return {
border: 0,
borderRadius: "6px",
padding: "6px 10px",
background: t.interactiveHover,
color: t.textSecondary,
fontWeight: 500,
fontSize: "12px",
cursor: "pointer",
fontFamily,
lineHeight: 1.4,
};
}
export function cardStyle(t: FoundryTokens): CSSProperties {
return {
background: t.surfaceSecondary,
border: `1px solid ${t.borderSubtle}`,
borderRadius: "8px",
};
}
export function badgeStyle(_t: FoundryTokens, background: string, color?: string): CSSProperties {
return {
display: "inline-flex",
alignItems: "center",
gap: "4px",
padding: "2px 6px",
borderRadius: "4px",
background,
color: color ?? _t.textSecondary,
fontSize: "10px",
fontWeight: 500,
};
}
export function inputStyle(t: FoundryTokens): CSSProperties {
return {
width: "100%",
borderRadius: "6px",
border: `1px solid ${t.borderDefault}`,
background: t.interactiveSubtle,
color: t.textPrimary,
padding: "6px 10px",
fontSize: "12px",
fontFamily,
outline: "none",
lineHeight: 1.5,
};
}
export function settingsLabelStyle(t: FoundryTokens): CSSProperties {
return {
fontSize: "11px",
fontWeight: 500,
color: t.textTertiary,
};
}

View file

@ -0,0 +1,141 @@
import type { Theme } from "baseui";
/**
* Semantic design tokens for the Foundry UI.
*
* These map visual intent to concrete color values and flip automatically
* when the BaseUI theme switches between dark and light.
*/
export interface FoundryTokens {
// ── Surfaces ──────────────────────────────────────────────────────────
surfacePrimary: string;
surfaceSecondary: string;
surfaceTertiary: string;
surfaceElevated: string;
// ── Interactive overlays ──────────────────────────────────────────────
interactiveHover: string;
interactiveActive: string;
interactiveSubtle: string;
// ── Borders ───────────────────────────────────────────────────────────
borderSubtle: string;
borderDefault: string;
borderMedium: string;
borderFocus: string;
// ── Text ──────────────────────────────────────────────────────────────
textPrimary: string;
textSecondary: string;
textTertiary: string;
textMuted: string;
textOnAccent: string;
textOnPrimary: string;
// ── Accent ────────────────────────────────────────────────────────────
accent: string;
accentSubtle: string;
// ── Status ────────────────────────────────────────────────────────────
statusSuccess: string;
statusError: string;
statusWarning: string;
// ── Misc ──────────────────────────────────────────────────────────────
shadow: string;
}
const darkTokens: FoundryTokens = {
surfacePrimary: "#09090b",
surfaceSecondary: "#0f0f11",
surfaceTertiary: "#0c0c0e",
surfaceElevated: "#1a1a1d",
interactiveHover: "rgba(255, 255, 255, 0.06)",
interactiveActive: "rgba(255, 255, 255, 0.10)",
interactiveSubtle: "rgba(255, 255, 255, 0.03)",
borderSubtle: "rgba(255, 255, 255, 0.06)",
borderDefault: "rgba(255, 255, 255, 0.10)",
borderMedium: "rgba(255, 255, 255, 0.14)",
borderFocus: "rgba(255, 255, 255, 0.30)",
textPrimary: "#ffffff",
textSecondary: "#a1a1aa",
textTertiary: "#71717a",
textMuted: "rgba(255, 255, 255, 0.40)",
textOnAccent: "#ffffff",
textOnPrimary: "#09090b",
accent: "#ff4f00",
accentSubtle: "rgba(255, 79, 0, 0.10)",
statusSuccess: "#7ee787",
statusError: "#ffa198",
statusWarning: "#fbbf24",
shadow: "0 4px 24px rgba(0, 0, 0, 0.5)",
};
const lightTokens: FoundryTokens = {
surfacePrimary: "#ffffff",
surfaceSecondary: "#f4f4f5",
surfaceTertiary: "#fafafa",
surfaceElevated: "#ffffff",
interactiveHover: "rgba(0, 0, 0, 0.04)",
interactiveActive: "rgba(0, 0, 0, 0.08)",
interactiveSubtle: "rgba(0, 0, 0, 0.02)",
borderSubtle: "rgba(0, 0, 0, 0.06)",
borderDefault: "rgba(0, 0, 0, 0.10)",
borderMedium: "rgba(0, 0, 0, 0.14)",
borderFocus: "rgba(0, 0, 0, 0.25)",
textPrimary: "#09090b",
textSecondary: "#52525b",
textTertiary: "#a1a1aa",
textMuted: "rgba(0, 0, 0, 0.40)",
textOnAccent: "#ffffff",
textOnPrimary: "#ffffff",
accent: "#ff4f00",
accentSubtle: "rgba(255, 79, 0, 0.08)",
statusSuccess: "#16a34a",
statusError: "#dc2626",
statusWarning: "#ca8a04",
shadow: "0 4px 24px rgba(0, 0, 0, 0.08)",
};
/**
* Resolve tokens from the active BaseUI theme.
* Works inside `styled()` callbacks where `$theme` is available.
*/
export function getFoundryTokens(theme: Theme): FoundryTokens {
// BaseUI dark themes have backgroundPrimary near black
const isDark = (theme.colors.backgroundPrimary ?? "#09090b").startsWith("#0");
return isDark ? darkTokens : lightTokens;
}
/**
* Inject CSS custom properties onto :root so styles.css can reference tokens.
*/
export function applyCssTokens(tokens: FoundryTokens) {
const root = document.documentElement;
root.style.setProperty("--f-surface-primary", tokens.surfacePrimary);
root.style.setProperty("--f-surface-secondary", tokens.surfaceSecondary);
root.style.setProperty("--f-surface-tertiary", tokens.surfaceTertiary);
root.style.setProperty("--f-text-primary", tokens.textPrimary);
root.style.setProperty("--f-text-secondary", tokens.textSecondary);
root.style.setProperty("--f-text-tertiary", tokens.textTertiary);
root.style.setProperty("--f-text-muted", tokens.textMuted);
root.style.setProperty("--f-border-subtle", tokens.borderSubtle);
root.style.setProperty("--f-border-default", tokens.borderDefault);
root.style.setProperty("--f-accent", tokens.accent);
root.style.setProperty("--f-accent-subtle", tokens.accentSubtle);
root.style.setProperty("--f-status-success", tokens.statusSuccess);
root.style.setProperty("--f-status-error", tokens.statusError);
root.style.setProperty("--f-interactive-hover", tokens.interactiveHover);
}