fix(app): standardize workspace typography

Use the shared interface text tier across the workspace header, conversation, and composer so the working surface no longer shifts between adjacent font sizes.
This commit is contained in:
Mohamed Boudra
2026-08-16 15:59:44 +02:00
parent 42245d139a
commit a9e3be168c
6 changed files with 42 additions and 28 deletions
+2 -2
View File
@@ -28,11 +28,11 @@ Before adding a new component, read `components/ui/`. The primitive usually exis
## 3. Hierarchy
Hierarchy is conveyed through weight and color, not size. Most labels, titles, and hints across the app are `fontSize.base` or `fontSize.xs`. The distinction between a row's primary line and its secondary line is `foreground` versus `foregroundMuted`.
Hierarchy is conveyed through weight and color, not size. Most interface text is `fontSize.sm`; compact metadata and hints use `fontSize.xs`. The distinction between a row's primary line and its secondary line is `foreground` versus `foregroundMuted`.
Weight has three tiers, applied by role:
- **Screen titles** — the title at the top of a screen — use `<ScreenTitle>` (`packages/app/src/components/headers/screen-title.tsx`), which renders `fontSize.base` at weight `400` on compact and `300` on desktop. Top-of-screen titles are lighter on desktop, not heavier. The workspace screen header follows the same rule (`packages/app/src/screens/workspace/workspace-screen.tsx`).
- **Screen titles** — the title at the top of a screen — use `<ScreenTitle>` (`packages/app/src/components/headers/screen-title.tsx`), which renders `fontSize.base` at weight `400` on compact and `300` on desktop. Top-of-screen titles are lighter on desktop, not heavier. The workspace header overrides the size to `fontSize.sm` so its title, project name, tabs, pane chrome, conversation text, and composer share the desktop working-surface scale (`packages/app/src/screens/workspace/workspace-screen.tsx`).
- **Structural labels** use `fontWeight.medium`. This applies to section labels above a stack of rows (`packages/app/src/components/agent-list.tsx:519-523`, `packages/app/src/components/keyboard-shortcuts-dialog.tsx:63-67`), form field labels above an input inside a modal (`packages/app/src/components/add-host-modal.tsx:19-23`, `packages/app/src/components/pair-link-modal.tsx:24-28`), the title at the top of a modal/sheet/dialog (`packages/app/src/components/adaptive-modal-sheet.tsx:90-94`, `packages/app/src/components/ui/combobox.tsx:1607-1611`, `packages/app/src/components/welcome-screen.tsx:48-53`), action button labels in tight components such as the sidebar callout actions (`packages/app/src/components/sidebar-callout.tsx:218-221`), and inline data emphasis on dense metadata rows (`packages/app/src/components/git-diff-pane.tsx:2322-2327`, `packages/app/src/components/file-explorer-pane.tsx:1115-1122`).
- **Content** uses `fontWeight.normal`. This applies to settings rows (`packages/app/src/styles/settings.ts`), sidebar primary list-item titles (`packages/app/src/components/sidebar-workspace-list.tsx:2680-2686`, `packages/app/src/components/agent-list.tsx:572-578`), `<Button>` text (`packages/app/src/components/ui/button.tsx:80-84`), `<StatusBadge>` text (`packages/app/src/components/ui/status-badge.tsx:56-60`), and `<SidebarCallout>` titles (`packages/app/src/components/sidebar-callout.tsx:175-180`).
+11 -9
View File
@@ -354,8 +354,10 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
},
text: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
...(isWeb ? { lineHeight: 22, overflowWrap: "anywhere" as const } : {}),
fontSize: theme.fontSize.sm,
...(isWeb
? { lineHeight: Math.round(theme.fontSize.sm * 1.4), overflowWrap: "anywhere" as const }
: {}),
},
imagePreviewContainer: {
flexDirection: "row",
@@ -1093,7 +1095,7 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
},
label: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
flexShrink: 0,
},
@@ -1108,7 +1110,7 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
flexShrink: 1,
minWidth: 0,
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
marginLeft: theme.spacing[2],
},
@@ -1117,7 +1119,7 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
},
shimmerText: {
color: "transparent",
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
},
spacer: {
@@ -1950,14 +1952,14 @@ const speakMessageStylesheet = StyleSheet.create((theme) => ({
},
headerLabel: {
fontFamily: theme.fontFamily.ui,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
},
text: {
fontFamily: theme.fontFamily.ui,
fontSize: theme.fontSize.base,
lineHeight: 22,
fontSize: theme.fontSize.sm,
lineHeight: Math.round(theme.fontSize.sm * 1.4),
color: theme.colors.foreground,
},
}));
@@ -2260,7 +2262,7 @@ const todoListCardStylesheet = StyleSheet.create((theme) => ({
},
emptyText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
},
}));
+2 -2
View File
@@ -1915,9 +1915,9 @@ const styles = StyleSheet.create((theme: Theme) => ({
textInput: {
width: "100%",
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
lineHeight: theme.fontSize.base * 1.4,
lineHeight: theme.fontSize.sm * 1.4,
...(isWeb
? ({
outlineStyle: "none",
@@ -3942,14 +3942,8 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
minHeight: 0,
},
// Compact steps the whole header down one rung of the scale. The title and the project line
// stack there, so at the shared size they read as two headings rather than a subject and its
// caption, and they cost two full lines of a header that has none to spare.
headerTitle: {
fontSize: {
xs: theme.fontSize.sm,
md: theme.fontSize.base,
},
fontSize: theme.fontSize.sm,
},
headerTitleContainer: {
flex: 1,
@@ -3999,7 +3993,7 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: {
xs: theme.fontSize.xs,
md: theme.fontSize.base,
md: theme.fontSize.sm,
},
flexShrink: 1,
minWidth: 0,
@@ -3,6 +3,24 @@ import { createCompactMarkdownStyles, createMarkdownStyles } from "./markdown-st
import { darkTheme } from "./theme";
describe("createMarkdownStyles", () => {
it("uses the standard interface size for conversation prose and list markers", () => {
const styles = createMarkdownStyles(darkTheme);
const proseLineHeight = Math.round(darkTheme.fontSize.sm * 1.4);
expect(styles.body).toMatchObject({
fontSize: darkTheme.fontSize.sm,
lineHeight: proseLineHeight,
});
expect(styles.bullet_list_icon).toMatchObject({
fontSize: darkTheme.fontSize.sm,
lineHeight: proseLineHeight,
});
expect(styles.ordered_list_icon).toMatchObject({
fontSize: darkTheme.fontSize.sm,
lineHeight: proseLineHeight,
});
});
it("applies shrink-and-wrap constraints to long markdown text and links", () => {
const styles = createMarkdownStyles(darkTheme);
+7 -7
View File
@@ -19,10 +19,10 @@ export function createMarkdownStyles(theme: Theme) {
body: {
...webSelectableTextStyle,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
// Prose line-height scales with the UI ramp (≈22 at base 16), NOT the
fontSize: theme.fontSize.sm,
// Prose line-height scales with the UI ramp, not the
// code-size-coupled lineHeight.diff token used by code/diff surfaces.
lineHeight: Math.round(theme.fontSize.base * 1.4),
lineHeight: Math.round(theme.fontSize.sm * 1.4),
flexShrink: 1,
minWidth: 0,
width: "100%" as const,
@@ -283,17 +283,17 @@ export function createMarkdownStyles(theme: Theme) {
...webSelectableTextStyle,
color: theme.colors.foregroundMuted,
marginRight: 4,
fontSize: theme.fontSize.base,
lineHeight: 22,
fontSize: theme.fontSize.sm,
lineHeight: Math.round(theme.fontSize.sm * 1.4),
},
ordered_list_icon: {
...webSelectableTextStyle,
color: theme.colors.foregroundMuted,
marginRight: 4,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
lineHeight: 22,
lineHeight: Math.round(theme.fontSize.sm * 1.4),
minWidth: 12,
},