diff --git a/Documentation/work-log/681-typography-parity.md b/Documentation/work-log/681-typography-parity.md
new file mode 100644
index 000000000..88c27f4a8
--- /dev/null
+++ b/Documentation/work-log/681-typography-parity.md
@@ -0,0 +1,137 @@
+# Typography parity with iOS + consolidate hardcoded text sizes
+
+**Issue:** #681 ("Match Android typography to iOS font-size scale, consolidate hardcoded text
+sizes").
+
+## What was needed
+
+vocable-ios uses `UIFont.systemFont` (San Francisco, the OS default) everywhere, sized in a
+compact/regular scale (22/24/28/40/48pt, almost always bold). Android's `Type.kt` already uses
+`FontFamily.Default` (Roboto, the OS default) — the font family already matched, no change needed
+there. But a few Android per-breakpoint sizes drifted from their iOS compact/regular equivalents
+more than unit/density rounding explains, and a number of hardcoded `.sp` literals bypassed this
+app's own `dimens.xml` per-breakpoint convention entirely, so those spots didn't scale on tablet
+the way the rest of the app does.
+
+## What changed
+
+### An accidental Compose-rewrite regression, found via git blame — not just an iOS mismatch
+
+Several screens used `MaterialTheme.typography.headlineLarge`/`headlineMedium` with no `fontSize`
+override, silently falling back to Material3's flat, non-scaling defaults (32sp/28sp) instead of a
+deliberate per-breakpoint size. `git log -S "keyboard_text_size"` showed this dimen (and its
+sibling `keyboard_input_text_size`) dates to the pre-Compose View-based keyboard layouts
+(`e9a6f96d`, `bcf61a58`), while the Compose rewrite (`f5b06b93`, "Refactor with jetpack compose
+commit#1") introduced the keyboard key `Text(...)` fresh and never reconnected it to that dimen —
+it sat in `dimens.xml`, fully unused, ever since. Fixed by wiring the already-designed (just
+orphaned) dimens back in, rather than inventing new values:
+
+- **Keyboard letter keys** (`KeyboardScreen.kt`) — the actual key glyphs, now use
+ `keyboard_text_size` (48sp phone/land, 34sp tablet portrait, 40sp tablet landscape — all
+ pre-existing breakpoint values, untouched).
+- **Keyboard typed-input display** (`KeyboardScreen.kt`, both the head-tracking `Text` and the
+ touch-fallback `TextField` + placeholder, in both the landscape/tablet and portrait layout
+ branches — 6 spots total) — now use `keyboard_input_text_size` (34sp across all breakpoints).
+
+Two similarly-orphaned dimens, found the same way, revived for their evident original targets:
+
+- **`EditCategoryMenuScreen`'s category title** — now uses `settings_edit_individual_category_text_size`
+ (30sp phone; added a 42sp tablet override, since none existed — sized in proportion to
+ `edit_categories_title_text_size`'s 34→48sp ratio, the sibling list-screen's title).
+- **`SensitivityScreen`'s "Hover Time"/"Cursor Sensitivity" section labels** — now use
+ `timing_subtitle_text_size` (24sp phone, 34sp tablet). Its **dwell-time value display** ("1
+ second", "2.5 seconds") now uses `hover_time_text_size` (24sp phone; added a 34sp tablet override
+ to match `timing_subtitle_text_size`'s ratio, since none existed). The screen's own main title
+ ("Timing and Sensitivity") was left alone — it already uses `BasicText`/`TextAutoSize.StepBased`
+ for responsive sizing, a different (and already-adequate) strategy than the breakpoint-dimens
+ approach used everywhere else.
+- **`VoiceSelectionScreen`'s "Change Voice" header** — this is the screen's own title, playing the
+ same role as every other Settings-adjacent screen's title, so it now reuses
+ `settings_title_text_size` directly rather than getting a new near-duplicate `voice_*` dimen.
+
+### `settings_title_text_size` phone value corrected to match iOS
+
+iOS's `VocableNavigationBar` title is 28pt compact / 48pt regular. Android's tablet value already
+matched exactly (48sp); only the phone value was off (34sp) beyond what unit/density differences
+explain. Changed to 28sp. This dimen backs most Settings-adjacent screen titles (`SettingsScreen`,
+`SettingsVoiceScreen`, `SelectionModeScreen`, `ResetSettingsScreen`, and now `VoiceSelectionScreen`
+above), so the fix applies consistently everywhere in one change.
+
+### Hardcoded `.sp` sweep
+
+A broad literal grep (`[0-9]+\.sp`) initially suggested ~50 hardcoded sizes, but nearly all of them
+turned out to be `minFontSize`/`maxFontSize`/`stepSize` values inside already-responsive
+`TextAutoSize.StepBased(...)` configs, or `letterSpacing`/`lineHeight` (spacing, not size), or the
+base `Typography` scale definition in `Type.kt` itself (not a screen bypassing anything — it's the
+canonical place that scale lives, and per-screen dimens already layer on top of it, as this whole
+pass confirmed). None of those represent an actual "doesn't scale on tablet" bug: auto-sizing
+already adapts to available space at runtime regardless of breakpoint. Filtering those out left
+exactly 6 genuine fixed, non-scaling hardcoded sizes, all coincidentally `18.sp`:
+
+- `EditCategoryMenuScreen`'s four action-button labels (Rename/Toggle/Edit Phrases/Remove) — new
+ `edit_category_menu_action_text_size` (18sp phone, 24sp tablet).
+- `EditCategoryPhrasesScreen`'s phrase-row text — new `edit_category_phrases_item_text_size` (18sp
+ phone, 24sp tablet).
+- `FaceTrackingScreen`'s "Head tracking is paused" message — new `head_tracking_paused_text_size`
+ (18sp phone, 24sp tablet).
+
+Three `PresetsScreen.kt` instances of `fontSize = 20.sp` were investigated and deliberately left
+alone: each sits inside a `TextStyle` immediately followed by
+`TextAutoSize.StepBased(minFontSize = 10.sp, maxFontSize = 20.sp, ...)` — the base `fontSize`
+exactly matches `maxFontSize`, meaning auto-sizing already governs the actual rendered size at
+runtime regardless of screen breakpoint. Moving these to a dimen would be cosmetic churn with zero
+behavior change.
+
+## Key decisions, and why
+
+### No font-family change on either platform
+
+iOS uses `.systemFont` (San Francisco) everywhere; Android's `Type.kt` already uses
+`FontFamily.Default` (Roboto). Both are already using their OS's default font, which is correct —
+bundling one platform's typeface onto the other would look foreign to users of that platform, and
+neither app has ever shipped a custom font asset. Out of scope by design, not an oversight.
+
+### `timing_title_text_size` restored after initially removing it
+
+Confirmed via code search that this dimen (and `SensitivityScreen`'s main title in general) wasn't
+used by any Compose screen — the title uses `BasicText`/`TextAutoSize` instead. Removed it as dead
+weight, which broke `processDebugResources`: `styles.xml`'s `TimingSensitivityTitle` style (a
+leftover from the pre-Compose, XML-View era — this app is 100% Compose today, no XML layouts) still
+references it via `autoSizeMaxTextSize`. That style is itself unreferenced by any layout or Kotlin
+code, but Android's resource linker doesn't care whether a *referencing* resource is itself dead —
+it only cares that the reference resolves. Restored the dimen rather than also hunting down and
+deleting the legacy XML style tree, which is a separate, broader "dead pre-Compose resource
+cleanup" concern outside this ticket's scope.
+
+### Screen-scoped dimen names over one shared generic one
+
+For the three genuine `18.sp` hardcodes (three unrelated screens, coincidentally the same value),
+named each dimen after its own screen/role (`edit_category_menu_action_text_size`,
+`edit_category_phrases_item_text_size`, `head_tracking_paused_text_size`) rather than introducing
+one shared `action_text_size`-style constant. Matches this codebase's existing naming convention
+(`edit_categories_title_text_size`, `settings_button_text_size`, etc., all screen-prefixed) and
+keeps each screen independently tunable later without an unrelated screen's needs colliding.
+
+### Added tablet overrides even where sibling dimens don't have one
+
+`settings_button_text_size` and `edit_categories_list_text_size` (pre-existing, untouched) have no
+`values-sw600dp` override either — the codebase's existing tablet-scaling coverage is itself
+incomplete. Chose to add tablet overrides for every dimen touched or created in this pass anyway:
+the point of this ticket is closing tablet-scaling gaps, so shipping a new dimen that reproduces the
+same gap (mechanically compliant with "use dimens.xml," but not actually fixing the user-facing
+issue) would defeat the purpose. Pre-existing dimens with the same gap were left alone — fixing
+every instance of that separate, smaller issue across the whole app is out of scope here.
+
+## Verification
+
+`./gradlew compileDebugKotlin testDebugUnitTest` and
+`./gradlew assembleDebug assembleDebugAndroidTest` all pass. Not done: manual verification in a
+running app/emulator (none available in this session) and no design sign-off.
+
+## Pointers
+
+- Issue: #681, branch `feature/681/typography-parity` (linked via `createLinkedBranch`, branched
+ from `main`).
+- iOS reference: `willowtreeapps/vocable-ios`, `Vocable/Extensions/UIFont+Helpers.swift`,
+ `Vocable/Common/VocableNavigationBar.swift`, `Vocable/Common/PresetItemCollectionViewCell.swift`,
+ `Vocable/Common/Views/EmptyStateView.swift`.
diff --git a/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt
index b844c3fd0..67bad125e 100644
--- a/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt
+++ b/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt
@@ -155,7 +155,8 @@ private fun EditCategoryMenuContent(
text = categoryName,
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.Bold,
- color = TextColor
+ color = TextColor,
+ fontSize = dimensionResource(id = R.dimen.settings_edit_individual_category_text_size).value.sp
),
textAlign = TextAlign.Center,
modifier = Modifier
@@ -184,7 +185,7 @@ private fun EditCategoryMenuContent(
text = stringResource(R.string.rename_category),
color = TextColor,
fontWeight = FontWeight.SemiBold,
- fontSize = 18.sp,
+ fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
@@ -208,7 +209,7 @@ private fun EditCategoryMenuContent(
text = stringResource(R.string.show_category),
color = TextColor,
fontWeight = FontWeight.SemiBold,
- fontSize = 18.sp,
+ fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
@@ -244,7 +245,7 @@ private fun EditCategoryMenuContent(
text = stringResource(R.string.edit_phrases),
color = TextColor,
fontWeight = FontWeight.SemiBold,
- fontSize = 18.sp,
+ fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
@@ -276,7 +277,7 @@ private fun EditCategoryMenuContent(
text = stringResource(R.string.remove_category),
color = Color.White,
fontWeight = FontWeight.Bold,
- fontSize = 18.sp,
+ fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
diff --git a/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt
index 80a8e7d2a..7e72c1408 100644
--- a/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt
+++ b/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt
@@ -358,7 +358,7 @@ private fun PhraseEditItem(
text = phraseText,
color = TextColor,
fontWeight = FontWeight.SemiBold,
- fontSize = 18.sp,
+ fontSize = dimensionResource(id = R.dimen.edit_category_phrases_item_text_size).value.sp,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
diff --git a/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt
index 4f629fe80..c21cabf7c 100644
--- a/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt
+++ b/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -113,7 +114,7 @@ private fun ErrorBanner() {
text = stringResource(id = R.string.head_tracking_paused_message),
color = Color.White,
fontWeight = FontWeight.Bold,
- fontSize = 18.sp
+ fontSize = dimensionResource(id = R.dimen.head_tracking_paused_text_size).value.sp
)
}
}
diff --git a/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt
index 2f7538858..8279dee40 100644
--- a/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt
+++ b/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.integerResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringArrayResource
@@ -40,6 +41,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import com.willowtree.vocable.R
import com.willowtree.vocable.ui.base.MviScreen
import com.willowtree.vocable.ui.components.GazeButton
@@ -115,6 +117,8 @@ fun KeyboardContent(
) {
val keys = stringArrayResource(id = R.array.keyboard_keys)
val numColumns = integerResource(id = R.integer.keyboard_columns)
+ val inputTextSize = dimensionResource(id = R.dimen.keyboard_input_text_size).value.sp
+ val keyTextSize = dimensionResource(id = R.dimen.keyboard_text_size).value.sp
val isLandscapeOrTablet = numColumns > 5
@@ -165,6 +169,7 @@ fun KeyboardContent(
Text(
text = inputText.ifEmpty { stringResource(R.string.keyboard_select_letters) },
style = MaterialTheme.typography.headlineMedium.copy(
+ fontSize = inputTextSize,
fontWeight = FontWeight.Bold,
color = if (inputText.isEmpty()) Color.Gray else Color.White
),
@@ -178,12 +183,14 @@ fun KeyboardContent(
Text(
stringResource(R.string.keyboard_select_letters),
style = MaterialTheme.typography.headlineMedium.copy(
+ fontSize = inputTextSize,
fontWeight = FontWeight.Bold,
color = Color.Gray
)
)
},
textStyle = MaterialTheme.typography.headlineMedium.copy(
+ fontSize = inputTextSize,
fontWeight = FontWeight.Bold,
color = Color.White
),
@@ -206,8 +213,8 @@ fun KeyboardContent(
autoCorrectEnabled = true
),
keyboardActions = KeyboardActions(
- onDone = {
- onSpeak()
+ onDone = {
+ onSpeak()
}
)
)
@@ -324,6 +331,7 @@ fun KeyboardContent(
Text(
text = inputText.ifEmpty { stringResource(R.string.keyboard_select_letters) },
style = MaterialTheme.typography.headlineMedium.copy(
+ fontSize = inputTextSize,
fontWeight = FontWeight.Bold,
color = if (inputText.isEmpty()) Color.Gray else Color.White
),
@@ -337,12 +345,14 @@ fun KeyboardContent(
Text(
stringResource(R.string.keyboard_select_letters),
style = MaterialTheme.typography.headlineMedium.copy(
+ fontSize = inputTextSize,
fontWeight = FontWeight.Bold,
color = Color.Gray
)
)
},
textStyle = MaterialTheme.typography.headlineMedium.copy(
+ fontSize = inputTextSize,
fontWeight = FontWeight.Bold,
color = Color.White
),
@@ -365,8 +375,8 @@ fun KeyboardContent(
autoCorrectEnabled = true
),
keyboardActions = KeyboardActions(
- onDone = {
- onSpeak()
+ onDone = {
+ onSpeak()
}
)
)
@@ -507,7 +517,11 @@ fun KeyboardContent(
.weight(1f)
.fillMaxHeight()
) {
- Text(text = key, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold)
+ Text(
+ text = key,
+ style = MaterialTheme.typography.headlineLarge.copy(fontSize = keyTextSize),
+ fontWeight = FontWeight.Bold
+ )
}
}
if (rowKeys.size < numColumns) {
diff --git a/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt
index 3ca8fa84b..39cbe43f7 100644
--- a/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt
+++ b/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt
@@ -110,7 +110,10 @@ private fun SensitivityContent(
// Hover Time Section
Text(
text = stringResource(id = R.string.hover_time_text),
- style = MaterialTheme.typography.titleLarge.copy(color = TextColor),
+ style = MaterialTheme.typography.titleLarge.copy(
+ color = TextColor,
+ fontSize = dimensionResource(id = R.dimen.timing_subtitle_text_size).value.sp
+ ),
modifier = Modifier.constrainAs(hoverTitleRef) {
top.linkTo(backButtonRef.bottom, margin = 32.dp)
start.linkTo(parent.start)
@@ -143,7 +146,10 @@ private fun SensitivityContent(
Text(
text = dwellTimeText,
- style = MaterialTheme.typography.headlineMedium.copy(color = TextColor),
+ style = MaterialTheme.typography.headlineMedium.copy(
+ color = TextColor,
+ fontSize = dimensionResource(id = R.dimen.hover_time_text_size).value.sp
+ ),
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
@@ -161,7 +167,10 @@ private fun SensitivityContent(
// Cursor Sensitivity Section
Text(
text = stringResource(id = R.string.cursor_sensitivity_text),
- style = MaterialTheme.typography.titleLarge.copy(color = TextColor),
+ style = MaterialTheme.typography.titleLarge.copy(
+ color = TextColor,
+ fontSize = dimensionResource(id = R.dimen.timing_subtitle_text_size).value.sp
+ ),
modifier = Modifier.constrainAs(cursorTitleRef) {
top.linkTo(hoverControlsRef.bottom, margin = 32.dp)
start.linkTo(parent.start)
diff --git a/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt
index 20f137219..012c82d36 100644
--- a/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt
+++ b/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt
@@ -185,7 +185,8 @@ fun VoiceSelectionScreen(
Text(
text = stringResource(R.string.voice_settings_change_voice),
style = MaterialTheme.typography.headlineMedium.copy(
- fontWeight = FontWeight.Bold
+ fontWeight = FontWeight.Bold,
+ fontSize = dimensionResource(id = R.dimen.settings_title_text_size).value.sp
)
)
diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml
index 8e0e20365..6aed58682 100644
--- a/app/src/main/res/values-sw600dp/dimens.xml
+++ b/app/src/main/res/values-sw600dp/dimens.xml
@@ -103,6 +103,7 @@
44dp
48sp
34sp
+ 34sp
88dp
44dp
@@ -116,6 +117,10 @@
44dp
48sp
76dp
+ 42sp
+ 24sp
+ 24sp
+ 24sp
56dp
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index b5b9953e0..aaa9b1fc8 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -42,7 +42,7 @@
16dp
88dp
40dp
- 34sp
+ 28sp
8dp
24dp
80dp
@@ -182,6 +182,9 @@
8dp
30sp
80dp
+ 18sp
+ 18sp
+ 18sp
45dp
8dp
10dp
@@ -214,6 +217,10 @@
16dp
8dp
16dp
+
24sp
24sp
24sp