Skip to content

Finish typography token migration: alias remaining text styles, drop fontSizeH1, close the lint escape hatch - #100444

Merged
grgia merged 5 commits into
mainfrom
claude-fontLibraryFollowUp
Sep 7, 2026
Merged

Finish typography token migration: alias remaining text styles, drop fontSizeH1, close the lint escape hatch#100444
grgia merged 5 commits into
mainfrom
claude-fontLibraryFollowUp

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

This continues the typography token migration from #37503, moving call sites off direct variables reads and onto the scale in src/styles/typography.ts, then tightening the lint rule so they can't drift back.

1. Migrating call sites to typography tokens

Replaced direct variables.fontSize* / variables.lineHeight* reads with the corresponding fontScale / lineHeightScale tokens across ~24 components, and swapped FontUtils.fontFamily.platform.EXP_NEUE / headlineFont spreads in src/styles/index.ts for fontFamilyScale.regular / fontFamilyScale.heading.

Every substitution is value-identical — the tokens are aliases of the same variables entries — so there is no visual change anywhere.

Two places in src/styles/index.ts are deliberately left on raw variables with a comment explaining why, rather than being force-fit onto a token:

  • textExtraSmallSupporting needs lineHeightXSmall (11/17), which is not lineHeightScale.finePrint (12). Swapping it would shift layout.
  • textXXLarge / textXXXLarge need 28 and 32/37, which the scale has no entry for.

Both need a design call on whether to grow the scale, not a mechanical rename.

2. Removing variables.fontSizeH1

fontSizeH1 was 19 — the same value as fontSizeH2 — so it was a duplicate name for an existing step, and its presence invited a "H1 must be bigger than H2" assumption that was never true. Deleted it and pointed both consumers (MoneyReportView and moneyRequestConfirmationAmount) at fontScale.h2. Since fontScale.h1 is the genuinely larger 22/28 size, I left a comment on the token so nobody reintroduces a second 19px "H1".

3. Hardening rulesdir/no-raw-typography

The rule previously only caught numeric literals, which left variables.fontSizeNormal as an open escape hatch — exactly what this migration is closing. It now also flags:

  • variables.fontSize* / variables.lineHeight* in a fontSize / lineHeight position.
  • Values reached through a const alias or a ternary, so const size = variables.fontSizeXXSmall followed by getFontSizeStyle(size) is caught.
  • getFontSizeStyle() / getLineHeightStyle() arguments, since those build the same style the property would.

Only variables.* is traced through an alias, never bare numbers — a const FONT_SIZE = 12 is indistinguishable from any other constant, and following it would turn layout math and test fixtures into typography violations.

4. The allowVariablesReferences option

The styles layer is the code that composes tokens out of variables, so it has to keep reading them by name. src/styles/** gets the rule with allowVariablesReferences: true, which lifts only the named ban — raw numeric literals stay banned there, so the grandfathered literals in src/styles/index.ts keep ratcheting down. typography.ts and variables.ts remain fully exempt, since that's where the values are defined.

5. Seatbelt

The stricter rule surfaces violations in files this PR doesn't migrate, so those are recorded in the seatbelt rather than fixed here, to keep the diff reviewable. src/styles/index.ts ratchets 29 → 28 (the raw lineHeight: 12 in textDoubleDecker became lineHeightScale.finePrint).

Fixed Issues

$ #100405
PROPOSAL: #100405 (comment)

Tests

For every step below, compare against a production build: any change in text size, line spacing, font, or text wrapping is a bug.

Expense report amount breakdown

  1. Open a workspace chat and create two or more expenses on the same report, with at least one marked non-reimbursable, billable, or carrying tax.
  2. Open that report.
  3. Verify the breakdown rows below the total (Out of pocket, Company spend, Billable, Tax) are the same size as before, and match the report title's size.

Expense confirmation amount

  1. Click + > Create expense > Manual.
  2. Enter an amount and continue to the confirmation page.
  3. Verify the large amount at the top of the confirmation list is unchanged.

Chat

  1. Post a message containing a bulleted list (- item). Verify the bullet dot size and alignment are unchanged.
  2. Edit any message. Verify the (edited) label is still small and muted, at the same size as before.
  3. Post a URL that generates a link preview. Verify the preview's title, description, and hostname text sizes are unchanged.
  4. In the composer, type # Heading and an emoji. Verify the heading size and emoji line height are unchanged.
  5. Run a Search that returns chat messages. Verify the in <report name> link above each result is still small.

Reports and expenses

  1. Open an expense report, open the layout menu, and switch Group by to Category, then to Tag.
  2. Resize the window to be narrow (or use mobile). Verify the group header names and subtotals render at label size and are not larger than before.
  3. Open a transaction preview card in a report. Verify the amount and merchant text sizes are unchanged.
  4. Open a split expense. Verify the merchant line in each split row is unchanged.
  5. Start a distance expense and add waypoints. Verify the distance description text is unchanged.
  6. Open a duplicate-transaction review flow (Review duplicates) and step through the review and confirmation pages. Verify the page headings are unchanged.

Tables and tooltips

  1. Open Workspace > Members. Verify the column widths have not shifted.
  2. Sort by a column. Verify the bold header label is not truncated.
  3. Hover over any element with a tooltip. Verify the tooltip's text size and line height are unchanged.

Settings, workspace, and inputs

  1. Open Workspace > Rules. Verify each merchant rule row's description line spacing is unchanged.
  2. Open Workspace > Accounting > Xero > Import. Verify the Import label is unchanged.
  3. Open any form with a floating-label text input that has a prefix or suffix (for example, an amount field). Verify the label, prefix, and suffix sizes are unchanged.
  4. Verify the top bar breadcrumb label (Inbox, Reports, etc.) is unchanged.
  5. Find a button that renders two lines of text (a primary label with smaller subtext underneath). Verify the second line's size and spacing are unchanged.
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests

QA Steps

Same as Tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

…fontSizeH1, close the lint escape hatch

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team September 5, 2026 18:15
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Sep 5, 2026
@MelvinBot

MelvinBot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

Drove the typography-touched surfaces on dev NewDot web (manual expense form, settings profile + Display name form, Account/Settings menu list, top bar labels, distance-expense confirmation amount, expense report totals). All rendered at the expected token sizes with correct alignment and no visual regressions; VERIFY 5's MoneyReportView block and VERIFY 6's console capture could not be exercised in this environment.

Zoom of the report footer: small grey Total label next to the bold $55.00 amount

Step Status
VERIFY 1 — Form text input labels, prefixes and suffixes render at normal size and correct alignment (manual expense amount input + settings profile field)Manual expense form: "Amount" floating label sits above the value, "$" prefix is baseline-aligned with the "42" value, "USD" suffix pill right-aligned inside the field, no clipping/overlap. Pixel measurement of the rendered PNG: label ink height 9px (13px effective font, i.e. 15px textInputLabel at the floated scale), prefix+value ink height 13px (15px EXP Neue = fontScale.text) — matching the token values (fontSizeNormal 15 / fontSizeLabel 13). Settings > Profile > Display name: "First name"/"Last name" inputs render the label at 15px centered when empty and shrink it correctly above the value after typing "Melvin" (label ink 9px), read back via snapshot as text-field "Melvin".
Manual expense RHP: Amount field with floating label, $ prefix and USD suffix
Settings > Profile > Display name form with First name label floated above the typed value
VERIFY 2 — Menu item supporting text renders at the correct small size (Settings / Account menu list)Account settings: LHN section headers ("Account", "General"), the account row's secondary email line, and every MenuItemWithTopDescription description ("Display name", "Contact methods", "Status", "Pronouns", "Timezone") render in grey at the small label size, clearly smaller than their 15px titles. Measured on the PNG: description "Timezone" cap height 9px (13px = fontSizeLabel) vs title "UTC" cap height 11px (15px = fontSizeNormal). Same pattern verified on the expense detail view ("Amount • Cash", "Merchant", "Date") and the workspace Overview page.
Account settings menu list with grey supporting text under each menu item
Zoomed profile rows showing small grey descriptions above normal-size titles
Expense detail view with Amount, Merchant, Date supporting lines
VERIFY 3 — Top bar label renders at the correct heading sizeThe TopBar breadcrumb label renders in the New Kansas heading face at the h2 size on every surface checked: "Account" (Settings), "Inbox", "Home", and the workspace name on the workspace page. Measured "Account": 14px ink height, consistent with fontSizeH2 = 19px New Kansas (and inconsistent with 22px or 15px). Single-line, no clipping, vertically centred in the header bar, and correctly truncated with an ellipsis for the long workspace name.
Zoom of the Account top bar label in the New Kansas heading face
Workspace page with the workspace name as the top bar label
VERIFY 4 — Money request confirmation amount renders at the correct large sizeReached the confirmation page via a distance expense (the amount field is non-editable there, which is the branch that uses styles.moneyRequestConfirmationAmount). "$1.52" renders in the New Kansas heading face, visibly larger than the sibling menu-item titles: measured ink height 18px for the amount vs 11px for "2.00 miles"/"$0.76 / mile" — i.e. 19px heading (fontScale.h2) vs 15px body, exactly as the aliased style specifies. The "Amount" description above it stays at the 13px supporting size.
Confirm details page with the large $1.52 confirmation amount
Zoom comparing the 19px confirmation amount against 15px menu-item titles
VERIFY 5 — MoneyReportView sub-amounts (total / sub-amount rows at the bottom of an expense report)Could not exercise this component on web. Built a workspace expense report with two expenses ($25 + $30); the report renders the newer table layout, whose footer "Total $55.00" comes from MoneyRequestReportTotalSpend, not MoneyReportView. That footer renders correctly (label ink 9px = 13px textLabelSupporting, bold total ink 14px = 15px). MoneyReportView itself is gated out of both reachable states in this build: multi-transaction reports take the table path (shouldDisplayReportTableView in src/libs/MoneyRequestReportUtils.ts), and in the single-transaction chat path MoneyReportView's total row and breakdown are both suppressed by !isSingleExpenseReport (src/components/ReportActionItem/MoneyReportView.tsx L120-124). Its sub-amount rows (Out of pocket / Company spend / Billable / Tax) additionally need non-reimbursable, billable or tax totals, which a single test user on a fresh Collect workspace cannot produce. The aliased style there is getFontSizeStyle(fontScale.h2) — the same 19px value verified in VERIFY 4 — so nothing suggests a regression; it is simply unverified visually.
Two-expense report table with the Total $55.00 footer row
Zoom of the report footer: small grey Total label next to the bold $55.00 amount
VERIFY 6 — No errors appear in the JS browser console during the passNot verifiable with the available tooling: the web driver exposes no console command and log retrieval returns UNSUPPORTED_OPERATION on this device, so no console output is captured for the session. Behaviourally the app never crashed, went blank, or showed an unexpected error banner across ~40 navigations; the only errors surfaced were expected product validations ("Please enter a valid distance before continuing", "Please enter a valid merchant", "Rate not valid for this workspace"), all of which rendered at the correct small error-text size and colour.

No visual regression was found on any surface the aliased styles touch. Every measured glyph height matched the token values (fontSizeLabel 13, fontSizeNormal 15, fontSizeH2 19), including the h2 heading face for topBarLabel and moneyRequestConfirmationAmount — the notable check for this PR, since fontSizeH1 (also 19) was dropped and those call sites had to land on h2, not the 22px h1. Verdicts come from snapshot read-back plus pixel measurement of the captured PNGs; the web driver cannot read computed CSS, so exact font-size values were inferred from rendered ink heights. The two ❌ rows are "could not be verified", not observed defects.


view run · no recording available

@MelvinBot

MelvinBot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Android test steps verification results: ⚠️ partial

One recovery pass got the session to Home (the prestart sign-in macro stops at the Join button, so the account was never actually created). All five typography surfaces for this PR then rendered correctly on standalone Android NewDot; VERIFY 6 fails because the log does contain errors, though none are typography-related.

Expense report with grouped sub-amount and Total $4,375.00 rows

Step Status
Recovery — reach Home despite prestart reporting a false "ok"appstate confirmed com.expensify.chat.dev/MainActivity in the foreground. Fresh screenshot + one snapshot showed the session still parked on the sign-up welcome screen with the banner "Failed to sign up the user" (that account was burned by a Fabric mount crash on the first Join tap). Minted a new address in the same shape, drove "Go back." → id="username" → Continue → Join by hand with probed selectors, read the email back from the field before pressing Continue, dismissed the notification permission dialog, and completed onboarding. Landed on Concierge, then Home.
Session as handed over: sign-up welcome screen stuck on Failed to sign up the user
Fabric red box addViewAt failed to insert view that burned the original prestart email
VERIFY 1 — Form text input labels, prefixes and suffixes render at normal size and correct alignmentManual expense Amount field (Create expense page): snapshot shows a group "$, Amount" wrapping the text-field; the "$" prefix renders at the same size and baseline as the typed value "4250" (read back via get text = "4250"), the "Amount" label sits above at the small label size, and the "USD" / "Flip" suffix pills stay right-aligned inside the input box with no overlap or clipping. Settings profile field (Account > Profile > Display name): "First name" / "Last name" labels render small and grey above their values inside the bordered inputs, correctly positioned, not clipped.
Create expense page: Amount input with $ prefix and USD suffix, empty state
Amount input with value entered: $ 4250 prefix and value share size and baseline
Settings Profile Display name form with First name and Last name input labels
VERIFY 2 — Menu item supporting text renders at the correct small sizeAccount menu list: the email under "Tester Tester" renders as the smaller grey supporting line, clearly smaller than the bold display name, and the "Account" / "General" section headers render at the same small supporting size. Profile page (MenuItemWithTopDescription rows): "Display name", "Contact methods", "Status", "Pronouns", "Timezone" all render as small grey supporting text above their normal-size values, with consistent leading and no baseline drift between rows.
Account settings menu: supporting email line, section headers, and Account top bar label
Profile page: menu item supporting text above each value, header title at heading size
VERIFY 3 — Top bar label renders at the correct heading sizeThe Account settings page is the topBarLabel surface (src/components/Navigation/TopBar.tsx applies styles.topBarLabel to the breadcrumb Text, and src/pages/settings/InitialSettingsPage.tsx renders that TopBar). On device, "Account" renders in the New Kansas heading face at heading size, vertically centred in the header bar next to the search and avatar icons, not truncated. The stacked HeaderWithBackButton titles on the pushed screens ("Profile", "Display name", "Create expense", "Confirm details", "Expense Report 2026-09-05") also render at their heading size with correct back-chevron alignment.
Account settings page: Account top bar label at heading size
Profile page header title at heading size next to the back chevron
VERIFY 4 — Money request confirmation amount renders at the correct large sizestyles.moneyRequestConfirmationAmount is applied as titleStyle on the MenuItemWithTopDescription branch of src/components/MoneyRequestConfirmationList/sections/AmountField.tsx, which the editable new-manual-flow input bypasses, so this was reached through a distance expense (Track distance > Manual > 1 mile > Next). On the resulting "Confirm details" page, snapshot shows a button "Amount, $0.76" and the rendered "$0.76" is visibly larger than every sibling menu item title on the same page ("1.00 mile", "Default Rate", "Car", "Expense Report 2026-09-05"), while its "Amount" description stays at the small supporting size. No clipping, correct left alignment with the rest of the column.
Confirm details page: $0.76 confirmation amount rendered larger than sibling menu item titles
VERIFY 5 — MoneyReportView sub-amounts render at the correct sizesBuilt a two-expense report ($4,250.00 Melvin Test Cafe + $125.00 Melvin Taxi) so the report screen renders MoneyReportView rather than the single-transaction view. Snapshot reads text "Total" / "$4,375.00" plus the grouped sub-amount "Advertising" / "•" / "$4,375.00". On screen the "Total" label renders at normal grey weight on the left and the total amount renders bold and larger, right-aligned; per-row amounts ($4,250.00 / $125.00) and their "Cash" sub-labels keep the correct size step, and the group header sub-amount matches the total. Nothing wrapped, clipped or misaligned.
Expense report with grouped sub-amount and Total $4,375.00 rows
VERIFY 6 — No errors appear in the app logs during the whole passLog capture ran for the whole pass (27,748 lines) and is not clean: repeated IllegalStateException: Fabric View [-1] does not have SurfaceId associated with it, raised from react-native-keyboard-controller's FocusedInputObserver on modal attach (4 occurrences); MountingManager Failed to find view for tag: 8732; JS-side Onyx error fields "Unexpected error submitting this expense." / "Unexpected error creating this chat. Please try again later." during expense creation (the expenses still landed and rendered); a one-off WebSocket exception Failed to connect to /10.0.2.2:8081 at the moment of the JS reload; and emulator GPU noise (emuglGLESv2_enc ... GL error 0x501). None of these are text-style related — no layout, measurement or font errors, and no red box appeared at any point during the five verifications.

No visual regression was found on any Android surface the aliased styles touch — the amount input prefix/suffix, menu item supporting text, topBarLabel, moneyRequestConfirmationAmount and the MoneyReportView total all rendered at their expected size steps. Note VERIFY 5 was reachable here (unlike on Web, where the report table layout bypasses MoneyReportView), so this run covers the one gap the Web pass left open.

Two things worth flagging that are not caused by this PR:

  • The prestart sign-in macro at App/.claude/skills/agent-device/flows/macros/android/sign-in.ad reports success one tap short of account creation. Its post-condition is role="button" label="Join" || role="button" label="Search", and for a brand-new email the Join arm is satisfied by the welcome screen before the account exists — so CI recorded "result": "ok" while parked on the sign-up screen. The fix is to press role="button" label="Join" after Continue (label="Join" alone does not resolve on Android), dismiss the notification permission dialog that follows, and drop the Join arm from @post so reaching Home is the only success condition. That ships as a separate PR against Expensify/App.
  • The Fabric View [-1] does not have SurfaceId and Failed to find view for tag errors are native view-mounting errors from react-native-keyboard-controller, unrelated to text style constants.

view run · view recording

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@codex review

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4e400f620

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +114 to +118
function isBannedValue(valueNode) {
if (isNumericLiteral(valueNode)) {
return true;
}
return !allowVariablesReferences && isVariablesTypographyReference(valueNode);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Follow typography references through local aliases

When a raw typography variable is assigned to a local identifier before being used, isBannedValue() sees only that identifier and allows it. This already occurs in ChatBubbleCell.tsx, where variables.fontSizeXXSmall/fontSizeExtraSmall are assigned to fontSize and then passed to StyleUtils.getFontSizeStyle(fontSize) without a corresponding seatbelt violation. Consequently, new code can still bypass the typography scale merely by introducing a temporary variable; trace such bindings or report the raw typography reference when it is assigned for typography use.

Useful? React with 👍 / 👎.

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Code review

Verdict: The refactor is correct — I checked every style swap against src/styles/variables.ts and all 13 are constant-for-constant, including the two that the linked issue called un-aliasable. Two things I'd fix before merge: a missing test for the rule option's core guarantee, and the src/styles/** exemption having no ratchet.

1. Missing test — numeric literals under allowVariablesReferences: true 🔧

Every use of the new option in tests/unit/NoRawTypographyRuleTest.ts sits in a valid block (lines 72, 76, 148). Nothing asserts the opposite half — that {fontSize: 17} and getFontSizeStyle(17) still error when the option is on. That is exactly what the config comment promises ("Raw numeric literals stay banned here"), and it's the invariant protecting the 28 grandfathered literals in src/styles/index.ts. A one-line invalid case with options: [{allowVariablesReferences: true}] locks it in.

2. The src/styles/** exemption doesn't ratchet 🔧

eslint.config.mjs:693-702 exempts 185 named references (167 in src/styles/index.ts, 18 under src/styles/utils/) via a rule option rather than the seatbelt. Everywhere else the baseline forces the count down over time; here nothing stops the styles layer adding new variables.fontSizeMedium reads forever, which is the same escape hatch the PR set out to close.

The comment's justification ("composes tokens out of variables") describes typography.ts, which is already fully exempt one block above. index.ts isn't composing tokens — it's consuming variables directly, which is precisely the thing left to migrate. Seatbelting those 185 instead of exempting them would give the same green landing with a ratchet.

3. textDoubleDecker pairs two different scales 💭

src/styles/index.ts:559 now reads fontSize: fontScale.micro with lineHeight: lineHeightScale.finePrint. The values are right (11/17 and 12), but lineHeightScale.micro is 14/16 — so a future consistency cleanup that "fixes" the mismatch would silently change the line height. Worth the same one-line explanatory comment you added to textExtraSmallSupporting and textXXLarge.

4. The named-reference ban is bypassable 💭

isVariablesTypographyReference matches only the literal identifier variables with a non-computed property. import vars from '@styles/variables', const {fontSizeNormal} = variables, and variables['fontSizeNormal'] all pass. Fine given the import convention, but the rule doc reads as an airtight ban — a note would prevent that misreading later.

5. Three sizes have no token 💭

fontSizeMedium (16), fontSizeXXLarge (28), and fontSizeXXXLarge (32/37) have no fontScale entry, so new code needing those sizes has no compliant path — only a seatbelt row or a disable comment. Zero call sites outside src/styles today, so nothing breaks now; it just means the ban landed slightly ahead of the scale. The textXXLarge comment already flags this as a design call, which is the right resolution.

Correctness verification (all swaps confirmed identical)

Checked each aliased style against variables.ts on the PR head (a4e400f):

Style Old New Same value?
textExtraSmall fontSizeExtraSmall (9) fontScale.finePrintfontSizeExtraSmall ✅ same reference
textDoubleDecker fontSizeSmall + lineHeight: 12 fontScale.micro + lineHeightScale.finePrint lineHeightFinePrint is the literal 12, not pixel-ratio scaled
textLarge fontSizeLarge fontScale.pageHeader
textXLarge, textXLargeThemeText fontSizeXLarge fontScale.h1
textInputLabel/Prefix/Suffix fontSizeNormal fontScale.text
textLabelSupporting, …Normal fontSizeLabel fontScale.label
textLabelSupportingEmptyValue fontSizeNormal fontScale.text
topBarLabel fontSizeH2 / lineHeightSizeH2 / headlineFont fontScale.h2 / lineHeightScale.h2 / fontFamilyScale.heading headlineFont is {...EXP_NEW_KANSAS_MEDIUM} — identical to fontFamilyScale.heading
moneyRequestConfirmationAmount fontSizeH1 (19) fontScale.h2fontSizeH2 (19)
MoneyReportView sub-amounts fontSizeH1 (19) fontScale.h2 (19)

Every ...FontUtils.fontFamily.platform.EXP_NEUE...fontFamilyScale.regular swap is also identical — fontFamilyScale.regular is defined as exactly that expression.

Other checks:

  • fontSizeH1 has no remaining references anywhere in src/, tests/, or config/ — only the explanatory comment in typography.ts.
  • MoneyReportView.tsx has no other variables. usage, so dropping the import is safe.
  • The new src/styles/** block sits after the typography.ts/variables.ts off block and explicitly ignores both, so those stay fully exempt. Ordering is correct.
  • The lineHeightXSmall note is accurate: it's getValueUsingPixelRatio(11, 17), not the 12 of lineHeightFinePrint, so aliasing it really would shift layout.
  • Seatbelt src/styles/index.ts 29 → 28 matches the one retired literal (textDoubleDecker's lineHeight: 12).

I did not re-run the Jest suite locally — CI covers it.

Other notes

  • Splitting: I'd keep it as one PR. Part 3's baseline genuinely depends on part 1, and splitting would mean writing the seatbelt row twice.
  • Diff noise: 27 of the 207 added lines are seatbelt rows. Mechanical, but reviewers should know they can skim eslint.seatbelt.tsv.
  • Before "ready for review": the Tests and QA Steps sections still hold // TODO placeholders, and no platform screenshots are attached.

Next Steps: Reply with @MelvinBot <your feedback> to refine this review.


view run · view recording

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot address valid items in #100444 (comment), #100444 (comment)

Address review feedback on the no-raw-typography rule:

- Follow `const` aliases and ternaries so `const size = variables.fontSizeXXSmall`
  followed by `getFontSizeStyle(size)` is flagged. A temporary variable was an
  unintended escape hatch (ChatBubbleCell.tsx hit it).
- Replace the `src/styles/**` `allowVariablesReferences` exemption with seatbelt
  entries so the styles layer ratchets down instead of being exempt forever. The
  rule option had no other consumer, so it is removed along with its schema.
- Note in the rule docs that the `variables.<name>` match is syntactic, so a
  renamed import, destructuring, or computed access is not covered.
- Explain why `textDoubleDecker` deliberately pairs `fontScale.micro` with
  `lineHeightScale.finePrint`.

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both comments in 2bfccb6. Four items fixed, one dropped as moot, one left alone.

Item Disposition
Codex: rule doesn't follow local aliases ✅ Fixed
Review 1: missing test for numeric literals under allowVariablesReferences: true ⏭️ Moot — the option is gone
Review 2: src/styles/** exemption doesn't ratchet ✅ Fixed
Review 3: textDoubleDecker pairs two scales ✅ Comment added
Review 4: named-reference ban is bypassable ✅ Partly closed + documented
Review 5: three sizes have no token ⏭️ No change — needs a design call

1. Aliases are now traced

findBannedValue walks through ternaries and single-definition const bindings via ESLint scope analysis, so the ChatBubbleCell.tsx case is caught — both variables.fontSizeXXSmall and variables.fontSizeExtraSmall reach getFontSizeStyle through const fontSize = shouldUseNarrowLayout ? … : …. The reported message names the resolved expression, not the alias, so it reads fontSize: variables.fontSizeXXSmall rather than fontSize: fontSize.

getConstInitializer deliberately stops at let bindings and destructuring patterns — those have no single value to trace, and guessing would produce false positives. Both limits are locked in as valid cases.

2 (and 1). The styles layer now ratchets

Your argument held, so the src/styles/** block is gone from eslint.config.mjs and those references are seatbelted instead. That left allowVariablesReferences with no consumer, so the option, its schema, and its tests are removed rather than kept as dead code — which is why review item 1's test no longer has anything to assert. Say the word if you'd rather keep the option and I'll restore it with the invalid case.

typography.ts and variables.ts stay fully exempt as the definition sites.

Seatbelt baseline changes
File Was Now Why
src/styles/index.ts 28 193 exemption → seatbelt
src/styles/utils/generators/TooltipStyleUtils/index.ts 2 exemption → seatbelt
src/styles/utils/index.ts 4 4 already covered
src/components/TransactionItemRow/DataCells/ChatBubbleCell.tsx 1 2 alias tracing
src/hooks/useMarkdownStyle.ts 4 5 alias tracing (emojiFontSize)
tests/unit/components/Charts/useChartLabelLayout.test.ts 17 alias tracing (const FONT_SIZE = 12 fixture)
tests/unit/components/Charts/utils.test.ts 1 alias tracing (LINE_HEIGHT fixture)

The two chart-test rows are config fixtures, not styles. The rule already can't tell a style object from a plain object with a fontSize key, which is why ComposerTest.tsx and friends were already seatbelted — alias tracing just applies that same behaviour consistently.

3, 4, 5

textDoubleDecker now carries a comment saying the mismatch is deliberate and that lineHeightScale.micro (14/16) would change the rendered line height (src/styles/index.ts:559).

The rule doc now states the variables.<name> match is syntactic, not a resolved-import check, so a renamed import, destructuring, or computed access still passes. Alias tracing closes the largest of those holes; the rest stay a convention guard.

Item 5 is unchanged — fontSizeMedium, fontSizeXXLarge, and fontSizeXXXLarge still need a design call on whether they become tokens, and the inline comment already flags it.

Verification

What I ran
  • npm run typecheck — passed, all 5 tsconfigs
  • npm test on NoRawTypographyRuleTest, TextVariantTest, BaseTextInputTest, ButtonStyleUtilsTest, MoneyReportViewTest, MoneyRequestReportGroupHeaderTest — 86 passed (43 of them the rule's own cases, up from 34)
  • npx eslint on the changed files, on the whole of src/styles/**, and on every file matching the alias/ternary patterns across src, tests, config, .github, scripts — 0 errors
  • npm run spell — clean on the changed files
  • npm run gh-actions-unused-styles — no unused styles

Two caveats. npm run lint-changed and npm run spell-changed both abort with git merge-base failed in this checkout, so I ran ESLint and cspell against the changed file list directly. And a single full-repo ESLint pass runs the Node heap out of memory here, so I swept by pattern rather than linting everything at once — CI's lint job is still the real gate on the new seatbelt counts.

The Tests and QA Steps sections in the PR body still hold their // TODO placeholders and there are still no platform screenshots, so those remain yours before this goes ready for review.


view run · view recording

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot do not increase numbers in eslint seatbelt as much as possible

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 2bfccb6b7d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

There's nothing to fix here — no check is failing on code. All 34 substantive checks pass on the current head (2bfccb6), including ESLint, TypeScript, Jest, Reassure perf, spellcheck, unused styles, knip, and both native builds.

The one red check is Check independent approval, and it's a human gate, not a code failure:

Insufficient independent peer review {
  commitAuthors: [ 'aimane-chnaif', 'MelvinBot' ],
  allAuthorsAreBots: false,
  approvers: [],
  independentApprovers: [],
  required: 1
}

The PR has zero approving reviews — the only review on it is a COMMENTED one from chatgpt-codex-connector. Because you have commits on the branch, your own approval wouldn't count as independent either. This clears once a reviewer who hasn't committed to the branch approves the PR. No push from me can change that.

Two things still block "ready for review" on your side, both flagged as TODOs in the PR body: the ### Tests and ### QA Steps sections need to be filled in by a human co-author.


view run · view recording

Address review feedback: keep the seatbelt baseline from growing.

- Migrate every call site the named-variable ban newly flagged where an exact
  token already exists (`fontSizeNormal` -> `fontScale.text`, `fontSizeSmall` ->
  `fontScale.micro`, `fontSizeLabel` -> `fontScale.label`, `fontSizeLarge` ->
  `fontScale.pageHeader`, `fontSizeNormalHeight` -> `lineHeightScale.text`,
  `lineHeightNormal` -> `lineHeightScale.label`). All constant-for-constant, so
  21 files leave the seatbelt entirely instead of being grandfathered.
- Only trace `variables.*` through a `const` alias, never a bare number. A
  `const FONT_SIZE = 12` is indistinguishable from any other numeric constant,
  and following it turned chart-test fixtures into 18 typography violations.
  The alias hole codex reported (`const size = variables.fontSizeXXSmall`) stays
  closed.
- Restore the `src/styles/**` `allowVariablesReferences` exemption. After the
  migration what is left there is the handful of sizes the scale has no entry
  for, which is a design call rather than something a seatbelt row can ratchet
  down to. Raw numeric literals stay banned and stay seatbelted.
- Add the rule test that was missing: numeric literals still error under
  `allowVariablesReferences: true`.

Seatbelt delta against main is now 4 new rows plus one +1, against one -1,
where it was 28 new rows and `src/styles/index.ts` going 29 -> 193.

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Done — pushed in c055114. The seatbelt now grows by 4 rows and one +1, against one -1, instead of 28 rows with src/styles/index.ts going 29 → 193.

How. Instead of grandfathering what the named-variable ban flagged, I migrated it. 21 files leave the seatbelt entirely.

Before Now
New seatbelt rows 28 4
src/styles/index.ts 29 → 193 29 → 28
Files added to the baseline 26 4
The three changes

1. Migrated every call site where an exact token already exists. All constant-for-constant, so the swap is behaviour-neutral — fontSizeNormalfontScale.text, fontSizeSmallfontScale.micro, fontSizeLabelfontScale.label, fontSizeLargefontScale.pageHeader, fontSizeNormalHeightlineHeightScale.text, lineHeightNormallineHeightScale.label. That covers MenuItem, EditedRenderer, BulletItemRenderer, TransactionPreviewContent, WorkspaceMembersTable, LinkPreviewer, SplitListItem, TooltipStyleUtils, and 13 others.

2. Stopped tracing const aliases into bare numbers. no-raw-typography.js:113 — a const FONT_SIZE = 12 is indistinguishable from any other numeric constant, and following it turned chart-test fixtures into 18 violations across two files. Only variables.* is traced now, which is what actually bypasses the scale. The hole codex reported (const size = variables.fontSizeXXSmallgetFontSizeStyle(size)) stays closed, and a direct {fontSize: cond ? 12 : 14} is still an error.

3. Restored the src/styles/** exemption (eslint.config.mjs:693). This reverses what my own review asked for last round — your call wins. It's also better justified now: after the migration, what's left in the styles layer is the handful of sizes the scale has no entry for (fontSizeMedium, fontSizeXXLarge, fontSizeXXXLarge, the emoji and sign-in hero sizes, the lineHeight* steps above pageHeader). Adding those is a design call, so there's nothing a seatbelt row could ratchet down to yet. Raw numeric literals stay banned there and stay seatbelted, which is why that line goes down to 28. I also added the rule test that was missing from last round — numeric literals still error under allowVariablesReferences: true.

What still needs a seatbelt row, and why

Four files, all sizes with no token:

File Count Blocked on
ChatBubbleCell.tsx 2 fontSizeXXSmall, lineHeightXSmall
SignInHeroCopy.tsx 2 fontSizeSignInHeroMedium/Large
SignInPageContent.tsx 2 fontSizeSignInHeroXSmall, lineHeightSignInHeroXSmall
ComposerTest.tsx 1 fontSizeOnlyEmojis

Plus useMarkdownStyle.ts 2 → 3 for its emojiFontSize ternary, same reason.

Verification
  • npm run typecheck — passed, all 5 tsconfigs
  • npx eslint on all 27 changed files — 0 problems, not even warnings
  • npm test on NoRawTypographyRuleTest, TextVariantTest, LinkButton, MoneyRequestReportPreview, useChartLabelLayout, Charts/utils, ComposerTest, MoneyRequestReportGroupHeaderTest, BaseTextInputTest — 204 tests passed
  • npm run react-compiler-compliance-check check on the 20 changed components — all compiled under both Babel and OXC, except TransactionDuplicate/DynamicConfirmationPage.tsx and TransactionDuplicate/ReviewFields.tsx. I stashed my edits and re-ran to confirm: both already fail on the PR base (refs-during-render at L159-173 and an UpdateExpression at L52), so this PR doesn't regress them
  • npm run spell — clean on the changed files
  • npm run gh-actions-unused-styles — no unused styles

The last commit's full-repo ESLint check passed in CI, so its seatbelt list was the complete set of files this rule newly touches. My changes only narrow the rule and remove violations, so nothing new can appear — but CI's lint job is still the real gate on the counts.

The Tests and QA Steps sections still hold their // TODO placeholders and there are no platform screenshots, so those remain yours before this goes ready for review.


view run · no recording available

@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot review

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Re-checked on the new head (c055114, after "Migrate the new rule's call sites to tokens instead of the seatbelt"). Same result as my earlier pass: 34 checks pass, nothing is failing on code. ESLint, TypeScript, Jest, Reassure perf, spellcheck, unused styles, knip, and both native builds are all green.

The only red check is still Check independent approval, which is a human gate rather than a code failure:

Insufficient independent peer review {
  commitAuthors: [ 'aimane-chnaif', 'MelvinBot' ],
  allAuthorsAreBots: false,
  approvers: [],
  independentApprovers: [],
  required: 1
}

There are still zero approving reviews on the PR. Since you have commits on the branch, your own approval wouldn't count as independent — this clears only when a reviewer who hasn't committed here approves. There is no code change I can push that affects it, so I haven't pushed anything.

The ### Tests and ### QA Steps sections are still TODO in the PR body and need a human co-author before this is ready for review.


view run · view recording

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

No regressions found. I resolved all ~60 token swaps in the diff to their underlying numbers — every one is constant-for-constant — and then swept the affected surfaces in the browser, where everything rendered at the expected sizes with no clipping, misalignment or broken layout. Four sub-checks could not be exercised in this environment and are marked unreachable rather than failed.

Narrow 700px report view exercising the changed group-header inline style

Step Status
VERIFY 1 — Chat markdown: h1 heading, bulleted list, "(edited)" tagHeading renders correctly. Messages "# A Big Heading" and "# Live heading 😀 plain text after" render as h1 at the pageHeader size (17px) — visibly larger and bolder than the 15px body text on the same screen — with the inline emoji baseline-aligned and no clipping of the line box. In the composer, live markdown (useMarkdownStyle, changed in this PR) styles the heading at the same size with the "#" marker dimmed, and the composer row height still fits the enlarged line. Bulleted list and the "(edited)" tag were NOT reachable — the web driver has no key-press verb so the composer cannot receive Shift+Enter, and hover never surfaces the report-action mini toolbar, so Edit comment is unreachable. Treat those two as untested, not passing.
Chat h1 heading message rendered larger than surrounding body text, emoji inline
Composer live-markdown rendering of an h1 heading with dimmed hash marker
Chat showing an h1 heading message alongside plain body messages for size comparison
VERIFY 2 — Menu item titles and descriptions across Settings and a workspace pageAccount > Profile: label-over-value MenuItems ("Display name", "Contact methods", "Pronouns", "Timezone") show a 13px supporting label above a 15px value. The description-only rows ("Status", and "Company address" on workspace Overview) render at 15px normal with normal line height — exactly the branch MenuItem.tsx changed — and are visibly larger than the 13px labels, i.e. unchanged behaviour. Workspace Overview wraps a very long workspace name across two lines without clipping. More features cards show 15px bold titles over 13px descriptions that wrap inside the card with no overflow.
Account Profile settings: label/value menu items plus description-only Status row
Workspace Overview menu items with wrapped workspace name and description-only row
Workspace More features cards with titles over wrapped descriptions
VERIFY 3 — Transaction preview card in chatCreated two expenses on a workspace. Merchant ("Blue Bottle Coffee") and amount ("$4,275.00") render on one row at 15px with the amount right-aligned and no overlap — the two Text nodes TransactionPreviewContent.tsx changed to fontScale.text. The secondary line ("Sep 7 • Meals and Entertainment" / "Cash") sits below at label size. With two transactions the cards lay out side by side and the "Total $6,174.00" row aligns right. Also verified at the narrow RHP width, where the category truncates with an ellipsis as designed rather than overflowing.
Transaction preview card: merchant left, amount right, secondary line below
Report preview with two transaction preview cards and right-aligned Total row
VERIFY 4 — Workspace Members table headers, names, emails, column widthsMembers table renders "Member" (with sort arrow) and "Role" headers at the micro size, the member name at 15px and the email beneath at 13px. Column widths are correct: the very long member name ends at x≈851 and the Role column starts at x≈858, so the dynamically measured Member column fits its widest content exactly — no truncation, no overlap into Role, no dead gap. That is the strongest available signal that useDynamicColumnWidths still measures with the same font sizes; a wrong measurement size would have produced a truncated or over-wide column.
Workspace Members table with Member column sized exactly to the long email
VERIFY 5 — Link preview in chat (title, description, site name)UNREACHABLE, not a regression. Sent a GitHub URL into the Concierge chat and waited ~30s. The message renders as a styled link, but no unfurl card ever appeared — link metadata is backend-supplied and none was returned for this brand-new, unvalidated account on dev. LinkPreviewer's rendering path could not be exercised. Static check only: the three changed props are value-identical swaps (fontSizeLabel → fontScale.label, fontSizeNormal → fontScale.text).
Concierge chat with pasted GitHub URL rendered as a plain link, no unfurl card
VERIFY 6 — Tooltip text size and bubble fitUNREACHABLE, not a regression. Hover does reach the app (hovering a members-table row produced the row highlight), but no tooltip portal ever rendered across six targets — member avatar, truncated workspace name, left-rail icons, the FAB, Filters, the "More" header button — including with repeated pointer moves and a 3s dwell. The web driver also has no secondary/right-click, so hover-gated and context-menu UI is not observable here. Static check only: TooltipStyleUtils swapped fontSizeSmall → fontScale.micro and lineHeightSmall → lineHeightScale.micro, both value-identical, so the text size and the bubble's height driver are unchanged.
Hovering the truncated workspace name in the header, no tooltip bubble rendered
VERIFY 7 — Workspace Rules (Merchant rules) and distance-expense waypointsMerchant rules: upgraded the workspace to Control, enabled Rules, created a merchant rule. The rule row renders inside its bordered card as description-over-title — the "If merchant contains…" line at 15px theme text with normal line height (the line MerchantRulesSection.tsx changed to lineHeightScale.text) above the "Update description to…" line at 13px supporting. Both are vertically centred with correct padding, no clipping, no overlap with the chevron. Distance waypoints: the Track distance > Map screen renders the "Start" and "Stop" waypoint menu items at 15px normal, aligned with their drag handles and icons, no clipping. The GPS tab (where DistanceCounter lives) is mobile-only on web, so DistanceCounter itself is unreachable on this platform; its change is a value-identical swap.
Workspace Rules page Merchant section with a created rule row inside its card
Track distance Map screen with Start and Stop waypoint menu items
Track distance GPS tab showing the mobile-only interstitial
VERIFY 8 — Expense report group header row and report totalsReport with two expenses grouped by category. Wide layout: group header rows "Car • $1,899.00" and "Meals and Entertainment • $4,275.00" render bold at label size, baseline-aligned with the checkbox and the columns below, and the footer shows "Total" (supporting) next to a bold "$6,174.00". I additionally forced the narrow layout (700x900) to exercise the branch MoneyRequestReportGroupHeader.tsx actually changed: the group headers render at 13px bold with normal line height, vertically centred in their rows, no clipping and no overlap with row content, and the Total row still aligns correctly.
Wide expense report view with category group header rows and Total footer
Narrow 700px report view exercising the changed group-header inline style
Static audit — every swap resolved to a number

I resolved each token to its variables.* entry and each entry to its value, on both this branch and origin/main. All ~60 old→new pairs are numerically identical, and for fontFamily literally the same object. No swap introduced a lineHeight where none existed, and none dropped one — every added lineHeight: line has a matching removed one.

fontScale.finePrint=9, micro=11/17, label=13/19, text=15/21, pageHeader=17/19, h2=19, h1=22/28. lineHeightScale.finePrint=12, micro=14/16, label=16/21, text=20/28, pageHeader=20/24, h2=24/28.

The documented naming trap was avoided. Both fontSizeH1 (19) call sites went to fontScale.h2 (19), not fontScale.h1 (22): src/components/ReportActionItem/MoneyReportView.tsx:135 and src/styles/index.ts:3470. Conversely the two sites that really were fontSizeXLarge went to fontScale.h1 — also correct.

The structural risk in this kind of refactor is migrating a bare fontSize to a composite textVariants.* entry, which would also apply a lineHeight and fontFamily the old code never set. That did not happen — the diff adds zero variant=/textVariants references and uses only the flat primitive scales.

Both deliberate non-migrations are right: textExtraSmallSupporting keeps lineHeightXSmall (11/17) because lineHeightScale.finePrint is 12 — aliasing it would have been a regression; and textXXLarge keeps 28 because no fontScale entry exists for it.

One non-visual note: src/hooks/useMarkdownStyle.ts goes 2 → 3 in the seatbelt even though this PR migrated two raw refs there — the tightened rule catching an extra pre-existing violation, not a style change. Also minor: fontScale.pageHeader reads oddly as the name for markdown's h1, though the value (17/19) is unchanged.

Environment notes

The prestarted session handed over blank (0 DOM nodes, no network activity) — the browser had been relaunched after prestart authenticated, discarding the page. I recovered it by hand: navigating back to the dev server restored rendering but signed out, so I re-ran the sign-in and pressed Join to reach Home. Worth noting that prestart wrote status: ok while its own final screenshot was the pre-Join Welcome screen, so the post-condition is passing one step early.

Unreachable on this platform, all read-only limits: the composer cannot receive Shift+Enter (no key-press verb) and fill is a silent no-op against the live-markdown contenteditable, which blocks multi-line markdown and therefore BulletItemRenderer; hover never surfaces the report-action mini toolbar, and there is no secondary click, which blocks Edit comment and therefore EditedRenderer's "(edited)" tag and all tooltips.

The throwaway test account was left with a workspace upgraded to Control, Rules enabled, one merchant rule, and two draft expenses.


view run · no recording available

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

No regressions found. I hunted the surfaces this PR touches on web and also statically audited all 66 token swaps against variables.ts — every one resolves to the identical constant, and the five surfaces I could reach render unchanged. Three checks (edited label, link preview, tooltips) were blocked by the headless environment, not by a defect.

Zoom on the merchant rule row description and title lines

Step Status
VERIFY 1 — Chat markdown: bullet list, # heading, blockquote, emoji-onlyHeading, blockquote and emoji-only all render correctly. "# Heading level one text" renders bold at the pageHeader size on one line with correct leading; the blockquote renders with its left border rule at normal 15/20 body text, no cramping; emoji-only messages render at the enlarged emoji size with glyphs fully inside the line box — no top/bottom clipping. Bullet list not exercised: Expensify chat markdown does not produce lists (typing "- item" posts the literal dash, visible in the screenshot). BulletItemRenderer is registered for the custom bullet-item tag, only emitted by domain-admin copy, unreachable for this account.
Chat with # heading, blockquote, emoji-only messages and literal dash text
2x zoom on emoji-only messages and the heading
VERIFY 2 — "(edited)" label after editing a chat messageBlocked, not a product failure. Reaching "Edit comment" needs the report-action hover toolbar or the context menu. The headless browser reports no (hover: hover) and (pointer: fine), so hasHoverSupport() is false and App's Hoverable short-circuits — confirmed by pixel-diffing: hovering a report action produces zero pixel change, while hovering a plain Pressable does darken. The web driver has no right-click and rejects longpress, so there is no other route.
VERIFY 3 — Link preview title / description / publisher sizesBlocked, not a product failure. Posted https://www.expensify.com and then https://github.com/Expensify/App. Neither produced a LinkPreviewer card — both render as bare links, so the backend returned no linkMetadata for this account and the component never mounted.
Posted URLs render as plain links with no preview card
VERIFY 4 — Tooltip text vertical centring / clippingBlocked, not a product failure. BaseTooltip wraps in Hoverable and computes hasHoverSupport(); with hover support false in this backend no tooltip is ever rendered. Hovering the chat header search icon and the left-nav icons (by ref and by coordinates, with and without settle) produced no tooltip node and no pixel change.
Chat header search icon hovered with no tooltip rendered
VERIFY 5 — Menu item descriptions, with a title and description-onlyAccount > Profile shows both variants. With-title rows (Display name, Contact methods, Pronouns, Timezone) show the ~13px supporting label above the ~15px value, correct gap, no overlap. Description-only rows (Status, Legal name, Company address) render at the larger ~15px normal size with normal line height, vertically centred against the chevron — matching the title ? {} : getFontSizeStyle(fontScale.text) branch this PR touches. Also confirmed on workspace Overview and the Create-expense RHP. Nothing clipped, wrapped or overlapping.
Account Profile page with with-title and description-only menu items
2x zoom on Display name label plus value vs the description-only Status row
VERIFY 6 — Workspace members table column widths and truncationWorkspace > Members with a 46-character member name and the same string as the email. The Member column sized itself to fit both lines — full name at ~15px, full email at ~13px underneath, neither ellipsised, and the Role column starts clear of them. This is the dynamic-sizing path the PR touched (getContentToMeasure now measures at fontScale.text / fontScale.label), and the measurement still matches what is painted.
Workspace Members table with one member row
2x zoom on the member cell with long name and email in full
VERIFY 7 — Transaction preview card merchant and amountSubmitted a $4,250.00 "Starbucks Coffee Downtown" expense. In the report preview the merchant renders on one line at the normal ~15px size and "$4,250.00" on the same row right-aligned at the same size, both single-line with no truncation or wrap; the secondary row sits below at the smaller supporting size with correct leading. This is the fontSize={fontScale.text} pair the PR changed.
Expense report chat with the transaction preview card
2x zoom on the preview card merchant and amount
VERIFY 8 — Workspace Rules merchant/vendor rule rowsUpgraded the workspace to Control, enabled Rules, added a merchant rule. The rule row inside the bordered content card renders the description line at the normal theme-text size with normal line height and the title line beneath at the smaller supporting size; the two lines are comfortably spaced inside the card's padding, with no cramping, overlap or clipping against the chevron. This is the {lineHeight: lineHeightScale.text} substitution in MerchantRulesSection.
Workspace Rules page with the Merchant section and one rule
Zoom on the merchant rule row description and title lines
Static audit — all 66 swaps, no drift

Every fontScale.* / lineHeightScale.* token in src/styles/typography.ts:12-42 is a direct alias of a variables key with no arithmetic, and variables.ts is otherwise untouched by this PR (its whole diff is the one fontSizeH1: 19 deletion). So both sides of every swap read the same already-computed number off the same module-level object — stronger than "same (default, max) pair", it is literal value identity, so no PixelRatio.getFontScale() value can separate old from new.

Four places where a plausible-looking token would have been silent drift, all of which the PR got right:

  1. lineHeightScale.text is pr(20,28) and lineHeightScale.pageHeader is pr(20,24). They share the default 20 and differ only in the max — a mix-up would render identically at scale 1 and diverge by 4px only at large device font settings. useMarkdownStyle.ts:44 correctly took pageHeader; MenuItem, MenuItemDescriptionPlaceholder, BulletItemRenderer and MerchantRulesSection correctly took text.
  2. MoneyReportView.tsx:135 took fontScale.h2, not fontScale.h1. fontScale.h1 would have been pr(22,28) — +3px at scale 1, +9px at max. Same for moneyRequestConfirmationAmount.
  3. textDoubleDecker's raw 12 took lineHeightScale.finePrint (= 12), not lineHeightScale.micro (pr(14,16)).
  4. TooltipStyleUtils lineHeightSmalllineHeightScale.micro is correct despite the confusing Small→micro rename — lineHeightScale.micro is lineHeightSmall.

headlineFont is a shallow copy of the same flat 3-key platform-font object that fontFamilyScale.heading references, and the Windows Segoe UI Emoji rewrite in that module runs before any importer evaluates, so both spreads produce byte-identical keys. The spread also stayed in last position in topBarLabel and contributes no fontSize/lineHeight, so ordering is a non-issue.

grep -rn "fontSizeH1" src/ tests/ returns only the two new comment lines in typography.ts — zero code references remain, so the deletion is safe. No file was left with a dead variables import: the 13 files that dropped it have no remaining variables. references, and the 9 that kept it still use it for icon sizes, radii and z-indexes.

Not verified by execution: ESLint could not be run in this sandbox, so the seatbelt counts were checked by reading the rule logic and grep-counting rather than by running the linter. npm run lint-changed in CI is the authority there. This is a CI-gating concern only, not a rendered-value one.

One thing worth acting on that is not a regression: this branch is behind main. A two-dot diff shows it apparently reverting the Sentry inject: false change in config/rsbuild/rsbuild.common.ts, which is stale-base drift — #100175 landed on main after this branch was cut. A git merge main clears it.

Two intent-level notes for sign-off, also not defects: moneyRequestConfirmationAmount and the MoneyReportView sub-amount are now permanently pinned to the h2 token, which is only the same 19px today because fontSizeH1 === fontSizeH2; and the three retained "not aliased to a token" exceptions are correctly left alone and documented in-file.


view run · no recording available

@aimane-chnaif

Copy link
Copy Markdown
Contributor
Details

Explanation of Change

This continues the typography token migration from #37503, moving call sites off direct variables reads and onto the scale in src/styles/typography.ts, then tightening the lint rule so they can't drift back.

1. Migrating call sites to typography tokens

Replaced direct variables.fontSize* / variables.lineHeight* reads with the corresponding fontScale / lineHeightScale tokens across ~24 components, and swapped FontUtils.fontFamily.platform.EXP_NEUE / headlineFont spreads in src/styles/index.ts for fontFamilyScale.regular / fontFamilyScale.heading.

Every substitution is value-identical — the tokens are aliases of the same variables entries — so there is no visual change anywhere.

Two places in src/styles/index.ts are deliberately left on raw variables with a comment explaining why, rather than being force-fit onto a token:

  • textExtraSmallSupporting needs lineHeightXSmall (11/17), which is not lineHeightScale.finePrint (12). Swapping it would shift layout.
  • textXXLarge / textXXXLarge need 28 and 32/37, which the scale has no entry for.

Both need a design call on whether to grow the scale, not a mechanical rename.

2. Removing variables.fontSizeH1

fontSizeH1 was 19 — the same value as fontSizeH2 — so it was a duplicate name for an existing step, and its presence invited a "H1 must be bigger than H2" assumption that was never true. Deleted it and pointed both consumers (MoneyReportView and moneyRequestConfirmationAmount) at fontScale.h2. Since fontScale.h1 is the genuinely larger 22/28 size, I left a comment on the token so nobody reintroduces a second 19px "H1".

3. Hardening rulesdir/no-raw-typography

The rule previously only caught numeric literals, which left variables.fontSizeNormal as an open escape hatch — exactly what this migration is closing. It now also flags:

  • variables.fontSize* / variables.lineHeight* in a fontSize / lineHeight position.
  • Values reached through a const alias or a ternary, so const size = variables.fontSizeXXSmall followed by getFontSizeStyle(size) is caught.
  • getFontSizeStyle() / getLineHeightStyle() arguments, since those build the same style the property would.

Only variables.* is traced through an alias, never bare numbers — a const FONT_SIZE = 12 is indistinguishable from any other constant, and following it would turn layout math and test fixtures into typography violations.

4. The allowVariablesReferences option

The styles layer is the code that composes tokens out of variables, so it has to keep reading them by name. src/styles/** gets the rule with allowVariablesReferences: true, which lifts only the named ban — raw numeric literals stay banned there, so the grandfathered literals in src/styles/index.ts keep ratcheting down. typography.ts and variables.ts remain fully exempt, since that's where the values are defined.

5. Seatbelt

The stricter rule surfaces violations in files this PR doesn't migrate, so those are recorded in the seatbelt rather than fixed here, to keep the diff reviewable. src/styles/index.ts ratchets 29 → 28 (the raw lineHeight: 12 in textDoubleDecker became lineHeightScale.finePrint).

Fixed Issues

$ #100405
PROPOSAL: #100405 (comment)

Tests

For every step below, compare against a production build: any change in text size, line spacing, font, or text wrapping is a bug.

Expense report amount breakdown

  1. Open a workspace chat and create two or more expenses on the same report, with at least one marked non-reimbursable, billable, or carrying tax.
  2. Open that report.
  3. Verify the breakdown rows below the total (Out of pocket, Company spend, Billable, Tax) are the same size as before, and match the report title's size.

Expense confirmation amount

  1. Click + > Create expense > Manual.
  2. Enter an amount and continue to the confirmation page.
  3. Verify the large amount at the top of the confirmation list is unchanged.

Chat

  1. Post a message containing a bulleted list (- item). Verify the bullet dot size and alignment are unchanged.
  2. Edit any message. Verify the (edited) label is still small and muted, at the same size as before.
  3. Post a URL that generates a link preview. Verify the preview's title, description, and hostname text sizes are unchanged.
  4. In the composer, type # Heading and an emoji. Verify the heading size and emoji line height are unchanged.
  5. Run a Search that returns chat messages. Verify the in <report name> link above each result is still small.

Reports and expenses

  1. Open an expense report, open the layout menu, and switch Group by to Category, then to Tag.
  2. Resize the window to be narrow (or use mobile). Verify the group header names and subtotals render at label size and are not larger than before.
  3. Open a transaction preview card in a report. Verify the amount and merchant text sizes are unchanged.
  4. Open a split expense. Verify the merchant line in each split row is unchanged.
  5. Start a distance expense and add waypoints. Verify the distance description text is unchanged.
  6. Open a duplicate-transaction review flow (Review duplicates) and step through the review and confirmation pages. Verify the page headings are unchanged.

Tables and tooltips

  1. Open Workspace > Members. Verify the column widths have not shifted.
  2. Sort by a column. Verify the bold header label is not truncated.
  3. Hover over any element with a tooltip. Verify the tooltip's text size and line height are unchanged.

Settings, workspace, and inputs

  1. Open Workspace > Rules. Verify each merchant rule row's description line spacing is unchanged.
  2. Open Workspace > Accounting > Xero > Import. Verify the Import label is unchanged.
  3. Open any form with a floating-label text input that has a prefix or suffix (for example, an amount field). Verify the label, prefix, and suffix sizes are unchanged.
  4. Verify the top bar breadcrumb label (Inbox, Reports, etc.) is unchanged.
  5. Find a button that renders two lines of text (a primary label with smaller subtext underneath). Verify the second line's size and spacing are unchanged.
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests

QA Steps

Same as Tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

@MelvinBot please set the PR body to the content in the details section above

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Done — the PR body is now the content from your <details> block, verbatim.

The old body's // TODO: placeholders in Tests and QA Steps are gone, replaced by your full test plan (expense report amount breakdown, confirmation amount, chat, reports and expenses, tables and tooltips, settings/workspace/inputs). Offline tests and QA Steps now both read "Same as Tests", and the PR Author Checklist is fully checked.

One note: the old body carried empty ### Screenshots/Videos <details> sections for the five platforms. Your content didn't include them, so they're no longer in the body — say the word if you want them added back.


view run · view recording

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

Recovered the signed-out session by hand and drove all 8 surfaces on dev NewDot web. No visual regression found on any surface I could reach — all migrated typography call sites render at the same sizes, faces and line heights, and both high-risk table surfaces (VERIFY 6, 7) are correct. Three checks are unreached, not failed: the "(edited)" marker, markdown bullet lists, and tooltips could not be produced in this headless browser.

Report preview with two transaction preview cards: 15px merchant and amount, 13px date and category

Step Status
Session recovery — complete sign-in by hand (prestart stopped on the Join screen)The prestart browser had died and a fresh one was launched but never navigated, so the first attempt saw a blank page with 0 accessibility nodes for 11 minutes while the dev server served 200s. After a reload, probed selectors one arm at a time: role="textbox" label="Phone or email" resolved, role="button" label="Continue" resolved, role="button" label="Join" resolved after Continue. Reused the prestart address, read the value back before submitting, pressed Continue, landed on the Join screen (proving the account had never been created), pressed Join, landed on Home. Root cause is in Melvin's own lifecycle script, not selector drift: melvin-session.sh only replays sign-in.ad and never chains join.ad, and sign-in.ad's post-condition accepts the Join button as an authenticated end state — so prestart reports ok while the account does not exist.
Static audit — every typography substitution in all three commitsRe-derived all 17 tokens from src/styles/variables.ts and src/styles/typography.ts rather than trusting the PR table, then checked all 66 changed typography lines. Every one is value-for-value identical. The 19→22 trap was avoided: legacy fontSizeH1 was 19 and fontScale.h1 is 22/28, and both call sites correctly landed on fontScale.h2 (19) — src/styles/index.ts:3469 and src/components/ReportActionItem/MoneyReportView.tsx:135. No lineHeight is added or dropped anywhere. headlineFont and fontFamilyScale.heading point at the same family object, and the family objects carry no size keys so the spread cannot clobber a sibling. The three measurement/layout files — useDynamicColumnWidths.ts:98, TooltipStyleUtils/index.ts:266-268, useMarkdownStyle.ts:39-44 — all feed identical numbers. Import hygiene is clean: the 13 files that dropped import variables have zero remaining references.
VERIFY 1 — Chat message text baseline and the "(edited)" markerBody text verified, "(edited)" marker unreached. The sent message renders in EXP Neue at the normal 15px/20 size, single line, no clipping, correctly separated from the 13px timestamp — this is the TextCommentFragment path the PR touched. The "(edited)" suffix could not be produced: the hover-gated report-action toolbar never renders in this headless browser. Hover does reach the app (the composer Emoji button paints its hover highlight), but repeated hovers over the message row by coordinates, fresh ref and full label selector all gave a settle diff of +0/-0 with no Edit affordance. No alternative entry point exists — secondary click returns UNSUPPORTED_OPERATION, longpress is unsupported, and there is no key-press verb for the ArrowUp edit shortcut. So EditedRenderer was confirmed statically but never exercised at runtime.
Workspace expense chat: body message at normal 15px next to the smaller 13px timestamp
VERIFY 2 — Markdown heading size/line spacing and bullet glyph alignmentHeading verified, bullets unreached. "# Typography Heading Check" renders bold and visibly larger than the surrounding 15px body text with clean spacing and no overlap. The same markdown also renders correctly in the live-markdown composer (workspace Description editor), where the heading is drawn bold at the larger pageHeader size while following lines stay at body size — that is the useMarkdownStyle h1 path the PR touched, unchanged. Bullets could not be produced: this build's markdown does not convert "- item" into a <ul>, in the chat composer or in the Description field (which does accept real newlines — confirmed by read-back before saving). It renders as literal hyphen text every time, so BulletItemRenderer never mounted.
Chat with rendered markdown heading; hyphen line renders as literal text, not a bullet list
Live-markdown Description input: heading drawn bold at the larger pageHeader size
VERIFY 3 — Link preview title, description and domain line sizesUnreached. Sent a message containing a GitHub URL and waited ~40s across two screenshots. The URL renders as a plain inline TextLink at normal body size, correctly coloured and not clipped, but no preview card ever appeared — the backend never attached link metadata to the report action on this dev environment, so LinkPreviewer never mounted and there were no title/description/domain lines to measure.
Chat message with a GitHub URL as a plain inline link, no preview card generated
VERIFY 4 — Menu item title and description line spacing (Settings)Account > Profile: the two-line menu items (Display name, Contact methods, Pronouns, Timezone) each render a 13px supporting-grey description above a 15px title with roughly 23px between baselines and a consistent 64px row pitch. Nothing is cramped, overlapping or clipped, and the long email title fits on one line at full width. Also exercised the specific branch the PR changed — the description-only MenuItem, where the title ? {} : getFontSizeStyle(fontScale.text) fallback applies. On the workspace Overview page, "Company address" and "Policy text" (description, no title) render at the larger 15px/20 body size, visibly bigger than the 13px descriptions that sit above titles on the same card. That is the correct pre-existing behaviour and it is preserved.
Account Profile settings: two-line menu items with 13px descriptions above 15px titles
Workspace Overview: description-only menu items at 15px next to title+description items at 13px
VERIFY 5 — Tooltip text size and box fitUnreached. No tooltip renders on hover in this headless browser, so TooltipStyleUtils was never exercised. Hover is genuinely reaching the app — the composer Emoji button paints its circular hover highlight — but after waiting 2-3s on both the Emoji button and the header Search icon, moving the pointer in from a neutral point first each time, no tooltip appeared in the settle diff, a fresh snapshot, or a screenshot. Same root cause as the missing report-action toolbar in VERIFY 1: hover-gated overlays do not render on this path.
Composer Emoji button with its hover highlight painted but no tooltip rendered
VERIFY 6 — Expense report table: header widths, group header row, cell truncationCreated a workspace expense report with two expenses ($42.50 Starbucks Coffee Downtown, $1,234.56 Blue Bottle Coffee Roasters International, both Meals and Entertainment). Wide layout (1280px): every column header label renders in full — Receipt, Type, Date with its sort arrow, Merchant, Category, Amount — none elided or overlapping, which is what measureHeaderLabelWidth in useDynamicColumnWidths sizes. The group header row reads "Meals and Entertainment • $1,277.06" in the bold labelStrong style, correctly aligned with the row checkbox. No cell text is truncated that shouldn't be: the 41-character merchant renders in full, categories render in full, amounts are right-aligned and complete, footer reads "Total $1,277.06". Also drove the narrow-layout branch (700x900), which is the line the PR actually changed in MoneyRequestReportGroupHeader; the group header renders at the smaller 13px/16 bold size, fully legible, not cramped, not clipped, with rows correctly reflowing to the stacked card layout.
Expense report table at 1280px with full-width column headers, group header row and untruncated rows
Same report at 700px: narrow-layout group header at the smaller 13px bold size
VERIFY 7 — Workspace members table: name/email/role column sizing and clippingOpened the workspace Members page. Snapshot read-back gives a sortable "Member" columnheader and a "Role" columnheader, plus one row. The Member column is the dynamically sized one (getContentToMeasure measures the name at fontScale.text and the email at fontScale.label) and it is wide enough for the worst case here: the 46-character address renders in full at 15px on the name line with the same address in full at 13px beneath it, avatar included, no ellipsis, no bleed into the Role column. The Role column uses shouldFitContent and is sized to its content — header "Role" and value "Owner" both render complete and left-aligned to the same edge. Header labels render at the 11px bold micro size that measureHeaderLabelWidth assumes, neither clipped nor overlapping.
Workspace Members table: Member column fits the full name and email lines, Role column fits Owner
VERIFY 8 — Transaction preview card merchant/amount/date line sizesThe report preview in the workspace expense chat embeds two TransactionPreviewContent cards, the component the PR migrated to fontScale.text. In the full-width inbox layout, merchant and amount render at the same normal 15px size on one baseline ("Starbucks Coffee Downtown" / "$42.50"), with the secondary line beneath at the smaller 13px supporting size ("Sep 7 • Meals and Entertainment" / "Cash"). The second card's longer merchant ellipsises to one line as intended by its numberOfLines={1}, and its amount "$1,234.56" stays complete on the right because it is the flexShrink0 side — correct, not clipping. Card heights are equal, nothing overlaps the receipt thumbnail above, and the footer total renders at the bold total size.
Report preview with two transaction preview cards: 15px merchant and amount, 13px date and category
VERIFY 9 — JS console errorsUnreached as specified. Browser console capture is out of scope for the agent-device web path, and log retrieval returns UNSUPPORTED_OPERATION for this platform. The closest available signal is the network log, and it is clean: a 30-entry network dump across the whole drive shows every request at status 200 — Ping, PusherPing, AuthenticatePusher, OpenReport, ReadNewestAction, GetMissingOnyxMessages, OpenPolicyProfilePage, UpdateWorkspaceDescription, OpenInitialSettingsPage and the lazy bundle fetches — with no 4xx/5xx and no failed chunk loads. That rules out network-level failures but is not evidence about console errors either way.

No regression found. The static audit and the runtime drive agree, so the "behaviour-neutral" claim holds on the surfaces reachable here. The five ❌ rows are all "could not be produced in a headless browser", not observed defects.

Two things that are not visual regressions but are worth your attention before merge:

  1. The seatbelt does net grow, against your explicit ask. config/eslint/eslint.seatbelt.tsv gains 4 new no-raw-typography rows (ChatBubbleCell.tsx 2, SignInHeroCopy.tsx 2, SignInPageContent.tsx 2, ComposerTest.tsx 1) plus useMarkdownStyle.ts going 2 → 3, against src/styles/index.ts dropping 29 → 28. I checked each new row and they are all genuinely un-migratable — fontSizeSignInHeroXSmall/Medium/Large, fontSizeXXSmall, lineHeightXSmall and fontSizeOnlyEmojis have no fontScale/lineHeightScale entry — so the growth is defensible, but net +5 is real. The useMarkdownStyle +1 is caused by the new alias tracing following const emojiFontSize = hasMessageOnlyEmojis ? … : … — the hole the tracing was added to close, not a false positive.

  2. Two documented gaps in the new rule. isVariablesTypographyReference matches the literal identifier variables, so a renamed import, const {fontSizeNormal} = variables, or variables['fontSizeNormal'] all slip through; and isBehindAlias means a bare number behind a const is never flagged. Both are called out in the rule's own docblock, and the second was a deliberate walk-back, so this is a note rather than a request.

One pre-existing layout oddity, outside this diff: in Spend > Reports the Title and From cells truncate while ~200px of empty space sits between To and Total. That list is not built on @components/Table, so it never goes through useDynamicColumnWidths — not attributable to this PR.


view run · view recording

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot find regressions

@melvin-bot
melvin-bot Bot removed the request for review from a team September 7, 2026 07:45
@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Marked this PR as ready for review.

Two things still need a human before review can complete:

  • Tests and QA Steps sections still hold the // TODO: placeholders — I don't fill those in on someone's behalf. The Web regression pass I ran earlier (results) covers the input labels/prefixes/suffixes, menu item supporting text, top bar label, and confirmation amount if you want to lift the steps from there. MoneyReportView sub-amounts and the JS console check were not verifiable in that environment.
  • PR Author Checklist is unchecked.

All CI checks pass. The only failure is Check independent approval, which just needs a reviewer.


view run · view recording

@aimane-chnaif

aimane-chnaif commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
android.mov
Android: mWeb Chrome
iOS: HybridApp
ios.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
web2.mov
web.mov

Comment thread src/styles/typography.ts Outdated
text: variables.fontSizeNormal,
pageHeader: variables.fontSizeLarge,
h2: variables.fontSizeH2,
// `h1` is the 22/28 size. The legacy `fontSizeH1` variable was 19 — the same value as `fontSizeH2` — so anything

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-16 (docs)

This newly added comment uses em dashes (), which the plain-comment-style rule disallows. Comments should read as plain sentences without stylistic tics like em dashes.

Rewrite the two clauses as separate sentences, e.g.:

// `h1` is the 22/28 size. The legacy `fontSizeH1` variable was 19, the same value as `fontSizeH2`, so
// anything that used to read `fontSizeH1` belongs on `h2`, not here. Do not reintroduce a separate 19px "H1".

Reviewed at: c055114 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a96fab8 — took the suggested wording, with the line break moved so both lines stay under the print width.

// `h1` is the 22/28 size. The legacy `fontSizeH1` variable was 19, the same value as `fontSizeH2`, so
// anything that used to read `fontSizeH1` belongs on `h2`, not here. Do not reintroduce a separate 19px "H1".

Comment thread src/styles/index.ts Outdated
lineHeight: variables.lineHeightXSmall,
},
// The two scales are deliberately mismatched: this needs `micro`'s size with `finePrint`'s 12 line
// height, which is what it always had. Do not "fix" it to `lineHeightScale.micro` (14/16) — that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-16 (docs)

This newly added comment uses an em dash (), which the plain-comment-style rule disallows.

Replace the em dash with plain sentence punctuation, e.g.:

// height, which is what it always had. Do not "fix" it to `lineHeightScale.micro` (14/16), because that
// changes the rendered line height.

Reviewed at: c055114 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a96fab8.

// height, which is what it always had. Do not "fix" it to `lineHeightScale.micro` (14/16), because
// that changes the rendered line height.

Comment thread config/eslint/eslint.config.mjs Outdated
// The styles layer is what composes tokens out of `variables`, so it keeps reading them by name.
// What is left here after the token migration is the handful of sizes the scale has no entry for
// (`fontSizeMedium`, `fontSizeXXLarge`, `fontSizeXXXLarge`, the emoji and sign-in hero sizes, and the
// `lineHeight*` steps above `pageHeader`) — adding those is a design call, not a refactor, so there

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-16 (docs)

This newly added comment uses an em dash (), which the plain-comment-style rule disallows.

Rewrite without the em dash, e.g.:

// `lineHeight*` steps above `pageHeader`). Adding those is a design call, not a refactor, so there

Reviewed at: c055114 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a96fab8 — took the suggested wording verbatim.

// `lineHeight*` steps above `pageHeader`). Adding those is a design call, not a refactor, so there

/**
* Flags object properties (`{fontSize: 17}`) and JSX attributes (`<Text fontSize={17}>`) that set
* `fontSize`/`lineHeight` to a numeric literal. References to tokens or computed values are allowed.
* Matches `variables.fontSizeNormal`, `variables.lineHeightXLarge`, and friends — the named escape

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-16 (docs)

Several newly added comments in this file use em dashes () and semicolons to join clauses, which the plain-comment-style rule disallows (this JSDoc block and the ones on getConstInitializer and create are all affected). Comments should read as plain sentences.

Replace em dashes with periods/commas and split semicolon-joined clauses into separate sentences, e.g.:

 * Matches `variables.fontSizeNormal`, `variables.lineHeightXLarge`, and friends. This is the named escape
 * hatch around the typography scale. Only the `variables` module is matched. Unrelated objects that
 * happen to have a `fontSize*` key are left alone.

Reviewed at: c055114 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed all three blocks in a96fab8. This one took the suggested wording, with the line break shifted to keep the width:

 * Matches `variables.fontSizeNormal`, `variables.lineHeightXLarge`, and friends. This is the named
 * escape hatch around the typography scale. Only the `variables` module is matched. Unrelated objects
 * that happen to have a `fontSize*` key are left alone.

getConstInitializer now reads ...are followed. A \let` binding or a destructuring pattern has no single value to trace..., and createreads...outside the typography scale. That covers both numeric literals and...`.

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot address #100444 (comment), #100444 (comment), #100444 (comment), #100444 (comment)

Drop the em dashes and the clause-joining semicolon from the comments this
branch added, and split the affected clauses into separate sentences. Also
covers the three em dashes in NoRawTypographyRuleTest that the review did not
flag but that break the same rule. Comment-only, no behavior change.

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner September 7, 2026 08:00
@melvin-bot
melvin-bot Bot requested review from grgia and removed request for a team September 7, 2026 08:00
@melvin-bot

melvin-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown

@grgia Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Addressed all four CONSISTENCY-16 findings in a96fab8. Comment-only changes, no behavior change.

Comment File Change
r3947595943 src/styles/typography.ts Em dashes around "the same value as fontSizeH2" replaced with commas
r3947596165 src/styles/index.ts (14/16) — that changes became (14/16), because that changes
r3947596397 config/eslint/eslint.config.mjs Suggested wording taken verbatim, split into two sentences
r3947596634 eslint-plugin-local-rules/no-raw-typography.js All three JSDoc blocks fixed: em dashes on isVariablesTypographyReference, getConstInitializer and create, plus the clause-joining semicolon

I also fixed three em dashes this branch added in tests/unit/NoRawTypographyRuleTest.ts that the review did not flag but that break the same rule, so a re-review does not come back with them.

I did not touch three pre-existing em dashes in the files I edited (config/eslint/eslint.config.mjs:512, :531, src/styles/index.ts:1035) — they are not lines this PR added, and CONSISTENCY-16 only applies to added or modified comments.

Verification
  • npx eslint on the five edited files — 0 errors (30 warnings, all pre-existing grandfathered no-raw-typography seatbelt entries)
  • npm run typecheck — passed, all 5 tsconfigs
  • npm test -- tests/unit/NoRawTypographyRuleTest.ts tests/unit/TextVariantTest.tsx — 53 tests passed
  • npm run spell — no findings in any edited file
  • Grep confirms zero em dashes, en dashes, or clause-joining semicolons remain in comments this branch added

npm run lint-changed and npm run spell-changed both failed with git merge-base failed because this runner has a shallow clone with no merge base against main. I ran ESLint and cspell directly on the changed files instead, which is the same check without the diff-scoping step.

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Still nothing to push — every code check is green on the current head (a96fab8). 35 checks pass, including ESLint, TypeScript, Jest (8 shards), Reassure perf, Oxfmt, spellcheck, unused styles, knip, React Compiler, and both native builds. ESLint and the perf tests were mid-run when I started; I waited for them and they passed.

The two red checks are both human actions:

1. checklist (PR Reviewer Checklist) — the checklist comment exists but no boxes are ticked:

There are 43 items in the reviewer checklist.
Found the reviewer checklist!
You completed 0 out of 43 checklist items with 43 unfinished items
##[error]PR Reviewer Checklist is not completely filled out.

A reviewer needs to check the boxes. I can't tick them on someone's behalf — that's the point of the check.

2. Check independent approval — no approving review exists:

Insufficient independent peer review {
  commitAuthors: [ 'aimane-chnaif', 'MelvinBot' ],
  approvers: [],
  required: 1
}

Every review on the PR is COMMENTED (from chatgpt-codex-connector, github-actions, and me). Since aimane-chnaif has commits on the branch, their own approval wouldn't count as independent either — this needs an approval from a reviewer who hasn't committed here.

Also still outstanding from the PR body: the ### Tests and ### QA Steps sections are TODOs waiting on a human co-author.


view run · no recording available

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Still nothing to push — all 35 code checks are green on the current head (a96fab8). Both red checks are human/CI-state gates, and one of them is just stale.

1. checklist (PR Reviewer Checklist) — stale, needs a re-run.

The job read your checklist comment before you finished ticking the boxes:

Event Time
Checklist comment posted (boxes empty) 07:46:32
checklist job ran → You completed 0 out of 43 checklist items with 43 unfinished items 08:01:43
You finished checking the boxes 08:09:55

Your comment now has 50 checked, 0 unchecked, which clears the >= 41 checked && 0 unchecked bar at reviewerChecklist.ts:74. The workflow only triggers on pull_request_review, so editing the comment doesn't re-trigger it — the stale failure just sits there.

Re-run the checklist job (or submit any new review) and it goes green. I don't have permission to trigger the re-run myself.

2. Check independent approval — needs a reviewer who hasn't committed here.

commitAuthors: [ 'aimane-chnaif', 'MelvinBot' ]
approvers: [], independentApprovers: [], required: 1

There are still zero approving reviews — every review on the PR is COMMENTED. Note your own approval won't satisfy this: all four commits carry Co-authored-by: Aimane Chnaif, so you count as a commit author and aren't independent. grgia was already pinged for the checklist, so an approval from them would clear it.

Neither is fixable by a commit, so I made no code changes.


view run · view recording

Comment thread config/eslint/eslint.config.mjs Outdated
Comment on lines +693 to +698
// The styles layer is what composes tokens out of `variables`, so it keeps reading them by name.
// What is left here after the token migration is the handful of sizes the scale has no entry for
// (`fontSizeMedium`, `fontSizeXXLarge`, `fontSizeXXXLarge`, the emoji and sign-in hero sizes, and the
// `lineHeight*` steps above `pageHeader`). Adding those is a design call, not a refactor, so there
// is nothing a seatbelt row here could ratchet down to yet. Raw numeric literals stay banned, and
// the seatbelt still carries those so they keep ratcheting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@MelvinBot unnecessary comment paragrpahs

@MelvinBot MelvinBot Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, these are over-written. The rule and the aliases are self-explanatory; only the non-obvious "why" needs to survive. Concretely, ~40 prose lines collapse to ~12:

config/eslint/eslint.config.mjs:693-698 — 6 lines → 1. The inventory of un-migrated sizes will go stale immediately, and the seatbelt-ratchet aside belongs in the PR description, not the config.

// The styles layer composes tokens out of `variables`, so it reads them by name. Raw numeric literals stay banned.

eslint-plugin-local-rules/no-raw-typography.js:67-80 — 10 prose lines → 2. The enumeration of every bypass shape is what makes it long, and one clause covers it.

/**
 * Matches `variables.fontSize*` / `variables.lineHeight*`, the named escape hatch around the scale.
 * Syntactic match on the `variables.<name>` shape, so a renamed or destructured import is not flagged.
 */

eslint-plugin-local-rules/no-raw-typography.js:110-119 — 8 prose lines → 2.

/**
 * The expression a single-definition `const` alias was assigned, so `const size = variables.fontSizeXXSmall`
 * plus `getFontSizeStyle(size)` is still caught. Only `variables.*` is traced, never bare numbers.
 */

eslint-plugin-local-rules/no-raw-typography.js:135-139 — 4 lines → 2, dropping the restatement of what the selectors already show.

src/styles/index.ts:883-885 — 3 lines → 1.

// Deliberate mismatch: `micro`'s size with `finePrint`'s 12 line height, which is what it always had.

src/styles/typography.ts:1010-1011 — 2 lines → 1.

// Legacy `fontSizeH1` was 19, the same as `fontSizeH2`, so old `fontSizeH1` call sites belong on `h2`.

Also droppable: the two-line aside inside report() about naming the identifier, the isBehindAlias param prose, and the per-case one-liners in tests/unit/NoRawTypographyRuleTest.ts where the test name already says the same thing.

Worth keeping in some form: the three "not aliased to a token, needs a design call" notes on textExtraSmallSupporting, textXXLarge, and textXXXLarge. Without them the next person reads those as an oversight and aliases them, which changes rendered type. They already fit on one line each.

I can only make code changes when directed by the PR author, an assignee, or an existing co-author. To have me implement this, the PR assignee or author should reply with a request tagging me — aimane-chnaif, say the word and I'll push the condensed version.


view run · view recording

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 9d16d22 — 43 comment lines out, 13 in, no code touched.

Site Was → now
config/eslint/eslint.config.mjs:693 6 → 1
eslint-plugin-local-rules/no-raw-typography.js:67-68 10 → 2
no-raw-typography.js:103-104 8 → 2
no-raw-typography.js:121-122 4 → 2
src/styles/index.ts:559 3 → 1
src/styles/typography.ts:19 2 → 1

Also dropped: the report() aside about naming the identifier, and the isBehindAlias prose is now one line. The schema note on allowVariablesReferences went 2 → 1.

Two deviations from the plan, both deliberate:

  • The NoRawTypographyRuleTest.ts comments are condensed, not deleted. They label groups of RuleTester cases rather than individual ones, so a bare fixture list reads as undifferentiated. The two 2-line ones are now 1 line each; the rest were already 1 line.
  • The three "needs a design call" notes stay as-is on textExtraSmallSupporting, textXXLarge and textXXXLarge — already one line each, and without them those read as an oversight.
Checks run on the new commit
  • npx eslint on all 5 changed files — 0 errors (30 warnings, all pre-existing seatbelt entries)
  • npm run typecheck — passed, all 5 tsconfigs
  • npm test -- NoRawTypographyRuleTest TextVariantTest MoneyReportViewTest — 66 passed
  • npm run gh-actions-unused-styles — no unused styles

npm run lint-changed and npm run spell-changed both abort with git merge-base failed in this environment — the checkout is shallow, so there is no reachable common ancestor with main. The direct eslint run above covers the lint half; spelling is unverified, though the diff introduces no new terms.


view run

Comment on lines +68 to +76
* Matches `variables.fontSizeNormal`, `variables.lineHeightXLarge`, and friends. This is the named
* escape hatch around the typography scale. Only the `variables` module is matched. Unrelated objects
* that happen to have a `fontSize*` key are left alone.
*
* This is a syntactic match on the `variables.<name>` shape, not a resolved-import check, so it is a
* convention guard rather than an airtight ban. Renaming the import (`import vars from '@styles/variables'`),
* destructuring (`const {fontSizeNormal} = variables`), and computed access (`variables['fontSizeNormal']`)
* all read as something else and are not flagged. The app imports the module as `variables` everywhere,
* so in practice the only bypass is a deliberate one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@aimane-chnaif let's condense all these comment blocks

@MelvinBot MelvinBot Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Condensed in 9d16d22. The three blocks in this file:

Also dropped the report() aside about naming the identifier, and shortened the isBehindAlias param prose and the schema note to one line each. Across all five files that is 43 comment lines out, 13 in, no code changed.

The full breakdown, including the two places I deviated from the plan, is in the other thread on eslint.config.mjs.


view run · no recording available

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot do #100444 (comment) and address #100444 (comment)

@MelvinBot

MelvinBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Still nothing to fix in code — all 36 substantive checks pass on the current head (a96fab8), including ESLint, TypeScript, Jest, Reassure perf, spellcheck, unused styles, knip, and both native builds.

The one red check is Check independent approval, a human gate no push can clear:

commitAuthors: [ 'aimane-chnaif', 'MelvinBot' ],
allAuthorsAreBots: false,
approvers: [],
independentApprovers: [],
required: 1

Your approval on a96fab8 doesn't count, for three independent reasons. The check needs one APPROVED review from someone who is all of: has write access to Expensify/App, is in the Expensify/expensify-expensify team, and has no commits on the branch. You fail all three — read access only, not in the team, and every commit carries a Co-authored-by: Aimane Chnaif trailer, which makes you a commit author.

What clears it: grgia has left only COMMENTED reviews (10:46, 10:47), and COMMENTED is invisible to this check. grgia has write access, is an active team member, and isn't a commit author — so a single APPROVED review from them (or any other Expensify employee reviewer) flips the check green.

Why each gate rejects the approval, and two traps to avoid

The check is scripts/verifyPeerReview.ts in Expensify/GitHub-Actions.

1. Write-access filter — the reason approvers is empty. getLatestApprovers.ts:27 queries latestOpinionatedReviews(last: 100, writersOnly: true). GitHub applies that filter server-side, so the approval is dropped before the script ever sees it. Verified against this PR: writersOnly: true returns totalCount: 0; writersOnly: false returns the one APPROVED review from aimane-chnaif. The review is genuinely still APPROVED on the current head — not dismissed, not stale — but it reports authorCanPushToRepository: false, and repos/Expensify/App/collaborators/aimane-chnaif/permission is read. GitHub's own reviewDecision agrees: REVIEW_REQUIRED.

2. Commit-author filter. verifyPeerReview.ts:68 drops any approver who is in commitAuthors. All four commits have GitHub author MelvinBot, which is a known bot, so verifyPeerReview.ts:39-55 parses the Co-authored-by trailers and resolves aimane-chnaif@users.noreply.github.com to aimane-chnaif.

3. Eligible-team filter. verifyPeerReview.ts:73 keeps only members of expensify-expensify plus any repo-specific reviewer teams — and REPOSITORY_REVIEWER_TEAMS has no entry for Expensify/App. orgs/Expensify/teams/expensify-expensify/memberships/aimane-chnaif returns 404.

Trap 1: granting write access wouldn't fix it — gates 2 and 3 still reject the approval.

Trap 2: dropping the Co-authored-by trailer makes the check stricter, not looser. Authors would become ['MelvinBot'] alone, so allAuthorsAreBots flips true and verifyPeerReview.ts:121-122 raises the requirement to two independent employee approvals.

Separately, the ### Tests and ### QA Steps sections of the PR body are still TODOs that need a human co-author to fill in before this can go ready for review.


view run · view recording

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@aimane-chnaif let's condense all these comment blocks

Comments look better now

@grgia grgia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

changes lgtm

@grgia
grgia merged commit 32b5850 into main Sep 7, 2026
44 of 46 checks passed
@grgia
grgia deleted the claude-fontLibraryFollowUp branch September 7, 2026 11:54
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🚧 grgia has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants