diff --git a/foundry/packages/frontend/src/app/theme.ts b/foundry/packages/frontend/src/app/theme.ts index debc0fc..5b62453 100644 --- a/foundry/packages/frontend/src/app/theme.ts +++ b/foundry/packages/frontend/src/app/theme.ts @@ -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({ + 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 + } +} diff --git a/foundry/packages/frontend/src/components/mock-layout.tsx b/foundry/packages/frontend/src/components/mock-layout.tsx index a8dec5d..5a72d5e 100644 --- a/foundry/packages/frontend/src/components/mock-layout.tsx +++ b/foundry/packages/frontend/src/components/mock-layout.tsx @@ -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("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}`, }} > void; }) { const [css] = useStyletron(); + const t = useFoundryTokens(); const railRef = useRef(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, })} >
@@ -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}`, })} > @@ -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, }} >
{organization.settings.displayName} - {organization.settings.primaryDomain} + {organization.settings.primaryDomain}
{leftSidebarOpen ? : null} - + {!leftSidebarOpen || !rightSidebarOpen ? (
{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, }} diff --git a/foundry/packages/frontend/src/components/mock-layout/history-minimap.tsx b/foundry/packages/frontend/src/components/mock-layout/history-minimap.tsx index b62faa1..e744d28 100644 --- a/foundry/packages/frontend/src/components/mock-layout/history-minimap.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/history-minimap.tsx @@ -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(events[events.length - 1]?.id ?? null); @@ -42,10 +44,10 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: })} >
- + Task Events - {events.length} + {events.length}
{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}
- {event.sessionName} + {event.sessionName}
- {formatMessageTimestamp(event.createdAtMs)} + {formatMessageTimestamp(event.createdAtMs)} ); })} @@ -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", })} diff --git a/foundry/packages/frontend/src/components/mock-layout/message-list.tsx b/foundry/packages/frontend/src/components/mock-layout/message-list.tsx index 5a5b4ba..b493e1a 100644 --- a/foundry/packages/frontend/src/components/mock-layout/message-list.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/message-list.tsx @@ -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 ? ( - + {displayFooter} ) : 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 }, })} > @@ -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( @@ -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", })} > - + {!tab.created ? "Choose an agent and model, then send your first message" : "No messages yet in this session"}
@@ -241,15 +244,15 @@ export const MessageList = memo(function MessageList({ renderThinkingState={() => (
- + Agent is thinking {thinkingTimerLabel ? ( void; close: () => void; }) { - const [css, theme] = useStyletron(); + const [css] = useStyletron(); + const t = useFoundryTokens(); const [hoveredId, setHoveredId] = useState(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 }, })} > {model.label} - {isDefault ? : null} + {isDefault ? : null} {!isDefault && isHovered ? ( { 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)} diff --git a/foundry/packages/frontend/src/components/mock-layout/prompt-composer.tsx b/foundry/packages/frontend/src/components/mock-layout/prompt-composer.tsx index a63c600..08d72ae 100644 --- a/foundry/packages/frontend/src/components/mock-layout/prompt-composer.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/prompt-composer.tsx @@ -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 = { 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, })} > diff --git a/foundry/packages/frontend/src/components/mock-layout/right-sidebar.tsx b/foundry/packages/frontend/src/components/mock-layout/right-sidebar.tsx index 65b057b..d3c67f3 100644 --- a/foundry/packages/frontend/src/components/mock-layout/right-sidebar.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/right-sidebar.tsx @@ -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; }) { - const [css, theme] = useStyletron(); + const [css] = useStyletron(); + const t = useFoundryTokens(); const [collapsed, setCollapsed] = useState>(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({ ) : ( - + )} {node.name}
@@ -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 ( - - + +
{!isTerminal ? (
@@ -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 }, })} > @@ -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 }, })} > @@ -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 }, })} > @@ -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 }, })} > @@ -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({
{task.fileChanges.length === 0 ? (
- No changes yet + No changes yet
) : 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 (
@@ -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', })} > - +{file.added} - -{file.removed} + +{file.added} + -{file.removed} {file.type}
@@ -446,7 +449,7 @@ export const RightSidebar = memo(function RightSidebar({ ) : (
- No files yet + No files yet
)}
diff --git a/foundry/packages/frontend/src/components/mock-layout/sidebar.tsx b/foundry/packages/frontend/src/components/mock-layout/sidebar.tsx index a665289..6df6326 100644 --- a/foundry/packages/frontend/src/components/mock-layout/sidebar.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/sidebar.tsx @@ -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>({}); const dragIndexRef = useRef(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 }, })} > @@ -122,7 +125,7 @@ export const Sidebar = memo(function Sidebar({ ) : null} @@ -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 }, })} > @@ -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({
- + Repo
-
+
- {createError ?
{createError}
: null} + {createError ?
{createError}
: null}
{processes.length === 0 ? ( -
No processes yet.
+
No processes yet.
) : ( 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, })} /> - + {formatCommandSummary(process)}
@@ -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, })} > {process.pid ? `PID ${process.pid}` : "PID ?"} @@ -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}`, })} >
- {formatCommandSummary(selectedProcess)} - {selectedProcess.status} + {formatCommandSummary(selectedProcess)} + {selectedProcess.status}
-
+
{selectedProcess.pid ? `PID ${selectedProcess.pid}` : "PID ?"} {selectedProcess.id} {selectedProcess.exitCode != null ? exit={selectedProcess.exitCode} : 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}`, })} > - Logs + Logs
- {logsError ?
{logsError}
: null} + {logsError ?
{logsError}
: null}
@@ -827,7 +829,7 @@ export function TerminalPane({ workspaceId, taskId }: TerminalPaneProps) {
     }
 
     return (
-      
+
{formatCommandSummary(activeTerminalProcess)} @@ -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, })} > @@ -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", diff --git a/foundry/packages/frontend/src/components/mock-layout/transcript-header.tsx b/foundry/packages/frontend/src/components/mock-layout/transcript-header.tsx index 0364b9d..08c6130 100644 --- a/foundry/packages/frontend/src/components/mock-layout/transcript-header.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/transcript-header.tsx @@ -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 ( - + {sidebarCollapsed && onToggleSidebar ? (
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 }, })} > {" "} @@ -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} > diff --git a/foundry/packages/frontend/src/components/mock-layout/ui.tsx b/foundry/packages/frontend/src/components/mock-layout/ui.tsx index 8f432a0..66722f1 100644 --- a/foundry/packages/frontend/src/components/mock-layout/ui.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/ui.tsx @@ -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 (
{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 (
@@ -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 ; if (hasUnread) return ; - if (isDraft) return ; - return ; + if (isDraft) return ; + return ; }); 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 ( ); }); const CursorIcon = memo(function CursorIcon({ size = 14 }: { size?: number }) { + const t = useFoundryTokens(); + return ( - - + + ); }); @@ -166,21 +179,27 @@ export const TabAvatar = memo(function TabAvatar({ tab }: { tab: AgentTab }) { return ; }); -export const Shell = styled("div", ({ $theme }) => ({ - display: "flex", - height: "100dvh", - backgroundColor: $theme.colors.backgroundSecondary, - overflow: "hidden", -})); +export const Shell = styled("div", ({ $theme }) => { + const t = getFoundryTokens($theme); + return { + display: "flex", + height: "100dvh", + backgroundColor: t.surfaceSecondary, + overflow: "hidden", + }; +}); -export const SPanel = styled("section", ({ $theme }) => ({ - minHeight: 0, - flex: 1, - display: "flex", - flexDirection: "column" as const, - backgroundColor: $theme.colors.backgroundSecondary, - overflow: "hidden", -})); +export const SPanel = styled("section", ({ $theme }) => { + const t = getFoundryTokens($theme); + return { + minHeight: 0, + flex: 1, + display: "flex", + flexDirection: "column" as const, + 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 }) => ({ - display: "flex", - alignItems: "center", - minHeight: HEADER_HEIGHT, - maxHeight: HEADER_HEIGHT, - padding: "0 14px", - borderBottom: `1px solid ${$theme.colors.borderOpaque}`, - backgroundColor: $theme.colors.backgroundTertiary, - gap: "8px", - flexShrink: 0, - position: "relative" as const, - zIndex: 9999, -})); +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 ${t.borderDefault}`, + backgroundColor: t.surfaceTertiary, + gap: "8px", + flexShrink: 0, + position: "relative" as const, + zIndex: 9999, + }; +}); diff --git a/foundry/packages/frontend/src/components/mock-onboarding.tsx b/foundry/packages/frontend/src/components/mock-onboarding.tsx index 5a44c9d..f583397 100644 --- a/foundry/packages/frontend/src/components/mock-onboarding.tsx +++ b/foundry/packages/frontend/src/components/mock-onboarding.tsx @@ -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 Personal workspace; + return Personal workspace; } - return GitHub organization; + return GitHub organization; } -function githubBadge(organization: FoundryOrganization) { +function githubBadge(t: FoundryTokens, organization: FoundryOrganization) { if (organization.github.installationStatus === "connected") { - return GitHub connected; + return GitHub connected; } if (organization.github.installationStatus === "reconnect_required") { - return Reconnect required; + return Reconnect required; } - return Install GitHub App; + return Install GitHub App; } function StatCard({ label, value, caption }: { label: string; value: string; caption: string }) { + const t = useFoundryTokens(); return (
-
{label}
+
{label}
{value}
-
{caption}
+
{caption}
); } function MemberRow({ member }: { member: FoundryOrganizationMember }) { + const t = useFoundryTokens(); return (
{member.name}
-
{member.email}
+
{member.email}
-
{member.role}
+
{member.role}
@@ -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 (
@@ -416,7 +344,7 @@ export function MockOrganizationSelectorPage() {

Select a workspace

-

Choose where you want to work.

+

Choose where you want to work.

{/* 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 */}
{organization.settings.displayName}
-
+
{organization.kind === "personal" ? "Personal" : "Organization"} · {planCatalog[organization.billing.planId]!.label} ·{" "} {organization.members.length} member{organization.members.length !== 1 ? "s" : ""}
@@ -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 (
+ + - +
- -
@@ -777,7 +712,7 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F label="Sandbox Agent connection" description="Manage your Sandbox Agent integration and API keys." action={ - } @@ -792,9 +727,9 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F
@@ -858,13 +793,13 @@ export function MockOrganizationSettingsPage({ organization }: { organization: F

Docs

-

Documentation and resources.

+

Documentation and resources.

window.open("https://sandbox-agent.dev", "_blank", "noopener,noreferrer")} style={secondaryButtonStyle()}> + } @@ -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

Billing & Invoices

-

Manage your plan, task hours, and invoices.

+

Manage your plan, task hours, and invoices.

{/* Overview stats */} @@ -909,17 +845,17 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
{/* Task hours usage bar */} -
+
- + Task Hours
- + {taskHoursUsed.toFixed(1)} / {taskHoursIncluded}h used
-
+
- Metered by the minute - $0.12 / task hour overage + Metered by the minute + $0.12 / task hour overage
@@ -940,7 +876,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo {isFree ? (
Upgrade to Pro
-
+
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.
@@ -957,7 +893,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo @@ -976,7 +912,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo
{ - (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; }} >
{pkg.hours}h
-
${((pkg.price / pkg.hours) * 60).toFixed(1)}¢/min
-
@@ -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"} {organization.billing.status === "scheduled_cancel" ? ( - ) : ( - )} @@ -1031,7 +967,7 @@ export function MockOrganizationBillingPage({ organization }: { organization: Fo {/* Invoices */} {organization.billing.invoices.length === 0 ? ( -
No invoices yet.
+
No invoices yet.
) : (
{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}`, }} >
{invoice.label}
-
{invoice.issuedAt}
+
{invoice.issuedAt}
${invoice.amountUsd}

Checkout {plan.label}

-

Complete payment to activate the {plan.label} plan.

+

Complete payment to activate the {plan.label} plan.

@@ -1095,12 +1033,12 @@ export function MockHostedCheckoutPage({ organization, planId }: { organization:
-
@@ -1128,6 +1066,7 @@ export function MockHostedCheckoutPage({ organization, planId }: { organization: } function CheckoutLine({ label, value }: { label: string; value: string }) { + const t = useFoundryTokens(); return (
-
{label}
+
{label}
{value}
); @@ -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 ( -
+
{/* 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() {
{user?.name ?? "User"} - {user?.email ?? ""} + {user?.email ?? ""}
} label="General" active onClick={() => {}} /> @@ -1205,24 +1145,24 @@ export function MockAccountSettingsPage() {

Account

-

Manage your personal account settings.

+

Manage your personal account settings.

-
@@ -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" }} > Sign out @@ -1258,9 +1198,9 @@ export function MockAccountSettingsPage() { + } + /> +
+ ); } diff --git a/foundry/packages/frontend/src/main.tsx b/foundry/packages/frontend/src/main.tsx index 45c98f4..9c5acbb 100644 --- a/foundry/packages/frontend/src/main.tsx +++ b/foundry/packages/frontend/src/main.tsx @@ -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(); +function App() { + const [colorMode, setColorModeState] = useState(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 ( + + + + + + + + + + ); +} + createRoot(document.getElementById("root")!).render( - - - - - - - + , ); diff --git a/foundry/packages/frontend/src/styles.css b/foundry/packages/frontend/src/styles.css index 8f634e6..9967938 100644 --- a/foundry/packages/frontend/src/styles.css +++ b/foundry/packages/frontend/src/styles.css @@ -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; } diff --git a/foundry/packages/frontend/src/styles/shared-styles.ts b/foundry/packages/frontend/src/styles/shared-styles.ts new file mode 100644 index 0000000..c3f225f --- /dev/null +++ b/foundry/packages/frontend/src/styles/shared-styles.ts @@ -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, + }; +} diff --git a/foundry/packages/frontend/src/styles/tokens.ts b/foundry/packages/frontend/src/styles/tokens.ts new file mode 100644 index 0000000..85101a7 --- /dev/null +++ b/foundry/packages/frontend/src/styles/tokens.ts @@ -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); +}