Skip to content

feat(mobile): configurable font size with larger iOS default#2559

Open
dmarticus wants to merge 2 commits into
mainfrom
posthog-code/mobile-configurable-font-size
Open

feat(mobile): configurable font size with larger iOS default#2559
dmarticus wants to merge 2 commits into
mainfrom
posthog-code/mobile-configurable-font-size

Conversation

@dmarticus

Copy link
Copy Markdown
Contributor

What

Two related asks for the mobile app:

  1. Bigger default font size on iOS — text reads noticeably smaller on iOS than Android at the same point size.
  2. Make the font size configurable — a user setting that scales text across the app.

Approach

The mobile app has no central font-size token — sizes are set per-component via NativeWind classes (text-sm, text-base, …) across ~15+ files. Rather than touch every usage, this adds a single global scale multiplier at the one place text styling is already centralized: the Text.render patch in lib/textDefaults.ts (which already injects the default font family).

  • preferencesStore — new persisted fontSize preference (small | default | large | xlarge) with a FONT_SCALE_BY_PREFERENCE map (0.9 / 1.0 / 1.15 / 1.3). Default is large on iOS, default (1.0, unchanged) on Android — so Android behavior is untouched and iOS ships larger out of the box.
  • textDefaults.ts — the global Text.render patch now multiplies each node's explicit fontSize by the selected scale. Nodes without an explicit size are left alone so they keep inheriting their already-scaled parent — this preserves React Native's text-inheritance semantics for nested <Text>. Scaling is skipped entirely when the multiplier is 1.
  • Settings → Appearance — a new "Font size" row + select sheet, matching the existing discrete-picker pattern (Theme, Sound, etc.).
  • Tests — unit coverage for the new preference and the scale ordering.

Decisions made (easy to change)

  • Discrete presets (Small / Default / Large / Extra Large) rather than a continuous slider, to match the existing settings UX.
  • iOS-only larger default; Android keeps the current 1.0 baseline.
  • Scale values 0.9 / 1.0 / 1.15 / 1.3 — tweakable in FONT_SCALE_BY_PREFERENCE.

Notes

  • The patch reads the scale imperatively on each render, so a change takes effect as screens re-render (immediate on the Settings screen itself, which subscribes) and is persisted across launches. There's no forced global remount, to avoid resetting navigation state.
  • Couldn't run vitest/biome in this environment (deps not installed); targeted tsc of the changed files is clean.

🤖 Generated with Claude Code

Add a user-configurable font size to the mobile app, and ship a larger
default on iOS where text reads noticeably smaller than Android at the same
point size.

The app has no central font-size token — sizes are set per-component via
NativeWind classes (text-sm, text-base, …) across many files. Rather than
touch every usage, this introduces a single global scale multiplier applied
at the one place font styling is already centralized: the `Text.render`
patch in `lib/textDefaults`.

- Add a `fontSize` preference ("small" | "default" | "large" | "xlarge")
  to `preferencesStore`, persisted alongside the other preferences, with a
  `FONT_SCALE_BY_PREFERENCE` map (0.9 / 1.0 / 1.15 / 1.3). The default is
  "large" on iOS and "default" (1.0, unchanged) on Android.
- Extend the global `Text.render` patch to multiply each node's explicit
  `fontSize` by the selected scale. Nodes without an explicit size are left
  untouched so they keep inheriting their already-scaled parent — preserving
  React Native's text-inheritance semantics for nested <Text>. Scaling is
  skipped entirely when the multiplier is 1.
- Add a "Font size" row + select sheet under Settings → Appearance, matching
  the existing discrete-picker pattern.
- Unit tests for the new preference and scale ordering.

Generated-By: PostHog Code
Task-Id: 6d87d732-7ac9-4633-89e2-d2be6efdcd67
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 88a25fe.

@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/mobile/src/features/preferences/stores/preferencesStore.test.ts:65-76
The `if/else` branch inside the `it.each` body is a superfluous part — the boolean flag `smaller` encodes the direction of comparison, but that direction could simply be expressed as an expected numeric value in each row. Passing exact values removes the conditional, makes each row self-documenting, and tests precise scale constants rather than just their ordering relative to 1.

```suggestion
  it.each([
    ["small", 0.9],
    ["large", 1.15],
    ["xlarge", 1.3],
  ] as const)("FONT_SCALE_BY_PREFERENCE.%s equals %s", (size, expected) => {
    expect(FONT_SCALE_BY_PREFERENCE[size]).toBe(expected);
  });
```

Reviews (1): Last reviewed commit: "feat(mobile): configurable font size wit..." | Re-trigger Greptile

Comment on lines +65 to +76
it.each([
["small", true],
["large", false],
["xlarge", false],
] as const)("orders the %s scale relative to default", (size, smaller) => {
const scale = FONT_SCALE_BY_PREFERENCE[size];
if (smaller) {
expect(scale).toBeLessThan(1);
} else {
expect(scale).toBeGreaterThan(1);
}
});

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.

P2 The if/else branch inside the it.each body is a superfluous part — the boolean flag smaller encodes the direction of comparison, but that direction could simply be expressed as an expected numeric value in each row. Passing exact values removes the conditional, makes each row self-documenting, and tests precise scale constants rather than just their ordering relative to 1.

Suggested change
it.each([
["small", true],
["large", false],
["xlarge", false],
] as const)("orders the %s scale relative to default", (size, smaller) => {
const scale = FONT_SCALE_BY_PREFERENCE[size];
if (smaller) {
expect(scale).toBeLessThan(1);
} else {
expect(scale).toBeGreaterThan(1);
}
});
it.each([
["small", 0.9],
["large", 1.15],
["xlarge", 1.3],
] as const)("FONT_SCALE_BY_PREFERENCE.%s equals %s", (size, expected) => {
expect(FONT_SCALE_BY_PREFERENCE[size]).toBe(expected);
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/features/preferences/stores/preferencesStore.test.ts
Line: 65-76

Comment:
The `if/else` branch inside the `it.each` body is a superfluous part — the boolean flag `smaller` encodes the direction of comparison, but that direction could simply be expressed as an expected numeric value in each row. Passing exact values removes the conditional, makes each row self-documenting, and tests precise scale constants rather than just their ordering relative to 1.

```suggestion
  it.each([
    ["small", 0.9],
    ["large", 1.15],
    ["xlarge", 1.3],
  ] as const)("FONT_SCALE_BY_PREFERENCE.%s equals %s", (size, expected) => {
    expect(FONT_SCALE_BY_PREFERENCE[size]).toBe(expected);
  });
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- Collapse the react-native import in textDefaults to a single line so it
  matches Biome's formatter (the `quality` check was failing on this).
- Assert exact FONT_SCALE_BY_PREFERENCE values instead of an if/else on
  ordering, per Greptile review feedback — each row is now self-documenting
  and pins the precise scale constants.

Generated-By: PostHog Code
Task-Id: 6d87d732-7ac9-4633-89e2-d2be6efdcd67
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant