diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx
index 3958f0149fc7e..030d34adb40cc 100644
--- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx
+++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx
@@ -1839,7 +1839,7 @@ function ComposerPromptEditorTiptapInner(
{isEmpty && contextRecords.size === 0 && placeholder ? (
diff --git a/apps/web/src/components/chat/ComposerSurface.tsx b/apps/web/src/components/chat/ComposerSurface.tsx
index f2420eefa046b..7f977dea9c056 100644
--- a/apps/web/src/components/chat/ComposerSurface.tsx
+++ b/apps/web/src/components/chat/ComposerSurface.tsx
@@ -34,8 +34,9 @@ function Shell({
);
}
+// Focus sets the border directly so named themes, which override the outline variable, still show it.
const outlineClasses =
- "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-(--chat-composer-outline) dark:after:inset-shadow-2xs dark:after:inset-shadow-(color:--chat-composer-highlight)";
+ "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-(--chat-composer-outline) dark:after:inset-shadow-2xs dark:after:inset-shadow-(color:--chat-composer-highlight) group-has-[[contenteditable]:focus]/composer-surface:after:border-ring/65 dark:group-has-[[contenteditable]:focus]/composer-surface:after:border-ring/90";
const contextSeamClasses =
"group-data-with-context/composer-surface:after:[clip-path:polygon(0_0,100%_0,100%_100%,calc(100%-22px)_100%,calc(100%-22px)_calc(100%-2px),22px_calc(100%-2px),22px_100%,0_100%)]";
diff --git a/apps/web/src/components/settings/SettingsSidebarNav.tsx b/apps/web/src/components/settings/SettingsSidebarNav.tsx
index 9d4642bb4785b..739ab040dfa8c 100644
--- a/apps/web/src/components/settings/SettingsSidebarNav.tsx
+++ b/apps/web/src/components/settings/SettingsSidebarNav.tsx
@@ -310,6 +310,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
aria-selected={index === activeResultIndex}
tabIndex={-1}
size="sm"
+ variant="option"
isActive={index === activeResultIndex}
className="h-auto min-h-10 items-start"
onMouseMove={() => setActiveResultIndex(index)}
diff --git a/apps/web/src/components/sidebar/SidebarThreadRow.tsx b/apps/web/src/components/sidebar/SidebarThreadRow.tsx
index 1daec98502a3b..cc3bd0c5355ae 100644
--- a/apps/web/src/components/sidebar/SidebarThreadRow.tsx
+++ b/apps/web/src/components/sidebar/SidebarThreadRow.tsx
@@ -896,9 +896,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
!props.isActive &&
"[&:not(:hover):not(:focus-within)_*]:text-secondary-label/70",
props.isActive
- ? "bg-sidebar-row-active text-sidebar-foreground light:shadow-xs light:ring-1 light:ring-inset light:ring-sidebar-border/70"
+ ? "bg-sidebar-row-active text-sidebar-foreground light:shadow-xs light:ring-1 light:ring-inset light:ring-sidebar-row-ring light:sidebar-row-tinted-text"
: isSelected || props.sweepAction !== null
- ? "bg-sidebar-row-selected text-sidebar-foreground light:ring-1 light:ring-inset light:ring-sidebar-border/70"
+ ? "bg-sidebar-row-selected text-sidebar-foreground light:ring-1 light:ring-inset light:ring-sidebar-row-ring light:sidebar-row-tinted-text"
: hasUnsentDraft
? cn(draftSurfaceClassName, "text-sidebar-foreground")
: shouldRecede
diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx
index 226d168715640..0684d2ea66a04 100644
--- a/apps/web/src/components/ui/sidebar.tsx
+++ b/apps/web/src/components/ui/sidebar.tsx
@@ -682,7 +682,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
}
const sidebarMenuButtonVariants = cva(
- "peer/menu-button flex w-full cursor-pointer items-center gap-[var(--sidebar-control-gap)] overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-inset active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-64 aria-disabled:pointer-events-none aria-disabled:opacity-64 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-[var(--sidebar-content-inset)]! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-[var(--sidebar-icon-color)] hover:[&>svg]:text-sidebar-foreground active:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:text-sidebar-foreground",
+ "peer/menu-button flex w-full cursor-pointer items-center gap-[var(--sidebar-control-gap)] overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-inset active:bg-sidebar-row-hover active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-64 aria-disabled:pointer-events-none aria-disabled:opacity-64 data-[active=true]:bg-sidebar-row-active data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-[var(--sidebar-content-inset)]! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-[var(--sidebar-icon-color)] hover:[&>svg]:text-sidebar-foreground active:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:text-sidebar-foreground",
{
defaultVariants: {
size: "default",
@@ -699,6 +699,9 @@ const sidebarMenuButtonVariants = cva(
variant: {
default: "font-medium text-sidebar-muted-foreground/80",
outline: "bg-sidebar-control-surface ring-1 ring-sidebar-border",
+ // Listbox results: the highlight follows the pointer, so it stays neutral; blue marks the current page.
+ option:
+ "font-medium text-sidebar-muted-foreground/80 data-[active=true]:bg-sidebar-row-hover",
},
},
},
@@ -795,8 +798,8 @@ function SidebarMenuSubButton({
}) {
const defaultProps = {
className: cn(
- "-translate-x-px flex h-7 min-w-0 cursor-pointer items-center gap-2 overflow-hidden rounded-lg px-2 text-sidebar-foreground outline-hidden ring-ring hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-inset active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-64 aria-disabled:pointer-events-none aria-disabled:opacity-64 [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-muted-foreground",
- "data-[active=true]:bg-sidebar-row-selected data-[active=true]:text-sidebar-foreground",
+ "-translate-x-px flex h-7 min-w-0 cursor-pointer items-center gap-2 overflow-hidden rounded-lg px-2 text-sidebar-foreground outline-hidden ring-ring hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-inset active:bg-sidebar-row-hover active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-64 aria-disabled:pointer-events-none aria-disabled:opacity-64 [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-muted-foreground",
+ "data-[active=true]:bg-sidebar-row-active data-[active=true]:text-sidebar-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index cfb320a6c9e58..9be6248ec2d3b 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -281,6 +281,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
--color-sidebar-row-hover: var(--sidebar-row-hover);
--color-sidebar-row-active: var(--sidebar-row-active);
--color-sidebar-row-selected: var(--sidebar-row-selected);
+ --color-sidebar-row-ring: var(--sidebar-row-ring);
--color-sidebar-border: var(--contrast-sidebar-border);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
@@ -339,6 +340,41 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
}
}
+/* Tinted current rows: muted text steps toward the foreground to keep 4.5:1
+ on the light accent fill, inside the same contrast wrap as other text. */
+@utility sidebar-row-tinted-text {
+ --contrast-secondary-label: color-mix(
+ in oklab,
+ color-mix(
+ in oklab,
+ color-mix(in oklab, var(--secondary-label), var(--sidebar-foreground) 30%)
+ var(--appearance-contrast-base),
+ var(--background)
+ ),
+ var(--appearance-contrast-target) var(--appearance-contrast-boost)
+ );
+ --contrast-muted-foreground: color-mix(
+ in oklab,
+ color-mix(
+ in oklab,
+ color-mix(in oklab, var(--muted-foreground), var(--sidebar-foreground) 30%)
+ var(--appearance-contrast-base),
+ var(--background)
+ ),
+ var(--appearance-contrast-target) var(--appearance-contrast-boost)
+ );
+ --contrast-sidebar-muted-foreground: color-mix(
+ in oklab,
+ color-mix(
+ in oklab,
+ color-mix(in oklab, var(--sidebar-muted-foreground), var(--sidebar-foreground) 30%)
+ var(--appearance-contrast-base),
+ var(--sidebar)
+ ),
+ var(--appearance-contrast-target) var(--appearance-contrast-boost)
+ );
+}
+
@utility surface-glass {
background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
@@ -1123,6 +1159,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
--message-action: var(--primary);
--message-action-foreground: var(--primary-foreground);
--message-action-hover: color-mix(in srgb, var(--primary) 90%, var(--background));
+ /* Links share the action color so blue always means "clickable". */
+ --link: var(--primary);
--accent: var(--color-zinc-100);
--accent-foreground: var(--color-zinc-900);
--error: var(--color-red-500);
@@ -1155,8 +1193,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
--sidebar-muted-foreground: var(--muted-foreground);
--sidebar-control-surface: var(--color-zinc-100);
--sidebar-row-hover: var(--color-zinc-25);
- --sidebar-row-active: var(--color-white);
- --sidebar-row-selected: var(--color-white);
+ /* The current row and settings page carry the accent: blue means "you are here". */
+ --sidebar-row-active: color-mix(in oklab, var(--primary) 18%, var(--color-white));
+ --sidebar-row-selected: color-mix(in oklab, var(--primary) 12%, var(--color-white));
+ --sidebar-row-ring: color-mix(in oklab, var(--primary) 35%, transparent);
--sidebar-border: var(--border);
--sidebar-stage-fade: var(--sidebar);
--code-background: color-mix(in srgb, var(--card) 90%, var(--background));
@@ -1168,16 +1208,17 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
@variant dark {
color-scheme: dark;
- /* Keep controls and floating surfaces close to the neutral-black canvas.
- Borders and hover states provide separation without milky gray fills. */
+ /* Raised surfaces (composer, cards, popovers) sit one step above the
+ neutral-black canvas; menus separate by border and shadow. */
--background: var(--color-neutral-950);
- --surface-raised: color-mix(in srgb, var(--background) 97%, var(--color-white));
+ --surface-raised: color-mix(in srgb, var(--background) 93%, var(--color-white));
--foreground: var(--color-neutral-100);
- --card: color-mix(in srgb, var(--background) 97%, var(--color-white));
+ --card: color-mix(in srgb, var(--background) 93%, var(--color-white));
--card-foreground: var(--color-neutral-100);
- --popover: color-mix(in srgb, var(--background) 97%, var(--color-white));
+ --popover: color-mix(in srgb, var(--background) 93%, var(--color-white));
--popover-foreground: var(--color-neutral-100);
--primary: oklch(0.571 0.21 264);
+ --link: color-mix(in oklab, var(--primary), var(--color-white) 28%);
--secondary: --alpha(var(--color-white) / 3%);
--secondary-foreground: var(--color-neutral-100);
--muted: --alpha(var(--color-white) / 3%);
@@ -1197,20 +1238,22 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
--warning-surface: color-mix(in srgb, var(--warning) 16%, transparent);
--update-foreground: var(--color-blue-400);
--update-surface: color-mix(in srgb, var(--update) 18%, transparent);
- --sidebar: var(--card);
+ /* Sidebar surfaces outside [data-app-sidebar] (mobile sheet) keep the flat
+ step instead of following the lifted card. */
+ --sidebar: color-mix(in srgb, var(--background) 97%, var(--color-white));
--sidebar-control-surface: var(--muted);
--sidebar-row-hover: var(--accent);
- --sidebar-row-active: var(--accent);
- --sidebar-row-selected: var(--muted);
- --sidebar-stage-fade: var(--card);
+ --sidebar-row-active: color-mix(in oklab, var(--primary) 24%, transparent);
+ --sidebar-row-selected: color-mix(in oklab, var(--primary) 14%, transparent);
+ --sidebar-stage-fade: var(--sidebar);
--terminal-cursor: rgb(180 203 255);
--terminal-selection-background: rgb(180 203 255 / 25%);
}
}
/* Keep both sidebar implementations (default and legacy) on the same quiet
- zinc hierarchy: zinc-50 navigation, zinc-25 hover, and white selected/raised
- surfaces. */
+ zinc hierarchy: zinc-50 navigation, zinc-25 hover, white raised surfaces,
+ and accent-tinted current rows. */
[data-app-sidebar] {
--background: var(--color-zinc-25);
--foreground: var(--color-zinc-900);
@@ -1227,8 +1270,9 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
--sidebar-muted-foreground: var(--color-zinc-500);
--sidebar-control-surface: var(--color-zinc-100);
--sidebar-row-hover: var(--color-zinc-25);
- --sidebar-row-active: var(--color-white);
- --sidebar-row-selected: var(--color-white);
+ --sidebar-row-active: color-mix(in oklab, var(--primary) 18%, var(--color-white));
+ --sidebar-row-selected: color-mix(in oklab, var(--primary) 12%, var(--color-white));
+ --sidebar-row-ring: color-mix(in oklab, var(--primary) 35%, transparent);
--sidebar-border: var(--color-zinc-200);
--sidebar-stage-fade: var(--sidebar);
@@ -1248,8 +1292,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
--sidebar-muted-foreground: var(--muted-foreground);
--sidebar-control-surface: var(--muted);
--sidebar-row-hover: color-mix(in srgb, var(--contrast-foreground) 8%, transparent);
- --sidebar-row-active: color-mix(in srgb, var(--contrast-foreground) 11%, transparent);
- --sidebar-row-selected: color-mix(in srgb, var(--contrast-foreground) 7%, transparent);
+ --sidebar-row-active: color-mix(in oklab, var(--primary) 24%, transparent);
+ --sidebar-row-selected: color-mix(in oklab, var(--primary) 14%, transparent);
--sidebar-border: var(--border);
--sidebar-stage-fade: var(--card);
}
@@ -1283,6 +1327,9 @@ html[data-theme-id]:not([data-theme-id=""]) {
a distinct action color make these identical. */
--primary: var(--app-theme-message-action);
--primary-foreground: var(--app-theme-message-action-foreground);
+ /* Theme action colors are tuned for filled buttons, not text, so themed
+ links keep the info color. */
+ --link: var(--info-foreground);
--secondary: var(--app-theme-secondary);
--secondary-foreground: var(--app-theme-secondary-foreground);
--muted: var(--app-theme-muted);
@@ -1319,6 +1366,17 @@ html[data-theme-id]:not([data-theme-id=""]) {
--sidebar-row-hover: var(--app-theme-sidebar-row-hover);
--sidebar-row-active: var(--app-theme-sidebar-row-active);
--sidebar-row-selected: var(--app-theme-sidebar-row-selected);
+ --sidebar-row-ring: color-mix(in oklab, var(--primary) 35%, transparent);
+
+ /* Light theme row fills can sit near white, so the current row also takes
+ the theme's action color. */
+ @variant light {
+ --sidebar-row-active: color-mix(
+ in oklab,
+ var(--primary) 20%,
+ var(--app-theme-sidebar-row-active)
+ );
+ }
--sidebar-border: var(--app-theme-sidebar-border);
--sidebar-stage-fade: var(--app-theme-sidebar);
--code-background: var(--app-theme-code-background);
@@ -1519,6 +1577,17 @@ html[data-theme-id] [data-app-sidebar] {
--sidebar-row-hover: var(--app-theme-sidebar-row-hover);
--sidebar-row-active: var(--app-theme-sidebar-row-active);
--sidebar-row-selected: var(--app-theme-sidebar-row-selected);
+ --sidebar-row-ring: color-mix(in oklab, var(--primary) 35%, transparent);
+
+ /* Light theme row fills can sit near white, so the current row also takes
+ the theme's action color. */
+ @variant light {
+ --sidebar-row-active: color-mix(
+ in oklab,
+ var(--primary) 20%,
+ var(--app-theme-sidebar-row-active)
+ );
+ }
--sidebar-border: var(--app-theme-sidebar-border);
--sidebar-stage-fade: var(--app-theme-sidebar);
border-color: color-mix(in srgb, var(--contrast-sidebar-foreground) 10%, transparent);
@@ -1973,7 +2042,7 @@ code {
/* File links and context chips own their color and focus ring. */
.chat-markdown a:not(.chat-markdown-file-link, :has([data-slot="context-chip-label"])) {
- color: var(--info-foreground);
+ color: var(--link);
}
.chat-markdown