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