From 6f4c838281cef13bbb038426d3fdf53bb34c22de Mon Sep 17 00:00:00 2001 From: oobabooga Date: Thu, 23 Jul 2026 01:55:45 -0300 Subject: [PATCH] Studio: calibrate Linux chat typography against macOS (#7337) --- studio/frontend/src/index.css | 13 ++++- tests/studio/playwright_chat_ui.py | 82 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index 52ca81e064..1fafe09d17 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -633,11 +633,20 @@ html.no-font-smoothing body { -moz-osx-font-smoothing: auto; } -/* Match Inter's lighter macOS rendering. Keep 410 when smoothing is off or a - custom font reaches chat. */ +/* Match Inter's lighter macOS rendering. Dark surfaces need a stronger + correction than light surfaces. Keep 410 when smoothing is off or a custom + font reaches chat. */ html.render-linux:not(.no-font-smoothing):not([data-chat-font]):not([data-ui-font]) + :is(.aui-assistant-message-root, .aui-user-message-root) { + font-weight: 390; +} + +html.dark.render-linux:not(.no-font-smoothing):not([data-chat-font]):not([data-ui-font]) :is(.aui-assistant-message-root, .aui-user-message-root) { font-weight: 350; + /* The lighter variable-font instance has narrower advances. Reduce + dark-mode line-wrap drift without changing custom-font paths. */ + letter-spacing: 0.023em; } /* Chat font: only applies while a custom chat font is set. Elements with diff --git a/tests/studio/playwright_chat_ui.py b/tests/studio/playwright_chat_ui.py index 4d13889878..a06e559100 100644 --- a/tests/studio/playwright_chat_ui.py +++ b/tests/studio/playwright_chat_ui.py @@ -936,6 +936,70 @@ with sync_playwright() as p: page.keyboard.press("Escape") page.wait_for_timeout(300) + def read_chat_typography(): + """Read message typography after a user-driven theme transition.""" + return robust_evaluate( + page, + """() => { + const root = document.documentElement; + const assistant = Array.from( + document.querySelectorAll('.aui-assistant-message-root') + ); + const user = Array.from( + document.querySelectorAll('.aui-user-message-root') + ); + if (assistant.length === 0 || user.length === 0) { + return { error: 'chat message roots are missing' }; + } + const ua = navigator.userAgent.toLowerCase(); + const role = (nodes) => { + const styles = nodes.map((node) => getComputedStyle(node)); + return { + fontWeight: [...new Set(styles.map((style) => style.fontWeight))], + letterSpacing: [...new Set(styles.map((style) => style.letterSpacing))], + }; + }; + return { + actualRenderLinux: root.classList.contains('render-linux'), + isDesktopLinux: ua.includes('linux') && !ua.includes('android'), + isDark: root.classList.contains('dark'), + usesBaselineTypography: ( + root.classList.contains('no-font-smoothing') || + root.hasAttribute('data-chat-font') || + root.hasAttribute('data-ui-font') + ), + assistant: role(assistant), + user: role(user), + }; + }""", + ) + + def assert_chat_typography(label, typography): + if typography.get("error"): + fail(typography["error"]) + if typography["actualRenderLinux"] != typography["isDesktopLinux"]: + fail(f"desktop Linux detection mismatch: {typography!r}") + is_dark = typography["isDark"] + expected_spacing = "0.31px" if is_dark else "0.155px" + if typography["isDesktopLinux"] and not typography["usesBaselineTypography"]: + expected_weight = "350" if is_dark else "390" + if is_dark: + expected_spacing = "0.3565px" + else: + expected_weight = "410" + for role in ("assistant", "user"): + actual = typography[role] + if actual["fontWeight"] != [expected_weight]: + fail( + f"chat font weight {label}/{role}: expected {expected_weight}, " + f"got {actual['fontWeight']!r}" + ) + if actual["letterSpacing"] != [expected_spacing]: + fail( + f"chat letter spacing {label}/{role}: expected {expected_spacing}, " + f"got {actual['letterSpacing']!r}" + ) + # ───────────────────────────────────────────────────── # 9. Theme toggle -- multiple cycles + computed-bg-color check # (light is near-white >240; dark is near-black <40). @@ -944,6 +1008,7 @@ with sync_playwright() as p: if acct.count() > 0: step("theme toggle x3 with computed-color assertion") observed = [] + typography_states = [] for cycle in range(3): # Wait for any prior dropdown to fully detach: clicking while # the view-transition is still open no-ops silently. The @@ -1032,6 +1097,9 @@ with sync_playwright() as p: }""", ) observed.append(bg) + typography = read_chat_typography() + assert_chat_typography(f"theme-cycle-{cycle + 1}", typography) + typography_states.append(typography) shoot(f"10-theme-cycle-{cycle + 1}") info(f" cycle {cycle + 1}: dark={bg['isDark']} body bg={bg['bg']!r}") # Across cycles we should see both a near-white (light) and a @@ -1054,6 +1122,20 @@ with sync_playwright() as p: "(toggle may not flip on this runner's color-scheme)" ) + # These are user-driven theme transitions, not synthetic class + # changes. A completed three-cycle toggle must expose both typography + # states before we check the Linux selector. + if len(typography_states) != 3: + soft_fail( + f"chat typography observed {len(typography_states)} theme state(s), expected 3" + ) + elif {state["isDark"] for state in typography_states} != {False, True}: + soft_fail(f"chat typography did not observe both themes: {typography_states!r}") + else: + info("OK chat typography platform and theme behavior") + else: + soft_fail("chat typography requires the account-menu theme control") + # ───────────────────────────────────────────────────── # 10. Sidebar nav: New Chat, Compare, Search, Recipes. # ─────────────────────────────────────────────────────