From 3ccd96f15056fcc72ac7537987c8a15c30610715 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Thu, 9 Jul 2026 15:16:43 +0000 Subject: [PATCH] feat(webapp): add light theme behind feature flag - semantic token overrides for light, trigger.light editor/code palettes - Interface theme dropdown on /account, gated by hasThemeSwitcher flag - unify light surfaces: buttons, inputs, radios, checkboxes, tabs, clipboard fields - fix checkbox :read-only override and useThemeColor hydration mismatch - resolve Firefox panel animation check from request UA --- .server-changes/light-theme.md | 6 + apps/webapp/app/components/AskAI.tsx | 2 +- .../app/components/admin/debugTooltip.tsx | 2 +- .../billing/AnimatedOrgBannerBar.tsx | 4 +- .../billing/BillingLimitRecoveryPanel.tsx | 2 +- .../app/components/billing/UsageBar.tsx | 2 +- .../app/components/code/ChartConfigPanel.tsx | 4 +- .../components/primitives/AppliedFilter.tsx | 4 +- .../app/components/primitives/Avatar.tsx | 2 +- .../app/components/primitives/Buttons.tsx | 22 ++- .../app/components/primitives/Checkbox.tsx | 14 +- .../app/components/primitives/ClientTabs.tsx | 4 +- .../components/primitives/ClipboardField.tsx | 8 +- .../app/components/primitives/Input.tsx | 8 +- .../app/components/primitives/RadioButton.tsx | 14 +- .../app/components/primitives/Resizable.tsx | 32 +++- .../primitives/SegmentedControl.tsx | 5 +- .../app/components/primitives/Select.tsx | 2 +- .../app/components/primitives/Switch.tsx | 7 +- .../app/components/runs/v3/RunFilters.tsx | 4 +- apps/webapp/app/components/runs/v3/RunTag.tsx | 25 ++- .../app/components/runs/v3/SpanEvents.tsx | 2 +- .../runs/v3/agent/AgentMessageView.tsx | 4 +- .../components/runs/v3/ai/AIChatMessages.tsx | 2 +- .../components/runs/v3/ai/AIModelSummary.tsx | 2 +- .../components/runs/v3/ai/SpanMetricRow.tsx | 2 +- apps/webapp/app/hooks/useThemeColor.ts | 17 +- apps/webapp/app/root.tsx | 21 ++- .../route.tsx | 41 +++-- .../route.tsx | 4 +- .../route.tsx | 4 +- .../route.tsx | 2 +- .../app/routes/account._index/route.tsx | 95 +++++++++- .../route.tsx | 2 +- ...cts.$projectParam.env.$envParam.vercel.tsx | 2 +- ...ces.orgs.$organizationSlug.select-plan.tsx | 2 +- .../services/dashboardPreferences.server.ts | 27 +++ apps/webapp/app/tailwind.css | 166 ++++++++++++++++++ apps/webapp/app/v3/featureFlags.ts | 3 + 39 files changed, 468 insertions(+), 103 deletions(-) create mode 100644 .server-changes/light-theme.md diff --git a/.server-changes/light-theme.md b/.server-changes/light-theme.md new file mode 100644 index 0000000000..ceaae7914f --- /dev/null +++ b/.server-changes/light-theme.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: feature +--- + +Adds an opt-in light theme behind a feature flag. When enabled, an Interface theme setting appears on the account page (dark stays the default). Code editors and syntax highlighting use the trigger.light palette. diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index d62ffa5b33..e7008e114e 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -389,7 +389,7 @@ function ChatMessages({
Error generating answer: - + {error} If the problem persists after retrying, please contact support. diff --git a/apps/webapp/app/components/admin/debugTooltip.tsx b/apps/webapp/app/components/admin/debugTooltip.tsx index b4ccb74f88..0b0954ab36 100644 --- a/apps/webapp/app/components/admin/debugTooltip.tsx +++ b/apps/webapp/app/components/admin/debugTooltip.tsx @@ -40,7 +40,7 @@ function Content({ children }: { children: React.ReactNode }) { const user = useUser(); return ( -
+
User ID diff --git a/apps/webapp/app/components/billing/AnimatedOrgBannerBar.tsx b/apps/webapp/app/components/billing/AnimatedOrgBannerBar.tsx index b0f11b7eba..58dcc27b3b 100644 --- a/apps/webapp/app/components/billing/AnimatedOrgBannerBar.tsx +++ b/apps/webapp/app/components/billing/AnimatedOrgBannerBar.tsx @@ -44,7 +44,9 @@ export function AnimatedOrgBannerBar({ /> {children} diff --git a/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx b/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx index 53b3c1695d..1dfcdd7370 100644 --- a/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx +++ b/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx @@ -113,7 +113,7 @@ export function BillingLimitRecoveryPanel({
Action required - + {isGrace ? ( <> Your organization has reached its billing limit. Processing is paused and new runs diff --git a/apps/webapp/app/components/billing/UsageBar.tsx b/apps/webapp/app/components/billing/UsageBar.tsx index 4934649228..fcb6377757 100644 --- a/apps/webapp/app/components/billing/UsageBar.tsx +++ b/apps/webapp/app/components/billing/UsageBar.tsx @@ -67,7 +67,7 @@ export function UsageBar({ current, billingLimit, tierLimit, isPaying }: UsageBa animate={{ width: tierRunLimitPercentage + "%" }} transition={{ duration: 1.5, type: "spring" }} style={{ width: `${tierRunLimitPercentage}%` }} - className="absolute h-3 rounded-l-sm bg-green-900/50" + className="absolute h-3 rounded-l-sm bg-green-900/20" > @@ -587,7 +587,7 @@ function SeriesColorPicker({ onColorChange(c); setOpen(false); }} - className="group/swatch flex h-6 w-6 items-center justify-center rounded-full border border-white/30" + className="group/swatch flex h-6 w-6 items-center justify-center rounded-full border border-text-bright/30" style={{ backgroundColor: c }} title={c} > diff --git a/apps/webapp/app/components/primitives/AppliedFilter.tsx b/apps/webapp/app/components/primitives/AppliedFilter.tsx index c8fbc4d3ac..6390a5826d 100644 --- a/apps/webapp/app/components/primitives/AppliedFilter.tsx +++ b/apps/webapp/app/components/primitives/AppliedFilter.tsx @@ -4,11 +4,11 @@ import { cn } from "~/utils/cn"; const variants = { "secondary/small": { - box: "h-6 bg-secondary rounded pl-1.5 gap-1.5 text-xs divide-x divide-black/15 group-hover:bg-surface-control group-hover:border-border-brighter text-text-bright border border-border-bright", + box: "h-6 bg-secondary rounded pl-1.5 gap-1.5 text-xs divide-x divide-black/15 shadow-xs group-hover:bg-background-raised text-text-bright border border-border-bright/50", clear: "size-6 text-text-bright hover:text-text-bright transition-colors", }, "tertiary/small": { - box: "h-6 bg-tertiary rounded pl-1.5 gap-1.5 text-xs divide-x divide-black/15 group-hover:bg-surface-control", + box: "h-6 bg-tertiary rounded pl-1.5 gap-1.5 text-xs divide-x divide-black/15 group-hover:bg-background-raised", clear: "size-6 text-text-dimmed hover:text-text-bright transition-colors", }, "minimal/medium": { diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index 9fc6832fcc..e4de8a8b47 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -138,7 +138,7 @@ function AvatarLetters({ return ( {/* This is the square container */} diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 323f374382..67d511e394 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -49,18 +49,17 @@ type Size = keyof typeof sizes; const theme = { primary: { - textColor: - "text-text-bright group-hover/button:text-white transition group-disabled/button:text-text-dimmed", + textColor: "text-white transition group-disabled/button:text-white/60", button: "bg-indigo-600 border border-indigo-500 group-hover/button:bg-indigo-500 group-hover/button:border-indigo-400 group-disabled/button:opacity-50 group-disabled/button:bg-indigo-600 group-disabled/button:border-indigo-500 group-disabled/button:pointer-events-none", shortcut: - "border-text-bright/40 text-text-bright group-hover/button:border-text-bright/60 group-hover/button:text-text-bright", - icon: "text-text-bright", + "border-white/40 text-white group-hover/button:border-white/60 group-hover/button:text-white", + icon: "text-white", }, secondary: { textColor: "text-text-bright transition group-disabled/button:text-text-dimmed/80", button: - "bg-secondary group-hover/button:bg-surface-control group-hover/button:border-border-brighter border border-border-bright group-disabled/button:bg-secondary group-disabled/button:opacity-60 group-disabled/button:pointer-events-none", + "bg-secondary border border-border-bright/50 shadow-xs group-hover/button:bg-background-raised group-disabled/button:bg-secondary group-disabled/button:opacity-60 group-disabled/button:pointer-events-none", shortcut: "border-text-dimmed/40 text-text-dimmed group-hover/button:text-text-bright group-hover/button:border-text-dimmed", icon: "text-text-bright", @@ -82,17 +81,16 @@ const theme = { icon: "text-text-dimmed", }, danger: { - textColor: - "text-text-bright group-hover/button:text-white transition group-disabled/button:text-text-bright/80", + textColor: "text-white transition group-disabled/button:text-white/80", button: "bg-error group-hover/button:bg-rose-500 disabled:opacity-50 group-disabled/button:bg-error group-disabled/button:pointer-events-none", - shortcut: "border-text-bright text-text-bright group-hover/button:border-text-bright/60", - icon: "text-text-bright", + shortcut: "border-white text-white group-hover/button:border-white/60", + icon: "text-white", }, docs: { - textColor: "text-blue-200/70 transition group-disabled/button:text-text-dimmed/80", + textColor: "text-callout-docs-text/70 transition group-disabled/button:text-text-dimmed/80", button: - "bg-background-raised border border-border-bright/50 shadow-sm group-hover/button:bg-secondary group-disabled/button:bg-tertiary group-disabled/button:opacity-60 group-disabled/button:pointer-events-none", + "bg-secondary border border-border-bright/50 shadow-xs group-hover/button:bg-background-raised group-disabled/button:bg-tertiary group-disabled/button:opacity-60 group-disabled/button:pointer-events-none", shortcut: "border-text-dimmed/40 text-text-dimmed group-hover/button:text-text-bright group-hover/button:border-text-dimmed", icon: "text-blue-500", @@ -167,7 +165,7 @@ const variant = { }; const allVariants = { - $all: "font-normal text-center font-sans justify-center items-center shrink-0 transition duration-150 rounded-[3px] select-none group-focus/button:outline-hidden group-disabled/button:opacity-75 group-disabled/button:pointer-events-none focus-custom", + $all: "cursor-pointer font-normal text-center font-sans justify-center items-center shrink-0 transition duration-150 rounded-[3px] select-none group-focus/button:outline-hidden group-disabled/button:opacity-75 group-disabled/button:pointer-events-none focus-custom", variant: variant, }; diff --git a/apps/webapp/app/components/primitives/Checkbox.tsx b/apps/webapp/app/components/primitives/Checkbox.tsx index 75850b304d..da63e2ed99 100644 --- a/apps/webapp/app/components/primitives/Checkbox.tsx +++ b/apps/webapp/app/components/primitives/Checkbox.tsx @@ -139,7 +139,11 @@ export const CheckboxWithLabel = React.forwardRef( : props.readOnly ? "cursor-default" : "cursor-pointer", - "read-only:border-border-bright disabled:border-border-bright disabled:opacity-50 rounded-sm border border-border-bright bg-transparent transition checked:bg-indigo-500! read-only:bg-background-raised! group-hover:bg-background-deep checked:group-hover:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-hidden focus-visible:ring-indigo-500 disabled:bg-background-raised!" + // NB: don't use the `read-only:` variant here — checkboxes always + // match :read-only, so it would override the checked style. + "rounded-sm border border-border-bright bg-transparent transition checked:bg-indigo-500! group-hover:bg-background-deep checked:group-hover:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-hidden focus-visible:ring-indigo-500", + props.disabled && "opacity-50", + (props.disabled || props.readOnly) && + "bg-background-raised! checked:bg-background-raised! checked:group-hover:bg-background-raised! group-hover:bg-background-raised!", + className )} {...props} ref={ref} diff --git a/apps/webapp/app/components/primitives/ClientTabs.tsx b/apps/webapp/app/components/primitives/ClientTabs.tsx index 533a09c84e..c564c42516 100644 --- a/apps/webapp/app/components/primitives/ClientTabs.tsx +++ b/apps/webapp/app/components/primitives/ClientTabs.tsx @@ -122,10 +122,10 @@ const ClientTabsTrigger = React.forwardRef< ) : ( -
+
) ) : null} diff --git a/apps/webapp/app/components/primitives/ClipboardField.tsx b/apps/webapp/app/components/primitives/ClipboardField.tsx index 0304e5bca6..07d82f5efe 100644 --- a/apps/webapp/app/components/primitives/ClipboardField.tsx +++ b/apps/webapp/app/components/primitives/ClipboardField.tsx @@ -5,7 +5,7 @@ import { CopyButton } from "./CopyButton"; const variants = { "primary/small": { container: - "flex items-center text-text-dimmed font-mono rounded border bg-background-hover text-xs transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", + "flex items-center text-text-dimmed font-mono rounded border border-border-bright/50 shadow-xs bg-input-bg text-xs transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", input: "bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent", buttonVariant: "primary" as const, @@ -14,7 +14,7 @@ const variants = { }, "secondary/small": { container: - "flex items-center text-text-dimmed font-mono rounded border bg-background-hover text-xs transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", + "flex items-center text-text-dimmed font-mono rounded border border-border-bright/50 shadow-xs bg-input-bg text-xs transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", input: "bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent", buttonVariant: "tertiary" as const, @@ -33,7 +33,7 @@ const variants = { }, "primary/medium": { container: - "flex items-center text-text-dimmed font-mono rounded border bg-background-hover text-sm transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", + "flex items-center text-text-dimmed font-mono rounded border border-border-bright/50 shadow-xs bg-input-bg text-sm transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", input: "bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent", buttonVariant: "primary" as const, @@ -42,7 +42,7 @@ const variants = { }, "secondary/medium": { container: - "flex items-center text-text-dimmed font-mono rounded bg-background-hover text-sm transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", + "flex items-center text-text-dimmed font-mono rounded border border-border-bright/50 shadow-xs bg-input-bg text-sm transition hover:bg-background-raised focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-hidden focus:ring-0 focus:ring-transparent", input: "bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent", buttonVariant: "tertiary" as const, diff --git a/apps/webapp/app/components/primitives/Input.tsx b/apps/webapp/app/components/primitives/Input.tsx index 4f20c0c11d..5c3235a66c 100644 --- a/apps/webapp/app/components/primitives/Input.tsx +++ b/apps/webapp/app/components/primitives/Input.tsx @@ -12,21 +12,21 @@ const inputBase = const variants = { large: { container: - "px-1 w-full h-10 rounded-[3px] border border-background-bright bg-background-hover hover:border-border-bright hover:bg-secondary", + "px-1 w-full h-10 rounded-[3px] border border-border-bright/50 shadow-xs bg-input-bg hover:bg-background-raised", input: "px-2 text-sm", iconSize: "size-4 ml-1", accessory: "pr-1", }, medium: { container: - "px-1 h-8 w-full rounded border border-background-bright bg-background-hover hover:border-border-bright hover:bg-secondary", + "px-1 h-8 w-full rounded border border-border-bright/50 shadow-xs bg-input-bg hover:bg-background-raised", input: "px-1.5 rounded text-sm", iconSize: "size-4 ml-0.5", accessory: "pr-1", }, small: { container: - "px-1 h-6 w-full rounded border border-background-bright bg-background-hover hover:border-border-bright hover:bg-secondary", + "px-1 h-6 w-full rounded border border-border-bright/50 shadow-xs bg-input-bg hover:bg-background-raised", input: "px-1 rounded text-xs", iconSize: "size-3 ml-0.5", accessory: "pr-0.5", @@ -39,7 +39,7 @@ const variants = { }, "secondary-small": { container: - "px-1 h-6 w-full rounded border border-border-bright hover:border-border-brighter bg-grid-dimmed hover:bg-secondary", + "px-1 h-6 w-full rounded border border-border-bright/50 shadow-xs bg-input-bg hover:bg-background-raised", input: "px-1 rounded text-xs", iconSize: "size-3 ml-0.5", accessory: "pr-0.5", diff --git a/apps/webapp/app/components/primitives/RadioButton.tsx b/apps/webapp/app/components/primitives/RadioButton.tsx index c52389f788..bf7c736d72 100644 --- a/apps/webapp/app/components/primitives/RadioButton.tsx +++ b/apps/webapp/app/components/primitives/RadioButton.tsx @@ -22,7 +22,7 @@ const variants = { }, "button/small": { button: - "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border hover:data-[state=checked]:border-border-bright border-border-bright hover:border-border-bright transition data-disabled:opacity-70 data-disabled:hover:bg-transparent hover:data-[state=checked]:bg-white/4 data-[state=checked]:bg-white/4", + "flex items-center w-fit h-8 pl-2 pr-3 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 data-disabled:hover:bg-secondary hover:data-[state=checked]:bg-text-bright/4 data-[state=checked]:bg-text-bright/4", label: "text-sm text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-0", @@ -30,7 +30,7 @@ const variants = { }, button: { button: - "w-fit py-2 pl-3 pr-4 rounded border border-border-bright hover:bg-background-dimmed hover:border-border-brightest transition data-[state=checked]:bg-background-dimmed data-disabled:opacity-70", + "w-fit py-2 pl-3 pr-4 rounded border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition data-[state=checked]:bg-background-dimmed data-disabled:opacity-70", label: "text-text-bright select-none", description: "text-text-dimmed", inputPosition: "mt-1", @@ -38,7 +38,7 @@ const variants = { }, description: { button: - "w-full p-2.5 hover:data-[state=checked]:bg-white/4 data-[state=checked]:bg-white/4 transition data-disabled:opacity-70 hover:border-border-bright border-border-bright hover:data-[state=checked]:border-border-bright border rounded-md", + "w-full p-2.5 rounded-md border border-border-bright/50 shadow-xs bg-secondary transition hover:bg-background-raised data-disabled:opacity-70 hover:data-[state=checked]:bg-text-bright/4 data-[state=checked]:bg-text-bright/4", label: "text-text-bright font-semibold -mt-0.5 text-left text-sm", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", @@ -46,7 +46,7 @@ const variants = { }, icon: { button: - "w-full p-2.5 pb-4 hover:bg-background-dimmed transition data-disabled:opacity-70 data-[state=checked]:bg-background-dimmed border-border-bright border rounded-sm", + "w-full p-2.5 pb-4 rounded-sm border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised transition data-disabled:opacity-70 data-[state=checked]:bg-background-dimmed", label: "text-text-bright font-semibold -mt-1 text-left", description: "text-text-dimmed mt-0 text-left", inputPosition: "mt-0", @@ -81,9 +81,7 @@ export function RadioButtonCircle({ outerCircleClassName )} > - +
)}
@@ -136,7 +134,7 @@ export const RadioGroupItem = React.forwardRef< )} > - +
diff --git a/apps/webapp/app/components/primitives/Resizable.tsx b/apps/webapp/app/components/primitives/Resizable.tsx index 59550332c2..0bd4f8e86d 100644 --- a/apps/webapp/app/components/primitives/Resizable.tsx +++ b/apps/webapp/app/components/primitives/Resizable.tsx @@ -2,6 +2,8 @@ import React, { useRef } from "react"; import { PanelGroup, Panel, PanelResizer } from "@window-splitter/react"; +import { useTypedMatchesData } from "~/hooks/useTypedMatchData"; +import type { loader as rootLoader } from "~/root"; import { cn } from "~/utils/cn"; const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps) => ( @@ -14,7 +16,25 @@ const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps ); -const ResizablePanel = Panel; +// react-window-splitter drives the collapse animation through @react-spring/rafz, +// which has timing/interaction issues with Firefox that produce visual glitches +// (alternating frames, panels stuck at min, panelHasSpace invariant violations), +// so the animation is dropped on Firefox. The browser check must agree between +// SSR and hydration (a client-only `navigator` check made the panel tree differ +// and shifted useIds), so it comes from the root loader's user-agent sniff. +const ResizablePanel = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(function ResizablePanel({ collapseAnimation, ...props }, ref) { + const rootData = useTypedMatchesData({ id: "root" }); + return ( + + ); +}); const ResizableHandle = ({ withHandle = true, @@ -69,14 +89,8 @@ const ResizableHandle = ({ ); -// react-window-splitter drives the collapse animation through @react-spring/rafz, -// which has timing/interaction issues with Firefox that produce visual glitches -// (alternating frames, panels stuck at min, panelHasSpace invariant violations). -// Disable the animation on Firefox; it works correctly in Chromium and Safari. -const RESIZABLE_PANEL_ANIMATION = - typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent) - ? undefined - : ({ easing: "ease-in-out", duration: 300 } as const); +// Firefox filtering happens inside ResizablePanel (see above). +const RESIZABLE_PANEL_ANIMATION = { easing: "ease-in-out", duration: 300 } as const; const COLLAPSIBLE_HANDLE_CLASSNAME = "transition-opacity duration-200"; diff --git a/apps/webapp/app/components/primitives/SegmentedControl.tsx b/apps/webapp/app/components/primitives/SegmentedControl.tsx index 2f749215c2..a3c365b930 100644 --- a/apps/webapp/app/components/primitives/SegmentedControl.tsx +++ b/apps/webapp/app/components/primitives/SegmentedControl.tsx @@ -24,10 +24,11 @@ const theme = { selected: "absolute inset-0 rounded-[2px] outline-solid outline-3 outline-primary", }, secondary: { - base: "bg-background-raised/50", + base: "bg-transparent dark:bg-background-raised/50", active: "text-text-bright", inactive: "text-text-dimmed transition hover:text-text-bright", - selected: "absolute inset-0 rounded bg-background-raised border border-border-bright", + selected: + "absolute inset-0 rounded bg-white border border-[#e2e4e9] dark:bg-background-raised dark:border-border-bright", }, }; diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index 66b291b752..54b160edfa 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -30,7 +30,7 @@ const style = { }, secondary: { button: - "bg-secondary focus-custom border border-border-bright hover:text-text-bright hover:border-border-brighter text-text-bright hover:bg-surface-control", + "bg-secondary focus-custom border border-border-bright/50 shadow-xs hover:text-text-bright text-text-bright hover:bg-background-raised", }, }; diff --git a/apps/webapp/app/components/primitives/Switch.tsx b/apps/webapp/app/components/primitives/Switch.tsx index 96943e9beb..27ed530492 100644 --- a/apps/webapp/app/components/primitives/Switch.tsx +++ b/apps/webapp/app/components/primitives/Switch.tsx @@ -36,7 +36,7 @@ const variations = { "secondary/small": { container: cn( small.container, - "border border-border-bright hover:border-border-brighter bg-secondary hover:bg-surface-control" + "border border-border-bright/50 shadow-xs bg-secondary hover:bg-background-raised" ), root: cn( small.root, @@ -96,10 +96,7 @@ export const Switch = React.forwardRef
); diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index f5980d9e59..8d812a413f 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -673,7 +673,7 @@ function PermanentStatusFilter() { className="pl-1" /> ) : ( -
+
@@ -852,7 +852,7 @@ function PermanentTasksFilter({ possibleTasks }: Pick ) : ( -
+
{filterIcon("tasks")} Tasks
diff --git a/apps/webapp/app/components/runs/v3/RunTag.tsx b/apps/webapp/app/components/runs/v3/RunTag.tsx index 3d6c6751ce..1defae0167 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 c79a147b86..069246c89b 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 a82d8b9d0c..3941283be4 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 a5115897f9..cc02a280e0 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 (
-
+
{part.text}
diff --git a/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx b/apps/webapp/app/components/runs/v3/ai/AIModelSummary.tsx index 11ef7aa8a5..775efd7c30 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 d78fd39fa7..61e1473e14 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 66df9ddff5..0270e7be4e 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 e27b11b754..37d7c58bc5 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 34d9a613d9..aafb685f7d 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 e58afdb65d..1e92cb08ee 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 @@ -668,7 +668,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 b4b92c8a13..45e14a007b 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 +
+
+ + + value={theme} + setValue={(value) => + themeFetcher.submit( + { action: "update-theme", theme: value }, + { method: "post" } + ) + } + variant="secondary/small" + dropdownIcon + items={["dark", "light"]} + text={(value) => ( + + {value === "dark" ? ( + + + + ) : ( + + )} + {value === "dark" ? "Dark" : "Light"} + + )} + className="w-32" + > + {(items) => + items.map((item) => ( + + + + ) : ( + + ) + } + > + {item === "dark" ? "Dark" : "Light"} + + )) + } + +
+ + )} 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 4c8f77a582..6db0b6ff56 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 7a5f61a48d..b05d22439d 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 7414960f47..67e68e1360 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 7af007dc38..fafd143842 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 5a6429fce8..8bf9c3e130 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 637830aef0..9a6270fa6d 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