+
{filterIcon("tasks")}
Tasks
diff --git a/apps/webapp/app/components/runs/v3/RunTag.tsx b/apps/webapp/app/components/runs/v3/RunTag.tsx
index 3d6c6751ce3..1defae01674 100644
--- a/apps/webapp/app/components/runs/v3/RunTag.tsx
+++ b/apps/webapp/app/components/runs/v3/RunTag.tsx
@@ -1,5 +1,4 @@
import { useCallback, useMemo, useState } from "react";
-import tagLeftPath from "./tag-left.svg";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
@@ -7,6 +6,26 @@ import { ClipboardCheckIcon, ClipboardIcon, XIcon } from "lucide-react";
type Tag = string | { key: string; value: string };
+function TagNotch() {
+ return (
+
+ );
+}
+
export function RunTag({
tag,
to,
@@ -26,7 +45,7 @@ export function RunTag({
if (typeof tagResult === "string") {
return (
<>
-

+
{tag}
@@ -35,7 +54,7 @@ export function RunTag({
} else {
return (
<>
-

+
{tagResult.key}
diff --git a/apps/webapp/app/components/runs/v3/SpanEvents.tsx b/apps/webapp/app/components/runs/v3/SpanEvents.tsx
index c79a147b86f..069246c89b7 100644
--- a/apps/webapp/app/components/runs/v3/SpanEvents.tsx
+++ b/apps/webapp/app/components/runs/v3/SpanEvents.tsx
@@ -85,7 +85,7 @@ export function SpanEventError({
/>
{enhancedException.message && (
-
+
{enhancedException.message}
diff --git a/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx b/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx
index a82d8b9d0c6..3941283be40 100644
--- a/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx
+++ b/apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx
@@ -136,7 +136,9 @@ export function renderPart(part: UIMessage["parts"][number], i: number) {
return (
- {p.text ?? ""}
+
+ {p.text ?? ""}
+
);
diff --git a/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
index a5115897f97..cc02a280e0d 100644
--- a/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
+++ b/apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx
@@ -483,7 +483,7 @@ function SubAgentContent({ parts }: { parts: any[] }) {
if (partType === "reasoning" && part.text) {
return (
-
diff --git a/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx b/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
index 11ef7aa8a52..775efd7c308 100644
--- a/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
+++ b/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx
@@ -128,7 +128,7 @@ function MetricRow({
bold?: boolean;
}) {
return (
-
+
{label}
+
{label}
{value}
diff --git a/apps/webapp/app/hooks/useThemeColor.ts b/apps/webapp/app/hooks/useThemeColor.ts
index d78fd39fa79..61e1473e140 100644
--- a/apps/webapp/app/hooks/useThemeColor.ts
+++ b/apps/webapp/app/hooks/useThemeColor.ts
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
/**
* Normalize any CSS color (hex, oklch, hsl, ...) to rgb()/rgba() by rendering
@@ -17,16 +17,17 @@ function toRgb(color: string): string {
}
/**
- * Resolve a theme CSS variable to a concrete, animatable color once on mount.
+ * Resolve a theme CSS variable to a concrete, animatable color on mount.
* framer-motion can't interpolate `var()` strings or oklch values, so animated
- * colors must be resolved and normalized first. The fallback is used during
- * SSR and should match the default dark theme (see tailwind.css).
+ * colors must be resolved and normalized first. Resolution happens in an
+ * effect so server and hydration renders both use the fallback — resolving
+ * during render caused hydration style mismatches.
*/
export function useThemeColor(variable: `--${string}`, fallback: string): string {
- const [color] = useState(() => {
- if (typeof document === "undefined") return fallback;
+ const [color, setColor] = useState(fallback);
+ useEffect(() => {
const value = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
- return value ? toRgb(value) : fallback;
- });
+ if (value) setColor(toRgb(value));
+ }, [variable]);
return color;
}
diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx
index 66df9ddff53..0270e7be4ee 100644
--- a/apps/webapp/app/root.tsx
+++ b/apps/webapp/app/root.tsx
@@ -18,6 +18,7 @@ import { env } from "./env.server";
import { featuresForRequest } from "./features.server";
import { usePostHog } from "./hooks/usePostHog";
import { getUser } from "./services/session.server";
+import { flag } from "~/v3/featureFlags.server";
import { getTimezonePreference } from "./services/preferences/uiPreferences.server";
import { appEnvTitleTag } from "./utils";
@@ -63,6 +64,11 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
};
const user = await getUser(request);
+ // Theme switching is feature-flagged; while off, everyone stays on dark
+ // even if a preference was saved earlier.
+ const showThemeSwitcher = user
+ ? await flag({ key: "hasThemeSwitcher", defaultValue: false })
+ : false;
const headers = new Headers();
headers.append("Set-Cookie", await commitSession(session));
@@ -80,6 +86,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
triggerCliTag: env.TRIGGER_CLI_TAG,
kapa,
timezone,
+ showThemeSwitcher,
+ // Consumed by ResizablePanel: the browser check must match between SSR
+ // and hydration, so it is derived from the request user-agent.
+ isFirefox: /firefox/i.test(request.headers.get("user-agent") ?? ""),
},
{ headers }
);
@@ -121,12 +131,19 @@ export function ErrorBoundary() {
}
export default function App() {
- const { posthogProjectKey, posthogUiHost, kapa: _kapa } = useTypedLoaderData();
+ const {
+ posthogProjectKey,
+ posthogUiHost,
+ kapa: _kapa,
+ user,
+ showThemeSwitcher,
+ } = useTypedLoaderData();
usePostHog(posthogProjectKey, posthogUiHost);
+ const theme = (showThemeSwitcher ? user?.dashboardPreferences.theme : "dark") ?? "dark";
return (
<>
-
+
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
index e27b11b7545..37d7c58bc5e 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
@@ -11,7 +11,6 @@ import { Feedback } from "~/components/Feedback";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { EnvironmentSelector } from "~/components/navigation/EnvironmentSelector";
import { AnimatedNumber } from "~/components/primitives/AnimatedNumber";
-import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Header2 } from "~/components/primitives/Headers";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
@@ -445,21 +444,24 @@ function RateLimitTypeBadge({
switch (rateLimitType) {
case "tokenBucket":
return (
-
- Token bucket
-
+
);
case "fixedWindow":
return (
-
- Fixed window
-
+
);
case "slidingWindow":
return (
-
- Sliding window
-
+
);
default:
return null;
@@ -858,19 +860,32 @@ function getUsageColorClass(
}
}
+function RateLimitTypePill({ className, label }: { className: string; label: string }) {
+ return (
+
+ {label}
+
+ );
+}
+
function SourceBadge({ source }: { source: "default" | "plan" | "override" }) {
const variants: Record = {
default: {
label: "Default",
- className: "bg-indigo-500/20 text-indigo-400",
+ className: "bg-indigo-500/20 text-indigo-600 dark:text-indigo-400",
},
plan: {
label: "Plan",
- className: "bg-purple-500/20 text-purple-400",
+ className: "bg-purple-500/20 text-purple-600 dark:text-purple-400",
},
override: {
label: "Override",
- className: "bg-amber-500/20 text-amber-400",
+ className: "bg-amber-500/20 text-amber-700 dark:text-amber-400",
},
};
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
index 34d9a613d9d..aafb685f7d2 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
@@ -1482,7 +1482,7 @@ function TimelineView({
{(ms) => (
setFaviconError(false)}
/>
) : (
-
+
)}
{
return json && typeof json === "object" ? (json as Record) : {};
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx
index aeb54c4dfd8..487c43dc627 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx
@@ -688,7 +688,7 @@ function DomainList({ domains }: { domains: ReadonlyArray }) {
{d.domain}
{d.state === "failed" && d.verificationFailedReason && (
-
+
Reason: {d.verificationFailedReason}
)}
diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx
index b4b92c8a133..45e14a007b8 100644
--- a/apps/webapp/app/routes/account._index/route.tsx
+++ b/apps/webapp/app/routes/account._index/route.tsx
@@ -1,7 +1,14 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { conformZodMessage, parseWithZod } from "@conform-to/zod";
-import { Form, type MetaFunction, useActionData } from "@remix-run/react";
-import { type ActionFunction, json } from "@remix-run/server-runtime";
+import { MoonIcon, SunIcon } from "@heroicons/react/20/solid";
+import {
+ Form,
+ type MetaFunction,
+ useActionData,
+ useFetcher,
+ useLoaderData,
+} from "@remix-run/react";
+import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { AvatarCircleIcon } from "~/assets/icons/AvatarCircleIcon";
import { EnvelopeIcon } from "~/assets/icons/EnvelopeIcon";
@@ -13,6 +20,7 @@ import {
} from "~/components/layout/AppLayout";
import { Button } from "~/components/primitives/Buttons";
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
+import { Select, SelectItem } from "~/components/primitives/Select";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
@@ -26,7 +34,9 @@ import { prisma } from "~/db.server";
import { useUser } from "~/hooks/useUser";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { updateUser } from "~/models/user.server";
-import { requireUserId } from "~/services/session.server";
+import { updateThemePreference } from "~/services/dashboardPreferences.server";
+import { flag } from "~/v3/featureFlags.server";
+import { requireUser, requireUserId } from "~/services/session.server";
import { accountPath } from "~/utils/pathBuilder";
export const meta: MetaFunction = () => {
@@ -75,11 +85,28 @@ function createSchema(
});
}
+export async function loader({ request }: LoaderFunctionArgs) {
+ await requireUserId(request);
+ const showThemeSwitcher = await flag({ key: "hasThemeSwitcher", defaultValue: false });
+ return json({ showThemeSwitcher });
+}
+
export const action: ActionFunction = async ({ request }) => {
const userId = await requireUserId(request);
const formData = await request.formData();
+ if (formData.get("action") === "update-theme") {
+ const showThemeSwitcher = await flag({ key: "hasThemeSwitcher", defaultValue: false });
+ if (!showThemeSwitcher) {
+ return json({ error: "Not available" }, { status: 404 });
+ }
+ const user = await requireUser(request);
+ const theme = formData.get("theme") === "light" ? "light" : "dark";
+ await updateThemePreference({ user, theme });
+ return json({ success: true });
+ }
+
const formSchema = createSchema({
isEmailUnique: async (email) => {
const existingUser = await prisma.user.findFirst({
@@ -126,7 +153,14 @@ export const action: ActionFunction = async ({ request }) => {
export default function Page() {
const user = useUser();
+ const { showThemeSwitcher } = useLoaderData();
const lastSubmission = useActionData();
+ const themeFetcher = useFetcher();
+ const pendingTheme = themeFetcher.formData?.get("theme");
+ const theme =
+ typeof pendingTheme === "string"
+ ? (pendingTheme as "dark" | "light")
+ : (user.dashboardPreferences.theme ?? "dark");
const [form, { name, email, marketingEmails }] = useForm({
id: "account",
@@ -195,6 +229,61 @@ export default function Page() {
/>
+ {showThemeSwitcher && (
+ <>
+
+ Appearance
+
+
+
+
+
+ >
+ )}
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
index 4c8f77a582a..6db0b6ff566 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
@@ -1226,7 +1226,7 @@ function RunError({ error }: { error: TaskRunError }) {
{name}
{enhancedError.message && (
-
+
{enhancedError.message}
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx
index 7a5f61a48dc..b05d22439da 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx
@@ -1125,7 +1125,7 @@ function VercelSettingsPanel({
Failed to load Vercel settings
-
+
There was an error loading the Vercel integration settings. Please refresh the page to
try again.
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
index 7414960f47a..67e68e1360a 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
@@ -759,7 +759,7 @@ export function TierEnterprise() {
+
Contact us
}
diff --git a/apps/webapp/app/services/dashboardPreferences.server.ts b/apps/webapp/app/services/dashboardPreferences.server.ts
index 7af007dc381..fafd1438427 100644
--- a/apps/webapp/app/services/dashboardPreferences.server.ts
+++ b/apps/webapp/app/services/dashboardPreferences.server.ts
@@ -25,6 +25,7 @@ export type { SideMenuSectionId };
const DashboardPreferences = z.object({
version: z.literal("1"),
+ theme: z.enum(["dark", "light"]).optional(),
currentProjectId: z.string().optional(),
projects: z.record(
z.string(),
@@ -101,6 +102,32 @@ export async function updateCurrentProjectEnvironmentId({
});
}
+export async function updateThemePreference({
+ user,
+ theme,
+}: {
+ user: UserFromSession;
+ theme: "dark" | "light";
+}) {
+ if (user.isImpersonating) {
+ return;
+ }
+
+ if (user.dashboardPreferences.theme === theme) {
+ return;
+ }
+
+ const updatedPreferences: DashboardPreferences = {
+ ...user.dashboardPreferences,
+ theme,
+ };
+
+ return prisma.user.update({
+ where: { id: user.id },
+ data: { dashboardPreferences: updatedPreferences },
+ });
+}
+
export async function clearCurrentProject({ user }: { user: UserFromSession }) {
if (user.isImpersonating) {
return;
diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css
index 5a6429fce80..8bf9c3e1305 100644
--- a/apps/webapp/app/tailwind.css
+++ b/apps/webapp/app/tailwind.css
@@ -158,6 +158,7 @@
--color-surface-control: var(--color-charcoal-600);
--color-surface-control-hover: var(--color-charcoal-550);
--color-surface-control-active: var(--color-charcoal-500);
+ --color-input-bg: var(--color-charcoal-750);
/* Borders, from subtlest to most visible */
--color-grid-dimmed: var(--color-charcoal-750);
@@ -599,3 +600,168 @@
@apply leading-relaxed;
}
}
+
+/*
+ Light theme. Overrides the themable variables only; raw palettes stay put.
+ Code/editor values come from the trigger.light VS Code theme.
+*/
+[data-theme="light"] {
+ --color-secondary: #ffffff;
+ --color-input-bg: #ffffff;
+
+ /* shadcn vars consumed by charts/streamdown (tooltip cursor fill etc.) */
+ --background: #ffffff;
+ --foreground: #1a1b1f;
+ --muted: #eceef1;
+ --muted-foreground: #5f6570;
+ --border: #e2e4e9;
+ --sidebar: #f6f7f8;
+ --primary-foreground: #ffffff;
+
+ /* Text */
+ --color-primary: var(--color-apple-600);
+ --color-tertiary: #eef0f3;
+ --color-text-link: var(--color-lavender-600);
+ --color-text-faint: var(--color-charcoal-400);
+ --color-text-dimmed: var(--color-charcoal-500);
+ --color-text-bright: var(--color-charcoal-800);
+
+ /* Surfaces */
+ --color-background-deep: #f1f2f4;
+ --color-background-dimmed: #fbfbfc;
+ --color-background-bright: #ffffff;
+ --color-background-hover: #f2f3f5;
+ --color-background-raised: #e9eaee;
+ --color-surface-control: #dcdee3;
+ --color-surface-control-hover: #cfd2d9;
+ --color-surface-control-active: #b8bcc6;
+
+ /* Borders */
+ --color-grid-dimmed: #eceef1;
+ --color-grid-bright: #e2e4e9;
+ --color-border-bright: #d2d5db;
+ --color-border-brighter: #b9bdc7;
+ --color-border-brightest: #9ba1ad;
+
+ /* Status - darker steps for contrast on white */
+ --color-success: var(--color-mint-600);
+ --color-warning: var(--color-amber-600);
+ --color-dev: var(--color-pink-600);
+ --color-prod: var(--color-mint-600);
+ --color-staging: var(--color-orange-600);
+ --color-preview: var(--color-yellow-700);
+
+ /* Neutral run statuses: soft light grays for sparkbars and charts */
+ --color-run-pending: #ccd0d6;
+ --color-run-delayed: #d3d6db;
+ --color-run-waiting-to-resume: #c5c9d0;
+ --color-run-canceled: #d8dbdf;
+ --color-run-expired: #dee0e4;
+
+ /* Icons that fail contrast on white */
+ --color-schedules: var(--color-yellow-600);
+ --color-previewBranches: var(--color-yellow-600);
+ --color-customDashboards: var(--color-charcoal-500);
+
+ /* Callouts - deep tints instead of the dark theme's pastels */
+ --color-callout-warning-text: var(--color-yellow-800);
+ --color-callout-error-text: var(--color-rose-700);
+ --color-callout-success-text: var(--color-green-800);
+ --color-callout-docs-text: var(--color-blue-800);
+ --color-callout-pending-bg: var(--color-blue-100);
+ --color-callout-pending-text: var(--color-blue-800);
+ --color-callout-pricing-bg: var(--color-indigo-100);
+ --color-callout-pricing-text: var(--color-indigo-800);
+
+ /* Code syntax - trigger.light */
+ --color-code-background: #ffffff;
+ --color-code-foreground: #333333;
+ --color-code-line-number: #a8a8ad;
+ --color-code-plain: #2e2e4b;
+ --color-code-muted: #333333;
+ --color-code-comment: #767a81;
+ --color-code-keyword: #b114d3;
+ --color-code-storage: #b114d3;
+ --color-code-type: #b114d3;
+ --color-code-function: #6532f5;
+ --color-code-variable: #404040;
+ --color-code-constant: #1e1e1e;
+ --color-code-language: #b114d3;
+ --color-code-object-key: #222222;
+ --color-code-string: #262626;
+ --color-code-template-punctuation: #0879e2;
+ --color-code-number: #262626;
+ --color-code-builtin: #3080e0;
+ --color-code-attribute: #222222;
+ --color-code-escape: #222222;
+ --color-code-regexp: #dc3545;
+ --color-code-regexp-constant: #5f6570;
+ --color-code-invalid: #dc3545;
+ --color-code-deleted: #dc3545;
+ --color-code-jsx-text: #2e2e4b;
+
+ /* CodeMirror - trigger.light */
+ --color-editor-background: #ffffff;
+ --color-editor-foreground: #2e2e4b;
+ --color-editor-keyword: #b114d3;
+ --color-editor-name: #197c3c;
+ --color-editor-function: #6532f5;
+ --color-editor-constant: #3080e0;
+ --color-editor-type: #2980b9;
+ --color-editor-operator: #333333;
+ --color-editor-comment: #767a81;
+ --color-editor-heading: #2c3e50;
+ --color-editor-string: #262626;
+ --color-editor-invalid: #dc3545;
+ --color-editor-panel-background: #f4f4f6;
+ --color-editor-highlight-background: #f8f8fa;
+ --color-editor-tooltip-background: #ffffff;
+ --color-editor-selection: #d9dce3;
+ --color-editor-cursor: #2e2e4b;
+ --color-editor-search-match: #0879e226;
+ --color-editor-search-match-outline: #0879e2;
+ --color-editor-search-match-selected: #0879e240;
+ --color-editor-selection-match: #e8e8ed;
+ --color-editor-matching-bracket: rgba(240, 241, 244, 0.9);
+ --color-editor-matching-bracket-outline: rgba(160, 166, 180, 0.5);
+ --color-editor-fold-placeholder: #555555;
+ --color-editor-scrollbar-track-active: #e8e9ec;
+ --color-editor-scrollbar-thumb: #c9ccd4;
+ --color-editor-scrollbar-thumb-active: #aeb3be;
+}
+
+/* Streamdown's muted surface has no semantic token (charcoal-775); theme it here */
+[data-theme="light"] .streamdown-container {
+ --muted: #eceef1;
+}
+
+/* The timeline label shadow is a dark-theme legibility aid; drop it on light */
+[data-theme="light"] .text-shadow-custom {
+ text-shadow: none;
+}
+
+/* Neutral timeline points: invert to a light dot with a gray ring */
+[data-theme="light"] .timeline-point.bg-surface-control-active {
+ border-color: var(--color-surface-control-active);
+ background-color: var(--color-background-bright);
+}
+
+/* Run timeline bars: no fade gradient on light */
+[data-theme="light"] .timeline-span {
+ background-image: none;
+}
+/* On saturated bars the duration label keeps the dark-theme treatment,
+ but only when the bar is wide enough to contain the label — on narrow
+ bars the sticky label overflows onto the page background, where the
+ default dark-on-light text is correct. */
+[data-theme="light"] .timeline-span.bg-success,
+[data-theme="light"] .timeline-span.bg-error {
+ container-type: inline-size;
+}
+@container (min-width: 3.5rem) {
+ [data-theme="light"] .timeline-span.bg-success .text-shadow-custom,
+ [data-theme="light"] .timeline-span.bg-error .text-shadow-custom {
+ color: #ffffff;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
+ }
+}
diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts
index 637830aef06..9a6270fa6d3 100644
--- a/apps/webapp/app/v3/featureFlags.ts
+++ b/apps/webapp/app/v3/featureFlags.ts
@@ -10,6 +10,7 @@ export const FEATURE_FLAG = {
hasComputeAccess: "hasComputeAccess",
hasPrivateConnections: "hasPrivateConnections",
hasSso: "hasSso",
+ hasThemeSwitcher: "hasThemeSwitcher",
mollifierEnabled: "mollifierEnabled",
workerQueueScheduledSplitEnabled: "workerQueueScheduledSplitEnabled",
realtimeBackend: "realtimeBackend",
@@ -33,6 +34,8 @@ export const FeatureFlagCatalog = {
[FEATURE_FLAG.hasComputeAccess]: z.coerce.boolean(),
[FEATURE_FLAG.hasPrivateConnections]: z.coerce.boolean(),
[FEATURE_FLAG.hasSso]: z.coerce.boolean(),
+ // Gates the Interface theme setting in /account. Off by default.
+ [FEATURE_FLAG.hasThemeSwitcher]: z.coerce.boolean(),
[FEATURE_FLAG.mollifierEnabled]: z.coerce.boolean(),
[FEATURE_FLAG.workerQueueScheduledSplitEnabled]: z.coerce.boolean(),
// Which backend serves the realtime run feed. Controllable