mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 12:04:15 +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,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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue