Skip to content

refactor(frontend): four text sizes, two greys, and a rule that keeps them - #2440

Merged
dzarlax merged 6 commits into
constructorfabric:mainfrom
dzarlax:feat/typography-scale
Aug 11, 2026
Merged

refactor(frontend): four text sizes, two greys, and a rule that keeps them#2440
dzarlax merged 6 commits into
constructorfabric:mainfrom
dzarlax:feat/typography-scale

Conversation

@dzarlax

@dzarlax dzarlax commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The interface had drifted to twelve text sizes, five greys and four weights. None of that was decided — every one was a reasonable local choice by someone who could not see the sum, which is how this happens and why it will happen again without the last commit here.

What was actually wrong

Measured before touching anything, because the shape was not what it looked like. The two smallest steps carried 301 of 356 usages: the noise was a tail of 55 spread across ten variants, not the body of the work.

The tail had a pattern. Nine, ten and eleven pixels were three ways of saying "small" that no reader can tell apart. Page titles were set at three different sizes on different screens. The same figure rendered at 24, 30 or 36 pixels depending on which screen it landed on — tabular-nums sitting beside each one is the tell that they were the same role all along.

Three of them turned out not to be choices at all. A tooltip set no size and inherited the root 16px, so the same explanation rendered at 16px in one place and 14px in another. A card figure stepped between two sizes on a container query, so one role rendered at two sizes depending on how wide its card happened to be.

Distinctions also failed to line up with meaning. A section heading differed from a caption by weight alone at the same size — two pixels of difference is not a distinction anybody sees. One step carried a control, a metric's name, a card's label AND the metric's own value, so a row read flat: the number looked like its label. And the same thing — a metric's name — was near-black in one place and grey in another.

What it is now

Four sizes, one role each: captions, body and names, titles, figures. Two greys. Colour says one thing — good and bad are success and destructive, attention without a verdict is warning, and nothing else is expressed in colour at all. Ten raw palette literals went back to tokens, several of them added by me a few hours earlier.

A name is ink because the reader scans by it. What is grey is context: medians, comparisons, units, the pills that qualify a value. Grey is what the eye may skip, so nothing that must be read is put there.

The commit that matters

The sweep is worth nothing on its own — it grows back, and the next person measures it from scratch. So a lint rule refuses hand-set sizes, fractional greys and raw palette colours, and each refusal names the alternative rather than just objecting.

It was checked by writing a violating file and confirming the rule fires on it. A guard that silently matches nothing is worse than no guard, because it is believed. It has already earned its place: the last commit here fixes a hand-set size the rule found rather than a reader did.

Also worth knowing: of eleven large numbers, only three went through the shared role — the other eight spelled their size out. A role nothing routes through is a comment, not a rule, and changing the figure size would have moved three of them and left eight behind. All eleven route through it now, which is why the figure could be reduced in one line.

Scope

Vendored primitives under src/components/ui are untouched: their sizing belongs to that library and shadcn add rewrites them. The rule exempts them for the same reason.

Checks

tsc -b clean, eslint clean, 1073 unit tests pass. Reviewed on screen at each step rather than only in the diff.

Summary by CodeRabbit

  • Style
    • Standardized typography across dashboards, metric views, portals, and settings.
    • Improved readability with more consistent heading, label, figure, and explanatory text sizes.
    • Refined muted colors, chart marks, chevrons, warning accents, and text weight for clearer visual hierarchy.
    • Updated the person-zone label to “Personal metrics.”
  • Tests
    • Updated navigation expectations to reflect the renamed section label.

Alexey Panfilov added 5 commits August 11, 2026 18:17
Measured before touching anything: six sizes across nine size-and-weight
combinations on one screen, and the distinctions did not line up with meaning.
A section heading differed from a caption by weight alone at the same 12px. One
14px step carried a control, a metric's name, a card's label AND the metric's
own value, so a row read flat — the number looked like its label. The same
thing, a metric's name, was near-black in one place and grey in another. Two
pill sizes, 10px and 11px, sat next to each other.

Five roles now, declared in one module so the next change picks a role instead
of inventing a size. A name is ink because the reader scans by it; what is grey
is context — medians, comparisons, units, the pills that qualify a value.

Three sizes turned out not to be choices at all. The tooltip set none and
inherited the root 16px, so the same explanation rendered at 16px in one place
and 14px in another. 11px was hand-set in three files. The card figure stepped
between 24px and 30px on a container query, so one role rendered at two sizes
depending on how wide its card happened to be.

The zone's subtitle reads "Personal metrics" rather than "Pick a person": the
zone is the person's own numbers, not a chooser.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
Measured first, because the shape of the problem was not what it looked like.
Twelve sizes were in use — eight scale steps plus four written by hand — and
four weights. But the two smallest steps carried 301 of 356 usages: the noise
was a tail of 55 spread over ten variants, not the body of the work.

That tail is gone. Nine, ten and eleven pixels were three ways of saying
"small" that no reader can tell apart. Titles were set at three different sizes
on different screens; the same figure rendered at 24, 30 or 36 pixels depending
on which screen it landed on, and `tabular-nums` beside it is the tell that all
of them were the same role. Four sizes remain, one per rung.

Text greys go from five to two. A fractional shade is a third level the reader
has to rank against the other two, and nobody can — so "grey is skippable"
stops holding and every level has to be read after all.

Colour now says one thing. Ten raw amber literals went back to the warning
token, several of them added by me earlier today; good and bad were already
success and destructive, and hierarchy was already ink and grey. Nothing else
is expressed in colour.

Vendored primitives under components/ui are untouched: their sizing belongs to
that library and `shadcn add` rewrites them.

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
…tte colours

The sweep before this one is worth nothing on its own. The scale had already
drifted to twelve sizes and five greys once, and not through carelessness —
every one of them was a reasonable local choice by someone who could not see
the sum. Without something that says no, it grows back, and the next person
measures it again from scratch.

Three refusals, each naming the alternative rather than just objecting: a size
written by hand points at the roles in type-scale, a fractional grey points at
the two that exist, and a raw palette colour points at the semantic tokens.

Checked by writing a violating file and confirming the rule fires on it — a
guard that silently matches nothing is worse than no guard, because it is
believed.

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
…smaller

Eight of the eleven large numbers still spelled their size out; only three went
through the role. A role nothing routes through is a comment rather than a
rule, and the change below would have moved three figures and left eight where
they were — which is how the scale drifted the first time.

With all of them routed, the size is one line: 24px rather than 30. At 30 the
number was the card, and everything that makes it mean something — what it
counts, what it is measured against, which way it moved — sat around it as
small print. It only has to beat the label above it, and it does that
comfortably.

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
Found by the rule two commits back rather than by reading: the day strip set
its captions at 11px directly. Same step as every other caption now.

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
@dzarlax
dzarlax requested a review from a team as a code owner August 11, 2026 16:20
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dzarlax, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f4916f4b-c466-4bc3-a062-0bb3f61d8c6d

📥 Commits

Reviewing files that changed from the base of the PR and between 916fe75 and 9d6fef8.

📒 Files selected for processing (2)
  • src/frontend/eslint.config.js
  • src/frontend/src/components/widgets/metric-views/section-metric-index.tsx
📝 Walkthrough

Walkthrough

The frontend adds a shared typography scale and ESLint rules for consistent text and semantic colors. Portal views, dashboard widgets, metric views, and screens replace local styling with shared roles and standard utility classes.

Changes

Typography standardization

Layer / File(s) Summary
Typography scale and lint rules
src/frontend/eslint.config.js, src/frontend/src/lib/type-scale.ts
Adds shared typography roles and TSX lint rules for text sizes, grey utilities, and raw palette colors.
Portal typography and color updates
src/frontend/src/components/portal/...
Standardizes portal headings, figures, labels, badges, subtitles, chevrons, and warning colors.
Dashboard and metric widget updates
src/frontend/src/components/widgets/dashboard/..., src/frontend/src/components/widgets/metric-views/...
Replaces local widget typography with shared roles and semantic muted colors.
Screen and supporting widget typography
src/frontend/src/components/metric-evidence-dialog.tsx, src/frontend/src/components/sidebar-settings.tsx, src/frontend/src/components/widgets/metric-help-tooltip.tsx, src/frontend/src/components/widgets/period-selector-bar.tsx, src/frontend/src/screens/...
Normalizes text sizes and weights across supporting components and screens.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: aleksdotbar, hello1101n

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the frontend typography, color, and lint-rule changes described in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/frontend/src/lib/type-scale.ts (1)

30-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the role count in the module documentation.

type-scale.ts exports seven roles, not five.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/lib/type-scale.ts` around lines 30 - 66, Update the module
documentation in type-scale.ts to state that the type scale exports seven roles,
keeping the existing role definitions and styling unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/eslint.config.js`:
- Around line 109-121: Update the text utility selectors in the ESLint
configuration to include `:` alongside whitespace in their matching prefixes,
ensuring variant-prefixed utilities are checked. Expand the raw-palette selector
to cover every disallowed color and both unnumbered `text-black` and
`text-white`, while preserving the existing rule messages.

In `@src/frontend/src/components/widgets/metric-views/section-metric-index.tsx`:
- Line 98: Update the value container in the section metric view so its dd
retains only layout classes, apply the shared figure or foreground role to the
current metric value, and apply TEXT_LABEL exclusively to the median cell.

In `@src/frontend/src/lib/type-scale.ts`:
- Line 2: Update the documentation comment in the type-scale module to
accurately state that it provides seven text roles, or remove the role count
while preserving the description.

---

Nitpick comments:
In `@src/frontend/src/lib/type-scale.ts`:
- Around line 30-66: Update the module documentation in type-scale.ts to state
that the type scale exports seven roles, keeping the existing role definitions
and styling unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f5ff6aa9-535e-4187-82cf-714a3d2e7752

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb8ea5 and 916fe75.

📒 Files selected for processing (38)
  • src/frontend/eslint.config.js
  • src/frontend/src/components/metric-evidence-dialog.tsx
  • src/frontend/src/components/portal/ai-cost-view.tsx
  • src/frontend/src/components/portal/attention-list.tsx
  • src/frontend/src/components/portal/context-pane.test.tsx
  • src/frontend/src/components/portal/context-pane.tsx
  • src/frontend/src/components/portal/domain-lens-view.tsx
  • src/frontend/src/components/portal/employees-view.tsx
  • src/frontend/src/components/portal/manage-view.tsx
  • src/frontend/src/components/portal/metric-groups-view.tsx
  • src/frontend/src/components/portal/person-header.tsx
  • src/frontend/src/components/portal/single-group-view.tsx
  • src/frontend/src/components/portal/team-state-view.tsx
  • src/frontend/src/components/portal/zone-content.tsx
  • src/frontend/src/components/sidebar-settings.tsx
  • src/frontend/src/components/widgets/dashboard/dashboard-header.tsx
  • src/frontend/src/components/widgets/dashboard/ic-needs-attention.tsx
  • src/frontend/src/components/widgets/dashboard/kpi-tile.tsx
  • src/frontend/src/components/widgets/dashboard/members-grid.tsx
  • src/frontend/src/components/widgets/dashboard/metric-sublabel.tsx
  • src/frontend/src/components/widgets/dashboard/person-coverage.tsx
  • src/frontend/src/components/widgets/dashboard/sparkline.tsx
  • src/frontend/src/components/widgets/dashboard/team-members-attention.tsx
  • src/frontend/src/components/widgets/metric-help-tooltip.tsx
  • src/frontend/src/components/widgets/metric-views/metric-activity.tsx
  • src/frontend/src/components/widgets/metric-views/metric-group-card.tsx
  • src/frontend/src/components/widgets/metric-views/metric-summary-card.tsx
  • src/frontend/src/components/widgets/metric-views/peer-comparison.tsx
  • src/frontend/src/components/widgets/metric-views/peer-mark.tsx
  • src/frontend/src/components/widgets/metric-views/peer-story.tsx
  • src/frontend/src/components/widgets/metric-views/section-metric-index.tsx
  • src/frontend/src/components/widgets/metric-views/team-metric-group-card.tsx
  • src/frontend/src/components/widgets/period-selector-bar.tsx
  • src/frontend/src/lib/type-scale.ts
  • src/frontend/src/screens/metric-definitions.tsx
  • src/frontend/src/screens/metrics-console.tsx
  • src/frontend/src/screens/query-console.tsx
  • src/frontend/src/screens/whats-new.tsx

Comment thread src/frontend/eslint.config.js Outdated
Comment thread src/frontend/src/components/widgets/metric-views/section-metric-index.tsx Outdated
@@ -0,0 +1,66 @@
/**
* The five text roles a person-facing screen is allowed to use.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the documented role count.

Line 2 says that the module has five text roles. The module exports seven roles. Change the count to seven, or remove the count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/lib/type-scale.ts` at line 2, Update the documentation
comment in the type-scale module to accurately state that it provides seven text
roles, or remove the role count while preserving the description.

…a value

Two findings, and the first one lands on the commit that claimed to have
checked this.

The selectors anchored on `(^|\s)`, so a variant prefix sitting directly
against the utility walked straight past them: `hover:text-red-500`,
`sm:text-[13px]` and `focus:text-muted-foreground/50` were all invisible to the
rule. The palette list also held only the handful of families that happened to
be in the tree, and `text-black` and `text-white` carry no number so they were
missed for that reason alone.

The earlier commit said a guard that silently matches nothing is worse than no
guard, because it is believed — and then verified itself with a single probe
that had no variant on it. The probe now covers each bypass and a legitimate
line, so it demonstrates both halves: what the rule catches and what it leaves
alone.

Second: the metric index put the label role on the whole value container, which
greyed a person's own value along with its median. The subject of the row and
the context around it read the same, which is the one distinction this scale
exists to make. The container keeps layout, the value takes ink, and only the
median stays grey.

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
@dzarlax
dzarlax added this pull request to the merge queue Aug 11, 2026
Merged via the queue into constructorfabric:main with commit 94cbbdd Aug 11, 2026
53 checks passed
@dzarlax
dzarlax deleted the feat/typography-scale branch August 11, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants