diff --git a/apps/web/index.html b/apps/web/index.html index 5a4ccce76d6..afb4b2402a0 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -198,6 +198,9 @@ } }; + if (/^win(dows)?/i.test(navigator.platform)) { + document.documentElement.classList.add("windows"); + } try { const storedTheme = window.localStorage.getItem("t3code:theme"); const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; diff --git a/apps/web/package.json b/apps/web/package.json index dfec330a107..b78171a28dd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,6 +20,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@effect/atom-react": "catalog:", + "@fontsource-variable/inter": "^5.3.0", "@formkit/auto-animate": "^0.9.0", "@legendapp/list": "3.3.3", "@lexical/react": "^0.41.0", diff --git a/apps/web/src/appearanceFonts.test.ts b/apps/web/src/appearanceFonts.test.ts index 31a2f1d779c..28cb7ecbd86 100644 --- a/apps/web/src/appearanceFonts.test.ts +++ b/apps/web/src/appearanceFonts.test.ts @@ -7,6 +7,7 @@ import { clampPromptFontSize, DEFAULT_CODE_FONT_STACK, DEFAULT_SANS_FONT_STACK, + WINDOWS_SANS_FONT_STACK, appearanceFontStack, cssFontFamilies, resolveDefaultFamilyLabel, @@ -68,6 +69,12 @@ describe("appearanceFontStack", () => { it("falls back to the default stack when unset", () => { expect(appearanceFontStack("", DEFAULT_SANS_FONT_STACK)).toBe(DEFAULT_SANS_FONT_STACK); }); + + it("prepends to the Windows stack on Windows so Inter stays in the fallback chain", () => { + expect(appearanceFontStack("Cascadia Code", WINDOWS_SANS_FONT_STACK)).toBe( + `"Cascadia Code", ${WINDOWS_SANS_FONT_STACK}`, + ); + }); }); describe("resolveTerminalFontPreference", () => { diff --git a/apps/web/src/appearanceFonts.ts b/apps/web/src/appearanceFonts.ts index 6053e5fb0dd..cb6722c3346 100644 --- a/apps/web/src/appearanceFonts.ts +++ b/apps/web/src/appearanceFonts.ts @@ -20,6 +20,9 @@ import { export const DEFAULT_SANS_FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'; +export const WINDOWS_SANS_FONT_STACK = + '"Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'; + // Concrete names first: some engines alias `ui-monospace` to the // proportional system UI font, which would break every code surface. export const DEFAULT_CODE_FONT_STACK = @@ -100,8 +103,15 @@ export function applyAppearanceFontVariables( root: HTMLElement, preferences: AppearanceFontPreferences, ): void { + const windowsSans = root.classList.contains("windows"); const families: ReadonlyArray = [ - ["--font-sans", preferences.sans, DEFAULT_SANS_FONT_STACK], + // On Windows the stylesheet default already prefers Inter, so custom + // families must prepend to the same stack or glyph fallbacks skip it. + [ + "--font-sans", + preferences.sans, + windowsSans ? WINDOWS_SANS_FONT_STACK : DEFAULT_SANS_FONT_STACK, + ], ["--font-mono", preferences.code, DEFAULT_CODE_FONT_STACK], // The composer falls back to whatever the sans preference resolves to. ["--font-composer", preferences.composer, "var(--font-sans)"], diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 6743952ae26..6f0380d61fe 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -75,7 +75,7 @@ import { sortProviderInstanceEntries, } from "../../providerInstances"; import { ensureLocalApi, readLocalApi } from "../../localApi"; -import { isMacPlatform } from "../../lib/utils"; +import { isMacPlatform, isWindowsPlatform } from "../../lib/utils"; import { primaryServerObservabilityAtom, primaryServerProvidersAtom } from "../../state/server"; import { useProjects } from "../../state/entities"; import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState"; @@ -96,6 +96,7 @@ import { Input } from "../ui/input"; import { DEFAULT_CODE_FONT_STACK, DEFAULT_SANS_FONT_STACK, + WINDOWS_SANS_FONT_STACK, isFontFamilyAvailable, isMonospaceFamily, resolveDefaultFamilyLabel, @@ -1078,15 +1079,19 @@ export function AppearanceSettingsPanel() { function useFontDefaultFamilies() { const settings = usePrimarySettings(); - // An unset preference shows the font it resolves to on this machine; the - // default stacks are the platform's own faces, so the name is probed, not - // hardcoded. + const sansStack = isWindowsPlatform(navigator.platform) + ? WINDOWS_SANS_FONT_STACK + : DEFAULT_SANS_FONT_STACK; const defaults = useMemo( () => ({ - sans: resolveDefaultFamilyLabel(DEFAULT_SANS_FONT_STACK) ?? "System default", + // The canvas probe cannot measure the bundled Inter webfont before it + // renders, so on Windows fall back to naming it directly. + sans: + resolveDefaultFamilyLabel(sansStack) ?? + (isWindowsPlatform(navigator.platform) ? "Inter Variable" : "System default"), code: resolveDefaultFamilyLabel(DEFAULT_CODE_FONT_STACK) ?? "System monospace", }), - [], + [sansStack], ); return { sans: defaults.sans, diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 8600d004854..84e16686d01 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1442,6 +1442,11 @@ body { --desktop-window-right-resize-inset: 6px; } +html.windows { + --font-sans: + "Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; +} + /* App-chrome grain. Baked into each surface's own background (behind content) rather than a fixed overlay on top: a full-viewport overlay forces the compositor to re-blend every frame any animation produces, diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index c8f6f4de97b..9b510716978 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -7,6 +7,7 @@ import { createHashHistory, createBrowserHistory } from "@tanstack/react-router" import "./index.css"; +import "@fontsource-variable/inter/wght.css"; import { isElectron } from "./env"; import { ManagedRelayAuthProvider } from "./cloud/managedAuth"; import { hasCloudPublicConfig } from "./cloud/publicConfig"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b999993183..e786d7588f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -533,6 +533,9 @@ importers: '@effect/atom-react': specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(react@19.2.6)(scheduler@0.27.0) + '@fontsource-variable/inter': + specifier: ^5.3.0 + version: 5.3.0 '@formkit/auto-animate': specifier: ^0.9.0 version: 0.9.0 @@ -2708,6 +2711,9 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fontsource-variable/inter@5.3.0': + resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==} + '@formkit/auto-animate@0.9.0': resolution: {integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==} @@ -12805,6 +12811,8 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@fontsource-variable/inter@5.3.0': {} + '@formkit/auto-animate@0.9.0': {} '@hono/node-server@1.19.14(hono@4.12.27)':