mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
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:
parent
ed6e6f6fa5
commit
f09b9090bb
17 changed files with 887 additions and 523 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 }) => ({
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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={theme}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</BaseProvider>
|
||||
</StyletronProvider>
|
||||
</ColorModeCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<StyletronProvider value={styletronEngine}>
|
||||
<BaseProvider theme={appTheme}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</BaseProvider>
|
||||
</StyletronProvider>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
105
foundry/packages/frontend/src/styles/shared-styles.ts
Normal file
105
foundry/packages/frontend/src/styles/shared-styles.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
141
foundry/packages/frontend/src/styles/tokens.ts
Normal file
141
foundry/packages/frontend/src/styles/tokens.ts
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue