From 140b3fbe057398fa22b075b20366c62d10e936e5 Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:48:54 -0700 Subject: [PATCH] Studio: register text-ui tokens with tailwind-merge so cn() keeps them (#7396) * Studio: register text-ui tokens with tailwind-merge so cn keeps them Stock tailwind-merge classifies text-ui-* as a text color, so cn() dropped the size class whenever a color utility followed it in the same call. The element then fell back to the unscaled 16px root font, which made hub tabs and capability pills look oversized at small UI font sizes. Extend the merge config so text-ui-* and leading-ui-* resolve as font-size and line-height groups, and cover the failure in the contract and Playwright regression tests. * Studio: rename the Models page to Model hub Page heading, sidebar navigation label in all locales, and the chat download toasts that point at the tab. --- .../frontend/src/features/chat/chat-page.tsx | 4 ++-- .../features/hub/catalog/models-header.tsx | 2 +- studio/frontend/src/i18n/locales/ar.ts | 2 +- studio/frontend/src/i18n/locales/de.ts | 2 +- studio/frontend/src/i18n/locales/en.ts | 2 +- studio/frontend/src/i18n/locales/es.ts | 2 +- studio/frontend/src/i18n/locales/fr.ts | 2 +- studio/frontend/src/i18n/locales/hi.ts | 2 +- studio/frontend/src/i18n/locales/ja.ts | 2 +- studio/frontend/src/i18n/locales/ko.ts | 2 +- studio/frontend/src/i18n/locales/pt-br.ts | 2 +- studio/frontend/src/i18n/locales/ru.ts | 2 +- studio/frontend/src/i18n/locales/zh-CN.ts | 2 +- studio/frontend/src/lib/utils.ts | 16 +++++++++++++++- tests/studio/playwright_ui_font_scale.py | 19 +++++++++++++++++++ tests/studio/test_ui_font_scale_contract.py | 11 +++++++++++ 16 files changed, 59 insertions(+), 15 deletions(-) diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index c241607e28..7452cf3447 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -2289,7 +2289,7 @@ export function ChatPage({ } else if (outcome === "conflict") { toast.info("Resume this download from Models", { description: - "An earlier partial download used a different transport. Open the Models tab to resume or restart it.", + "An earlier partial download used a different transport. Open the Model hub tab to resume or restart it.", }); } else if (outcome === "busy") { toast.info("Download already in progress", { @@ -2410,7 +2410,7 @@ export function ChatPage({ // surface's onComplete auto-loads, mirroring the "started" branch. toast.info("Resume this download from Models", { description: - "An earlier partial download used a different transport. Open the Models tab to resume or restart it.", + "An earlier partial download used a different transport. Open the Model hub tab to resume or restart it.", }); return; } diff --git a/studio/frontend/src/features/hub/catalog/models-header.tsx b/studio/frontend/src/features/hub/catalog/models-header.tsx index f0e0950871..f844336d4a 100644 --- a/studio/frontend/src/features/hub/catalog/models-header.tsx +++ b/studio/frontend/src/features/hub/catalog/models-header.tsx @@ -64,7 +64,7 @@ export function ModelsHeader({ return (
/^ui-\d+(p5)?$/.test(value); + +const twMerge = extendTailwindMerge({ + extend: { + classGroups: { + "font-size": [{ text: [isUiToken] }], + leading: [{ leading: [isUiToken] }], + }, + }, +}); export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)); diff --git a/tests/studio/playwright_ui_font_scale.py b/tests/studio/playwright_ui_font_scale.py index 89c7894929..0f14c42422 100644 --- a/tests/studio/playwright_ui_font_scale.py +++ b/tests/studio/playwright_ui_font_scale.py @@ -193,6 +193,25 @@ def main(): page.set_viewport_size({"width": 1440, "height": 900}) page.wait_for_timeout(400) + step("cn keeps text-ui-* next to color classes (hub tabs)") + page.keyboard.press("Escape") + page.wait_for_timeout(400) + page.goto(f"{BASE}/hub", wait_until = "domcontentloaded") + page.wait_for_timeout(2000) + open_appearance(page) + set_input(page, "UI font size", 12) + page.keyboard.press("Escape") + page.wait_for_timeout(400) + tab = page.get_by_role("radio").filter(has_text = "Discover").first + tab.wait_for(state = "visible", timeout = 15000) + tab_font = tab.evaluate("el => parseFloat(getComputedStyle(el).fontSize)") + # text-ui-12p5 at scale 0.75; 16px means twMerge dropped the token. + if not near(tab_font, 12.5 * 12 / 16): + fail(f"hub tab font did not scale (twMerge drop?): {tab_font}") + page.goto(BASE, wait_until = "domcontentloaded") + page.wait_for_timeout(1500) + open_appearance(page) + step("default restores exactly") page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click() page.wait_for_timeout(500) diff --git a/tests/studio/test_ui_font_scale_contract.py b/tests/studio/test_ui_font_scale_contract.py index 393c60dd6b..1153eea643 100644 --- a/tests/studio/test_ui_font_scale_contract.py +++ b/tests/studio/test_ui_font_scale_contract.py @@ -17,6 +17,7 @@ SRC = REPO / "studio/frontend/src" INDEX_CSS = (SRC / "index.css").read_text(encoding = "utf-8") STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(encoding = "utf-8") SELECT = (SRC / "components/ui/select.tsx").read_text(encoding = "utf-8") +UTILS = (SRC / "lib/utils.ts").read_text(encoding = "utf-8") # Raw numeric fontSize props are only allowed where a scaled stylesheet rule # (.recharts-text) overrides the presentation attribute at render time. @@ -87,6 +88,16 @@ def test_radix_select_viewport_owns_the_scroll_state(): assert "overflow-y-auto" not in content_cls.group(1) +def test_cn_knows_the_ui_typography_tokens(): + """Stock tailwind-merge classifies text-ui-* as a text color and deletes + it whenever a real color class follows in the same cn() call, so the + element falls back to the unscaled inherited font size.""" + assert "extendTailwindMerge" in UTILS + assert '"font-size": [{ text: [isUiToken] }]' in UTILS + assert "leading: [{ leading: [isUiToken] }]" in UTILS + assert "/^ui-\\d+(p5)?$/.test(value)" in UTILS + + def test_no_raw_pixel_text_utilities(): offenders = [] for path in _frontend_sources():