Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/frontend-ui-audit-2026-08-03/StatusBarTypography.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Frontend UI Audit — StatusBarTypography

**Files:** `src/modules/WorkStation/shared/StatusBar/*.tsx`, `src/modules/WorkStation/shared/StatusBar/statusBarTokens.ts`, `src/components/DiffStatsBadge/index.tsx`
**Date:** 2026-08-03
**Auditor:** Codex

## D1 — Raw HTML vs Design System

| Line | Element | Verdict | Reason | Suggested change |
| ------------------------------------- | ------------------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `StatusBarBase.tsx:126` | `<button>` inside `StatusBarButton` | keep with reason | This is the status-bar design-system primitive itself; it owns status-bar sizing, variants, disabled state, tooltip forwarding, and accessible naming. | — |
| `GitSyncStatusMenu.tsx:241,266,293` | Git action menu `<button>` rows | keep with reason | Rows use shared dropdown tokens, `role="menuitem"`, visible labels, and menu-specific disabled behavior; the general Button component does not cover this menu-row contract. | — |
| `PortsStatusMenu.tsx:118,130,143,371` | Port action/menu `<button>` controls | keep with reason | These are compact menu-row actions with distinct copy/open/stop behavior and established dropdown styling; each has a visible or explicit accessible name. | — |
| `PortsStatusMenu.tsx:312` | Port filter `<input>` | keep with reason | This is embedded in the shared dropdown search container and uses the dropdown search token contract; replacing it independently would break the compact menu composition. | — |
| `CiStatusMenu.tsx:158,332,341` | CI menu action `<button>` controls | keep with reason | The controls are menu-local icon/action rows with accessible labels and shared dropdown styling. | — |

## D2 — Arbitrary Tailwind Value vs Token

| Line | Value | Verdict | Reason | Suggested change |
| -------------------------- | -------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `StatusBarRenderer.tsx:28` | `bg-[var(--cm-editor-background,...)]` | fix candidate (deferred) | Folder-wide sweep found a project-owned editor background variable outside the typography change. It should not be changed site-by-site in this single-responsibility typography patch. | Map the project-owned variable in Tailwind or fold it into the matching surface token in a separate sweep. |

## D3 — Hardcoded Sizes / Colors

| Line | Value | Verdict | Reason | Suggested change |
| ----------------------------------------------- | ------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `statusBarTokens.ts:20` | status-bar 11px type scale | fix (completed) | The previous local `text-[11px]` duplicated the global workstation secondary typography role. | `STATUS_BAR_TYPOGRAPHY.root` now derives from `TYPOGRAPHY.secondary`. |
| `statusBarTokens.ts:71` | extension item 11px/line-height | fix (completed) | Extension items repeated the base bar typography literal. | Extension items now compose `STATUS_BAR_TYPOGRAPHY.root`. |
| `EditorStatusBar.tsx:620` | language-service panel 13px | fix (completed) | The status-bar-owned dropdown repeated the existing dropdown item font size. | Reuse `DROPDOWN_ITEM.fontSizeClass`. |
| `BrowserStatusBar.tsx:118` | `max-w-[240px]` | fix candidate (deferred) | `max-w-60` is an equivalent spacing token, but width cleanup is unrelated to the requested typography lifecycle. | Replace in a focused status-bar sizing cleanup. |
| `EditorStatusBar.tsx:474` | `max-w-[200px]` | keep with reason | The truncation cap has no exact standard Tailwind spacing equivalent; 192px or 208px would change the commit-author allocation. | — |
| `DiffStatsBadge/diffStatsBadgeHelpers.ts:16-18` | named 11/12/13px scale | keep with reason | Pixel values are isolated behind the component's `size` API and intentionally match the established diff-viewer type scale. Callers do not repeat arbitrary font-size utilities. | — |

## D4 — Accessibility

| Line | Element | Verdict | Reason | Suggested change |
| ------------------------- | -------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `StatusBarBase.tsx:126` | `StatusBarButton` | keep with reason | Visible children provide a name; icon-only uses supply `ariaLabel` or `title`, which is forwarded to `aria-label`. | — |
| `EditorStatusBar.tsx:608` | click-away overlay `<div>` | keep with reason | The overlay is a pointer-dismiss backdrop, not the primary interactive control; the language-services trigger remains a keyboard-focusable button. | — |
| Git/ports/CI menus | menu controls | keep with reason | Menu action buttons have visible labels or explicit accessible names and retain native keyboard semantics. | — |

## D5 — Visual Patterns Observed

- Pattern: 11px status-bar text with normal/medium roles and tabular numeric values — previously repeated across Editor, Browser, Project, Git Sync, CI, and Ports status surfaces; now abstracted by `STATUS_BAR_TYPOGRAPHY`, `StatusBarLabel`, and `StatusBarText` semantic props.
- Pattern: normal-weight diff statistics in the status bar — now expressed through the shared `DiffStatsBadge weight="normal"` API while every existing consumer keeps the backwards-compatible medium default.

## Summary

- 4 fixes completed
- 8 kept with documented reason
- 2 deferred fix candidates outside the typography scope
- 2 repeated typography patterns abstracted
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";

import DiffStatsBadge from "..";

describe("DiffStatsBadge typography", () => {
it("preserves medium weight by default", () => {
const markup = renderToStaticMarkup(
createElement(DiffStatsBadge, { additions: 12, deletions: 3 })
);

expect(markup).toContain("font-medium");
expect(markup).not.toContain("font-normal");
});

it("supports a semantic normal-weight status-bar variant", () => {
const markup = renderToStaticMarkup(
createElement(DiffStatsBadge, {
additions: 12,
deletions: 3,
variant: "plain",
size: "xs",
weight: "normal",
})
);

expect(markup).toContain("text-[11px]");
expect(markup).toContain("font-mono");
expect(markup).toContain("font-normal");
expect(markup).toContain("tabular-nums");
expect(markup).not.toContain("font-medium");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { describe, expect, it } from "vitest";

import {
DIFF_STATS_SIZE_CLASSES,
DIFF_STATS_WEIGHT_CLASSES,
type DiffStatsBadgeSize,
type DiffStatsBadgeWeight,
getDiffStatsSizeClass,
getDiffStatsWeightClass,
} from "../diffStatsBadgeHelpers";

describe("getDiffStatsSizeClass", () => {
Expand Down Expand Up @@ -39,3 +42,23 @@ describe("getDiffStatsSizeClass", () => {
expect(DIFF_STATS_SIZE_CLASSES.sm).toContain("12px");
});
});

describe("getDiffStatsWeightClass", () => {
it("maps named weights to typography tokens", () => {
expect(getDiffStatsWeightClass("normal")).toBe("font-normal");
expect(getDiffStatsWeightClass("medium")).toBe("font-medium");
});

it("defaults and falls back to the backwards-compatible medium weight", () => {
expect(getDiffStatsWeightClass()).toBe("font-medium");
const unknown = "bold" as unknown as DiffStatsBadgeWeight;
expect(getDiffStatsWeightClass(unknown)).toBe("font-medium");
});

it("keeps every weight mapping as a single token", () => {
for (const cls of Object.values(DIFF_STATS_WEIGHT_CLASSES)) {
expect(cls.trim()).toBe(cls);
expect(cls.split(" ").filter(Boolean)).toHaveLength(1);
}
});
});
13 changes: 13 additions & 0 deletions src/components/DiffStatsBadge/diffStatsBadgeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* rendering for every pre-existing call site.
*/
export type DiffStatsBadgeSize = "inherit" | "xs" | "sm" | "md";
export type DiffStatsBadgeWeight = "normal" | "medium";

export const DIFF_STATS_SIZE_CLASSES: Record<DiffStatsBadgeSize, string> = {
inherit: "",
Expand All @@ -17,6 +18,11 @@ export const DIFF_STATS_SIZE_CLASSES: Record<DiffStatsBadgeSize, string> = {
md: "text-[13px]",
};

export const DIFF_STATS_WEIGHT_CLASSES: Record<DiffStatsBadgeWeight, string> = {
normal: "font-normal",
medium: "font-medium",
};

/**
* Maps a {@link DiffStatsBadgeSize} to its font-size class. Unknown/undefined
* sizes fall back to `inherit` (no class), so the badge never silently changes
Expand All @@ -28,3 +34,10 @@ export function getDiffStatsSizeClass(size?: DiffStatsBadgeSize): string {
}
return DIFF_STATS_SIZE_CLASSES[size] ?? DIFF_STATS_SIZE_CLASSES.inherit;
}

/** Maps the named badge weight to a single Tailwind typography token. */
export function getDiffStatsWeightClass(
weight: DiffStatsBadgeWeight = "medium"
): string {
return DIFF_STATS_WEIGHT_CLASSES[weight] ?? DIFF_STATS_WEIGHT_CLASSES.medium;
}
14 changes: 10 additions & 4 deletions src/components/DiffStatsBadge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { DIFF_STATS } from "@src/config/workstation/tokens";

import {
type DiffStatsBadgeSize,
type DiffStatsBadgeWeight,
getDiffStatsSizeClass,
getDiffStatsWeightClass,
} from "./diffStatsBadgeHelpers";

type DiffStatsBadgeVariant = "default" | "compact" | "chat" | "plain";
Expand All @@ -20,6 +22,8 @@ export interface DiffStatsBadgeProps {
* of re-specifying `text-[Npx]` externally.
*/
size?: DiffStatsBadgeSize;
/** Named font weight. Defaults to `medium` for backwards compatibility. */
weight?: DiffStatsBadgeWeight;
/**
* Gap utility between the additions and deletions values. Defaults to
* `"gap-1"`. Pass `"gap-0"` (or another gap token) to override — supplied
Expand All @@ -37,11 +41,11 @@ export interface DiffStatsBadgeProps {
}

const CONTAINER_CLASSES: Record<DiffStatsBadgeVariant, string> = {
default: `${DIFF_STATS.container} font-mono font-medium leading-none tabular-nums`,
compact: `${DIFF_STATS.containerCompact} font-mono font-medium leading-none tabular-nums`,
chat: "chat-block-xs flex shrink-0 items-center font-mono font-medium leading-none tabular-nums",
default: `${DIFF_STATS.container} font-mono leading-none tabular-nums`,
compact: `${DIFF_STATS.containerCompact} font-mono leading-none tabular-nums`,
chat: "chat-block-xs flex shrink-0 items-center font-mono leading-none tabular-nums",
plain:
"inline-flex shrink-0 items-center font-mono font-medium leading-none tabular-nums",
"inline-flex shrink-0 items-center font-mono leading-none tabular-nums",
};

const VALUE_BASE_CLASSES = "inline-flex";
Expand All @@ -56,6 +60,7 @@ const DiffStatsBadge = memo(function DiffStatsBadge({
deletions = 0,
variant = "default",
size = "inherit",
weight = "medium",
gapClassName = "gap-1",
reserveValueWidth = true,
className,
Expand All @@ -77,6 +82,7 @@ const DiffStatsBadge = memo(function DiffStatsBadge({
CONTAINER_CLASSES[variant],
gapClassName,
getDiffStatsSizeClass(size),
getDiffStatsWeightClass(weight),
className
)}
>
Expand Down
15 changes: 12 additions & 3 deletions src/modules/WorkStation/shared/StatusBar/BrowserStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import { AlertTriangle, BrushCleaning, Plus, XCircle } from "lucide-react";
import React, { memo, useMemo } from "react";
import { useTranslation } from "react-i18next";

import { BaseStatusBar, StatusBarButton, StatusBarText } from "./StatusBarBase";
import {
BaseStatusBar,
StatusBarButton,
StatusBarLabel,
StatusBarText,
} from "./StatusBarBase";

export interface BrowserStatusBarProps {
/** Current page URL */
Expand Down Expand Up @@ -79,13 +84,17 @@ const BrowserStatusBar: React.FC<BrowserStatusBarProps> = memo(
{errorCount > 0 && (
<span className={`flex items-center gap-1 ${itemTextClass}`}>
<XCircle size={13} />
<span className="font-medium">{errorCount}</span>
<StatusBarLabel emphasis numeric>
{errorCount}
</StatusBarLabel>
</span>
)}
{warningCount > 0 && (
<span className={`flex items-center gap-1 ${itemTextClass}`}>
<AlertTriangle size={13} />
<span className="font-medium">{warningCount}</span>
<StatusBarLabel emphasis numeric>
{warningCount}
</StatusBarLabel>
</span>
)}
</StatusBarButton>
Expand Down
6 changes: 3 additions & 3 deletions src/modules/WorkStation/shared/StatusBar/CiStatusMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { openExternalLink } from "@src/util/platform/ipcRenderer";
import { formatRelativeTime } from "@src/util/time/formatRelativeTime";
import { classNames } from "@src/util/ui/classNames";

import { StatusBarButton } from "./StatusBarBase";
import { StatusBarButton, StatusBarLabel } from "./StatusBarBase";
import { StatusBarTooltip } from "./StatusBarTooltip";

const MENU_ICON_SIZE = DROPDOWN_ITEM.iconSize;
Expand Down Expand Up @@ -292,9 +292,9 @@ export const CiStatusMenu: React.FC<CiStatusMenuProps> = memo(
dataTestId="status-bar-ci"
>
<BranchCiIcon status={ciStatus} />
<span className="font-medium tabular-nums text-text-1">
<StatusBarLabel emphasis numeric className="text-text-1">
{triggerLabel}
</span>
</StatusBarLabel>
</StatusBarButton>
</StatusBarTooltip>

Expand Down
Loading
Loading