From 8aaf2f78ebcd7346a83dc639eff1e192bdabc4f2 Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:26:56 -0700 Subject: [PATCH] Studio: drive UI font size through a typography scale instead of the root font size (#7359) * Studio: drive UI font size through a typography scale, not the root font size Follow up to #7355. The preference now writes --ui-font-scale (selected / 16) and a data-ui-font-size attribute on the root instead of mutating the root font size, and the applier clears any stale inline root font-size left by older builds. Because the rem base never moves, every layout-only rem-to-px conversion from #7355 is reverted to its original form: the spacing, radius and container tokens, sidebar and thread widths, grid tracks, calc margins and hub.css dimensions match pre-#7355 main again, which also restores rem-based accessibility scaling for users with a larger browser default font size. Typography scales through tokens in index.css, all exact at 16px: - The named Tailwind sizes (--text-xs through --text-4xl) multiply their defaults by the scale, so standard utilities scale - One token per design px size (--text-ui-8 ... --text-ui-34) replaces every arbitrary text-[Npx] class; leading-ui-* mirrors the exact line heights and the numeric --leading-3..10 scale as well - CSS font-size and line-height declarations multiply by the scale - Chart labels scale through a .recharts-text rule; streamdown and react-flow px text is re-based via scaled overrides; KaTeX's 1px layout trick stays fixed by design - The logo lockups keep their half-rate behavior via the scale var - The explicit Code font size remains unmultiplied Keeps the #7355 behavior fixes: color chip min width, voice select min/max widths, and the select and dropdown menus scrolling an inner viewport so their corners stay rounded. The whitespace-password and IME rename guards that merged alongside are preserved. * Studio: contract and Playwright coverage for the UI font size scale test_ui_font_scale_contract.py pins the mechanism (scale var written, root font size never mutated, tokens scaled, code font size not multiplied, the Radix select viewport owning scroll state) and guards against new raw pixel typography, with a documented allowlist for the recharts fontSize props covered by the stylesheet override and the offscreen clipboard textarea. playwright_ui_font_scale.py drives the real appearance controls: root font size fixed at 12/16/20, text and line height scale by size/16, sidebar width invariant, explicit code font size stays fixed, an overflowing dictation select scrolls its Radix viewport by keyboard and wheel, and the default restores exactly. Wired into the UI smoke workflow against the second studio boot. The thinking-compact and descender contracts move back to the rem and token forms now that layout values no longer need px pinning. --- .github/workflows/studio-ui-smoke.yml | 10 + studio/frontend/src/app/provider.tsx | 8 +- .../frontend/src/components/app-sidebar.tsx | 34 +-- .../components/assistant-ui/audio-player.tsx | 2 +- .../message-response-details-sheet.tsx | 4 +- .../assistant-ui/message-timing.tsx | 2 +- .../src/components/assistant-ui/reasoning.tsx | 2 +- .../src/components/assistant-ui/sources.tsx | 2 +- .../src/components/assistant-ui/thread.tsx | 52 ++-- .../assistant-ui/tool-ui-knowledge-base.tsx | 2 +- .../assistant-ui/tool-ui-render-html.tsx | 2 +- .../src/components/floating-monitor.tsx | 6 +- .../src/components/llama-update-banner.tsx | 8 +- .../frontend/src/components/section-card.tsx | 2 +- .../src/components/shutdown-dialog.tsx | 4 +- .../src/components/tauri/startup-screen.tsx | 2 +- .../src/components/tauri/update-banner.tsx | 16 +- .../src/components/tauri/update-screen.tsx | 4 +- .../src/components/tauri/window-titlebar.tsx | 8 +- .../frontend/src/components/ui/calendar.tsx | 4 +- studio/frontend/src/components/ui/chart.tsx | 2 +- .../src/components/ui/copyable-error-chip.tsx | 6 +- .../frontend/src/components/ui/data-table.tsx | 2 +- studio/frontend/src/components/ui/dialog.tsx | 2 +- .../src/components/ui/input-group.tsx | 4 +- studio/frontend/src/components/ui/sidebar.tsx | 8 +- .../src/components/web/update-banner.tsx | 8 +- .../frontend/src/features/auth/login-page.tsx | 2 +- .../features/chat/artifacts/artifact-card.tsx | 4 +- .../frontend/src/features/chat/chat-page.tsx | 38 +-- .../features/chat/chat-providers-dialog.tsx | 8 +- .../src/features/chat/chat-settings-sheet.tsx | 68 ++--- .../chat/components/chat-search-dialog.tsx | 6 +- .../chat/components/context-usage-bar.tsx | 4 +- .../chat/components/model-load-status.tsx | 12 +- .../components/openai-code-exec-section.tsx | 14 +- .../chat/components/project-switcher.tsx | 2 +- .../chat/hooks/use-chat-model-runtime.ts | 2 +- .../features/chat/permission-mode-select.tsx | 2 +- .../src/features/chat/projects-page.tsx | 16 +- .../prompt-storage/prompt-storage-dialog.tsx | 10 +- .../src/features/chat/thread-sidebar.tsx | 2 +- .../data-recipes/pages/data-recipes-page.tsx | 8 +- .../export/components/export-run-panel.tsx | 24 +- .../export/components/method-picker.tsx | 2 +- .../export/components/quant-picker.tsx | 10 +- .../src/features/export/export-page.tsx | 36 +-- .../features/hub/catalog/catalog-states.tsx | 32 +-- .../hub/catalog/dataset-download-section.tsx | 2 +- .../src/features/hub/catalog/dot-tag.tsx | 2 +- .../features/hub/catalog/download-card.tsx | 2 +- .../catalog/external-link-confirm-dialog.tsx | 4 +- .../hub/catalog/gguf-download-card.tsx | 10 +- .../hub/catalog/gguf-status-cards.tsx | 4 +- .../features/hub/catalog/hub-detail-view.tsx | 2 +- .../features/hub/catalog/hub-option-menu.tsx | 4 +- .../features/hub/catalog/hub-section-row.tsx | 2 +- .../hub/catalog/local-dataset-card.tsx | 2 +- .../hub/catalog/local-on-device-card.tsx | 20 +- .../src/features/hub/catalog/model-card.tsx | 6 +- .../features/hub/catalog/model-inspector.tsx | 36 +-- .../src/features/hub/catalog/model-readme.tsx | 20 +- .../hub/catalog/models-catalog-lists.tsx | 12 +- .../hub/catalog/models-catalog-rows.tsx | 30 +-- .../features/hub/catalog/models-header.tsx | 4 +- .../src/features/hub/catalog/models-table.tsx | 46 ++-- .../features/hub/catalog/models-toolbar.tsx | 10 +- .../hub/catalog/on-device-folders-dialog.tsx | 24 +- .../src/features/hub/catalog/owner-avatar.tsx | 8 +- .../hub/catalog/owner-scope-toggle.tsx | 2 +- .../features/hub/catalog/recent-searches.tsx | 6 +- .../hub/catalog/safetensors-download-card.tsx | 2 +- .../hub/catalog/sampling-settings-dialog.tsx | 10 +- .../src/features/hub/catalog/shared.tsx | 4 +- .../hub/catalog/transport-conflict-dialog.tsx | 2 +- .../features/hub/catalog/transport-toggle.tsx | 2 +- .../hub/components/hf-token-indicator.tsx | 6 +- .../features/hub/components/page-heading.tsx | 4 +- .../download-manager-panel.tsx | 8 +- .../download-progress-bar.tsx | 2 +- studio/frontend/src/features/hub/hub-page.tsx | 2 +- studio/frontend/src/features/hub/hub.css | 80 +++--- .../chat-template-editor-dialog.tsx | 4 +- .../components/model-config-page.tsx | 24 +- .../components/model-selector.tsx | 14 +- .../model-selector/folder-browser.tsx | 10 +- .../components/model-selector/pickers.tsx | 62 ++--- .../components/model-selector/pill-tabs.tsx | 2 +- .../components/native-model-chip.tsx | 2 +- .../components/native-model-drop-overlay.tsx | 4 +- .../components/steps/model-selection-step.tsx | 4 +- .../components/steps/model-type-step.tsx | 2 +- .../onboarding/components/wizard-sidebar.tsx | 8 +- .../profile-personalization-panel.tsx | 6 +- .../profile/components/user-avatar.tsx | 2 +- .../rag/components/document-preview-sheet.tsx | 2 +- .../rag/components/document-status-chip.tsx | 2 +- .../rag/components/project-sources-panel.tsx | 4 +- .../components/retrieval-settings-section.tsx | 22 +- .../recipe-studio/components/block-sheet.tsx | 4 +- .../executions/execution-sidebar.tsx | 2 +- .../components/executions/executions-view.tsx | 2 +- .../inline/inline-category-badges.tsx | 6 +- .../components/inline/inline-field.tsx | 2 +- .../components/inline/inline-llm.tsx | 2 +- .../components/inline/inline-seed.tsx | 6 +- .../components/recipe-graph-node.tsx | 4 +- .../components/recipe-studio-header.tsx | 10 +- .../runtime/execution-progress-island.tsx | 16 +- .../shared/available-references-inline.tsx | 14 +- .../models/local-recipe-model-selector.tsx | 20 +- .../recipe-studio/dialogs/preview-dialog.tsx | 2 +- .../dialogs/seed/seed-dialog.tsx | 2 +- .../tool-profile/tool-profile-dialog.tsx | 6 +- .../easy/github-crawler-easy-view.tsx | 2 +- .../recipe-studio/recipe-studio-page.tsx | 2 +- .../features/recipe-studio/utils/ui-tones.ts | 6 +- .../components/remote-code-consent-dialog.tsx | 8 +- .../settings/components/api-key-row.tsx | 4 +- .../components/api-monitor-console.tsx | 10 +- .../settings/components/create-key-form.tsx | 2 +- .../components/embedding-model-combobox.tsx | 4 +- .../settings/components/key-reveal-card.tsx | 2 +- .../settings/components/language-select.tsx | 2 +- .../components/sidebar-menu-customizer.tsx | 4 +- .../components/update-studio-instructions.tsx | 4 +- .../components/uploaded-files-dialog.tsx | 6 +- .../settings/components/usage-examples.tsx | 34 +-- .../src/features/settings/settings-dialog.tsx | 14 +- .../stores/appearance-custom-store.ts | 13 +- .../features/settings/tabs/resources-tab.tsx | 4 +- .../src/features/settings/tabs/voice-tab.tsx | 6 +- .../src/features/studio/history-card-grid.tsx | 12 +- .../studio/recent-trainings-section.tsx | 2 +- .../sections/charts/chart-settings-sheet.tsx | 2 +- .../sections/charts/eval-loss-chart-card.tsx | 8 +- .../sections/charts/grad-norm-chart-card.tsx | 4 +- .../charts/learning-rate-chart-card.tsx | 4 +- .../charts/training-loss-chart-card.tsx | 6 +- .../dataset-preview-dialog-mapping.tsx | 14 +- .../sections/dataset-preview-dialog.tsx | 20 +- .../studio/sections/dataset-section.tsx | 14 +- .../studio/sections/model-section.tsx | 16 +- .../studio/sections/params-section.tsx | 16 +- .../studio/sections/progress-section.tsx | 22 +- .../studio/sections/s3-config-form.tsx | 2 +- .../studio/sections/training-section.tsx | 4 +- .../src/features/studio/studio-page.tsx | 2 +- .../studio/training-start-overlay.tsx | 8 +- .../features/tour/components/guided-tour.tsx | 8 +- studio/frontend/src/index.css | 238 ++++++++++-------- tests/studio/playwright_ui_font_scale.py | 215 ++++++++++++++++ .../test_chat_thinking_compact_layout.py | 2 +- .../test_studio_text_descender_clipping.py | 2 +- tests/studio/test_ui_font_scale_contract.py | 147 +++++++++++ 155 files changed, 1222 insertions(+), 833 deletions(-) create mode 100644 tests/studio/playwright_ui_font_scale.py create mode 100644 tests/studio/test_ui_font_scale_contract.py diff --git a/.github/workflows/studio-ui-smoke.yml b/.github/workflows/studio-ui-smoke.yml index 0ad55ebd6d..97eb07b2d8 100644 --- a/.github/workflows/studio-ui-smoke.yml +++ b/.github/workflows/studio-ui-smoke.yml @@ -231,6 +231,15 @@ jobs: mkdir -p logs/playwright_extra python tests/studio/playwright_extra_ui.py + - name: UI font size scaling regression (Playwright) + env: + BASE_URL: http://127.0.0.1:18894 + STUDIO_PW: ${{ env.STUDIO_EXTRA_NEW_PW }} + PW_ART_DIR: logs/playwright_fontscale + run: | + mkdir -p logs/playwright_fontscale + python tests/studio/playwright_ui_font_scale.py + - name: Stop second Unsloth if: always() run: | @@ -352,6 +361,7 @@ jobs: logs/playwright logs/playwright-permissions-* logs/playwright_extra + logs/playwright_fontscale logs/playwright_modelcfg logs/playwright_ime logs/studio-permissions-*.log diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index 275c3c6623..e6c89b9cd7 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -213,7 +213,7 @@ function TauriUpdateLayer({ } return ( -
-
+
unsloth
-
+
{t("shell.beta")}
@@ -1221,7 +1221,7 @@ export function AppSidebar() {
hidden={isMobile}
>
{t("shell.navigation.search")}
-
+
{isMacPlatform ? "⌘K" : "Ctrl+K"}
@@ -1538,7 +1538,7 @@ export function AppSidebar() {
className="sidebar-nav-btn h-[33px] rounded-full gap-[8.5px] pl-3 pr-2.5 font-medium group-hover/recent-item:pr-16 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-8"
>
Generated image
{overlay.metadata ? ( -+
{overlay.metadata}
) : null} @@ -1390,7 +1390,7 @@ const ComposerAnimated: FC<{ disableQueue?: boolean; }> = ({ disabled, threadId, menuSide, disableQueue }) => { return ( -
+
{canvas.text}
+
{sizeLabel ? `${sizeLabel} download · ` : ""}No restart needed after update
@@ -209,7 +209,7 @@ export function LlamaUpdateBanner({LLMs can make mistakes. Double-check responses. @@ -651,7 +651,7 @@ const LoraCompareContent = memo(function LoraCompareContent({ handleName="base" header={
When off, all connections are disabled.
@@ -1616,7 +1616,7 @@ export function ChatProvidersSettings({ {provider.name} - + {provider.models.length}{" "} {provider.models.length === 1 ? "model" : "models"} @@ -1631,7 +1631,7 @@ export function ChatProvidersSettings({ ) : null}