Skip to content

feat: add content width preference for the details screen (desktop)#646

Merged
rainxchzed merged 9 commits into
OpenHub-Store:mainfrom
nitheeshdr:feat/633-content-width-preference
May 20, 2026
Merged

feat: add content width preference for the details screen (desktop)#646
rainxchzed merged 9 commits into
OpenHub-Store:mainfrom
nitheeshdr:feat/633-content-width-preference

Conversation

@nitheeshdr
Copy link
Copy Markdown
Contributor

@nitheeshdr nitheeshdr commented May 19, 2026

What

Adds a Content width setting under Settings → Appearance (desktop only) that controls the maximum column width of the details screen. Three options: Compact (480 dp), Wide (680 dp — the current default, unchanged), Extra wide (no max-width cap, fills the window).

Why

Issue #633 requests features that bring the desktop experience closer to github.com. One concrete and scoped item is the compact / wide / extra-wide reading column GitHub.com itself exposes. Today the details screen is hardcoded to 680 dp regardless of window size; on large monitors that leaves a lot of empty space that power users may prefer to use.

The setting follows the same pattern as every other desktop-only preference in the codebase (getScrollbarEnabled, getFontTheme, etc.):

  • ContentWidth enum in core/domain/model/
  • getContentWidth / setContentWidth in TweaksRepository and TweaksRepositoryImpl
  • LocalContentWidth composition local (same approach as LocalScrollbarEnabled)
  • Plumbed through MainViewModel → MainState → Main → AppNavigation → CompositionLocalProvider
  • Consumed in DetailsRoot replacing the two hardcoded widthIn(max = 680.dp) calls
  • ContentWidthCard added to the Appearance section of Tweaks, gated on getPlatform() != Platform.ANDROID
  • English strings added; translations welcome in follow-up PRs

How to test

Desktop only:

  1. Open Settings → Appearance.
  2. A "Content width" card should appear below the Scrollbar toggle.
  3. Select Compact → details screen content column narrows to ~480 dp.
  4. Select Wide → current behavior (680 dp max), unchanged.
  5. Select Extra wide → content fills the full window width.
  6. Setting persists across restarts.
  7. Android builds are unaffected — the card is not rendered on Android.

Addresses part of #633

Summary by CodeRabbit

  • New Features

    • Added a customizable "Content Width" setting in Appearance with presets: Compact, Wide, Extra Wide.
    • Selected width is applied across the app (including details screens and layout/navigation) to adjust content column limits.
    • The setting is persisted across sessions.
  • Localization

    • Added localized strings for the new setting in multiple languages.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Caution

Review failed

Failed to post review comments

Walkthrough

Adds a persisted content-width UI setting (COMPACT, WIDE, EXTRA_WIDE) from domain model through repository persistence into app state, published via a CompositionLocal, surfaced in Tweaks UI, and applied to details screen width constraints.

Changes

Content Width Setting Feature

Layer / File(s) Summary
Domain model, repository contract & resources
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ContentWidth.kt, core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt, core/presentation/src/commonMain/composeResources/values/strings.xml, core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ContentWidthUtil.kt, core/presentation/src/commonMain/composeResources/values-*/strings-*.xml
ContentWidth enum (COMPACT, WIDE, EXTRA_WIDE) with fromName(String?); TweaksRepository adds getContentWidth() and setContentWidth(...); added localized strings and a ContentWidth.displayName composable extension.
Persistence implementation
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
Implements getContentWidth() and setContentWidth(...) reading/writing K_CONTENT_WIDTH via KSafe with migration gating and adds the storage key constant.
State model extensions
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt, feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt
MainState and TweaksState add a contentWidth: ContentWidth property (defaulting to COMPACT).
ViewModel hydration & action handling
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt, feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt, feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt
MainViewModel collects repository flow into state; TweaksViewModel loads contentWidth on start and handles OnContentWidthSelected to persist via repository; TweaksAction adds the selection action.
Composition local & navigation threading
core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalContentWidth.kt, composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt, composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt
Adds LocalContentWidth composition local (default COMPACT), threads contentWidth into AppNavigation which provides it via CompositionLocalProvider, and Main passes state.contentWidth into navigation.
Tweaks appearance selection UI
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt
Adds ContentWidthCard composable showing title/description and selectable options; selection dispatches TweaksAction.OnContentWidthSelected.
Details screen width layout application
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
DetailsRoot reads LocalContentWidth.current, maps enum to a max Dp constraint, and applies it via Modifier.widthIn(max = contentWidthDp) to scrollable content and the list.

Sequence Diagram(s)

sequenceDiagram
  participant TweaksUI as Tweaks UI (ContentWidthCard)
  participant TweaksVM as TweaksViewModel
  participant Repo as TweaksRepository
  participant MainVM as MainViewModel
  participant State as AppState
  participant DetailUI as Details UI

  TweaksUI->>TweaksVM: OnContentWidthSelected(width)
  TweaksVM->>Repo: setContentWidth(width)
  Repo-->>Repo: persist K_CONTENT_WIDTH
  Repo-->>MainVM: getContentWidth() Flow emission
  MainVM->>State: update contentWidth
  State-->>DetailUI: LocalContentWidth provides width
  DetailUI->>DetailUI: map width to dp and apply Modifier.widthIn(max)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rainxchzed

Poem

🐰 I nudged a width switch, soft and neat,
Compact for coziness, Wide for more seat.
Extra Wide stretches like fields in light,
Saved in a burrow so settings stay right.
Hop, click, persisted — view tuned just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a content width preference feature for the details screen on desktop platforms.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR adds a "Content Width" preference to the Appearance settings (desktop only) that controls the maximum column width in the details screen, offering Compact, Wide, and Extra Wide options. The implementation follows established codebase patterns: a domain enum, repository getter/setter, composition local, and ViewModel plumbing.

  • ContentWidth enum added to core/domain; getContentWidth/setContentWidth wired through TweaksRepository and collected in both MainViewModel and TweaksViewModel. The preference is propagated via LocalContentWidth composition local in AppNavigation.
  • DetailsScreen replaces both hardcoded widthIn(max = 680.dp) calls with the preference-driven value; an outer Box gains a .scrollable modifier so gutter areas also scroll the list.
  • ContentWidthCard UI added to the Appearance section behind the existing getPlatform() != Platform.ANDROID guard; localised strings provided for all 12 supported locales.

Confidence Score: 3/5

The dp values assigned to each preset in DetailsRoot (COMPACT→680dp, WIDE→960dp) are inverted relative to the PR description and the original hardcoded default, and every fallback path—fromName, LocalContentWidth, MainState, TweaksState, AppNavigation—uses COMPACT as the initial value rather than WIDE.

The core mapping between enum entries and pixel values is wrong in DetailsRoot, and the preference falls back to the wrong preset on first launch. Together these mean the feature does not match its specification for any user.

DetailsRoot.kt (the when-block mapping ContentWidth to dp values), ContentWidth.kt (fromName fallback), MainState.kt and TweaksState.kt (field default), AppNavigation.kt and LocalContentWidth.kt (default parameter / composition local default).

Important Files Changed

Filename Overview
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ContentWidth.kt New domain enum defining COMPACT/WIDE/EXTRA_WIDE; fromName fallback and default values are inconsistent with the PR-stated Wide = current default intent.
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt Replaces two hardcoded 680dp widthIn calls with preference-driven value; pixel values for COMPACT (680dp) and WIDE (960dp) are transposed relative to the PR description (480/680dp).
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt Correctly implements getContentWidth/setContentWidth using gatedGetFlow and ksafe.safePut, matching the pattern of every other setting in this file.
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt Adds contentWidth field defaulting to COMPACT; the PR description states Wide (680dp) is the unchanged current default, making COMPACT the wrong initial value for upgrade users.
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt Adds contentWidth field defaulting to COMPACT; same wrong-default concern as MainState.
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt ContentWidthCard correctly gated inside the existing getPlatform() != Platform.ANDROID block; UI pattern mirrors ThemeSelectionCard with per-option text labels; no issues found.
core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ContentWidthUtil.kt Correctly defines displayName as a @composable extension in the presentation layer, matching the AppTheme.displayName pattern.

Reviews (7): Last reviewed commit: "cr: drop reverseDirection on gutter scro..." | Re-trigger Greptile

Comment thread core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ContentWidth.kt Outdated
Comment on lines +27 to +50
* reaches [CrashReporter], preventing a spurious crash dump.
*
* Trade-off: macOS VoiceOver may miss updates on those removed nodes for the
* remainder of the session. Remove once the upstream fix lands (track against
* Compose MP 1.11+).
*
* See [GitHub-Store#330](https://github.com/OpenHub-Store/GitHub-Store/issues/330)
* and [GitHub-Store#640](https://github.com/OpenHub-Store/GitHub-Store/issues/640).
*/
object A11yCrashGuard {
// Separate flags per path so each path logs its first suppression independently.
private val warnedEdt = AtomicBoolean(false)
private val warnedUncaught = AtomicBoolean(false)

// Must be called after CrashReporter.install() so the uncaught-exception handler
// chain is: A11yCrashGuard (filter) -> CrashReporter (log + dump) -> JVM default.
fun install() {
val osName = System.getProperty("os.name")?.lowercase().orEmpty()
if (!osName.contains("mac")) return

// Path 1: NPE propagates out of the coroutine dispatcher and through the
// AWT EventQueue dispatch chain.
Toolkit.getDefaultToolkit().systemEventQueue.push(FilteringEventQueue())

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 Unrelated fix bundled into a feature PR

The A11yCrashGuard changes (adding the uncaught-exception handler for the coroutine-failure path, promoting isComposeA11yNpe to a shared method, and referencing issue #640) are independent of the content-width preference. Neither the PR title nor the description mentions them. Separating unrelated fixes into their own PRs makes bisection, revert, and review much easier.

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!

Fix in Claude Code

nitheeshdr and others added 2 commits May 19, 2026 21:52
Follows the AppTheme.displayName pattern — removes the hardcoded English
string from the domain enum and replaces it with a @composable extension
property in core/presentation/utils that calls stringResource(), making
the labels translatable.
…h-preference

# Conflicts:
#	core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
Comment on lines +515 to +519
val contentWidthDp = when (LocalContentWidth.current) {
ContentWidth.COMPACT -> 680.dp
ContentWidth.WIDE -> 960.dp
ContentWidth.EXTRA_WIDE -> androidx.compose.ui.unit.Dp.Unspecified
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 The pixel values are swapped relative to every description of the feature. The PR description states Compact = 480 dp and Wide = 680 dp (the current unchanged default), but the code maps COMPACT → 680 dp and WIDE → 960 dp. Selecting "Compact" currently gives the same width as the old hardcoded default, while "Wide" widens the column beyond the original 680 dp cap, so the setting is functionally mislabeled for the first two options.

Suggested change
val contentWidthDp = when (LocalContentWidth.current) {
ContentWidth.COMPACT -> 680.dp
ContentWidth.WIDE -> 960.dp
ContentWidth.EXTRA_WIDE -> androidx.compose.ui.unit.Dp.Unspecified
}
val contentWidthDp = when (LocalContentWidth.current) {
ContentWidth.COMPACT -> 480.dp
ContentWidth.WIDE -> 680.dp
ContentWidth.EXTRA_WIDE -> androidx.compose.ui.unit.Dp.Unspecified
}

Fix in Claude Code

Comment on lines +18 to 19
val contentWidth: ContentWidth = ContentWidth.COMPACT,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 The default is COMPACT but the PR description explicitly says "Wide (680 dp — the current default, unchanged)." Any user who upgrades without a stored preference will have the layout read COMPACT from fromName(null) and, once the pixel values are corrected to 480/680, will silently see a narrowed 480 dp column instead of the expected 680 dp. The default must be WIDE to preserve existing behaviour. The same applies to TweaksState.

Suggested change
val contentWidth: ContentWidth = ContentWidth.COMPACT,
)
val contentWidth: ContentWidth = ContentWidth.WIDE,
)

Fix in Claude Code

;

companion object {
fun fromName(name: String?): ContentWidth = entries.find { it.name == name } ?: COMPACT
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 The fromName fallback returns COMPACT, so a first-launch user (no stored key) also starts on Compact instead of Wide. Changing the sentinel to WIDE here means every path that reads the preference falls back to the correct pre-existing default.

Suggested change
fun fromName(name: String?): ContentWidth = entries.find { it.name == name } ?: COMPACT
fun fromName(name: String?): ContentWidth = entries.find { it.name == name } ?: WIDE

Fix in Claude Code

@rainxchzed rainxchzed merged commit 6bc9a91 into OpenHub-Store:main May 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants