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.
This commit is contained in:
parent
47fa4ca6c1
commit
140b3fbe05
16 changed files with 59 additions and 15 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function ModelsHeader({
|
|||
return (
|
||||
<header className="font-heading flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
|
||||
<PageHeading
|
||||
title={isDataset ? "Datasets" : "Models"}
|
||||
title={isDataset ? "Datasets" : "Model hub"}
|
||||
onTitleClick={onTitleClick}
|
||||
subtitle={
|
||||
isDataset
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const ar = {
|
|||
returnToChat: "العودة إلى المحادثة",
|
||||
compare: "مقارنة",
|
||||
search: "بحث",
|
||||
hub: "النماذج",
|
||||
hub: "مركز النماذج",
|
||||
train: "تدريب",
|
||||
recipes: "الوصفات",
|
||||
export: "تصدير",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const de = {
|
|||
returnToChat: "Zurück zum Chat",
|
||||
compare: "Vergleichen",
|
||||
search: "Suchen",
|
||||
hub: "Modelle",
|
||||
hub: "Modell-Hub",
|
||||
train: "Trainieren",
|
||||
recipes: "Rezepte",
|
||||
export: "Exportieren",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const en = {
|
|||
returnToChat: "Return to Chat",
|
||||
compare: "Compare",
|
||||
search: "Search",
|
||||
hub: "Models",
|
||||
hub: "Model hub",
|
||||
train: "Train",
|
||||
recipes: "Recipes",
|
||||
export: "Export",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const es = {
|
|||
returnToChat: "Volver al chat",
|
||||
compare: "Comparar",
|
||||
search: "Buscar",
|
||||
hub: "Modelos",
|
||||
hub: "Centro de modelos",
|
||||
train: "Entrenar",
|
||||
recipes: "Recetas",
|
||||
export: "Exportar",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const fr = {
|
|||
returnToChat: "Retour à la discussion",
|
||||
compare: "Comparer",
|
||||
search: "Rechercher",
|
||||
hub: "Modèles",
|
||||
hub: "Hub de modèles",
|
||||
train: "Entraîner",
|
||||
recipes: "Recettes",
|
||||
export: "Exporter",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const hi = {
|
|||
returnToChat: "चैट पर लौटें",
|
||||
compare: "तुलना करें",
|
||||
search: "खोजें",
|
||||
hub: "मॉडल",
|
||||
hub: "मॉडल हब",
|
||||
train: "ट्रेनिंग",
|
||||
recipes: "रेसिपी",
|
||||
export: "एक्सपोर्ट",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const ja = {
|
|||
returnToChat: "チャットに戻る",
|
||||
compare: "比較",
|
||||
search: "検索",
|
||||
hub: "モデル",
|
||||
hub: "モデルハブ",
|
||||
train: "トレーニング",
|
||||
recipes: "レシピ",
|
||||
export: "エクスポート",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const ko = {
|
|||
returnToChat: "채팅으로 돌아가기",
|
||||
compare: "비교",
|
||||
search: "검색",
|
||||
hub: "모델",
|
||||
hub: "모델 허브",
|
||||
train: "학습",
|
||||
recipes: "레시피",
|
||||
export: "내보내기",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const ptBR = {
|
|||
returnToChat: "Retornar ao Chat",
|
||||
compare: "Comparar",
|
||||
search: "Buscar",
|
||||
hub: "Modelos",
|
||||
hub: "Hub de modelos",
|
||||
train: "Treinar",
|
||||
recipes: "Receitas",
|
||||
export: "Exportar",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const ru = {
|
|||
returnToChat: "Вернуться к чату",
|
||||
compare: "Сравнить",
|
||||
search: "Поиск",
|
||||
hub: "Модели",
|
||||
hub: "Хаб моделей",
|
||||
train: "Обучение",
|
||||
recipes: "Рецепты",
|
||||
export: "Экспорт",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const zhCN = {
|
|||
returnToChat: "返回聊天",
|
||||
compare: "对比",
|
||||
search: "搜索",
|
||||
hub: "模型",
|
||||
hub: "模型中心",
|
||||
train: "训练",
|
||||
recipes: "配方",
|
||||
export: "导出",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,21 @@
|
|||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { extendTailwindMerge } from "tailwind-merge";
|
||||
|
||||
// text-ui-* / leading-ui-* are the scaled typography tokens from index.css.
|
||||
// Register them as font-size / line-height so twMerge does not treat
|
||||
// text-ui-* as a text color and drop it when a color class follows.
|
||||
const isUiToken = (value: string) => /^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));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue