Skip to content

feat(settings): make typo Corrections a progressive-disclosure hierarchy#702

Merged
FuJacob merged 1 commit into
mainfrom
feat/corrections-progressive-disclosure
Jun 12, 2026
Merged

feat(settings): make typo Corrections a progressive-disclosure hierarchy#702
FuJacob merged 1 commit into
mainfrom
feat/corrections-progressive-disclosure

Conversation

@FuJacob

@FuJacob FuJacob commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

The Writing pane rendered "Hide Suggestions on Typo", "Offer Corrections on Typo", and "Automatically Fix Typos" as three flat peers, with the latter two (and the dictionary picker) merely dimmed when the first was off. The gate relationship was invisible, so toggling off "Hide Suggestions on Typo" silently killed the other controls with nothing to explain why (it briefly read as a freeze).

This reframes the section as the master/dependent progressive-disclosure idiom already used by EmojiPaneView: the gate lives alone in a Typos section; the Corrections actions appear only once it is on; the Spelling Dictionaries picker appears only once a correction action that can consume it is on. Revealing each level when it becomes relevant, instead of dimming dependents in place, makes the dependency unmistakable.

Validation

  • swiftlint lint --quiet — exit 0
  • xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' buildBUILD SUCCEEDED
  • Rendered the new WritingPaneView layout offscreen via NSHostingView across all three disclosure states and inspected each:
    • Gate off: only the Typos section, with copy telling the user enabling it unlocks corrections. No dimmed orphans.
    • Gate on: Corrections section appears with both actions, fully interactive.
    • A correction action on: Spelling Dictionaries section appears with its own header (no double title) and the checkboxes.
Gate off Gate on Fully disclosed
Typos only Typos + Corrections Typos + Corrections + Spelling Dictionaries

Linked issues

None.

Risk / rollout notes

  • Presentation only: no change to SuggestionSettingsModel, the persisted settings, or the typo/correction runtime behavior. The same three booleans drive the same gate logic; only their disclosure changes.
  • Row titles are unchanged, so the Settings search index (SettingsIndex) keeps resolving these rows. Section headers are cosmetic and not referenced by search.
  • SpellingDictionaryPicker dropped its inline "Spelling Dictionaries" title and its self-disabling logic; it is now always rendered live and gated purely by the parent's visibility condition. Its only caller is WritingPaneView.

Greptile Summary

This PR converts the flat "Corrections" section in WritingPaneView into a three-level progressive-disclosure hierarchy: a Typos gate section always shown, a Corrections section that appears only once the gate is on, and a Spelling Dictionaries section that appears only once at least one correction action is active. SpellingDictionaryPicker drops its own inline heading and disabled() guard in favour of pure parent-controlled visibility.

  • The restructuring is presentation-only: SuggestionSettingsModel, all three persisted booleans, and the runtime TypoGate logic are untouched.
  • The implementation correctly mirrors the EmojiPaneView master/dependent idiom already established in the codebase, and all four SettingsIndex entries remain registered so Settings search still resolves each row.

Confidence Score: 4/5

The change is pure UI restructuring with no model, persistence, or runtime logic touched — low risk to merge.

All three typo-related settings — and the dictionary picker — are conditionally removed from the view hierarchy when their gate is off. This means a Settings search that resolves one of those items while the gate is off navigates to the Writing pane but cannot scroll to or pulse the target row, leaving the user stranded with no visual feedback. The rest of the change is clean and follows the established EmojiPaneView pattern.

The conditional rendering blocks in WritingPaneView.swift (lines 94–123) are the area to examine for the search-navigation edge case.

Important Files Changed

Filename Overview
Cotabby/UI/Settings/Panes/WritingPaneView.swift Restructured typo/correction/dictionary controls from a flat disabled-peer layout into a three-level progressive-disclosure hierarchy using conditional if blocks and separate Section headings. No model or binding logic changed.
Cotabby/UI/SpellingDictionaryPicker.swift Removed the dictionariesAffectCorrections guard and its .disabled() call, and dropped the inline "Spelling Dictionaries" heading; gating is now fully delegated to the parent's conditional render.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["WritingPaneView renders"] --> B["Section: Typos\n(always visible)\n─────────────────\nHide Suggestions on Typo toggle"]
    B --> C{suppressCompletionsOnTypo?}
    C -- "false (gate off)" --> D["No further typo sections rendered\n(Corrections + Dictionaries hidden)"]
    C -- "true (gate on)" --> E["Section: Corrections\n─────────────────\nOffer Corrections on Typo toggle\nAutomatically Fix Typos toggle"]
    E --> F{offerTypoCorrections\nor automaticallyFixTypos?}
    F -- "false (neither on)" --> G["Spelling Dictionaries section hidden"]
    F -- "true (at least one on)" --> H["Section: Spelling Dictionaries\n─────────────────\nSpellingDictionaryPicker\n(always live — no disabled logic)"]
Loading

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(settings): make typo Corrections a ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

The Writing pane showed "Hide Suggestions on Typo", "Offer Corrections on
Typo", and "Automatically Fix Typos" as three flat peers, with the latter two
merely dimmed when the first was off. The gate relationship was invisible:
turning off "Hide Suggestions on Typo" silently killed the other two and the
dictionary picker, with nothing to explain why.

Reframe the section as the master/dependent idiom already used by EmojiPaneView.
The gate now lives alone in a "Typos" section; the "Corrections" actions appear
only once it is on; the "Spelling Dictionaries" picker appears only once a
correction action that can consume them is on. Revealing each level when it
becomes relevant (instead of dimming dependents in place) makes the dependency
unmistakable. SpellingDictionaryPicker loses its now-redundant inline title and
self-disabling logic since the enclosing Section supplies the header and the
parent gates visibility.
@FuJacob FuJacob force-pushed the feat/corrections-progressive-disclosure branch from 80ed2af to 7790127 Compare June 12, 2026 14:59
@FuJacob FuJacob merged commit 464dda8 into main Jun 12, 2026
@FuJacob FuJacob deleted the feat/corrections-progressive-disclosure branch June 12, 2026 14:59
Comment on lines +94 to +123
if suggestionSettings.suppressCompletionsOnTypo {
Section("Corrections") {
Toggle(isOn: offerTypoCorrectionsBinding) {
SettingsRowLabel(
title: "Offer Corrections on Typo",
description: "Shows a green replacement you can apply with your accept key.",
systemImage: "checkmark.bubble"
)
}
.settingsItem(.offerTypoCorrections)

Toggle(isOn: automaticallyFixTyposBinding) {
SettingsRowLabel(
title: "Automatically Fix Typos",
description: "After you press Space, replaces a misspelled word without requiring your accept key.",
systemImage: "checkmark.circle"
)
Toggle(isOn: automaticallyFixTyposBinding) {
SettingsRowLabel(
title: "Automatically Fix Typos",
description: "After you press Space, replaces a misspelled word without requiring your accept key.",
systemImage: "checkmark.circle"
)
}
.settingsItem(.automaticallyFixTypos)
}
.disabled(!suggestionSettings.suppressCompletionsOnTypo)
.settingsItem(.automaticallyFixTypos)

Divider()

SpellingDictionaryPicker(suggestionSettings: suggestionSettings)
.settingsItem(.spellingDictionaries)
// Dictionaries rank candidates for the two correction actions above, so they only
// appear once at least one action is on. With neither active, choosing a dictionary
// would have no observable effect.
if suggestionSettings.offerTypoCorrections || suggestionSettings.automaticallyFixTypos {
Section("Spelling Dictionaries") {
SpellingDictionaryPicker(suggestionSettings: suggestionSettings)
.settingsItem(.spellingDictionaries)
}
}

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 Search-navigation silently drops when dependent rows are hidden

When suppressCompletionsOnTypo is false, the .offerTypoCorrections, .automaticallyFixTypos, and .spellingDictionaries anchors are not in the view hierarchy. If the user finds one of those items via Settings search while the gate is off, SettingsNavigationModel.reveal(_:) correctly switches to the Writing pane, but SettingsPaneScaffold's scrollTo call targets a non-existent anchor and silently no-ops — no scroll and no arrival pulse. The user lands on the pane with nothing highlighted and no indication of why.

With the old flat layout, all three rows were always rendered (just .disabled()), so the scroll target was always present. This PR removes that guarantee. The same edge case exists today in EmojiPaneView for emoji sub-settings, so this is consistent with the existing pattern — but it's worth noting since the typo sub-settings are more likely to be searched independently (e.g., a user searching "Spelling Dictionaries" to choose a language before realising the gate exists).

A lightweight mitigation used elsewhere is to gate search result surfacing on the dependent condition, or to auto-enable the gate when search navigates directly to a dependent item.

Fix in Codex Fix in Claude Code

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