Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Michael Han
1e6b9aaca5 Fix UI font size scaling 2026-07-22 07:57:14 -07:00
12 changed files with 104 additions and 21 deletions

View file

@ -85,6 +85,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"postcss": "^8.5.15",
"typescript": "~5.9.3",
"typescript-eslint": "^8.55.0",
"vite": "^8.0.16"

View file

@ -104,6 +104,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"postcss": "^8.5.15",
"typescript": "~5.9.3",
"typescript-eslint": "^8.55.0",
"vite": "^8.0.16"

View file

@ -52,8 +52,8 @@ export function ShutdownDialog({
onAfterShutdown?.();
document.body.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;gap:12px">
<p style="font-size:1.1rem;font-weight:600;margin:0">Unsloth Studio has stopped.</p>
<p style="font-size:0.9rem;color:#888;margin:0">You can now close this tab.</p>
<p style="font-size:calc(1.1rem * var(--ui-font-scale, 1));font-weight:600;margin:0">Unsloth Studio has stopped.</p>
<p style="font-size:calc(0.9rem * var(--ui-font-scale, 1));color:#888;margin:0">You can now close this tab.</p>
</div>`;
};

View file

@ -510,11 +510,14 @@ export function applyCustomizationToDocument(
setVar("--custom-chat-font", null);
}
// Scale typography without changing the root rem or layout dimensions.
if (c.uiFontSize !== null && c.uiFontSize !== UI_FONT_SIZE_RANGE.default) {
style.fontSize = `${c.uiFontSize}px`;
setVar("--ui-font-scale", String(c.uiFontSize / UI_FONT_SIZE_RANGE.default));
} else {
style.removeProperty("font-size");
setVar("--ui-font-scale", null);
}
// Clear the root size used by older builds.
style.removeProperty("font-size");
if (c.codeFontSize !== null) {
el.setAttribute("data-code-font-size", "");

View file

@ -17,6 +17,7 @@ import type { ReactElement } from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import {
CHART_CONTAINER_CLASS,
CHART_FONT_SIZE,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
formatAxisMetric,
@ -70,7 +71,7 @@ export function EvalLossChartCard({
tickLine={false}
axisLine={false}
tickMargin={8}
fontSize={10}
fontSize={CHART_FONT_SIZE}
tickFormatter={(value) => formatStepTick(Number(value))}
interval="preserveStartEnd"
/>
@ -81,7 +82,7 @@ export function EvalLossChartCard({
axisLine={false}
tickMargin={8}
tickCount={5}
fontSize={10}
fontSize={CHART_FONT_SIZE}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => formatAxisMetric(Number(value))}
/>
@ -132,7 +133,7 @@ export function EvalLossChartCard({
tickLine={false}
axisLine={false}
tickMargin={8}
fontSize={10}
fontSize={CHART_FONT_SIZE}
interval="preserveStartEnd"
/>
<YAxis
@ -140,7 +141,7 @@ export function EvalLossChartCard({
axisLine={false}
tickMargin={8}
tickCount={5}
fontSize={10}
fontSize={CHART_FONT_SIZE}
width={DEFAULT_Y_AXIS_WIDTH}
/>
<Line

View file

@ -15,8 +15,9 @@ import type { ReactElement } from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import type { ScaleMode } from "./types";
import {
CHART_SYNC_ID,
CHART_CONTAINER_CLASS,
CHART_FONT_SIZE,
CHART_SYNC_ID,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
formatAxisMetric,
@ -78,7 +79,7 @@ export function GradNormChartCard({
tickLine={false}
axisLine={false}
tickMargin={8}
fontSize={10}
fontSize={CHART_FONT_SIZE}
tickFormatter={(value) => formatStepTick(Number(value))}
interval="preserveStartEnd"
/>
@ -89,7 +90,7 @@ export function GradNormChartCard({
axisLine={false}
tickMargin={8}
tickCount={5}
fontSize={10}
fontSize={CHART_FONT_SIZE}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => {
const num = Number(value);

View file

@ -16,6 +16,7 @@ import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import type { ScaleMode } from "./types";
import {
CHART_CONTAINER_CLASS,
CHART_FONT_SIZE,
CHART_SYNC_ID,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
@ -76,7 +77,7 @@ export function LearningRateChartCard({
tickLine={false}
axisLine={false}
tickMargin={8}
fontSize={10}
fontSize={CHART_FONT_SIZE}
tickFormatter={(value) => formatStepTick(Number(value))}
interval="preserveStartEnd"
/>
@ -87,7 +88,7 @@ export function LearningRateChartCard({
axisLine={false}
tickMargin={8}
tickCount={5}
fontSize={10}
fontSize={CHART_FONT_SIZE}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => {
const num = Number(value);

View file

@ -22,8 +22,9 @@ import {
} from "recharts";
import type { ScaleMode } from "./types";
import {
CHART_SYNC_ID,
CHART_CONTAINER_CLASS,
CHART_FONT_SIZE,
CHART_SYNC_ID,
DEFAULT_CHART_MARGIN,
DEFAULT_Y_AXIS_WIDTH,
formatAxisMetric,
@ -96,7 +97,7 @@ export function TrainingLossChartCard({
tickLine={false}
axisLine={false}
tickMargin={8}
fontSize={10}
fontSize={CHART_FONT_SIZE}
tickFormatter={(value) => formatStepTick(Number(value))}
interval="preserveStartEnd"
/>
@ -107,7 +108,7 @@ export function TrainingLossChartCard({
axisLine={false}
tickMargin={8}
tickCount={5}
fontSize={10}
fontSize={CHART_FONT_SIZE}
width={DEFAULT_Y_AXIS_WIDTH}
tickFormatter={(value) => {
const num = Number(value);
@ -152,7 +153,7 @@ export function TrainingLossChartCard({
value: formatMetric(avgRaw),
}),
position: "insideTopRight",
fontSize: 10,
fontSize: CHART_FONT_SIZE,
fill: "#3b82f6",
}}
/>

View file

@ -9,6 +9,7 @@ export const DEFAULT_VISIBLE_POINTS = 160;
export const CHART_CONTAINER_CLASS = "h-[220px] w-full";
export const DEFAULT_CHART_MARGIN = { top: 4, right: 8, bottom: 0, left: 4 };
export const DEFAULT_Y_AXIS_WIDTH = 45;
export const CHART_FONT_SIZE = "calc(10px * var(--ui-font-scale, 1))";
const TRAILING_ZEROES_RE = /\.?0+$/;
const NEGATIVE_ZERO_RE = /^-0$/;

View file

@ -140,13 +140,13 @@ export function TrainingSection() {
tickLine={false}
axisLine={false}
tickMargin={8}
fontSize={10}
fontSize="calc(10px * var(--ui-font-scale, 1))"
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
fontSize={10}
fontSize="calc(10px * var(--ui-font-scale, 1))"
/>
<Line
type="monotone"

View file

@ -577,6 +577,11 @@ html[data-code-font-size] :is(pre, code, kbd, samp) {
font-size: var(--custom-code-font-size) !important;
}
/* Scale inherited text without changing the root rem used by layout tokens. */
body {
font-size: calc(1rem * var(--ui-font-scale, 1));
}
/* Pointer cursors: hand cursor over interactive elements. */
html.pointer-cursors
:is(

View file

@ -4,11 +4,79 @@
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import postcss, { type Plugin as PostcssPlugin } from "postcss";
import { type Plugin as VitePlugin, defineConfig } from "vite";
const UI_FONT_SCALE_VAR = "--ui-font-scale";
const CSS_WIDE_FONT_SIZE_PATTERN =
/^(?:inherit|initial|revert(?:-layer)?|unset|smaller|larger)$/;
const RELATIVE_FONT_SIZE_PATTERN =
/(?:^|[^\w.-])-?(?:\d*\.)?\d+(?:em|ex|cap|ch|ic|lh|rlh|%)\b/;
const ABSOLUTE_FONT_SIZE_PATTERN =
/-?(?:\d*\.)?\d+(?:px|pt|pc|rem|cm|mm|q|in)\b/;
const FONT_SIZE_FUNCTION_PATTERN = /\b(?:calc|clamp|max|min|var)\(/;
const CSS_MODULE_ID_PATTERN = /\.css(?:$|\?)/;
// Relative sizes inherit the scale and must not compound through nested text.
function shouldScaleFontSize(value: string): boolean {
const normalized = value.trim().toLowerCase();
if (
normalized.includes(UI_FONT_SCALE_VAR) ||
normalized.includes("--custom-code-font-size") ||
CSS_WIDE_FONT_SIZE_PATTERN.test(normalized)
) {
return false;
}
// Root rem stays fixed, so rem typography still needs the multiplier.
if (RELATIVE_FONT_SIZE_PATTERN.test(normalized)) {
return false;
}
return (
ABSOLUTE_FONT_SIZE_PATTERN.test(normalized) ||
FONT_SIZE_FUNCTION_PATTERN.test(normalized)
);
}
const scaleAbsoluteFontSizes: PostcssPlugin = {
postcssPlugin: "unsloth-ui-font-scaling",
// biome-ignore lint/style/useNamingConvention: PostCSS visitor API key.
Declaration: {
"font-size": (declaration) => {
if (!shouldScaleFontSize(declaration.value)) {
return;
}
declaration.value = `calc(${declaration.value} * var(${UI_FONT_SCALE_VAR}, 1))`;
},
},
};
// Scale named, arbitrary, and vendor font sizes in Tailwind's generated CSS.
function uiFontScalingPlugin(): VitePlugin {
return {
name: "unsloth-ui-font-scaling",
// Run after Tailwind and before Vite converts CSS into a module.
enforce: "pre",
async transform(code, id) {
if (!CSS_MODULE_ID_PATTERN.test(id)) {
return null;
}
const result = await postcss([scaleAbsoluteFontSizes]).process(code, {
from: id,
map: false,
});
if (result.css === code) {
return null;
}
return { code: result.css, map: null };
},
};
}
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins: [react(), tailwindcss(), uiFontScalingPlugin()],
optimizeDeps: {
include: ["@dagrejs/dagre", "@dagrejs/graphlib"],
},