| author | |
| committer | |
| log | 6e9c57f7ba9eab9e987da7a660664a1df683e9cf |
| tree | 3225481ba0247938c6985573fd1b82d2b4dbf334 |
| parent | 6c693baecf75454cac96d1ae3f78afc73f2cbe53 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>26 files changed, 404 insertions(+), 102 deletions(-)
.macroscope/check-run-agents/ui-consistency.md+3| ... | ... | @@ -47,6 +47,9 @@ The goal is not to minimize CSS or class counts at any cost. The goal is to put |
| 47 | 47 | - light-only declarations use `@variant light`; |
| 48 | 48 | - raw `.dark` should remain only in the `dark` and `light` custom-variant definitions. |
| 49 | 49 | - Preserve custom themes and runtime token bridges. Removing a variable or selector is safe only when all runtime, inspector, generated, and theme-palette consumers are accounted for. |
| 50 | - Contrast and accessibility settings that target app chrome must derive from semantic color tokens. Do not apply `filter` to `html`, `body`, or the app root: it also changes user media, previews, terminals, glass backdrop ownership, and view-transition snapshots. | |
| 51 | - Preserve alpha and surface ownership when deriving contrast tokens. Soften translucent borders and inputs toward transparent rather than an opaque canvas, use a modest semantic-foreground mix for stronger borders, and adjust card, popover, accent, secondary, and message foregrounds against their own surfaces when the base foreground changes. | |
| 52 | - Runtime-adjusted roles must be ordinary custom properties shared by the Tailwind bridge, global CSS, imperative style strings, and bridge snapshots sent to other renderers. Audit literal `var(--foreground)`, `var(--border)`, and related role reads so headings, markdown chrome, menus, previews, and utilities do not split into adjusted and unadjusted colors. | |
| 50 | 53 | - Inspect emitted production CSS after unusual variants, arbitrary selectors, nested pseudo-elements, or attribute matching. Source syntax that looks valid is insufficient. |
| 51 | 54 | - Flag malformed or empty emitted selectors such as empty `:is()` or `:not(:is())`, selector branches that can never match their own class attribute, and transformations that silently drop the intended rule. |
| 52 | 55 | - Prefer source-level logic over clever selectors when behavior depends on consumer-provided class strings. Preserve `MenuPopup`'s current defaulting contract: a string `className` containing a `w-*`, `min-w-*`, or `max-w-*` utility after variant prefixes are stripped suppresses `min-w-32`; a string without one and a functional/non-string `className` keep the default. Arbitrary width values count as width utilities, and the consumer class must be merged last so it retains control. Do not replace this with a raw class-attribute substring selector. |
apps/desktop/src/settings/DesktopClientSettings.test.ts+1| ... | ... | @@ -13,6 +13,7 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
| 13 | 13 | import * as DesktopClientSettings from "./DesktopClientSettings.ts"; |
| 14 | 14 | |
| 15 | 15 | const clientSettings: ClientSettings = { |
| 16 | appearanceContrast: 100, | |
| 16 | 17 | browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" }, |
| 17 | 18 | browserDefaultZoomFactor: 1.25, |
| 18 | 19 | browserDefaultAppearance: "dark", |
apps/web/src/appearanceContrast.test.ts created+53| ... | ... | @@ -0,0 +1,53 @@ |
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { applyAppearanceContrast } from "./appearanceContrast"; | |
| 4 | ||
| 5 | function makeRoot() { | |
| 6 | const setProperty = vi.fn(); | |
| 7 | return { | |
| 8 | root: { style: { setProperty } } as unknown as HTMLElement, | |
| 9 | setProperty, | |
| 10 | }; | |
| 11 | } | |
| 12 | ||
| 13 | describe("applyAppearanceContrast", () => { | |
| 14 | it("boosts semantic contrast above the default", () => { | |
| 15 | const { root, setProperty } = makeRoot(); | |
| 16 | ||
| 17 | applyAppearanceContrast(root, 135); | |
| 18 | ||
| 19 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-base", "100%"); | |
| 20 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "35%"); | |
| 21 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-border-boost", "8.75%"); | |
| 22 | }); | |
| 23 | ||
| 24 | it("supports the maximum contrast boost", () => { | |
| 25 | const { root, setProperty } = makeRoot(); | |
| 26 | ||
| 27 | applyAppearanceContrast(root, 200); | |
| 28 | ||
| 29 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-base", "100%"); | |
| 30 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "100%"); | |
| 31 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-border-boost", "25%"); | |
| 32 | }); | |
| 33 | ||
| 34 | it("softens semantic contrast below the default", () => { | |
| 35 | const { root, setProperty } = makeRoot(); | |
| 36 | ||
| 37 | applyAppearanceContrast(root, 70); | |
| 38 | ||
| 39 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-base", "70%"); | |
| 40 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "0%"); | |
| 41 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-border-boost", "0%"); | |
| 42 | }); | |
| 43 | ||
| 44 | it("disables contrast mixing at the default", () => { | |
| 45 | const { root, setProperty } = makeRoot(); | |
| 46 | ||
| 47 | applyAppearanceContrast(root, 100); | |
| 48 | ||
| 49 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-base", "100%"); | |
| 50 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-boost", "0%"); | |
| 51 | expect(setProperty).toHaveBeenCalledWith("--appearance-contrast-border-boost", "0%"); | |
| 52 | }); | |
| 53 | }); |
apps/web/src/appearanceContrast.ts created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | import type { AppearanceContrast } from "@t3tools/contracts/settings"; | |
| 2 | ||
| 3 | export function applyAppearanceContrast(root: HTMLElement, contrast: AppearanceContrast): void { | |
| 4 | root.style.setProperty("--appearance-contrast-base", `${Math.min(contrast, 100)}%`); | |
| 5 | root.style.setProperty("--appearance-contrast-boost", `${Math.max(contrast - 100, 0)}%`); | |
| 6 | root.style.setProperty( | |
| 7 | "--appearance-contrast-border-boost", | |
| 8 | `${Math.max(contrast - 100, 0) / 4}%`, | |
| 9 | ); | |
| 10 | } |
apps/web/src/browser/annotationTheme.ts+6-6| ... | ... | @@ -10,17 +10,17 @@ export function readPreviewAnnotationTheme(): DesktopPreviewAnnotationTheme { |
| 10 | 10 | colorScheme: root.classList.contains("dark") ? "dark" : "light", |
| 11 | 11 | radius: readVariable(styles, "--radius", "0.625rem"), |
| 12 | 12 | background: readVariable(styles, "--background", "white"), |
| 13 | foreground: readVariable(styles, "--foreground", "oklch(0.269 0 0)"), | |
| 13 | foreground: readVariable(styles, "--contrast-foreground", "oklch(0.269 0 0)"), | |
| 14 | 14 | popover: readVariable(styles, "--popover", "white"), |
| 15 | popoverForeground: readVariable(styles, "--popover-foreground", "oklch(0.269 0 0)"), | |
| 15 | popoverForeground: readVariable(styles, "--contrast-popover-foreground", "oklch(0.269 0 0)"), | |
| 16 | 16 | primary: readVariable(styles, "--primary", "oklch(0.488 0.217 264)"), |
| 17 | 17 | primaryForeground: readVariable(styles, "--primary-foreground", "white"), |
| 18 | 18 | muted: readVariable(styles, "--muted", "rgb(0 0 0 / 4%)"), |
| 19 | mutedForeground: readVariable(styles, "--muted-foreground", "oklch(0.556 0 0)"), | |
| 19 | mutedForeground: readVariable(styles, "--contrast-muted-foreground", "oklch(0.556 0 0)"), | |
| 20 | 20 | accent: readVariable(styles, "--accent", "rgb(0 0 0 / 4%)"), |
| 21 | accentForeground: readVariable(styles, "--accent-foreground", "oklch(0.269 0 0)"), | |
| 22 | border: readVariable(styles, "--border", "rgb(0 0 0 / 8%)"), | |
| 23 | input: readVariable(styles, "--input", "rgb(0 0 0 / 10%)"), | |
| 21 | accentForeground: readVariable(styles, "--contrast-accent-foreground", "oklch(0.269 0 0)"), | |
| 22 | border: readVariable(styles, "--contrast-border", "rgb(0 0 0 / 8%)"), | |
| 23 | input: readVariable(styles, "--contrast-input", "rgb(0 0 0 / 10%)"), | |
| 24 | 24 | ring: readVariable(styles, "--ring", "oklch(0.488 0.217 264)"), |
| 25 | 25 | fontSans: readVariable(styles, "--font-sans", styles.fontFamily || "system-ui, sans-serif"), |
| 26 | 26 | fontMono: readVariable(styles, "--font-mono", "ui-monospace, monospace"), |
apps/web/src/components/ChatMarkdown.tsx+1-1| ... | ... | @@ -1412,7 +1412,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 1412 | 1412 | > |
| 1413 | 1413 | {/* The full path: the chip already shows the shortened form, and a link |
| 1414 | 1414 | to the workspace root collapses to a bare label that repeats it. */} |
| 1415 | <div className="overflow-x-auto whitespace-nowrap [scrollbar-color:color-mix(in_srgb,var(--border)_78%,transparent)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--border)_78%,transparent)] [&::-webkit-scrollbar-track]:bg-transparent"> | |
| 1415 | <div className="overflow-x-auto whitespace-nowrap [scrollbar-color:color-mix(in_srgb,var(--contrast-border)_78%,transparent)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--contrast-border)_78%,transparent)] [&::-webkit-scrollbar-track]:bg-transparent"> | |
| 1416 | 1416 | {targetPath} |
| 1417 | 1417 | </div> |
| 1418 | 1418 | </TooltipPopup> |
apps/web/src/components/chat/ChangedFilesTree.tsx+1-1| ... | ... | @@ -66,7 +66,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: { |
| 66 | 66 | className={cn( |
| 67 | 67 | "flex items-center justify-between gap-2 rounded-xl", |
| 68 | 68 | expanded && |
| 69 | "sticky top-2 z-10 mb-2 bg-secondary dark:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))]", | |
| 69 | "sticky top-2 z-10 mb-2 bg-secondary dark:bg-[color-mix(in_srgb,var(--contrast-foreground)_2.5%,var(--background))]", | |
| 70 | 70 | )} |
| 71 | 71 | > |
| 72 | 72 | <button |
apps/web/src/components/chat/ModelListRow.tsx+1-1| ... | ... | @@ -52,7 +52,7 @@ export const ModelListRow = memo(function ModelListRow(props: { |
| 52 | 52 | contentClassName="flex w-full items-center gap-3" |
| 53 | 53 | className={cn( |
| 54 | 54 | "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2 transition-[background-color,box-shadow,color]", |
| 55 | "hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-highlighted:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0 [&[data-highlighted][data-selected]]:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))]", | |
| 55 | "hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] data-highlighted:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0 [&[data-highlighted][data-selected]]:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))]", | |
| 56 | 56 | props.disabledReason && |
| 57 | 57 | "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent", |
| 58 | 58 | )} |
apps/web/src/components/chat/ModelPickerSidebar.tsx+2-2| ... | ... | @@ -107,7 +107,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { |
| 107 | 107 | render={ |
| 108 | 108 | <button |
| 109 | 109 | className={cn( |
| 110 | "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:outline-none", | |
| 110 | "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:outline-none", | |
| 111 | 111 | )} |
| 112 | 112 | onClick={() => handleSelect("favorites")} |
| 113 | 113 | type="button" |
| ... | ... | @@ -152,7 +152,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { |
| 152 | 152 | const button = ( |
| 153 | 153 | <button |
| 154 | 154 | className={cn( |
| 155 | "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:outline-none", | |
| 155 | "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:outline-none", | |
| 156 | 156 | isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent", |
| 157 | 157 | )} |
| 158 | 158 | data-provider-accent-color={entry.accentColor} |
apps/web/src/components/chat/ProviderModelPicker.tsx+1-1| ... | ... | @@ -166,7 +166,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 166 | 166 | showBadge={showInstanceBadge} |
| 167 | 167 | className="size-4" |
| 168 | 168 | iconClassName={cn("size-4", props.activeProviderIconClassName)} |
| 169 | indicatorBackground="var(--input)" | |
| 169 | indicatorBackground="var(--contrast-input)" | |
| 170 | 170 | badgeClassName={cn( |
| 171 | 171 | "right-[-0.125rem] bottom-[-0.125rem] h-3 min-w-3", |
| 172 | 172 | "px-0.5 text-[7px]", |
apps/web/src/components/clerk/clerkAppearance.test.ts+5-5| ... | ... | @@ -59,12 +59,12 @@ describe("clerkAppearance", () => { |
| 59 | 59 | colorDanger: "var(--error)", |
| 60 | 60 | colorSuccess: "var(--success)", |
| 61 | 61 | colorWarning: "var(--warning)", |
| 62 | colorNeutral: "var(--foreground)", | |
| 63 | colorForeground: "var(--foreground)", | |
| 64 | colorMuted: "color-mix(in srgb, var(--card) 98%, var(--foreground))", | |
| 65 | colorMutedForeground: "var(--muted-foreground)", | |
| 62 | colorNeutral: "var(--contrast-foreground)", | |
| 63 | colorForeground: "var(--contrast-foreground)", | |
| 64 | colorMuted: "color-mix(in srgb, var(--card) 98%, var(--contrast-foreground))", | |
| 65 | colorMutedForeground: "var(--contrast-muted-foreground)", | |
| 66 | 66 | colorBackground: "var(--card)", |
| 67 | colorInputForeground: "var(--foreground)", | |
| 67 | colorInputForeground: "var(--contrast-foreground)", | |
| 68 | 68 | colorInput: "var(--secondary)", |
| 69 | 69 | colorRing: "var(--ring)", |
| 70 | 70 | }, |
apps/web/src/components/clerk/clerkAppearance.ts+5-5| ... | ... | @@ -13,14 +13,14 @@ export const clerkAppearance = { |
| 13 | 13 | colorDanger: "var(--error)", |
| 14 | 14 | colorSuccess: "var(--success)", |
| 15 | 15 | colorWarning: "var(--warning)", |
| 16 | colorNeutral: "var(--foreground)", | |
| 17 | colorForeground: "var(--foreground)", | |
| 16 | colorNeutral: "var(--contrast-foreground)", | |
| 17 | colorForeground: "var(--contrast-foreground)", | |
| 18 | 18 | // The stock dark theme's muted token is translucent. Clerk uses this as |
| 19 | 19 | // the footer's background, so derive an opaque muted surface from the card. |
| 20 | colorMuted: "color-mix(in srgb, var(--card) 98%, var(--foreground))", | |
| 21 | colorMutedForeground: "var(--muted-foreground)", | |
| 20 | colorMuted: "color-mix(in srgb, var(--card) 98%, var(--contrast-foreground))", | |
| 21 | colorMutedForeground: "var(--contrast-muted-foreground)", | |
| 22 | 22 | colorBackground: "var(--card)", |
| 23 | colorInputForeground: "var(--foreground)", | |
| 23 | colorInputForeground: "var(--contrast-foreground)", | |
| 24 | 24 | colorInput: "var(--secondary)", |
| 25 | 25 | colorRing: "var(--ring)", |
| 26 | 26 | }, |
apps/web/src/components/color-selector.tsx+1-1| ... | ... | @@ -13,7 +13,7 @@ interface ColorSelectorProps { |
| 13 | 13 | } |
| 14 | 14 | |
| 15 | 15 | const colorMap = { |
| 16 | default: "var(--foreground)", | |
| 16 | default: "var(--contrast-foreground)", | |
| 17 | 17 | red: "var(--color-red-500)", |
| 18 | 18 | green: "var(--color-green-500)", |
| 19 | 19 | blue: "var(--color-blue-500)", |
apps/web/src/components/files/FileBrowserPanel.tsx+1-1| ... | ... | @@ -378,7 +378,7 @@ export default function FileBrowserPanel({ |
| 378 | 378 | className="min-h-0 flex-1 overflow-hidden" |
| 379 | 379 | style={{ |
| 380 | 380 | colorScheme: resolvedTheme, |
| 381 | ["--trees-fg-override" as string]: "var(--foreground)", | |
| 381 | ["--trees-fg-override" as string]: "var(--contrast-foreground)", | |
| 382 | 382 | }} |
| 383 | 383 | /> |
| 384 | 384 | )} |
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+1-1| ... | ... | @@ -225,7 +225,7 @@ const REVIEW_OUTCOME_PRESENTATION = { |
| 225 | 225 | toneClassName: "text-muted-foreground/70", |
| 226 | 226 | ringClassName: "ring-2 ring-muted-foreground/60", |
| 227 | 227 | staleRingClassName: |
| 228 | "ring-2 ring-[color-mix(in_srgb,var(--muted-foreground)_30%,var(--background))]", | |
| 228 | "ring-2 ring-[color-mix(in_srgb,var(--contrast-muted-foreground)_30%,var(--background))]", | |
| 229 | 229 | badgeVariant: "outline", |
| 230 | 230 | }, |
| 231 | 231 | } as const satisfies Record< |
apps/web/src/components/settings/SettingsPanels.tsx+62| ... | ... | @@ -20,6 +20,7 @@ import { |
| 20 | 20 | DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, |
| 21 | 21 | DEFAULT_UNIFIED_SETTINGS, |
| 22 | 22 | type EnvironmentIdentificationMode, |
| 23 | MAX_APPEARANCE_CONTRAST, | |
| 23 | 24 | MAX_CODE_FONT_SIZE, |
| 24 | 25 | MAX_GLASS_OPACITY, |
| 25 | 26 | MAX_INTERFACE_FONT_SIZE, |
| ... | ... | @@ -27,6 +28,7 @@ import { |
| 27 | 28 | MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, |
| 28 | 29 | MAX_TERMINAL_FONT_SIZE, |
| 29 | 30 | MIN_CODE_FONT_SIZE, |
| 31 | MIN_APPEARANCE_CONTRAST, | |
| 30 | 32 | MIN_GLASS_OPACITY, |
| 31 | 33 | MIN_INTERFACE_FONT_SIZE, |
| 32 | 34 | MIN_PROMPT_FONT_SIZE, |
| ... | ... | @@ -475,6 +477,9 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 475 | 477 | ...(theme !== "system" ? ["Theme"] : []), |
| 476 | 478 | ...(!followSystem ? ["Follow system"] : []), |
| 477 | 479 | ...(themeHalves !== null ? ["Theme mix"] : []), |
| 480 | ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast | |
| 481 | ? ["Contrast"] | |
| 482 | : []), | |
| 478 | 483 | ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), |
| 479 | 484 | ...(settings.environmentIdentificationMode !== |
| 480 | 485 | DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode |
| ... | ... | @@ -543,6 +548,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 543 | 548 | settings.browserDefaultZoomFactor, |
| 544 | 549 | settings.browserDefaultAppearance, |
| 545 | 550 | settings.browserAutoShowFloatingPreview, |
| 551 | settings.appearanceContrast, | |
| 546 | 552 | settings.enableAgentBrowserAccess, |
| 547 | 553 | settings.confirmQuit, |
| 548 | 554 | settings.confirmThreadArchive, |
| ... | ... | @@ -638,6 +644,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 638 | 644 | return; |
| 639 | 645 | } |
| 640 | 646 | updateSettings({ |
| 647 | appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, | |
| 641 | 648 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, |
| 642 | 649 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, |
| 643 | 650 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, |
| ... | ... | @@ -986,6 +993,13 @@ export function AppearanceSettingsPanel() { |
| 986 | 993 | "--settings-slider-progress": `${glassOpacityRatio * 100}%`, |
| 987 | 994 | "--settings-slider-fill-offset": `${0.5 - glassOpacityRatio}rem`, |
| 988 | 995 | } as CSSProperties; |
| 996 | const appearanceContrastRatio = | |
| 997 | (settings.appearanceContrast - MIN_APPEARANCE_CONTRAST) / | |
| 998 | (MAX_APPEARANCE_CONTRAST - MIN_APPEARANCE_CONTRAST); | |
| 999 | const appearanceContrastSliderStyle = { | |
| 1000 | "--settings-slider-progress": `${appearanceContrastRatio * 100}%`, | |
| 1001 | "--settings-slider-fill-offset": `${0.5 - appearanceContrastRatio}rem`, | |
| 1002 | } as CSSProperties; | |
| 989 | 1003 | |
| 990 | 1004 | return ( |
| 991 | 1005 | <SettingsPageContainer> |
| ... | ... | @@ -1006,6 +1020,54 @@ export function AppearanceSettingsPanel() { |
| 1006 | 1020 | /> |
| 1007 | 1021 | </div> |
| 1008 | 1022 | |
| 1023 | <SettingsRow | |
| 1024 | {...searchableSetting("setting-appearance-contrast")} | |
| 1025 | description="Adjust the contrast of colors and borders across the interface." | |
| 1026 | resetAction={ | |
| 1027 | settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast ? ( | |
| 1028 | <SettingResetButton | |
| 1029 | label="contrast" | |
| 1030 | onClick={() => | |
| 1031 | updateSettings({ | |
| 1032 | appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, | |
| 1033 | }) | |
| 1034 | } | |
| 1035 | /> | |
| 1036 | ) : null | |
| 1037 | } | |
| 1038 | control={ | |
| 1039 | <div className="flex w-full items-center gap-3 sm:w-52"> | |
| 1040 | <output | |
| 1041 | className="min-w-12 rounded-md bg-muted px-2 py-1 text-center font-mono text-xs font-medium tabular-nums text-foreground" | |
| 1042 | htmlFor="appearance-contrast" | |
| 1043 | > | |
| 1044 | {settings.appearanceContrast}% | |
| 1045 | </output> | |
| 1046 | <input | |
| 1047 | aria-label="Contrast" | |
| 1048 | className="settings-slider min-w-0 flex-1" | |
| 1049 | id="appearance-contrast" | |
| 1050 | max={MAX_APPEARANCE_CONTRAST} | |
| 1051 | min={MIN_APPEARANCE_CONTRAST} | |
| 1052 | onChange={(event) => { | |
| 1053 | const appearanceContrast = Number(event.currentTarget.value); | |
| 1054 | if ( | |
| 1055 | Number.isInteger(appearanceContrast) && | |
| 1056 | appearanceContrast >= MIN_APPEARANCE_CONTRAST && | |
| 1057 | appearanceContrast <= MAX_APPEARANCE_CONTRAST | |
| 1058 | ) { | |
| 1059 | updateSettings({ appearanceContrast }); | |
| 1060 | } | |
| 1061 | }} | |
| 1062 | step={5} | |
| 1063 | style={appearanceContrastSliderStyle} | |
| 1064 | type="range" | |
| 1065 | value={settings.appearanceContrast} | |
| 1066 | /> | |
| 1067 | </div> | |
| 1068 | } | |
| 1069 | /> | |
| 1070 | ||
| 1009 | 1071 | <SettingsRow |
| 1010 | 1072 | {...searchableSetting("setting-glass-opacity")} |
| 1011 | 1073 | description="Control how transparent glass surfaces are. Higher values make menus, dialogs, and the composer more solid." |
apps/web/src/components/settings/settingsSearch.ts+6| ... | ... | @@ -57,6 +57,12 @@ export const SETTINGS_SEARCH_ITEMS = [ |
| 57 | 57 | // stable scroll destination for both. |
| 58 | 58 | targetId: "appearance", |
| 59 | 59 | }, |
| 60 | { | |
| 61 | // Prefixed because the slider control already owns the `appearance-contrast` id. | |
| 62 | id: "setting-appearance-contrast", | |
| 63 | title: "Contrast", | |
| 64 | to: "/settings/appearance", | |
| 65 | }, | |
| 60 | 66 | { |
| 61 | 67 | // Prefixed because the slider control already owns the `glass-opacity` id. |
| 62 | 68 | id: "setting-glass-opacity", |
apps/web/src/components/ui/button.test.tsx+1-1| ... | ... | @@ -13,7 +13,7 @@ describe("button geometry tokens", () => { |
| 13 | 13 | ); |
| 14 | 14 | |
| 15 | 15 | expect(html).toContain("rounded-[var(--control-radius)]"); |
| 16 | expect(html).toContain("[--control-icon-color:var(--muted-foreground)]"); | |
| 16 | expect(html).toContain("[--control-icon-color:var(--contrast-muted-foreground)]"); | |
| 17 | 17 | expect(html).toContain("text-[var(--control-icon-color)]"); |
| 18 | 18 | expect(html).not.toContain("opacity-80"); |
| 19 | 19 | }); |
apps/web/src/components/ui/button.tsx+4-4| ... | ... | @@ -43,14 +43,14 @@ const buttonVariants = cva( |
| 43 | 43 | "destructive-outline": |
| 44 | 44 | "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4", |
| 45 | 45 | ghost: |
| 46 | "[--control-icon-color:var(--muted-foreground)] border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent", | |
| 46 | "[--control-icon-color:var(--contrast-muted-foreground)] border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent", | |
| 47 | 47 | "ghost-muted": |
| 48 | "[--control-icon-color:var(--muted-foreground)] border-transparent text-muted-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground", | |
| 48 | "[--control-icon-color:var(--contrast-muted-foreground)] border-transparent text-muted-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground", | |
| 49 | 49 | glass: |
| 50 | "surface-glass [--control-icon-color:var(--muted-foreground)] border-border/60 text-foreground shadow-sm [:hover,[data-pressed]]:border-border", | |
| 50 | "surface-glass [--control-icon-color:var(--contrast-muted-foreground)] border-border/60 text-foreground shadow-sm [:hover,[data-pressed]]:border-border", | |
| 51 | 51 | link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline", |
| 52 | 52 | outline: |
| 53 | "[--control-icon-color:var(--muted-foreground)] border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64", | |
| 53 | "[--control-icon-color:var(--contrast-muted-foreground)] border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64", | |
| 54 | 54 | secondary: |
| 55 | 55 | "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90", |
| 56 | 56 | }, |
apps/web/src/components/usage/usageProviders.ts+1-1| ... | ... | @@ -16,7 +16,7 @@ type UsageProviderPresentation = { |
| 16 | 16 | export const PROVIDER_PRESENTATION = { |
| 17 | 17 | codex: { |
| 18 | 18 | label: "Codex", |
| 19 | color: "var(--foreground)", | |
| 19 | color: "var(--contrast-foreground)", | |
| 20 | 20 | mark: OpenAI, |
| 21 | 21 | }, |
| 22 | 22 | claude: { |
apps/web/src/contextMenuFallback.test.ts+1-1| ... | ... | @@ -315,7 +315,7 @@ describe("showContextMenuFallback", () => { |
| 315 | 315 | expect(siblingButton).toBeTruthy(); |
| 316 | 316 | expect(childButton?.focused).toBe(true); |
| 317 | 317 | expect(childButton?.style.background).toBe("var(--accent)"); |
| 318 | expect(childButton?.style.color).toBe("var(--accent-foreground)"); | |
| 318 | expect(childButton?.style.color).toBe("var(--contrast-accent-foreground)"); | |
| 319 | 319 | siblingButton?.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); |
| 320 | 320 | expect(childButton?.focused).toBe(false); |
| 321 | 321 | expect(childButton?.style.background).toBe("transparent"); |
apps/web/src/contextMenuFallback.ts+7-6| ... | ... | @@ -278,7 +278,7 @@ export function showContextMenuFallback<T extends string>( |
| 278 | 278 | menu.className = |
| 279 | 279 | "dropdown-glass fixed z-[10000] min-w-32 max-w-sm overflow-hidden rounded-lg bg-clip-padding text-popover-foreground outline-none"; |
| 280 | 280 | menu.style.cssText = |
| 281 | "position:fixed;z-index:10000;min-width:8rem;max-width:24rem;overflow:hidden;border-radius:var(--radius-lg);background-clip:padding-box;color:var(--popover-foreground);outline:none;pointer-events:auto;"; | |
| 281 | "position:fixed;z-index:10000;min-width:8rem;max-width:24rem;overflow:hidden;border-radius:var(--radius-lg);background-clip:padding-box;color:var(--contrast-popover-foreground);outline:none;pointer-events:auto;"; | |
| 282 | 282 | menu.style.left = `${preferredLeft}px`; |
| 283 | 283 | menu.style.top = `${preferredTop}px`; |
| 284 | 284 | menu.dataset.level = String(level); |
| ... | ... | @@ -293,7 +293,8 @@ export function showContextMenuFallback<T extends string>( |
| 293 | 293 | if (item.separatorBefore === true && inner.children.length > 0) { |
| 294 | 294 | const separator = document.createElement("div"); |
| 295 | 295 | separator.className = "mx-2 my-1 h-px bg-border"; |
| 296 | separator.style.cssText = "height:1px;margin:0.25rem 0.5rem;background:var(--border);"; | |
| 296 | separator.style.cssText = | |
| 297 | "height:1px;margin:0.25rem 0.5rem;background:var(--contrast-border);"; | |
| 297 | 298 | separator.dataset.contextMenuSeparator = "true"; |
| 298 | 299 | separator.setAttribute("role", "separator"); |
| 299 | 300 | inner.appendChild(separator); |
| ... | ... | @@ -323,12 +324,12 @@ export function showContextMenuFallback<T extends string>( |
| 323 | 324 | ? `${rowBase} text-destructive-foreground hover:bg-destructive/10 hover:text-destructive-foreground` |
| 324 | 325 | : `${rowBase} text-foreground hover:bg-accent hover:text-accent-foreground`; |
| 325 | 326 | button.style.cssText = |
| 326 | "display:flex;width:100%;min-height:1.75rem;align-items:center;gap:0.5rem;border:0;border-radius:var(--radius-sm);background:transparent;padding:0.25rem 0.5rem;color:var(--foreground);font-family:var(--font-sans,system-ui,sans-serif);font-size:0.875rem;line-height:1.25rem;text-align:left;cursor:default;"; | |
| 327 | "display:flex;width:100%;min-height:1.75rem;align-items:center;gap:0.5rem;border:0;border-radius:var(--radius-sm);background:transparent;padding:0.25rem 0.5rem;color:var(--contrast-foreground);font-family:var(--font-sans,system-ui,sans-serif);font-size:0.875rem;line-height:1.25rem;text-align:left;cursor:default;"; | |
| 327 | 328 | if (isLeafDestructive) { |
| 328 | 329 | button.style.color = "var(--destructive-foreground)"; |
| 329 | 330 | } |
| 330 | 331 | if (isDisabled) { |
| 331 | button.style.color = "var(--muted-foreground)"; | |
| 332 | button.style.color = "var(--contrast-muted-foreground)"; | |
| 332 | 333 | button.style.opacity = "0.64"; |
| 333 | 334 | button.style.pointerEvents = "none"; |
| 334 | 335 | } |
| ... | ... | @@ -373,10 +374,10 @@ export function showContextMenuFallback<T extends string>( |
| 373 | 374 | button.style.color = isHighlighted |
| 374 | 375 | ? isLeafDestructive |
| 375 | 376 | ? "var(--destructive-foreground)" |
| 376 | : "var(--accent-foreground)" | |
| 377 | : "var(--contrast-accent-foreground)" | |
| 377 | 378 | : isLeafDestructive |
| 378 | 379 | ? "var(--destructive-foreground)" |
| 379 | : "var(--foreground)"; | |
| 380 | : "var(--contrast-foreground)"; | |
| 380 | 381 | }; |
| 381 | 382 | button.addEventListener("mouseenter", () => { |
| 382 | 383 | button.focus({ preventScroll: true }); |
apps/web/src/index.css+190-64| ... | ... | @@ -78,6 +78,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 78 | 78 | |
| 79 | 79 | :root { |
| 80 | 80 | --app-scrollbar-width: 6px; |
| 81 | --appearance-contrast-base: 100%; | |
| 82 | --appearance-contrast-boost: 0%; | |
| 83 | --appearance-contrast-border-boost: 0%; | |
| 84 | --appearance-contrast-target: black; | |
| 81 | 85 | --app-scrollbar-thumb: rgb(217 217 217); |
| 82 | 86 | --app-scrollbar-thumb-hover: rgb(191 191 191); |
| 83 | 87 | /* |
| ... | ... | @@ -87,7 +91,11 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 87 | 91 | --control-radius: 0.5rem; |
| 88 | 92 | --sidebar-content-inset: 0.5rem; |
| 89 | 93 | --sidebar-control-gap: 0.5rem; |
| 90 | --sidebar-icon-color: color-mix(in srgb, var(--sidebar-muted-foreground) 60%, var(--sidebar)); | |
| 94 | --sidebar-icon-color: color-mix( | |
| 95 | in srgb, | |
| 96 | var(--contrast-sidebar-muted-foreground) 60%, | |
| 97 | var(--sidebar) | |
| 98 | ); | |
| 91 | 99 | --sidebar-row-content-inset: 0.625rem; |
| 92 | 100 | --command-shell-inset: 0.5rem; |
| 93 | 101 | --command-content-inset: 1rem; |
| ... | ... | @@ -105,6 +113,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 105 | 113 | --workspace-titlebar-control-gap: 0.75rem; |
| 106 | 114 | |
| 107 | 115 | @variant dark { |
| 116 | --appearance-contrast-target: white; | |
| 108 | 117 | --app-scrollbar-thumb: rgb(255 255 255 / 8%); |
| 109 | 118 | --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%); |
| 110 | 119 | --glass-blur: 16px; |
| ... | ... | @@ -156,40 +165,40 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 156 | 165 | --color-info: var(--info); |
| 157 | 166 | --color-destructive-foreground: var(--destructive-foreground); |
| 158 | 167 | --color-ring: var(--ring); |
| 159 | --color-input: var(--input); | |
| 160 | --color-border: var(--border); | |
| 168 | --color-input: var(--contrast-input); | |
| 169 | --color-border: var(--contrast-border); | |
| 161 | 170 | --color-destructive: var(--destructive); |
| 162 | --color-accent-foreground: var(--accent-foreground); | |
| 171 | --color-accent-foreground: var(--contrast-accent-foreground); | |
| 163 | 172 | --color-accent: var(--accent); |
| 164 | --color-muted-foreground: var(--muted-foreground); | |
| 173 | --color-muted-foreground: var(--contrast-muted-foreground); | |
| 165 | 174 | --color-muted: var(--muted); |
| 166 | --color-placeholder: var(--placeholder); | |
| 167 | --color-secondary-label: var(--secondary-label); | |
| 168 | --color-icon-muted: var(--icon-muted); | |
| 169 | --color-secondary-foreground: var(--secondary-foreground); | |
| 175 | --color-placeholder: var(--contrast-placeholder); | |
| 176 | --color-secondary-label: var(--contrast-secondary-label); | |
| 177 | --color-icon-muted: var(--contrast-icon-muted); | |
| 178 | --color-secondary-foreground: var(--contrast-secondary-foreground); | |
| 170 | 179 | --color-secondary: var(--secondary); |
| 171 | 180 | --color-primary-foreground: var(--primary-foreground); |
| 172 | 181 | --color-primary: var(--primary); |
| 173 | --color-popover-foreground: var(--popover-foreground); | |
| 182 | --color-popover-foreground: var(--contrast-popover-foreground); | |
| 174 | 183 | --color-popover: var(--popover); |
| 175 | --color-card-foreground: var(--card-foreground); | |
| 184 | --color-card-foreground: var(--contrast-card-foreground); | |
| 176 | 185 | --color-card: var(--card); |
| 177 | --color-foreground: var(--foreground); | |
| 186 | --color-foreground: var(--contrast-foreground); | |
| 178 | 187 | --color-background: var(--background); |
| 179 | 188 | --color-surface-raised: var(--surface-raised); |
| 180 | 189 | --color-message: var(--message-surface); |
| 181 | --color-message-foreground: var(--message-foreground); | |
| 190 | --color-message-foreground: var(--contrast-message-foreground); | |
| 182 | 191 | --color-message-action: var(--message-action); |
| 183 | 192 | --color-message-action-foreground: var(--message-action-foreground); |
| 184 | 193 | --color-message-action-hover: var(--message-action-hover); |
| 185 | 194 | --color-sidebar: var(--sidebar); |
| 186 | --color-sidebar-foreground: var(--sidebar-foreground); | |
| 187 | --color-sidebar-muted-foreground: var(--sidebar-muted-foreground); | |
| 195 | --color-sidebar-foreground: var(--contrast-sidebar-foreground); | |
| 196 | --color-sidebar-muted-foreground: var(--contrast-sidebar-muted-foreground); | |
| 188 | 197 | --color-sidebar-control-surface: var(--sidebar-control-surface); |
| 189 | 198 | --color-sidebar-row-hover: var(--sidebar-row-hover); |
| 190 | 199 | --color-sidebar-row-active: var(--sidebar-row-active); |
| 191 | 200 | --color-sidebar-row-selected: var(--sidebar-row-selected); |
| 192 | --color-sidebar-border: var(--sidebar-border); | |
| 201 | --color-sidebar-border: var(--contrast-sidebar-border); | |
| 193 | 202 | --radius-sm: calc(var(--radius) - 4px); |
| 194 | 203 | --radius-md: calc(var(--radius) - 2px); |
| 195 | 204 | --radius-lg: var(--radius); |
| ... | ... | @@ -304,7 +313,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 304 | 313 | background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); |
| 305 | 314 | -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); |
| 306 | 315 | backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); |
| 307 | border-color: color-mix(in srgb, var(--foreground) 10%, transparent); | |
| 316 | border-color: color-mix(in srgb, var(--contrast-foreground) 10%, transparent); | |
| 308 | 317 | box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%); |
| 309 | 318 | |
| 310 | 319 | @variant dark { |
| ... | ... | @@ -337,7 +346,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 337 | 346 | ); |
| 338 | 347 | -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); |
| 339 | 348 | backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); |
| 340 | border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); | |
| 349 | border: 1px solid color-mix(in srgb, var(--contrast-foreground) 10%, transparent); | |
| 341 | 350 | |
| 342 | 351 | @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { |
| 343 | 352 | background: var(--popover) !important; |
| ... | ... | @@ -757,7 +766,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 757 | 766 | .chat-composer-shoulder-tab { |
| 758 | 767 | border-color: var( |
| 759 | 768 | --chat-composer-outline, |
| 760 | color-mix(in srgb, var(--foreground) 8%, transparent) | |
| 769 | color-mix(in srgb, var(--contrast-foreground) 8%, transparent) | |
| 761 | 770 | ); |
| 762 | 771 | background: color-mix( |
| 763 | 772 | in srgb, |
| ... | ... | @@ -781,7 +790,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 781 | 790 | --chat-composer-attached-surface: var(--chat-composer-glass-surface, var(--card)); |
| 782 | 791 | --chat-composer-attached-outline: var( |
| 783 | 792 | --chat-composer-outline, |
| 784 | color-mix(in srgb, var(--foreground) 8%, transparent) | |
| 793 | color-mix(in srgb, var(--contrast-foreground) 8%, transparent) | |
| 785 | 794 | ); |
| 786 | 795 | background: color-mix( |
| 787 | 796 | in srgb, |
| ... | ... | @@ -817,7 +826,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 817 | 826 | --chat-composer-attached-surface: var(--chat-composer-glass-surface, var(--card)); |
| 818 | 827 | --chat-composer-attached-outline: var( |
| 819 | 828 | --chat-composer-outline, |
| 820 | color-mix(in srgb, var(--foreground) 8%, transparent) | |
| 829 | color-mix(in srgb, var(--contrast-foreground) 8%, transparent) | |
| 821 | 830 | ); |
| 822 | 831 | --chat-composer-attached-tint: transparent; |
| 823 | 832 | position: relative; |
| ... | ... | @@ -1238,17 +1247,17 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1238 | 1247 | background: linear-gradient( |
| 1239 | 1248 | to right, |
| 1240 | 1249 | var(--primary) 0 var(--settings-slider-fill-position), |
| 1241 | color-mix(in srgb, var(--muted-foreground) 22%, transparent) | |
| 1250 | color-mix(in srgb, var(--contrast-muted-foreground) 22%, transparent) | |
| 1242 | 1251 | var(--settings-slider-fill-position) 100% |
| 1243 | 1252 | ); |
| 1244 | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 55%, transparent); | |
| 1253 | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--contrast-border) 55%, transparent); | |
| 1245 | 1254 | } |
| 1246 | 1255 | |
| 1247 | 1256 | .settings-slider::-moz-range-track { |
| 1248 | 1257 | height: 0.375rem; |
| 1249 | 1258 | border-radius: 9999px; |
| 1250 | background: color-mix(in srgb, var(--muted-foreground) 22%, transparent); | |
| 1251 | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 55%, transparent); | |
| 1259 | background: color-mix(in srgb, var(--contrast-muted-foreground) 22%, transparent); | |
| 1260 | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--contrast-border) 55%, transparent); | |
| 1252 | 1261 | } |
| 1253 | 1262 | |
| 1254 | 1263 | .settings-slider::-moz-range-progress { |
| ... | ... | @@ -1265,7 +1274,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1265 | 1274 | border: 2px solid var(--primary); |
| 1266 | 1275 | border-radius: 9999px; |
| 1267 | 1276 | background: var(--background); |
| 1268 | box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 12%, transparent); | |
| 1277 | box-shadow: 0 1px 2px color-mix(in srgb, var(--contrast-foreground) 12%, transparent); | |
| 1269 | 1278 | transition: |
| 1270 | 1279 | transform 120ms ease, |
| 1271 | 1280 | box-shadow 120ms ease; |
| ... | ... | @@ -1277,7 +1286,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1277 | 1286 | border: 2px solid var(--primary); |
| 1278 | 1287 | border-radius: 9999px; |
| 1279 | 1288 | background: var(--background); |
| 1280 | box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 12%, transparent); | |
| 1289 | box-shadow: 0 1px 2px color-mix(in srgb, var(--contrast-foreground) 12%, transparent); | |
| 1281 | 1290 | transition: |
| 1282 | 1291 | transform 120ms ease, |
| 1283 | 1292 | box-shadow 120ms ease; |
| ... | ... | @@ -1285,12 +1294,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1285 | 1294 | |
| 1286 | 1295 | .settings-slider:hover::-webkit-slider-thumb { |
| 1287 | 1296 | transform: scale(1.08); |
| 1288 | box-shadow: 0 1px 3px color-mix(in srgb, var(--foreground) 20%, transparent); | |
| 1297 | box-shadow: 0 1px 3px color-mix(in srgb, var(--contrast-foreground) 20%, transparent); | |
| 1289 | 1298 | } |
| 1290 | 1299 | |
| 1291 | 1300 | .settings-slider:hover::-moz-range-thumb { |
| 1292 | 1301 | transform: scale(1.08); |
| 1293 | box-shadow: 0 1px 3px color-mix(in srgb, var(--foreground) 20%, transparent); | |
| 1302 | box-shadow: 0 1px 3px color-mix(in srgb, var(--contrast-foreground) 20%, transparent); | |
| 1294 | 1303 | } |
| 1295 | 1304 | |
| 1296 | 1305 | .settings-slider:active::-webkit-slider-thumb { |
| ... | ... | @@ -1524,9 +1533,9 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1524 | 1533 | --sidebar-foreground: var(--foreground); |
| 1525 | 1534 | --sidebar-muted-foreground: var(--muted-foreground); |
| 1526 | 1535 | --sidebar-control-surface: var(--muted); |
| 1527 | --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent); | |
| 1528 | --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent); | |
| 1529 | --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent); | |
| 1536 | --sidebar-row-hover: color-mix(in srgb, var(--contrast-foreground) 8%, transparent); | |
| 1537 | --sidebar-row-active: color-mix(in srgb, var(--contrast-foreground) 11%, transparent); | |
| 1538 | --sidebar-row-selected: color-mix(in srgb, var(--contrast-foreground) 7%, transparent); | |
| 1530 | 1539 | --sidebar-border: var(--border); |
| 1531 | 1540 | --sidebar-stage-fade: var(--card); |
| 1532 | 1541 | } |
| ... | ... | @@ -1711,11 +1720,11 @@ html[data-theme-token-probe] *::after { |
| 1711 | 1720 | max-width: 12rem; |
| 1712 | 1721 | overflow: hidden; |
| 1713 | 1722 | padding: 0.2rem 0.45rem; |
| 1714 | border: 1px solid color-mix(in oklab, var(--ring) 48%, var(--border)); | |
| 1723 | border: 1px solid color-mix(in oklab, var(--ring) 48%, var(--contrast-border)); | |
| 1715 | 1724 | border-radius: 0.4rem; |
| 1716 | 1725 | background: var(--popover); |
| 1717 | 1726 | box-shadow: 0 4px 16px rgb(0 0 0 / 24%); |
| 1718 | color: var(--popover-foreground); | |
| 1727 | color: var(--contrast-popover-foreground); | |
| 1719 | 1728 | font-size: 0.6875rem; |
| 1720 | 1729 | font-weight: 600; |
| 1721 | 1730 | line-height: 1rem; |
| ... | ... | @@ -1736,16 +1745,16 @@ html[data-theme-token-probe] *::after { |
| 1736 | 1745 | dark-mode whites while preserving the same theme file across clients. */ |
| 1737 | 1746 | html[data-theme-id] [data-chat-header] { |
| 1738 | 1747 | background-color: var(--toolbar-background); |
| 1739 | color: var(--toolbar-foreground); | |
| 1748 | color: var(--contrast-toolbar-foreground); | |
| 1740 | 1749 | } |
| 1741 | 1750 | |
| 1742 | 1751 | html[data-theme-id] [data-chat-header] [data-slot="button"], |
| 1743 | 1752 | html[data-theme-id] [data-chat-header] [data-slot="menu-trigger"], |
| 1744 | 1753 | html[data-theme-id] [data-chat-header] [data-toolbar-control] { |
| 1745 | --control-icon-color: var(--toolbar-control-foreground); | |
| 1754 | --control-icon-color: var(--contrast-toolbar-control-foreground); | |
| 1746 | 1755 | background-color: var(--toolbar-control); |
| 1747 | border-color: var(--toolbar-border); | |
| 1748 | color: var(--toolbar-control-foreground); | |
| 1756 | border-color: var(--contrast-toolbar-border); | |
| 1757 | color: var(--contrast-toolbar-control-foreground); | |
| 1749 | 1758 | } |
| 1750 | 1759 | |
| 1751 | 1760 | /* The panel layout toggles stay ghost: they render both inside the header and |
| ... | ... | @@ -1757,8 +1766,8 @@ html[data-theme-id] [data-panel-layout-controls] [data-slot="toggle"], |
| 1757 | 1766 | html[data-theme-id] [data-panel-layout-controls] [data-slot="tooltip-trigger"], |
| 1758 | 1767 | html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="toggle"], |
| 1759 | 1768 | html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="tooltip-trigger"] { |
| 1760 | --control-icon-color: var(--toolbar-foreground); | |
| 1761 | color: var(--toolbar-foreground); | |
| 1769 | --control-icon-color: var(--contrast-toolbar-foreground); | |
| 1770 | color: var(--contrast-toolbar-foreground); | |
| 1762 | 1771 | } |
| 1763 | 1772 | |
| 1764 | 1773 | html[data-theme-id] [data-chat-header] [data-slot="button"]:hover, |
| ... | ... | @@ -1771,7 +1780,7 @@ html[data-theme-id] [data-chat-header] [data-toolbar-control][data-pressed] { |
| 1771 | 1780 | } |
| 1772 | 1781 | |
| 1773 | 1782 | html[data-theme-id] [data-chat-header] [data-slot="separator"] { |
| 1774 | background-color: var(--toolbar-border); | |
| 1783 | background-color: var(--contrast-toolbar-border); | |
| 1775 | 1784 | } |
| 1776 | 1785 | |
| 1777 | 1786 | /* Chat code blocks join the other code surfaces on the code tokens. The |
| ... | ... | @@ -1779,7 +1788,7 @@ html[data-theme-id] [data-chat-header] [data-slot="separator"] { |
| 1779 | 1788 | surfaces with the accent, which fights syntax highlighting. */ |
| 1780 | 1789 | html[data-theme-id] .chat-markdown .chat-markdown-codeblock { |
| 1781 | 1790 | background-color: var(--code-background); |
| 1782 | border-color: var(--border); | |
| 1791 | border-color: var(--contrast-border); | |
| 1783 | 1792 | color: var(--code-foreground); |
| 1784 | 1793 | } |
| 1785 | 1794 | |
| ... | ... | @@ -1814,7 +1823,7 @@ html[data-theme-id="t3-chat"] { |
| 1814 | 1823 | } |
| 1815 | 1824 | |
| 1816 | 1825 | & .chat-markdown :not(pre) > code { |
| 1817 | color: var(--message-foreground); | |
| 1826 | color: var(--contrast-message-foreground); | |
| 1818 | 1827 | } |
| 1819 | 1828 | } |
| 1820 | 1829 | } |
| ... | ... | @@ -1844,10 +1853,10 @@ html[data-theme-id] [data-app-sidebar] { |
| 1844 | 1853 | --sidebar-row-selected: var(--app-theme-sidebar-row-selected); |
| 1845 | 1854 | --sidebar-border: var(--app-theme-sidebar-border); |
| 1846 | 1855 | --sidebar-stage-fade: var(--app-theme-sidebar); |
| 1847 | border-color: color-mix(in srgb, var(--sidebar-foreground) 10%, transparent); | |
| 1856 | border-color: color-mix(in srgb, var(--contrast-sidebar-foreground) 10%, transparent); | |
| 1848 | 1857 | |
| 1849 | 1858 | @variant dark { |
| 1850 | border-color: color-mix(in srgb, var(--sidebar-foreground) 8%, transparent); | |
| 1859 | border-color: color-mix(in srgb, var(--contrast-sidebar-foreground) 8%, transparent); | |
| 1851 | 1860 | } |
| 1852 | 1861 | } |
| 1853 | 1862 | |
| ... | ... | @@ -1855,10 +1864,127 @@ html[data-theme-id] [data-app-sidebar] { |
| 1855 | 1864 | keeps that color while hovered. Do not neutralize this branded edge. */ |
| 1856 | 1865 | html[data-theme-id="t3-chat"] [data-app-sidebar] { |
| 1857 | 1866 | @variant dark { |
| 1858 | border-color: var(--sidebar-border); | |
| 1867 | border-color: var(--contrast-sidebar-border); | |
| 1859 | 1868 | } |
| 1860 | 1869 | } |
| 1861 | 1870 | |
| 1871 | /* Contrast stays in ordinary custom properties so both Tailwind utilities and | |
| 1872 | global/imperative chrome styles resolve the same adjusted role. Redeclare on | |
| 1873 | the sidebar because it owns a local semantic palette. */ | |
| 1874 | :root, | |
| 1875 | [data-app-sidebar] { | |
| 1876 | --contrast-toolbar-foreground: color-mix( | |
| 1877 | in oklab, | |
| 1878 | color-mix( | |
| 1879 | in oklab, | |
| 1880 | var(--toolbar-foreground) var(--appearance-contrast-base), | |
| 1881 | var(--toolbar-background) | |
| 1882 | ), | |
| 1883 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1884 | ); | |
| 1885 | --contrast-toolbar-border: color-mix( | |
| 1886 | in srgb, | |
| 1887 | color-mix(in srgb, var(--toolbar-border) var(--appearance-contrast-base), transparent), | |
| 1888 | var(--toolbar-foreground) var(--appearance-contrast-border-boost) | |
| 1889 | ); | |
| 1890 | --contrast-toolbar-control-foreground: color-mix( | |
| 1891 | in oklab, | |
| 1892 | color-mix( | |
| 1893 | in oklab, | |
| 1894 | var(--toolbar-control-foreground) var(--appearance-contrast-base), | |
| 1895 | var(--toolbar-control) | |
| 1896 | ), | |
| 1897 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1898 | ); | |
| 1899 | --contrast-input: color-mix( | |
| 1900 | in srgb, | |
| 1901 | color-mix(in srgb, var(--input) var(--appearance-contrast-base), transparent), | |
| 1902 | var(--foreground) var(--appearance-contrast-border-boost) | |
| 1903 | ); | |
| 1904 | --contrast-border: color-mix( | |
| 1905 | in srgb, | |
| 1906 | color-mix(in srgb, var(--border) var(--appearance-contrast-base), transparent), | |
| 1907 | var(--foreground) var(--appearance-contrast-border-boost) | |
| 1908 | ); | |
| 1909 | --contrast-foreground: color-mix( | |
| 1910 | in oklab, | |
| 1911 | color-mix(in oklab, var(--foreground) var(--appearance-contrast-base), var(--background)), | |
| 1912 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1913 | ); | |
| 1914 | --contrast-muted-foreground: color-mix( | |
| 1915 | in oklab, | |
| 1916 | color-mix(in oklab, var(--muted-foreground) var(--appearance-contrast-base), var(--background)), | |
| 1917 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1918 | ); | |
| 1919 | --contrast-placeholder: color-mix( | |
| 1920 | in oklab, | |
| 1921 | color-mix(in oklab, var(--placeholder) var(--appearance-contrast-base), var(--background)), | |
| 1922 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1923 | ); | |
| 1924 | --contrast-secondary-label: color-mix( | |
| 1925 | in oklab, | |
| 1926 | color-mix(in oklab, var(--secondary-label) var(--appearance-contrast-base), var(--background)), | |
| 1927 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1928 | ); | |
| 1929 | --contrast-icon-muted: color-mix( | |
| 1930 | in oklab, | |
| 1931 | color-mix(in oklab, var(--icon-muted) var(--appearance-contrast-base), var(--background)), | |
| 1932 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1933 | ); | |
| 1934 | --contrast-card-foreground: color-mix( | |
| 1935 | in oklab, | |
| 1936 | color-mix(in oklab, var(--card-foreground) var(--appearance-contrast-base), var(--card)), | |
| 1937 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1938 | ); | |
| 1939 | --contrast-popover-foreground: color-mix( | |
| 1940 | in oklab, | |
| 1941 | color-mix(in oklab, var(--popover-foreground) var(--appearance-contrast-base), var(--popover)), | |
| 1942 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1943 | ); | |
| 1944 | --contrast-accent-foreground: color-mix( | |
| 1945 | in oklab, | |
| 1946 | color-mix(in oklab, var(--accent-foreground) var(--appearance-contrast-base), var(--accent)), | |
| 1947 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1948 | ); | |
| 1949 | --contrast-secondary-foreground: color-mix( | |
| 1950 | in oklab, | |
| 1951 | color-mix( | |
| 1952 | in oklab, | |
| 1953 | var(--secondary-foreground) var(--appearance-contrast-base), | |
| 1954 | var(--secondary) | |
| 1955 | ), | |
| 1956 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1957 | ); | |
| 1958 | --contrast-message-foreground: color-mix( | |
| 1959 | in oklab, | |
| 1960 | color-mix( | |
| 1961 | in oklab, | |
| 1962 | var(--message-foreground) var(--appearance-contrast-base), | |
| 1963 | var(--message-surface) | |
| 1964 | ), | |
| 1965 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1966 | ); | |
| 1967 | --contrast-sidebar-foreground: color-mix( | |
| 1968 | in oklab, | |
| 1969 | color-mix(in oklab, var(--sidebar-foreground) var(--appearance-contrast-base), var(--sidebar)), | |
| 1970 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1971 | ); | |
| 1972 | --contrast-sidebar-muted-foreground: color-mix( | |
| 1973 | in oklab, | |
| 1974 | color-mix( | |
| 1975 | in oklab, | |
| 1976 | var(--sidebar-muted-foreground) var(--appearance-contrast-base), | |
| 1977 | var(--sidebar) | |
| 1978 | ), | |
| 1979 | var(--appearance-contrast-target) var(--appearance-contrast-boost) | |
| 1980 | ); | |
| 1981 | --contrast-sidebar-border: color-mix( | |
| 1982 | in srgb, | |
| 1983 | color-mix(in srgb, var(--sidebar-border) var(--appearance-contrast-base), transparent), | |
| 1984 | var(--sidebar-foreground) var(--appearance-contrast-border-boost) | |
| 1985 | ); | |
| 1986 | } | |
| 1987 | ||
| 1862 | 1988 | body { |
| 1863 | 1989 | /* Reference the theme token (not a literal stack) so the Settings -> |
| 1864 | 1990 | Appearance runtime override of --font-sans reaches all interface text. */ |
| ... | ... | @@ -2004,7 +2130,7 @@ code { |
| 2004 | 2130 | margin: 1.25rem 0 0.5rem; |
| 2005 | 2131 | font-weight: 600; |
| 2006 | 2132 | line-height: 1.3; |
| 2007 | color: var(--foreground); | |
| 2133 | color: var(--contrast-foreground); | |
| 2008 | 2134 | } |
| 2009 | 2135 | |
| 2010 | 2136 | .chat-markdown h1 { |
| ... | ... | @@ -2026,7 +2152,7 @@ code { |
| 2026 | 2152 | } |
| 2027 | 2153 | |
| 2028 | 2154 | .chat-markdown h6 { |
| 2029 | color: var(--muted-foreground); | |
| 2155 | color: var(--contrast-muted-foreground); | |
| 2030 | 2156 | } |
| 2031 | 2157 | |
| 2032 | 2158 | .chat-markdown ul { |
| ... | ... | @@ -2103,16 +2229,16 @@ code { |
| 2103 | 2229 | } |
| 2104 | 2230 | |
| 2105 | 2231 | .chat-markdown blockquote { |
| 2106 | border-left: 2px solid var(--border); | |
| 2232 | border-left: 2px solid var(--contrast-border); | |
| 2107 | 2233 | padding-left: 0.8rem; |
| 2108 | color: var(--muted-foreground); | |
| 2234 | color: var(--contrast-muted-foreground); | |
| 2109 | 2235 | } |
| 2110 | 2236 | |
| 2111 | 2237 | .chat-markdown section[data-footnotes] { |
| 2112 | 2238 | margin-top: 1.25rem; |
| 2113 | border-top: 1px solid var(--border); | |
| 2239 | border-top: 1px solid var(--contrast-border); | |
| 2114 | 2240 | padding-top: 0.75rem; |
| 2115 | color: var(--muted-foreground); | |
| 2241 | color: var(--contrast-muted-foreground); | |
| 2116 | 2242 | font-size: 0.75rem; |
| 2117 | 2243 | } |
| 2118 | 2244 | |
| ... | ... | @@ -2137,17 +2263,17 @@ code { |
| 2137 | 2263 | } |
| 2138 | 2264 | |
| 2139 | 2265 | .chat-markdown :not(pre) > code { |
| 2140 | border: 1px solid var(--border); | |
| 2266 | border: 1px solid var(--contrast-border); | |
| 2141 | 2267 | border-radius: 0.375rem; |
| 2142 | 2268 | background: var(--muted); |
| 2143 | 2269 | padding: 0.1rem 0.35rem; |
| 2144 | color: var(--foreground); | |
| 2270 | color: var(--contrast-foreground); | |
| 2145 | 2271 | font-size: 0.75rem; |
| 2146 | 2272 | } |
| 2147 | 2273 | |
| 2148 | 2274 | .chat-markdown a.chat-markdown-file-link, |
| 2149 | 2275 | .chat-markdown a.chat-markdown-file-link:hover { |
| 2150 | color: var(--foreground); | |
| 2276 | color: var(--contrast-foreground); | |
| 2151 | 2277 | text-decoration: none; |
| 2152 | 2278 | } |
| 2153 | 2279 | |
| ... | ... | @@ -2159,12 +2285,12 @@ code { |
| 2159 | 2285 | .chat-markdown pre { |
| 2160 | 2286 | max-width: 100%; |
| 2161 | 2287 | overflow-x: auto; |
| 2162 | border: 1px solid var(--border); | |
| 2288 | border: 1px solid var(--contrast-border); | |
| 2163 | 2289 | border-radius: 0.75rem; |
| 2164 | 2290 | background: var(--muted); |
| 2165 | 2291 | padding: 0.8rem 0.9rem; |
| 2166 | 2292 | scrollbar-width: thin; |
| 2167 | scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent; | |
| 2293 | scrollbar-color: color-mix(in srgb, var(--contrast-border) 78%, transparent) transparent; | |
| 2168 | 2294 | } |
| 2169 | 2295 | |
| 2170 | 2296 | .chat-markdown pre code { |
| ... | ... | @@ -2179,21 +2305,21 @@ code { |
| 2179 | 2305 | |
| 2180 | 2306 | .chat-markdown pre::-webkit-scrollbar-thumb { |
| 2181 | 2307 | border-radius: 999px; |
| 2182 | background: color-mix(in srgb, var(--border) 78%, transparent); | |
| 2308 | background: color-mix(in srgb, var(--contrast-border) 78%, transparent); | |
| 2183 | 2309 | } |
| 2184 | 2310 | |
| 2185 | 2311 | .chat-markdown .chat-markdown-codeblock-header, |
| 2186 | 2312 | .chat-markdown .chat-markdown-chrome-action { |
| 2187 | color: color-mix(in srgb, var(--foreground) 72%, transparent); | |
| 2313 | color: color-mix(in srgb, var(--contrast-foreground) 72%, transparent); | |
| 2188 | 2314 | } |
| 2189 | 2315 | |
| 2190 | 2316 | .chat-markdown .chat-markdown-chrome-action:hover { |
| 2191 | color: var(--foreground); | |
| 2317 | color: var(--contrast-foreground); | |
| 2192 | 2318 | } |
| 2193 | 2319 | |
| 2194 | 2320 | .chat-markdown .chat-markdown-chrome-action[aria-pressed="true"] { |
| 2195 | color: var(--foreground); | |
| 2196 | background: color-mix(in srgb, var(--foreground) 8%, transparent); | |
| 2321 | color: var(--contrast-foreground); | |
| 2322 | background: color-mix(in srgb, var(--contrast-foreground) 8%, transparent); | |
| 2197 | 2323 | } |
| 2198 | 2324 | |
| 2199 | 2325 | .chat-markdown .chat-markdown-codeblock pre { |
| ... | ... | @@ -2233,14 +2359,14 @@ code { |
| 2233 | 2359 | } |
| 2234 | 2360 | |
| 2235 | 2361 | .chat-markdown thead th { |
| 2236 | border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); | |
| 2362 | border-bottom: 1px solid color-mix(in srgb, var(--contrast-border) 60%, transparent); | |
| 2237 | 2363 | padding-block: 0.55rem; |
| 2238 | 2364 | font-weight: 600; |
| 2239 | 2365 | white-space: nowrap; |
| 2240 | 2366 | } |
| 2241 | 2367 | |
| 2242 | 2368 | .chat-markdown tbody td { |
| 2243 | border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); | |
| 2369 | border-bottom: 1px solid color-mix(in srgb, var(--contrast-border) 60%, transparent); | |
| 2244 | 2370 | } |
| 2245 | 2371 | |
| 2246 | 2372 | /* Collapsed (default): single-line cells truncate so arbitrary chat content |
apps/web/src/routes/__root.tsx+12| ... | ... | @@ -30,6 +30,7 @@ import { |
| 30 | 30 | } from "../components/ui/toast"; |
| 31 | 31 | import { resolveAndPersistPreferredEditor } from "../editorPreferences"; |
| 32 | 32 | import { applyAppearanceFontVariables } from "~/appearanceFonts"; |
| 33 | import { applyAppearanceContrast } from "~/appearanceContrast"; | |
| 33 | 34 | import { useClientSettings } from "../hooks/useSettings"; |
| 34 | 35 | import { PlanAgentSelectionHeal } from "../planAgentSelectionHeal"; |
| 35 | 36 | import { |
| ... | ... | @@ -131,6 +132,7 @@ function RootRouteView() { |
| 131 | 132 | <ToastProvider> |
| 132 | 133 | <AnchoredToastProvider> |
| 133 | 134 | <DocumentTitleSync /> |
| 135 | <ContrastAppearanceSync /> | |
| 134 | 136 | <GlassAppearanceSync /> |
| 135 | 137 | <FontAppearanceSync /> |
| 136 | 138 | {primaryEnvironmentAuthenticated ? <AuthenticatedTracingBootstrap /> : null} |
| ... | ... | @@ -152,6 +154,16 @@ function RootRouteView() { |
| 152 | 154 | ); |
| 153 | 155 | } |
| 154 | 156 | |
| 157 | function ContrastAppearanceSync() { | |
| 158 | const appearanceContrast = useClientSettings((settings) => settings.appearanceContrast); | |
| 159 | ||
| 160 | useEffect(() => { | |
| 161 | applyAppearanceContrast(document.documentElement, appearanceContrast); | |
| 162 | }, [appearanceContrast]); | |
| 163 | ||
| 164 | return null; | |
| 165 | } | |
| 166 | ||
| 155 | 167 | function GlassAppearanceSync() { |
| 156 | 168 | const glassOpacity = useClientSettings((settings) => settings.glassOpacity); |
| 157 | 169 |
packages/contracts/src/settings.test.ts+16| ... | ... | @@ -51,6 +51,22 @@ describe("ClientSettings glass opacity", () => { |
| 51 | 51 | }); |
| 52 | 52 | }); |
| 53 | 53 | |
| 54 | describe("ClientSettings appearance contrast", () => { | |
| 55 | it("defaults to the theme's original contrast", () => { | |
| 56 | expect(decodeClientSettings({}).appearanceContrast).toBe(100); | |
| 57 | }); | |
| 58 | ||
| 59 | it.each([49, 201, 92.5])("rejects an invalid appearance contrast: %s", (value) => { | |
| 60 | expect(() => decodeClientSettings({ appearanceContrast: value })).toThrow(); | |
| 61 | expect(() => decodeClientSettingsPatch({ appearanceContrast: value })).toThrow(); | |
| 62 | }); | |
| 63 | ||
| 64 | it.each([50, 100, 150, 200])("accepts an appearance contrast in range: %s", (value) => { | |
| 65 | expect(decodeClientSettings({ appearanceContrast: value }).appearanceContrast).toBe(value); | |
| 66 | expect(decodeClientSettingsPatch({ appearanceContrast: value }).appearanceContrast).toBe(value); | |
| 67 | }); | |
| 68 | }); | |
| 69 | ||
| 54 | 70 | describe("ClientSettings environment identification", () => { |
| 55 | 71 | it("defaults to artwork and accepts each presentation mode", () => { |
| 56 | 72 | expect(decodeClientSettings({}).environmentIdentificationMode).toBe("artwork"); |
packages/contracts/src/settings.ts+12| ... | ... | @@ -75,6 +75,14 @@ export const GlassOpacity = Schema.Int.check( |
| 75 | 75 | ); |
| 76 | 76 | export type GlassOpacity = typeof GlassOpacity.Type; |
| 77 | 77 | export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; |
| 78 | ||
| 79 | export const MIN_APPEARANCE_CONTRAST = 50; | |
| 80 | export const MAX_APPEARANCE_CONTRAST = 200; | |
| 81 | export const AppearanceContrast = Schema.Int.check( | |
| 82 | Schema.isBetween({ minimum: MIN_APPEARANCE_CONTRAST, maximum: MAX_APPEARANCE_CONTRAST }), | |
| 83 | ); | |
| 84 | export type AppearanceContrast = typeof AppearanceContrast.Type; | |
| 85 | export const DEFAULT_APPEARANCE_CONTRAST: AppearanceContrast = 100; | |
| 78 | 86 | /** |
| 79 | 87 | * Font size preferences, in CSS pixels. The ranges are deliberately narrow: |
| 80 | 88 | * the interface size scales every rem-based dimension in the app, so the |
| ... | ... | @@ -133,6 +141,9 @@ export const DEFAULT_BROWSER_VIEWPORT: PreviewViewportSetting = FILL_PREVIEW_VIE |
| 133 | 141 | export const DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW = true; |
| 134 | 142 | |
| 135 | 143 | export const ClientSettingsSchema = Schema.Struct({ |
| 144 | appearanceContrast: AppearanceContrast.pipe( | |
| 145 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_APPEARANCE_CONTRAST)), | |
| 146 | ), | |
| 136 | 147 | browserDefaultViewport: PreviewViewportSetting.pipe( |
| 137 | 148 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_VIEWPORT)), |
| 138 | 149 | ), |
| ... | ... | @@ -860,6 +871,7 @@ export const ServerSettingsPatch = Schema.Struct({ |
| 860 | 871 | export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; |
| 861 | 872 | |
| 862 | 873 | export const ClientSettingsPatch = Schema.Struct({ |
| 874 | appearanceContrast: Schema.optionalKey(AppearanceContrast), | |
| 863 | 875 | browserDefaultViewport: Schema.optionalKey(PreviewViewportSetting), |
| 864 | 876 | browserDefaultZoomFactor: Schema.optionalKey(PreviewZoomFactor), |
| 865 | 877 | browserDefaultAppearance: Schema.optionalKey(PreviewAppearancePreference), |