| 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,6 +47,9 @@ The goal is not to minimize CSS or class counts at any cost. The goal is to put |
| 47 | - light-only declarations use `@variant light`; | 47 | - light-only declarations use `@variant light`; |
| 48 | - raw `.dark` should remain only in the `dark` and `light` custom-variant definitions. | 48 | - raw `.dark` should remain only in the `dark` and `light` custom-variant definitions. |
| 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. | 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 | - Inspect emitted production CSS after unusual variants, arbitrary selectors, nested pseudo-elements, or attribute matching. Source syntax that looks valid is insufficient. | 53 | - Inspect emitted production CSS after unusual variants, arbitrary selectors, nested pseudo-elements, or attribute matching. Source syntax that looks valid is insufficient. |
| 51 | - 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. | 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 | - 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. | 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,6 +13,7 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
| 13 | import * as DesktopClientSettings from "./DesktopClientSettings.ts"; | 13 | import * as DesktopClientSettings from "./DesktopClientSettings.ts"; |
| 14 | 14 | ||
| 15 | const clientSettings: ClientSettings = { | 15 | const clientSettings: ClientSettings = { |
| 16 | appearanceContrast: 100, | ||
| 16 | browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" }, | 17 | browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" }, |
| 17 | browserDefaultZoomFactor: 1.25, | 18 | browserDefaultZoomFactor: 1.25, |
| 18 | browserDefaultAppearance: "dark", | 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,17 +10,17 @@ export function readPreviewAnnotationTheme(): DesktopPreviewAnnotationTheme { |
| 10 | colorScheme: root.classList.contains("dark") ? "dark" : "light", | 10 | colorScheme: root.classList.contains("dark") ? "dark" : "light", |
| 11 | radius: readVariable(styles, "--radius", "0.625rem"), | 11 | radius: readVariable(styles, "--radius", "0.625rem"), |
| 12 | background: readVariable(styles, "--background", "white"), | 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 | popover: readVariable(styles, "--popover", "white"), | 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 | primary: readVariable(styles, "--primary", "oklch(0.488 0.217 264)"), | 16 | primary: readVariable(styles, "--primary", "oklch(0.488 0.217 264)"), |
| 17 | primaryForeground: readVariable(styles, "--primary-foreground", "white"), | 17 | primaryForeground: readVariable(styles, "--primary-foreground", "white"), |
| 18 | muted: readVariable(styles, "--muted", "rgb(0 0 0 / 4%)"), | 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 | accent: readVariable(styles, "--accent", "rgb(0 0 0 / 4%)"), | 20 | accent: readVariable(styles, "--accent", "rgb(0 0 0 / 4%)"), |
| 21 | accentForeground: readVariable(styles, "--accent-foreground", "oklch(0.269 0 0)"), | 21 | accentForeground: readVariable(styles, "--contrast-accent-foreground", "oklch(0.269 0 0)"), |
| 22 | border: readVariable(styles, "--border", "rgb(0 0 0 / 8%)"), | 22 | border: readVariable(styles, "--contrast-border", "rgb(0 0 0 / 8%)"), |
| 23 | input: readVariable(styles, "--input", "rgb(0 0 0 / 10%)"), | 23 | input: readVariable(styles, "--contrast-input", "rgb(0 0 0 / 10%)"), |
| 24 | ring: readVariable(styles, "--ring", "oklch(0.488 0.217 264)"), | 24 | ring: readVariable(styles, "--ring", "oklch(0.488 0.217 264)"), |
| 25 | fontSans: readVariable(styles, "--font-sans", styles.fontFamily || "system-ui, sans-serif"), | 25 | fontSans: readVariable(styles, "--font-sans", styles.fontFamily || "system-ui, sans-serif"), |
| 26 | fontMono: readVariable(styles, "--font-mono", "ui-monospace, monospace"), | 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,7 +1412,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 1412 | > | 1412 | > |
| 1413 | {/* The full path: the chip already shows the shortened form, and a link | 1413 | {/* The full path: the chip already shows the shortened form, and a link |
| 1414 | to the workspace root collapses to a bare label that repeats it. */} | 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 | {targetPath} | 1416 | {targetPath} |
| 1417 | </div> | 1417 | </div> |
| 1418 | </TooltipPopup> | 1418 | </TooltipPopup> |
apps/web/src/components/chat/ChangedFilesTree.tsx+1-1| ... | @@ -66,7 +66,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: { | ... | @@ -66,7 +66,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: { |
| 66 | className={cn( | 66 | className={cn( |
| 67 | "flex items-center justify-between gap-2 rounded-xl", | 67 | "flex items-center justify-between gap-2 rounded-xl", |
| 68 | expanded && | 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 | <button | 72 | <button |
apps/web/src/components/chat/ModelListRow.tsx+1-1| ... | @@ -52,7 +52,7 @@ export const ModelListRow = memo(function ModelListRow(props: { | ... | @@ -52,7 +52,7 @@ export const ModelListRow = memo(function ModelListRow(props: { |
| 52 | contentClassName="flex w-full items-center gap-3" | 52 | contentClassName="flex w-full items-center gap-3" |
| 53 | className={cn( | 53 | className={cn( |
| 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]", | 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 | props.disabledReason && | 56 | props.disabledReason && |
| 57 | "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent", | 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,7 +107,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { |
| 107 | render={ | 107 | render={ |
| 108 | <button | 108 | <button |
| 109 | className={cn( | 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 | onClick={() => handleSelect("favorites")} | 112 | onClick={() => handleSelect("favorites")} |
| 113 | type="button" | 113 | type="button" |
| ... | @@ -152,7 +152,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { | ... | @@ -152,7 +152,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { |
| 152 | const button = ( | 152 | const button = ( |
| 153 | <button | 153 | <button |
| 154 | className={cn( | 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 | isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent", | 156 | isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent", |
| 157 | )} | 157 | )} |
| 158 | data-provider-accent-color={entry.accentColor} | 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,7 +166,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 166 | showBadge={showInstanceBadge} | 166 | showBadge={showInstanceBadge} |
| 167 | className="size-4" | 167 | className="size-4" |
| 168 | iconClassName={cn("size-4", props.activeProviderIconClassName)} | 168 | iconClassName={cn("size-4", props.activeProviderIconClassName)} |
| 169 | indicatorBackground="var(--input)" | 169 | indicatorBackground="var(--contrast-input)" |
| 170 | badgeClassName={cn( | 170 | badgeClassName={cn( |
| 171 | "right-[-0.125rem] bottom-[-0.125rem] h-3 min-w-3", | 171 | "right-[-0.125rem] bottom-[-0.125rem] h-3 min-w-3", |
| 172 | "px-0.5 text-[7px]", | 172 | "px-0.5 text-[7px]", |
apps/web/src/components/clerk/clerkAppearance.test.ts+5-5| ... | @@ -59,12 +59,12 @@ describe("clerkAppearance", () => { | ... | @@ -59,12 +59,12 @@ describe("clerkAppearance", () => { |
| 59 | colorDanger: "var(--error)", | 59 | colorDanger: "var(--error)", |
| 60 | colorSuccess: "var(--success)", | 60 | colorSuccess: "var(--success)", |
| 61 | colorWarning: "var(--warning)", | 61 | colorWarning: "var(--warning)", |
| 62 | colorNeutral: "var(--foreground)", | 62 | colorNeutral: "var(--contrast-foreground)", |
| 63 | colorForeground: "var(--foreground)", | 63 | colorForeground: "var(--contrast-foreground)", |
| 64 | colorMuted: "color-mix(in srgb, var(--card) 98%, var(--foreground))", | 64 | colorMuted: "color-mix(in srgb, var(--card) 98%, var(--contrast-foreground))", |
| 65 | colorMutedForeground: "var(--muted-foreground)", | 65 | colorMutedForeground: "var(--contrast-muted-foreground)", |
| 66 | colorBackground: "var(--card)", | 66 | colorBackground: "var(--card)", |
| 67 | colorInputForeground: "var(--foreground)", | 67 | colorInputForeground: "var(--contrast-foreground)", |
| 68 | colorInput: "var(--secondary)", | 68 | colorInput: "var(--secondary)", |
| 69 | colorRing: "var(--ring)", | 69 | colorRing: "var(--ring)", |
| 70 | }, | 70 | }, |
apps/web/src/components/clerk/clerkAppearance.ts+5-5| ... | @@ -13,14 +13,14 @@ export const clerkAppearance = { | ... | @@ -13,14 +13,14 @@ export const clerkAppearance = { |
| 13 | colorDanger: "var(--error)", | 13 | colorDanger: "var(--error)", |
| 14 | colorSuccess: "var(--success)", | 14 | colorSuccess: "var(--success)", |
| 15 | colorWarning: "var(--warning)", | 15 | colorWarning: "var(--warning)", |
| 16 | colorNeutral: "var(--foreground)", | 16 | colorNeutral: "var(--contrast-foreground)", |
| 17 | colorForeground: "var(--foreground)", | 17 | colorForeground: "var(--contrast-foreground)", |
| 18 | // The stock dark theme's muted token is translucent. Clerk uses this as | 18 | // The stock dark theme's muted token is translucent. Clerk uses this as |
| 19 | // the footer's background, so derive an opaque muted surface from the card. | 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))", | 20 | colorMuted: "color-mix(in srgb, var(--card) 98%, var(--contrast-foreground))", |
| 21 | colorMutedForeground: "var(--muted-foreground)", | 21 | colorMutedForeground: "var(--contrast-muted-foreground)", |
| 22 | colorBackground: "var(--card)", | 22 | colorBackground: "var(--card)", |
| 23 | colorInputForeground: "var(--foreground)", | 23 | colorInputForeground: "var(--contrast-foreground)", |
| 24 | colorInput: "var(--secondary)", | 24 | colorInput: "var(--secondary)", |
| 25 | colorRing: "var(--ring)", | 25 | colorRing: "var(--ring)", |
| 26 | }, | 26 | }, |
apps/web/src/components/color-selector.tsx+1-1| ... | @@ -13,7 +13,7 @@ interface ColorSelectorProps { | ... | @@ -13,7 +13,7 @@ interface ColorSelectorProps { |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | const colorMap = { | 15 | const colorMap = { |
| 16 | default: "var(--foreground)", | 16 | default: "var(--contrast-foreground)", |
| 17 | red: "var(--color-red-500)", | 17 | red: "var(--color-red-500)", |
| 18 | green: "var(--color-green-500)", | 18 | green: "var(--color-green-500)", |
| 19 | blue: "var(--color-blue-500)", | 19 | blue: "var(--color-blue-500)", |
apps/web/src/components/files/FileBrowserPanel.tsx+1-1| ... | @@ -378,7 +378,7 @@ export default function FileBrowserPanel({ | ... | @@ -378,7 +378,7 @@ export default function FileBrowserPanel({ |
| 378 | className="min-h-0 flex-1 overflow-hidden" | 378 | className="min-h-0 flex-1 overflow-hidden" |
| 379 | style={{ | 379 | style={{ |
| 380 | colorScheme: resolvedTheme, | 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,7 +225,7 @@ const REVIEW_OUTCOME_PRESENTATION = { |
| 225 | toneClassName: "text-muted-foreground/70", | 225 | toneClassName: "text-muted-foreground/70", |
| 226 | ringClassName: "ring-2 ring-muted-foreground/60", | 226 | ringClassName: "ring-2 ring-muted-foreground/60", |
| 227 | staleRingClassName: | 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 | badgeVariant: "outline", | 229 | badgeVariant: "outline", |
| 230 | }, | 230 | }, |
| 231 | } as const satisfies Record< | 231 | } as const satisfies Record< |
apps/web/src/components/settings/SettingsPanels.tsx+62| ... | @@ -20,6 +20,7 @@ import { | ... | @@ -20,6 +20,7 @@ import { |
| 20 | DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, | 20 | DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, |
| 21 | DEFAULT_UNIFIED_SETTINGS, | 21 | DEFAULT_UNIFIED_SETTINGS, |
| 22 | type EnvironmentIdentificationMode, | 22 | type EnvironmentIdentificationMode, |
| 23 | MAX_APPEARANCE_CONTRAST, | ||
| 23 | MAX_CODE_FONT_SIZE, | 24 | MAX_CODE_FONT_SIZE, |
| 24 | MAX_GLASS_OPACITY, | 25 | MAX_GLASS_OPACITY, |
| 25 | MAX_INTERFACE_FONT_SIZE, | 26 | MAX_INTERFACE_FONT_SIZE, |
| ... | @@ -27,6 +28,7 @@ import { | ... | @@ -27,6 +28,7 @@ import { |
| 27 | MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | 28 | MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, |
| 28 | MAX_TERMINAL_FONT_SIZE, | 29 | MAX_TERMINAL_FONT_SIZE, |
| 29 | MIN_CODE_FONT_SIZE, | 30 | MIN_CODE_FONT_SIZE, |
| 31 | MIN_APPEARANCE_CONTRAST, | ||
| 30 | MIN_GLASS_OPACITY, | 32 | MIN_GLASS_OPACITY, |
| 31 | MIN_INTERFACE_FONT_SIZE, | 33 | MIN_INTERFACE_FONT_SIZE, |
| 32 | MIN_PROMPT_FONT_SIZE, | 34 | MIN_PROMPT_FONT_SIZE, |
| ... | @@ -475,6 +477,9 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -475,6 +477,9 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 475 | ...(theme !== "system" ? ["Theme"] : []), | 477 | ...(theme !== "system" ? ["Theme"] : []), |
| 476 | ...(!followSystem ? ["Follow system"] : []), | 478 | ...(!followSystem ? ["Follow system"] : []), |
| 477 | ...(themeHalves !== null ? ["Theme mix"] : []), | 479 | ...(themeHalves !== null ? ["Theme mix"] : []), |
| 480 | ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast | ||
| 481 | ? ["Contrast"] | ||
| 482 | : []), | ||
| 478 | ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), | 483 | ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), |
| 479 | ...(settings.environmentIdentificationMode !== | 484 | ...(settings.environmentIdentificationMode !== |
| 480 | DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode | 485 | DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode |
| ... | @@ -543,6 +548,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -543,6 +548,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 543 | settings.browserDefaultZoomFactor, | 548 | settings.browserDefaultZoomFactor, |
| 544 | settings.browserDefaultAppearance, | 549 | settings.browserDefaultAppearance, |
| 545 | settings.browserAutoShowFloatingPreview, | 550 | settings.browserAutoShowFloatingPreview, |
| 551 | settings.appearanceContrast, | ||
| 546 | settings.enableAgentBrowserAccess, | 552 | settings.enableAgentBrowserAccess, |
| 547 | settings.confirmQuit, | 553 | settings.confirmQuit, |
| 548 | settings.confirmThreadArchive, | 554 | settings.confirmThreadArchive, |
| ... | @@ -638,6 +644,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -638,6 +644,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 638 | return; | 644 | return; |
| 639 | } | 645 | } |
| 640 | updateSettings({ | 646 | updateSettings({ |
| 647 | appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, | ||
| 641 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, | 648 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, |
| 642 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, | 649 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, |
| 643 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, | 650 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, |
| ... | @@ -986,6 +993,13 @@ export function AppearanceSettingsPanel() { | ... | @@ -986,6 +993,13 @@ export function AppearanceSettingsPanel() { |
| 986 | "--settings-slider-progress": `${glassOpacityRatio * 100}%`, | 993 | "--settings-slider-progress": `${glassOpacityRatio * 100}%`, |
| 987 | "--settings-slider-fill-offset": `${0.5 - glassOpacityRatio}rem`, | 994 | "--settings-slider-fill-offset": `${0.5 - glassOpacityRatio}rem`, |
| 988 | } as CSSProperties; | 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 | return ( | 1004 | return ( |
| 991 | <SettingsPageContainer> | 1005 | <SettingsPageContainer> |
| ... | @@ -1006,6 +1020,54 @@ export function AppearanceSettingsPanel() { | ... | @@ -1006,6 +1020,54 @@ export function AppearanceSettingsPanel() { |
| 1006 | /> | 1020 | /> |
| 1007 | </div> | 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 | <SettingsRow | 1071 | <SettingsRow |
| 1010 | {...searchableSetting("setting-glass-opacity")} | 1072 | {...searchableSetting("setting-glass-opacity")} |
| 1011 | description="Control how transparent glass surfaces are. Higher values make menus, dialogs, and the composer more solid." | 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,6 +57,12 @@ export const SETTINGS_SEARCH_ITEMS = [ |
| 57 | // stable scroll destination for both. | 57 | // stable scroll destination for both. |
| 58 | targetId: "appearance", | 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 | // Prefixed because the slider control already owns the `glass-opacity` id. | 67 | // Prefixed because the slider control already owns the `glass-opacity` id. |
| 62 | id: "setting-glass-opacity", | 68 | id: "setting-glass-opacity", |
apps/web/src/components/ui/button.test.tsx+1-1| ... | @@ -13,7 +13,7 @@ describe("button geometry tokens", () => { | ... | @@ -13,7 +13,7 @@ describe("button geometry tokens", () => { |
| 13 | ); | 13 | ); |
| 14 | 14 | ||
| 15 | expect(html).toContain("rounded-[var(--control-radius)]"); | 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 | expect(html).toContain("text-[var(--control-icon-color)]"); | 17 | expect(html).toContain("text-[var(--control-icon-color)]"); |
| 18 | expect(html).not.toContain("opacity-80"); | 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,14 +43,14 @@ const buttonVariants = cva( |
| 43 | "destructive-outline": | 43 | "destructive-outline": |
| 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", | 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 | ghost: | 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 | "ghost-muted": | 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 | glass: | 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 | link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline", | 51 | link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline", |
| 52 | outline: | 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 | secondary: | 54 | secondary: |
| 55 | "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90", | 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,7 +16,7 @@ type UsageProviderPresentation = { |
| 16 | export const PROVIDER_PRESENTATION = { | 16 | export const PROVIDER_PRESENTATION = { |
| 17 | codex: { | 17 | codex: { |
| 18 | label: "Codex", | 18 | label: "Codex", |
| 19 | color: "var(--foreground)", | 19 | color: "var(--contrast-foreground)", |
| 20 | mark: OpenAI, | 20 | mark: OpenAI, |
| 21 | }, | 21 | }, |
| 22 | claude: { | 22 | claude: { |
apps/web/src/contextMenuFallback.test.ts+1-1| ... | @@ -315,7 +315,7 @@ describe("showContextMenuFallback", () => { | ... | @@ -315,7 +315,7 @@ describe("showContextMenuFallback", () => { |
| 315 | expect(siblingButton).toBeTruthy(); | 315 | expect(siblingButton).toBeTruthy(); |
| 316 | expect(childButton?.focused).toBe(true); | 316 | expect(childButton?.focused).toBe(true); |
| 317 | expect(childButton?.style.background).toBe("var(--accent)"); | 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 | siblingButton?.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); | 319 | siblingButton?.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); |
| 320 | expect(childButton?.focused).toBe(false); | 320 | expect(childButton?.focused).toBe(false); |
| 321 | expect(childButton?.style.background).toBe("transparent"); | 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,7 +278,7 @@ export function showContextMenuFallback<T extends string>( |
| 278 | menu.className = | 278 | menu.className = |
| 279 | "dropdown-glass fixed z-[10000] min-w-32 max-w-sm overflow-hidden rounded-lg bg-clip-padding text-popover-foreground outline-none"; | 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 | menu.style.cssText = | 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 | menu.style.left = `${preferredLeft}px`; | 282 | menu.style.left = `${preferredLeft}px`; |
| 283 | menu.style.top = `${preferredTop}px`; | 283 | menu.style.top = `${preferredTop}px`; |
| 284 | menu.dataset.level = String(level); | 284 | menu.dataset.level = String(level); |
| ... | @@ -293,7 +293,8 @@ export function showContextMenuFallback<T extends string>( | ... | @@ -293,7 +293,8 @@ export function showContextMenuFallback<T extends string>( |
| 293 | if (item.separatorBefore === true && inner.children.length > 0) { | 293 | if (item.separatorBefore === true && inner.children.length > 0) { |
| 294 | const separator = document.createElement("div"); | 294 | const separator = document.createElement("div"); |
| 295 | separator.className = "mx-2 my-1 h-px bg-border"; | 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 | separator.dataset.contextMenuSeparator = "true"; | 298 | separator.dataset.contextMenuSeparator = "true"; |
| 298 | separator.setAttribute("role", "separator"); | 299 | separator.setAttribute("role", "separator"); |
| 299 | inner.appendChild(separator); | 300 | inner.appendChild(separator); |
| ... | @@ -323,12 +324,12 @@ export function showContextMenuFallback<T extends string>( | ... | @@ -323,12 +324,12 @@ export function showContextMenuFallback<T extends string>( |
| 323 | ? `${rowBase} text-destructive-foreground hover:bg-destructive/10 hover:text-destructive-foreground` | 324 | ? `${rowBase} text-destructive-foreground hover:bg-destructive/10 hover:text-destructive-foreground` |
| 324 | : `${rowBase} text-foreground hover:bg-accent hover:text-accent-foreground`; | 325 | : `${rowBase} text-foreground hover:bg-accent hover:text-accent-foreground`; |
| 325 | button.style.cssText = | 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 | if (isLeafDestructive) { | 328 | if (isLeafDestructive) { |
| 328 | button.style.color = "var(--destructive-foreground)"; | 329 | button.style.color = "var(--destructive-foreground)"; |
| 329 | } | 330 | } |
| 330 | if (isDisabled) { | 331 | if (isDisabled) { |
| 331 | button.style.color = "var(--muted-foreground)"; | 332 | button.style.color = "var(--contrast-muted-foreground)"; |
| 332 | button.style.opacity = "0.64"; | 333 | button.style.opacity = "0.64"; |
| 333 | button.style.pointerEvents = "none"; | 334 | button.style.pointerEvents = "none"; |
| 334 | } | 335 | } |
| ... | @@ -373,10 +374,10 @@ export function showContextMenuFallback<T extends string>( | ... | @@ -373,10 +374,10 @@ export function showContextMenuFallback<T extends string>( |
| 373 | button.style.color = isHighlighted | 374 | button.style.color = isHighlighted |
| 374 | ? isLeafDestructive | 375 | ? isLeafDestructive |
| 375 | ? "var(--destructive-foreground)" | 376 | ? "var(--destructive-foreground)" |
| 376 | : "var(--accent-foreground)" | 377 | : "var(--contrast-accent-foreground)" |
| 377 | : isLeafDestructive | 378 | : isLeafDestructive |
| 378 | ? "var(--destructive-foreground)" | 379 | ? "var(--destructive-foreground)" |
| 379 | : "var(--foreground)"; | 380 | : "var(--contrast-foreground)"; |
| 380 | }; | 381 | }; |
| 381 | button.addEventListener("mouseenter", () => { | 382 | button.addEventListener("mouseenter", () => { |
| 382 | button.focus({ preventScroll: true }); | 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,6 +78,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 78 | 78 | ||
| 79 | :root { | 79 | :root { |
| 80 | --app-scrollbar-width: 6px; | 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 | --app-scrollbar-thumb: rgb(217 217 217); | 85 | --app-scrollbar-thumb: rgb(217 217 217); |
| 82 | --app-scrollbar-thumb-hover: rgb(191 191 191); | 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,7 +91,11 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 87 | --control-radius: 0.5rem; | 91 | --control-radius: 0.5rem; |
| 88 | --sidebar-content-inset: 0.5rem; | 92 | --sidebar-content-inset: 0.5rem; |
| 89 | --sidebar-control-gap: 0.5rem; | 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 | --sidebar-row-content-inset: 0.625rem; | 99 | --sidebar-row-content-inset: 0.625rem; |
| 92 | --command-shell-inset: 0.5rem; | 100 | --command-shell-inset: 0.5rem; |
| 93 | --command-content-inset: 1rem; | 101 | --command-content-inset: 1rem; |
| ... | @@ -105,6 +113,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -105,6 +113,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 105 | --workspace-titlebar-control-gap: 0.75rem; | 113 | --workspace-titlebar-control-gap: 0.75rem; |
| 106 | 114 | ||
| 107 | @variant dark { | 115 | @variant dark { |
| 116 | --appearance-contrast-target: white; | ||
| 108 | --app-scrollbar-thumb: rgb(255 255 255 / 8%); | 117 | --app-scrollbar-thumb: rgb(255 255 255 / 8%); |
| 109 | --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%); | 118 | --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%); |
| 110 | --glass-blur: 16px; | 119 | --glass-blur: 16px; |
| ... | @@ -156,40 +165,40 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -156,40 +165,40 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 156 | --color-info: var(--info); | 165 | --color-info: var(--info); |
| 157 | --color-destructive-foreground: var(--destructive-foreground); | 166 | --color-destructive-foreground: var(--destructive-foreground); |
| 158 | --color-ring: var(--ring); | 167 | --color-ring: var(--ring); |
| 159 | --color-input: var(--input); | 168 | --color-input: var(--contrast-input); |
| 160 | --color-border: var(--border); | 169 | --color-border: var(--contrast-border); |
| 161 | --color-destructive: var(--destructive); | 170 | --color-destructive: var(--destructive); |
| 162 | --color-accent-foreground: var(--accent-foreground); | 171 | --color-accent-foreground: var(--contrast-accent-foreground); |
| 163 | --color-accent: var(--accent); | 172 | --color-accent: var(--accent); |
| 164 | --color-muted-foreground: var(--muted-foreground); | 173 | --color-muted-foreground: var(--contrast-muted-foreground); |
| 165 | --color-muted: var(--muted); | 174 | --color-muted: var(--muted); |
| 166 | --color-placeholder: var(--placeholder); | 175 | --color-placeholder: var(--contrast-placeholder); |
| 167 | --color-secondary-label: var(--secondary-label); | 176 | --color-secondary-label: var(--contrast-secondary-label); |
| 168 | --color-icon-muted: var(--icon-muted); | 177 | --color-icon-muted: var(--contrast-icon-muted); |
| 169 | --color-secondary-foreground: var(--secondary-foreground); | 178 | --color-secondary-foreground: var(--contrast-secondary-foreground); |
| 170 | --color-secondary: var(--secondary); | 179 | --color-secondary: var(--secondary); |
| 171 | --color-primary-foreground: var(--primary-foreground); | 180 | --color-primary-foreground: var(--primary-foreground); |
| 172 | --color-primary: var(--primary); | 181 | --color-primary: var(--primary); |
| 173 | --color-popover-foreground: var(--popover-foreground); | 182 | --color-popover-foreground: var(--contrast-popover-foreground); |
| 174 | --color-popover: var(--popover); | 183 | --color-popover: var(--popover); |
| 175 | --color-card-foreground: var(--card-foreground); | 184 | --color-card-foreground: var(--contrast-card-foreground); |
| 176 | --color-card: var(--card); | 185 | --color-card: var(--card); |
| 177 | --color-foreground: var(--foreground); | 186 | --color-foreground: var(--contrast-foreground); |
| 178 | --color-background: var(--background); | 187 | --color-background: var(--background); |
| 179 | --color-surface-raised: var(--surface-raised); | 188 | --color-surface-raised: var(--surface-raised); |
| 180 | --color-message: var(--message-surface); | 189 | --color-message: var(--message-surface); |
| 181 | --color-message-foreground: var(--message-foreground); | 190 | --color-message-foreground: var(--contrast-message-foreground); |
| 182 | --color-message-action: var(--message-action); | 191 | --color-message-action: var(--message-action); |
| 183 | --color-message-action-foreground: var(--message-action-foreground); | 192 | --color-message-action-foreground: var(--message-action-foreground); |
| 184 | --color-message-action-hover: var(--message-action-hover); | 193 | --color-message-action-hover: var(--message-action-hover); |
| 185 | --color-sidebar: var(--sidebar); | 194 | --color-sidebar: var(--sidebar); |
| 186 | --color-sidebar-foreground: var(--sidebar-foreground); | 195 | --color-sidebar-foreground: var(--contrast-sidebar-foreground); |
| 187 | --color-sidebar-muted-foreground: var(--sidebar-muted-foreground); | 196 | --color-sidebar-muted-foreground: var(--contrast-sidebar-muted-foreground); |
| 188 | --color-sidebar-control-surface: var(--sidebar-control-surface); | 197 | --color-sidebar-control-surface: var(--sidebar-control-surface); |
| 189 | --color-sidebar-row-hover: var(--sidebar-row-hover); | 198 | --color-sidebar-row-hover: var(--sidebar-row-hover); |
| 190 | --color-sidebar-row-active: var(--sidebar-row-active); | 199 | --color-sidebar-row-active: var(--sidebar-row-active); |
| 191 | --color-sidebar-row-selected: var(--sidebar-row-selected); | 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 | --radius-sm: calc(var(--radius) - 4px); | 202 | --radius-sm: calc(var(--radius) - 4px); |
| 194 | --radius-md: calc(var(--radius) - 2px); | 203 | --radius-md: calc(var(--radius) - 2px); |
| 195 | --radius-lg: var(--radius); | 204 | --radius-lg: var(--radius); |
| ... | @@ -304,7 +313,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -304,7 +313,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 304 | background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); | 313 | background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); |
| 305 | -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); | 314 | -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); |
| 306 | backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); | 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 | box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%); | 317 | box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%); |
| 309 | 318 | ||
| 310 | @variant dark { | 319 | @variant dark { |
| ... | @@ -337,7 +346,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -337,7 +346,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 337 | ); | 346 | ); |
| 338 | -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); | 347 | -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); |
| 339 | backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); | 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 | @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { | 351 | @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { |
| 343 | background: var(--popover) !important; | 352 | background: var(--popover) !important; |
| ... | @@ -757,7 +766,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -757,7 +766,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 757 | .chat-composer-shoulder-tab { | 766 | .chat-composer-shoulder-tab { |
| 758 | border-color: var( | 767 | border-color: var( |
| 759 | --chat-composer-outline, | 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 | background: color-mix( | 771 | background: color-mix( |
| 763 | in srgb, | 772 | in srgb, |
| ... | @@ -781,7 +790,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -781,7 +790,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 781 | --chat-composer-attached-surface: var(--chat-composer-glass-surface, var(--card)); | 790 | --chat-composer-attached-surface: var(--chat-composer-glass-surface, var(--card)); |
| 782 | --chat-composer-attached-outline: var( | 791 | --chat-composer-attached-outline: var( |
| 783 | --chat-composer-outline, | 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 | background: color-mix( | 795 | background: color-mix( |
| 787 | in srgb, | 796 | in srgb, |
| ... | @@ -817,7 +826,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -817,7 +826,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 817 | --chat-composer-attached-surface: var(--chat-composer-glass-surface, var(--card)); | 826 | --chat-composer-attached-surface: var(--chat-composer-glass-surface, var(--card)); |
| 818 | --chat-composer-attached-outline: var( | 827 | --chat-composer-attached-outline: var( |
| 819 | --chat-composer-outline, | 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 | --chat-composer-attached-tint: transparent; | 831 | --chat-composer-attached-tint: transparent; |
| 823 | position: relative; | 832 | position: relative; |
| ... | @@ -1238,17 +1247,17 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -1238,17 +1247,17 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1238 | background: linear-gradient( | 1247 | background: linear-gradient( |
| 1239 | to right, | 1248 | to right, |
| 1240 | var(--primary) 0 var(--settings-slider-fill-position), | 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 | var(--settings-slider-fill-position) 100% | 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 | .settings-slider::-moz-range-track { | 1256 | .settings-slider::-moz-range-track { |
| 1248 | height: 0.375rem; | 1257 | height: 0.375rem; |
| 1249 | border-radius: 9999px; | 1258 | border-radius: 9999px; |
| 1250 | background: color-mix(in srgb, var(--muted-foreground) 22%, transparent); | 1259 | background: color-mix(in srgb, var(--contrast-muted-foreground) 22%, transparent); |
| 1251 | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 55%, transparent); | 1260 | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--contrast-border) 55%, transparent); |
| 1252 | } | 1261 | } |
| 1253 | 1262 | ||
| 1254 | .settings-slider::-moz-range-progress { | 1263 | .settings-slider::-moz-range-progress { |
| ... | @@ -1265,7 +1274,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -1265,7 +1274,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1265 | border: 2px solid var(--primary); | 1274 | border: 2px solid var(--primary); |
| 1266 | border-radius: 9999px; | 1275 | border-radius: 9999px; |
| 1267 | background: var(--background); | 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 | transition: | 1278 | transition: |
| 1270 | transform 120ms ease, | 1279 | transform 120ms ease, |
| 1271 | box-shadow 120ms ease; | 1280 | box-shadow 120ms ease; |
| ... | @@ -1277,7 +1286,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -1277,7 +1286,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1277 | border: 2px solid var(--primary); | 1286 | border: 2px solid var(--primary); |
| 1278 | border-radius: 9999px; | 1287 | border-radius: 9999px; |
| 1279 | background: var(--background); | 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 | transition: | 1290 | transition: |
| 1282 | transform 120ms ease, | 1291 | transform 120ms ease, |
| 1283 | box-shadow 120ms ease; | 1292 | box-shadow 120ms ease; |
| ... | @@ -1285,12 +1294,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -1285,12 +1294,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1285 | 1294 | ||
| 1286 | .settings-slider:hover::-webkit-slider-thumb { | 1295 | .settings-slider:hover::-webkit-slider-thumb { |
| 1287 | transform: scale(1.08); | 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 | .settings-slider:hover::-moz-range-thumb { | 1300 | .settings-slider:hover::-moz-range-thumb { |
| 1292 | transform: scale(1.08); | 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 | .settings-slider:active::-webkit-slider-thumb { | 1305 | .settings-slider:active::-webkit-slider-thumb { |
| ... | @@ -1524,9 +1533,9 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil | ... | @@ -1524,9 +1533,9 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 1524 | --sidebar-foreground: var(--foreground); | 1533 | --sidebar-foreground: var(--foreground); |
| 1525 | --sidebar-muted-foreground: var(--muted-foreground); | 1534 | --sidebar-muted-foreground: var(--muted-foreground); |
| 1526 | --sidebar-control-surface: var(--muted); | 1535 | --sidebar-control-surface: var(--muted); |
| 1527 | --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent); | 1536 | --sidebar-row-hover: color-mix(in srgb, var(--contrast-foreground) 8%, transparent); |
| 1528 | --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent); | 1537 | --sidebar-row-active: color-mix(in srgb, var(--contrast-foreground) 11%, transparent); |
| 1529 | --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent); | 1538 | --sidebar-row-selected: color-mix(in srgb, var(--contrast-foreground) 7%, transparent); |
| 1530 | --sidebar-border: var(--border); | 1539 | --sidebar-border: var(--border); |
| 1531 | --sidebar-stage-fade: var(--card); | 1540 | --sidebar-stage-fade: var(--card); |
| 1532 | } | 1541 | } |
| ... | @@ -1711,11 +1720,11 @@ html[data-theme-token-probe] *::after { | ... | @@ -1711,11 +1720,11 @@ html[data-theme-token-probe] *::after { |
| 1711 | max-width: 12rem; | 1720 | max-width: 12rem; |
| 1712 | overflow: hidden; | 1721 | overflow: hidden; |
| 1713 | padding: 0.2rem 0.45rem; | 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 | border-radius: 0.4rem; | 1724 | border-radius: 0.4rem; |
| 1716 | background: var(--popover); | 1725 | background: var(--popover); |
| 1717 | box-shadow: 0 4px 16px rgb(0 0 0 / 24%); | 1726 | box-shadow: 0 4px 16px rgb(0 0 0 / 24%); |
| 1718 | color: var(--popover-foreground); | 1727 | color: var(--contrast-popover-foreground); |
| 1719 | font-size: 0.6875rem; | 1728 | font-size: 0.6875rem; |
| 1720 | font-weight: 600; | 1729 | font-weight: 600; |
| 1721 | line-height: 1rem; | 1730 | line-height: 1rem; |
| ... | @@ -1736,16 +1745,16 @@ html[data-theme-token-probe] *::after { | ... | @@ -1736,16 +1745,16 @@ html[data-theme-token-probe] *::after { |
| 1736 | dark-mode whites while preserving the same theme file across clients. */ | 1745 | dark-mode whites while preserving the same theme file across clients. */ |
| 1737 | html[data-theme-id] [data-chat-header] { | 1746 | html[data-theme-id] [data-chat-header] { |
| 1738 | background-color: var(--toolbar-background); | 1747 | background-color: var(--toolbar-background); |
| 1739 | color: var(--toolbar-foreground); | 1748 | color: var(--contrast-toolbar-foreground); |
| 1740 | } | 1749 | } |
| 1741 | 1750 | ||
| 1742 | html[data-theme-id] [data-chat-header] [data-slot="button"], | 1751 | html[data-theme-id] [data-chat-header] [data-slot="button"], |
| 1743 | html[data-theme-id] [data-chat-header] [data-slot="menu-trigger"], | 1752 | html[data-theme-id] [data-chat-header] [data-slot="menu-trigger"], |
| 1744 | html[data-theme-id] [data-chat-header] [data-toolbar-control] { | 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 | background-color: var(--toolbar-control); | 1755 | background-color: var(--toolbar-control); |
| 1747 | border-color: var(--toolbar-border); | 1756 | border-color: var(--contrast-toolbar-border); |
| 1748 | color: var(--toolbar-control-foreground); | 1757 | color: var(--contrast-toolbar-control-foreground); |
| 1749 | } | 1758 | } |
| 1750 | 1759 | ||
| 1751 | /* The panel layout toggles stay ghost: they render both inside the header and | 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,8 +1766,8 @@ html[data-theme-id] [data-panel-layout-controls] [data-slot="toggle"], |
| 1757 | html[data-theme-id] [data-panel-layout-controls] [data-slot="tooltip-trigger"], | 1766 | html[data-theme-id] [data-panel-layout-controls] [data-slot="tooltip-trigger"], |
| 1758 | html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="toggle"], | 1767 | html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="toggle"], |
| 1759 | html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="tooltip-trigger"] { | 1768 | html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="tooltip-trigger"] { |
| 1760 | --control-icon-color: var(--toolbar-foreground); | 1769 | --control-icon-color: var(--contrast-toolbar-foreground); |
| 1761 | color: var(--toolbar-foreground); | 1770 | color: var(--contrast-toolbar-foreground); |
| 1762 | } | 1771 | } |
| 1763 | 1772 | ||
| 1764 | html[data-theme-id] [data-chat-header] [data-slot="button"]:hover, | 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,7 +1780,7 @@ html[data-theme-id] [data-chat-header] [data-toolbar-control][data-pressed] { |
| 1771 | } | 1780 | } |
| 1772 | 1781 | ||
| 1773 | html[data-theme-id] [data-chat-header] [data-slot="separator"] { | 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 | /* Chat code blocks join the other code surfaces on the code tokens. The | 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,7 +1788,7 @@ html[data-theme-id] [data-chat-header] [data-slot="separator"] { |
| 1779 | surfaces with the accent, which fights syntax highlighting. */ | 1788 | surfaces with the accent, which fights syntax highlighting. */ |
| 1780 | html[data-theme-id] .chat-markdown .chat-markdown-codeblock { | 1789 | html[data-theme-id] .chat-markdown .chat-markdown-codeblock { |
| 1781 | background-color: var(--code-background); | 1790 | background-color: var(--code-background); |
| 1782 | border-color: var(--border); | 1791 | border-color: var(--contrast-border); |
| 1783 | color: var(--code-foreground); | 1792 | color: var(--code-foreground); |
| 1784 | } | 1793 | } |
| 1785 | 1794 | ||
| ... | @@ -1814,7 +1823,7 @@ html[data-theme-id="t3-chat"] { | ... | @@ -1814,7 +1823,7 @@ html[data-theme-id="t3-chat"] { |
| 1814 | } | 1823 | } |
| 1815 | 1824 | ||
| 1816 | & .chat-markdown :not(pre) > code { | 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,10 +1853,10 @@ html[data-theme-id] [data-app-sidebar] { |
| 1844 | --sidebar-row-selected: var(--app-theme-sidebar-row-selected); | 1853 | --sidebar-row-selected: var(--app-theme-sidebar-row-selected); |
| 1845 | --sidebar-border: var(--app-theme-sidebar-border); | 1854 | --sidebar-border: var(--app-theme-sidebar-border); |
| 1846 | --sidebar-stage-fade: var(--app-theme-sidebar); | 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 | @variant dark { | 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,10 +1864,127 @@ html[data-theme-id] [data-app-sidebar] { |
| 1855 | keeps that color while hovered. Do not neutralize this branded edge. */ | 1864 | keeps that color while hovered. Do not neutralize this branded edge. */ |
| 1856 | html[data-theme-id="t3-chat"] [data-app-sidebar] { | 1865 | html[data-theme-id="t3-chat"] [data-app-sidebar] { |
| 1857 | @variant dark { | 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 | body { | 1988 | body { |
| 1863 | /* Reference the theme token (not a literal stack) so the Settings -> | 1989 | /* Reference the theme token (not a literal stack) so the Settings -> |
| 1864 | Appearance runtime override of --font-sans reaches all interface text. */ | 1990 | Appearance runtime override of --font-sans reaches all interface text. */ |
| ... | @@ -2004,7 +2130,7 @@ code { | ... | @@ -2004,7 +2130,7 @@ code { |
| 2004 | margin: 1.25rem 0 0.5rem; | 2130 | margin: 1.25rem 0 0.5rem; |
| 2005 | font-weight: 600; | 2131 | font-weight: 600; |
| 2006 | line-height: 1.3; | 2132 | line-height: 1.3; |
| 2007 | color: var(--foreground); | 2133 | color: var(--contrast-foreground); |
| 2008 | } | 2134 | } |
| 2009 | 2135 | ||
| 2010 | .chat-markdown h1 { | 2136 | .chat-markdown h1 { |
| ... | @@ -2026,7 +2152,7 @@ code { | ... | @@ -2026,7 +2152,7 @@ code { |
| 2026 | } | 2152 | } |
| 2027 | 2153 | ||
| 2028 | .chat-markdown h6 { | 2154 | .chat-markdown h6 { |
| 2029 | color: var(--muted-foreground); | 2155 | color: var(--contrast-muted-foreground); |
| 2030 | } | 2156 | } |
| 2031 | 2157 | ||
| 2032 | .chat-markdown ul { | 2158 | .chat-markdown ul { |
| ... | @@ -2103,16 +2229,16 @@ code { | ... | @@ -2103,16 +2229,16 @@ code { |
| 2103 | } | 2229 | } |
| 2104 | 2230 | ||
| 2105 | .chat-markdown blockquote { | 2231 | .chat-markdown blockquote { |
| 2106 | border-left: 2px solid var(--border); | 2232 | border-left: 2px solid var(--contrast-border); |
| 2107 | padding-left: 0.8rem; | 2233 | padding-left: 0.8rem; |
| 2108 | color: var(--muted-foreground); | 2234 | color: var(--contrast-muted-foreground); |
| 2109 | } | 2235 | } |
| 2110 | 2236 | ||
| 2111 | .chat-markdown section[data-footnotes] { | 2237 | .chat-markdown section[data-footnotes] { |
| 2112 | margin-top: 1.25rem; | 2238 | margin-top: 1.25rem; |
| 2113 | border-top: 1px solid var(--border); | 2239 | border-top: 1px solid var(--contrast-border); |
| 2114 | padding-top: 0.75rem; | 2240 | padding-top: 0.75rem; |
| 2115 | color: var(--muted-foreground); | 2241 | color: var(--contrast-muted-foreground); |
| 2116 | font-size: 0.75rem; | 2242 | font-size: 0.75rem; |
| 2117 | } | 2243 | } |
| 2118 | 2244 | ||
| ... | @@ -2137,17 +2263,17 @@ code { | ... | @@ -2137,17 +2263,17 @@ code { |
| 2137 | } | 2263 | } |
| 2138 | 2264 | ||
| 2139 | .chat-markdown :not(pre) > code { | 2265 | .chat-markdown :not(pre) > code { |
| 2140 | border: 1px solid var(--border); | 2266 | border: 1px solid var(--contrast-border); |
| 2141 | border-radius: 0.375rem; | 2267 | border-radius: 0.375rem; |
| 2142 | background: var(--muted); | 2268 | background: var(--muted); |
| 2143 | padding: 0.1rem 0.35rem; | 2269 | padding: 0.1rem 0.35rem; |
| 2144 | color: var(--foreground); | 2270 | color: var(--contrast-foreground); |
| 2145 | font-size: 0.75rem; | 2271 | font-size: 0.75rem; |
| 2146 | } | 2272 | } |
| 2147 | 2273 | ||
| 2148 | .chat-markdown a.chat-markdown-file-link, | 2274 | .chat-markdown a.chat-markdown-file-link, |
| 2149 | .chat-markdown a.chat-markdown-file-link:hover { | 2275 | .chat-markdown a.chat-markdown-file-link:hover { |
| 2150 | color: var(--foreground); | 2276 | color: var(--contrast-foreground); |
| 2151 | text-decoration: none; | 2277 | text-decoration: none; |
| 2152 | } | 2278 | } |
| 2153 | 2279 | ||
| ... | @@ -2159,12 +2285,12 @@ code { | ... | @@ -2159,12 +2285,12 @@ code { |
| 2159 | .chat-markdown pre { | 2285 | .chat-markdown pre { |
| 2160 | max-width: 100%; | 2286 | max-width: 100%; |
| 2161 | overflow-x: auto; | 2287 | overflow-x: auto; |
| 2162 | border: 1px solid var(--border); | 2288 | border: 1px solid var(--contrast-border); |
| 2163 | border-radius: 0.75rem; | 2289 | border-radius: 0.75rem; |
| 2164 | background: var(--muted); | 2290 | background: var(--muted); |
| 2165 | padding: 0.8rem 0.9rem; | 2291 | padding: 0.8rem 0.9rem; |
| 2166 | scrollbar-width: thin; | 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 | .chat-markdown pre code { | 2296 | .chat-markdown pre code { |
| ... | @@ -2179,21 +2305,21 @@ code { | ... | @@ -2179,21 +2305,21 @@ code { |
| 2179 | 2305 | ||
| 2180 | .chat-markdown pre::-webkit-scrollbar-thumb { | 2306 | .chat-markdown pre::-webkit-scrollbar-thumb { |
| 2181 | border-radius: 999px; | 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 | .chat-markdown .chat-markdown-codeblock-header, | 2311 | .chat-markdown .chat-markdown-codeblock-header, |
| 2186 | .chat-markdown .chat-markdown-chrome-action { | 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 | .chat-markdown .chat-markdown-chrome-action:hover { | 2316 | .chat-markdown .chat-markdown-chrome-action:hover { |
| 2191 | color: var(--foreground); | 2317 | color: var(--contrast-foreground); |
| 2192 | } | 2318 | } |
| 2193 | 2319 | ||
| 2194 | .chat-markdown .chat-markdown-chrome-action[aria-pressed="true"] { | 2320 | .chat-markdown .chat-markdown-chrome-action[aria-pressed="true"] { |
| 2195 | color: var(--foreground); | 2321 | color: var(--contrast-foreground); |
| 2196 | background: color-mix(in srgb, var(--foreground) 8%, transparent); | 2322 | background: color-mix(in srgb, var(--contrast-foreground) 8%, transparent); |
| 2197 | } | 2323 | } |
| 2198 | 2324 | ||
| 2199 | .chat-markdown .chat-markdown-codeblock pre { | 2325 | .chat-markdown .chat-markdown-codeblock pre { |
| ... | @@ -2233,14 +2359,14 @@ code { | ... | @@ -2233,14 +2359,14 @@ code { |
| 2233 | } | 2359 | } |
| 2234 | 2360 | ||
| 2235 | .chat-markdown thead th { | 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 | padding-block: 0.55rem; | 2363 | padding-block: 0.55rem; |
| 2238 | font-weight: 600; | 2364 | font-weight: 600; |
| 2239 | white-space: nowrap; | 2365 | white-space: nowrap; |
| 2240 | } | 2366 | } |
| 2241 | 2367 | ||
| 2242 | .chat-markdown tbody td { | 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 | /* Collapsed (default): single-line cells truncate so arbitrary chat content | 2372 | /* Collapsed (default): single-line cells truncate so arbitrary chat content |
apps/web/src/routes/__root.tsx+12| ... | @@ -30,6 +30,7 @@ import { | ... | @@ -30,6 +30,7 @@ import { |
| 30 | } from "../components/ui/toast"; | 30 | } from "../components/ui/toast"; |
| 31 | import { resolveAndPersistPreferredEditor } from "../editorPreferences"; | 31 | import { resolveAndPersistPreferredEditor } from "../editorPreferences"; |
| 32 | import { applyAppearanceFontVariables } from "~/appearanceFonts"; | 32 | import { applyAppearanceFontVariables } from "~/appearanceFonts"; |
| 33 | import { applyAppearanceContrast } from "~/appearanceContrast"; | ||
| 33 | import { useClientSettings } from "../hooks/useSettings"; | 34 | import { useClientSettings } from "../hooks/useSettings"; |
| 34 | import { PlanAgentSelectionHeal } from "../planAgentSelectionHeal"; | 35 | import { PlanAgentSelectionHeal } from "../planAgentSelectionHeal"; |
| 35 | import { | 36 | import { |
| ... | @@ -131,6 +132,7 @@ function RootRouteView() { | ... | @@ -131,6 +132,7 @@ function RootRouteView() { |
| 131 | <ToastProvider> | 132 | <ToastProvider> |
| 132 | <AnchoredToastProvider> | 133 | <AnchoredToastProvider> |
| 133 | <DocumentTitleSync /> | 134 | <DocumentTitleSync /> |
| 135 | <ContrastAppearanceSync /> | ||
| 134 | <GlassAppearanceSync /> | 136 | <GlassAppearanceSync /> |
| 135 | <FontAppearanceSync /> | 137 | <FontAppearanceSync /> |
| 136 | {primaryEnvironmentAuthenticated ? <AuthenticatedTracingBootstrap /> : null} | 138 | {primaryEnvironmentAuthenticated ? <AuthenticatedTracingBootstrap /> : null} |
| ... | @@ -152,6 +154,16 @@ function RootRouteView() { | ... | @@ -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 | function GlassAppearanceSync() { | 167 | function GlassAppearanceSync() { |
| 156 | const glassOpacity = useClientSettings((settings) => settings.glassOpacity); | 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,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 | describe("ClientSettings environment identification", () => { | 70 | describe("ClientSettings environment identification", () => { |
| 55 | it("defaults to artwork and accepts each presentation mode", () => { | 71 | it("defaults to artwork and accepts each presentation mode", () => { |
| 56 | expect(decodeClientSettings({}).environmentIdentificationMode).toBe("artwork"); | 72 | expect(decodeClientSettings({}).environmentIdentificationMode).toBe("artwork"); |
packages/contracts/src/settings.ts+12| ... | @@ -75,6 +75,14 @@ export const GlassOpacity = Schema.Int.check( | ... | @@ -75,6 +75,14 @@ export const GlassOpacity = Schema.Int.check( |
| 75 | ); | 75 | ); |
| 76 | export type GlassOpacity = typeof GlassOpacity.Type; | 76 | export type GlassOpacity = typeof GlassOpacity.Type; |
| 77 | export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; | 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 | * Font size preferences, in CSS pixels. The ranges are deliberately narrow: | 87 | * Font size preferences, in CSS pixels. The ranges are deliberately narrow: |
| 80 | * the interface size scales every rem-based dimension in the app, so the | 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,6 +141,9 @@ export const DEFAULT_BROWSER_VIEWPORT: PreviewViewportSetting = FILL_PREVIEW_VIE |
| 133 | export const DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW = true; | 141 | export const DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW = true; |
| 134 | 142 | ||
| 135 | export const ClientSettingsSchema = Schema.Struct({ | 143 | export const ClientSettingsSchema = Schema.Struct({ |
| 144 | appearanceContrast: AppearanceContrast.pipe( | ||
| 145 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_APPEARANCE_CONTRAST)), | ||
| 146 | ), | ||
| 136 | browserDefaultViewport: PreviewViewportSetting.pipe( | 147 | browserDefaultViewport: PreviewViewportSetting.pipe( |
| 137 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_VIEWPORT)), | 148 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_VIEWPORT)), |
| 138 | ), | 149 | ), |
| ... | @@ -860,6 +871,7 @@ export const ServerSettingsPatch = Schema.Struct({ | ... | @@ -860,6 +871,7 @@ export const ServerSettingsPatch = Schema.Struct({ |
| 860 | export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; | 871 | export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; |
| 861 | 872 | ||
| 862 | export const ClientSettingsPatch = Schema.Struct({ | 873 | export const ClientSettingsPatch = Schema.Struct({ |
| 874 | appearanceContrast: Schema.optionalKey(AppearanceContrast), | ||
| 863 | browserDefaultViewport: Schema.optionalKey(PreviewViewportSetting), | 875 | browserDefaultViewport: Schema.optionalKey(PreviewViewportSetting), |
| 864 | browserDefaultZoomFactor: Schema.optionalKey(PreviewZoomFactor), | 876 | browserDefaultZoomFactor: Schema.optionalKey(PreviewZoomFactor), |
| 865 | browserDefaultAppearance: Schema.optionalKey(PreviewAppearancePreference), | 877 | browserDefaultAppearance: Schema.optionalKey(PreviewAppearancePreference), |