Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@shub39
Copy link
Owner

@shub39 shub39 commented Aug 22, 2025

Summary by CodeRabbit

  • New Features / UI

    • Updated screens with Material 3 large/flexible top bars, nested scrolling, modern Material icons, refined FABs, and new Analytics cards (blurred preview with unlock action).
  • Bug Fixes

    • RTL-aware padding/layout adjustments, smoother reorder/drag timing, and simplified bottom navigation without rounded clipping.
  • Chores

    • Dependency versions bumped and material icons switched to the extended package.
  • Removal

    • Legacy Tasks guide view removed.

Signed-off-by: shub39 <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Aug 22, 2025

Walkthrough

Renames several top-level screen composables to Graph variants, moves many files from plural components packages into consolidated component / ui.component / ui.section packages, updates imports/previews, refactors UI scaffolding (FlexibleTopAppBar, nested scroll, reorderable lists, FABs), adjusts NavHost padding and bottom-bar clipping, and bumps dependency versions.

Changes

Cohort / File(s) Summary
App navigation & wiring
app/src/main/java/com/shub39/grit/app/Grit.kt, app/src/main/java/com/shub39/grit/app/MainActivity.kt
Replaced Tasks/Habits/Settings usages with TasksGraph/HabitsGraph/SettingsGraph; NavHost padding changed to be layout-direction-aware (start/end/bottom); bottom bar clip/rounded corners removed; InitialLoading import path updated.
Core components (package rename)
app/src/main/java/com/shub39/grit/core/presentation/component/*
.../ColorPickerDialog.kt, Empty.kt, GritBottomSheet.kt, GritDialog.kt, PageFill.kt, InitialLoading.kt
Moved files from ...components...component; mostly package-line updates. PageFill signature changed to accept a BoxScope receiver composable and its Box invocation updated.
Settings module reorg
app/src/main/java/com/shub39/grit/core/presentation/settings/*
.../SettingsGraph.kt, ui/component/AboutApp.kt, ui/section/*
Renamed SettingsSettingsGraph; moved settings files to settings.ui.component / settings.ui.section; updated imports to new core component paths and Material icon substitutions.
Habits module rework
app/src/main/java/com/shub39/grit/habits/presentation/*
.../HabitsGraph.kt, ui/section/*, ui/component/*, util.kt
HabitsHabitsGraph; removed some PageFill wrappers; reorganized packages to ui.section/ui.component; added AnalyticsCard; replaced painterResource icons with Material vector icons; added FlexibleTopAppBar, nested scroll, reorderable lists, FAB adjustments.
Tasks module rework & theme change
app/src/main/java/com/shub39/grit/tasks/presentation/*
.../TasksGraph.kt, ui/section/*, ui/component/*, (deleted) component/TasksGuide.kt
TasksTasksGraph; Theme gained seedColor property and preview theme updated; moved TaskList/EditCategories to ui.section and TaskCard to ui.component; UI refactors: FlexibleTopAppBar, nested scroll, icon vectorization, reordering logic; TasksGuide.kt removed.
Habits UI components added/changed
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt, .../HabitCard.kt
New AnalyticsCard composable (blur gating for locked content + unlock button); HabitCard moved and converted to vector icons, bold title, layout/padding tweaks.
Tasks UI components moved/refined
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt, .../TaskList.kt, .../ui/component/TaskCard.kt
Packages moved to ui.section/ui.component; icons migrated to vector variants; nested scroll and FlexibleTopAppBar patterns added; reordering and drag-handle logic adjusted; some functions now expression-bodied returning PageFill.
ViewModel timing tweak
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt
Added kotlinx.coroutines.delay and inserted 200ms delays after upsert/delete category before updating currentCategory.
Gradle / dependency catalog
gradle/libs.versions.toml, app/build.gradle.kts
Bumped multiple dependency versions (AGP, Kotlin, Compose, Material3, reorderable, navigation, etc.); removed material-icons-core, added material-icons-extended; app dependency switched accordingly.
Minor / cosmetic
app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt
Trailing newline removed; no behavioral changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant MainActivity
  participant Grit as GritNavHost
  participant TasksGraph
  participant HabitsGraph
  participant SettingsGraph

  MainActivity->>Grit: initialize(app state, startDestination)
  Grit->>Grit: calculate padding (start/end via LocalLayoutDirection, bottom)
  Note right of Grit #DDFFDD: Bottom bar uses plain NavigationBar (no rounded clip)
  alt Navigate → Tasks
    Grit->>TasksGraph: render(state, onAction, nav callbacks)
  else Navigate → Habits
    Grit->>HabitsGraph: render(state, onAction)
  else Navigate → Settings
    Grit->>SettingsGraph: render(state, onAction)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ✨ revenuecat #115 — Overlaps on Grit.kt changes and navigation/composable wiring; likely touches the same call-site updates.
  • ✨ Tweaks #125 — Related to Theme API changes (seedColor) and screens that now use the updated Theme.
  • 🔧 Tweaks #111 — Modifies analytics UI and AnalyticsCard usage; related to the new AnalyticsCard and analytics pages.

Poem

I hop through folders, tidy and spry,
I rename graphs and watch imports fly.
PageFill now nests with BoxScope grace,
Top bars stretch, icons find their place.
A rabbit cheers — the UI’s set to fly. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch state-changes

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Signed-off-by: shub39 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (1)

80-89: Bug: Random colors will change on every recomposition (flicker).
Colors for each habit should be stable. Seed the RNG per habit and remember the lines.

-    val weeklyGraphData = state.habitsWithStatuses.entries.map { entry ->
-        val habit = entry.key
-        val statuses = entry.value
-        val color = Color(
-            red = Random.nextFloat(),
-            green = Random.nextFloat(),
-            blue = Random.nextFloat()
-        ).harmonize(primary, true).fixIfDisliked()
-
-        Line(
-            label = habit.title,
-            values = prepareLineChartData(state.startingDay, statuses),
-            color = SolidColor(color),
+    val weeklyGraphData = remember(state.habitsWithStatuses.keys.toList(), primary) {
+        state.habitsWithStatuses.entries.map { (habit, statuses) ->
+            val seed = habit.hashCode() // or use habit.id hash if available
+            val rnd = Random(seed)
+            val color = Color(
+                red = rnd.nextFloat(),
+                green = rnd.nextFloat(),
+                blue = rnd.nextFloat()
+            ).harmonize(primary, true).fixIfDisliked()
+
+            Line(
+                label = habit.title,
+                values = prepareLineChartData(state.startingDay, statuses),
+                color = SolidColor(color),
                 dotProperties = DotProperties(
                     enabled = false,
                     color = SolidColor(color),
                     strokeWidth = 4.dp,
                     radius = 7.dp
                 ),
                 firstGradientFillColor = color.copy(alpha = 0.8f),
                 secondGradientFillColor = Color.Transparent,
                 popupProperties = PopupProperties(
                     enabled = false
                 ),
                 drawStyle = DrawStyle.Stroke(
                     width = 3.dp,
                     strokeStyle = StrokeStyle.Normal
                 )
-        )
-    }
+            )
+        }
+    }
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)

281-299: Description field validates the wrong variable.

Both the label and isError for the description OutlinedTextField reference newHabitName instead of newHabitDescription. This breaks validation and UX.

             OutlinedTextField(
                 value = newHabitDescription,
                 shape = MaterialTheme.shapes.medium,
                 modifier = Modifier.fillMaxWidth(),
                 keyboardOptions = KeyboardOptions.Default.copy(
                     capitalization = KeyboardCapitalization.Sentences,
                     imeAction = ImeAction.Done
                 ),
                 onValueChange = { newHabitDescription = it },
                 label = {
-                    if (newHabitName.length <= 50) {
+                    if (newHabitDescription.length <= 50) {
                         Text(text = stringResource(id = R.string.description))
                     } else {
                         Text(text = stringResource(id = R.string.too_long))
                     }
                 },
-                isError = newHabitName.length > 50
+                isError = newHabitDescription.length > 50
             )
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (2)

155-161: Likely missing import for draggableHandle extension (build break risk)

Modifier.draggableHandle() is provided by sh.calvin.reorderable.draggableHandle and needs an explicit import. Without it, this won’t compile.

Apply this import:

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle

154-162: Avoid “clickable but does nothing” drag handle for accessibility

Using IconButton with onClick = {} creates a focusable/clickable target that performs no action. Prefer a plain Icon (or non-clickable container) with the draggable handle modifier.

Apply this diff:

-                                IconButton(
-                                    modifier = Modifier.draggableHandle(),
-                                    onClick = {}
-                                ) {
-                                    Icon(
-                                        painter = painterResource(R.drawable.baseline_drag_indicator_24),
-                                        contentDescription = null
-                                    )
-                                }
+                                Icon(
+                                    painter = painterResource(R.drawable.baseline_drag_indicator_24),
+                                    contentDescription = null,
+                                    modifier = Modifier.draggableHandle()
+                                )
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (3)

119-121: Avoid force-unwrapping (!!) — handle missing habit gracefully

currentHabit/statuses use !! which will crash if analyticsHabitId is absent or stale. Prefer a safe lookup and early return.

Apply this diff:

-    val currentHabit = state.habitsWithStatuses.keys.find { it.id == state.analyticsHabitId }!!
-    val statuses = state.habitsWithStatuses[currentHabit]!!
+    val currentHabit = state.habitsWithStatuses.keys.firstOrNull { it.id == state.analyticsHabitId }
+        ?: return@PageFill
+    val statuses = state.habitsWithStatuses[currentHabit].orEmpty()

122-126: Derived UI data stored in plain vars and updated in LaunchedEffect will not recompose

lineChartData/weekDayData/currentStreak/bestStreak are plain locals; updating them inside LaunchedEffect doesn’t trigger recomposition. Use remember with keys instead and drop the effect.

Apply this diff:

-    var lineChartData = prepareLineChartData(state.startingDay, statuses)
-    var weekDayData = prepareWeekDayData(statuses.map { it.date }, primary)
-    var currentStreak = countCurrentStreak(statuses.map { it.date })
-    var bestStreak = countBestStreak(statuses.map { it.date })
+    val lineChartData = remember(statuses, state.startingDay) {
+        prepareLineChartData(state.startingDay, statuses)
+    }
+    val weekDayData = remember(statuses, primary) {
+        prepareWeekDayData(statuses.map { it.date }, primary)
+    }
+    val currentStreak = remember(statuses) { countCurrentStreak(statuses.map { it.date }) }
+    val bestStreak = remember(statuses) { countBestStreak(statuses.map { it.date }) }
@@
-    LaunchedEffect(statuses) {
-        lineChartData = prepareLineChartData(state.startingDay, statuses)
-        currentStreak = countCurrentStreak(statuses.map { it.date })
-        bestStreak = countBestStreak(statuses.map { it.date })
-        weekDayData = prepareWeekDayData(statuses.map { it.date }, primary)
-    }
+    // Derived values above will recompute when their keys change; no effect needed.

Also applies to: 143-148


505-513: Nullable today leaks into non-null usages — comparison/ChronoUnit calls won’t compile

The helper composables accept today: LocalDate? but then use it in comparisons and ChronoUnit without null checks. Make today non-null in signatures (it’s always non-null at call site).

Apply this diff:

-private fun CalendarMap(
+private fun CalendarMap(
     state: HabitPageState,
     onAction: (HabitsPageAction) -> Unit,
     calendarState: CalendarState,
     statuses: List<HabitStatus>,
-    today: LocalDate?,
+    today: LocalDate,
     currentHabit: Habit,
     primary: Color
 ) {
-private fun WeeklyBooleanHeatMap(
+private fun WeeklyBooleanHeatMap(
     heatMapState: HeatMapCalendarState,
     statuses: List<HabitStatus>,
-    today: LocalDate?,
+    today: LocalDate,
     onAction: (HabitsPageAction) -> Unit,
     currentHabit: Habit,
     primary: Color
 ) {
-private fun HabitStats(
+private fun HabitStats(
     currentHabit: Habit,
-    today: LocalDate?,
+    today: LocalDate,
     currentStreak: Int,
     bestStreak: Int
 ) {

Also applies to: 664-672, 772-777

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/LookAndFeelPage.kt (1)

115-123: Compile error: use Icons.AutoMirrored.Filled.ArrowBack (not Default).

The imported symbol is androidx.compose.material.icons.automirrored.filled.ArrowBack; the correct usage is Icons.AutoMirrored.Filled.ArrowBack.

-                    Icon(
-                        imageVector = Icons.AutoMirrored.Default.ArrowBack,
-                        contentDescription = "Navigate Back"
-                    )
+                    Icon(
+                        imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                        contentDescription = "Navigate Back"
+                    )
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

224-228: Reorder callback applies moves to the wrong source list.

Using tasks.toMutableList() discards prior in-drag mutations and can produce incorrect final order. Apply moves to reorderableTasks instead.

-                        rememberReorderableLazyListState(lazyListState) { from, to ->
-                            reorderableTasks = tasks.toMutableList().apply {
-                                add(to.index, removeAt(from.index))
-                            }
-                        }
+                        rememberReorderableLazyListState(lazyListState) { from, to ->
+                            reorderableTasks = reorderableTasks.toMutableList().apply {
+                                add(to.index, removeAt(from.index))
+                            }
+                        }

592-600: Bug: incorrect index when adding to a different category.

Index is derived from the current category, not the selected newTaskCategoryId. This yields wrong ordering when adding to another category from the sheet.

-                                index = state.tasks[state.currentCategory]?.size ?: 0
+                                index = state.tasks.entries
+                                    .firstOrNull { it.key.id == newTaskCategoryId }
+                                    ?.value?.size ?: 0
🧹 Nitpick comments (36)
app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt (2)

13-13: Nit: add a trailing newline at EOF.

Keeps diffs cleaner and satisfies common linters/ktlint rules.


9-12: Guard against in-place mutations of collections in @immutable state.

Because Map/List are read-only views, accidental in-place mutation of their backing data won’t be detected by Compose. Ensure producers always replace these with new instances (copy-on-write) rather than mutating in place. If this has bitten us before, consider persistent collections (kotlinx.collections.immutable) as a follow-up.

Would you like me to scan call sites to detect any .add, .remove, or casting-to-mutable on these properties?

app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)

11-13: Fix extra space and make PageFill more reusable (modifier + BoxScope).

  • Remove the double space after fun.
  • Expose modifier and alignment so callers can customize without wrapping.
  • Accept BoxScope.() -> Unit to enable alignment APIs inside the slot.

Apply this diff:

@@
-// Wrapper to center everything
+// Wrapper to center everything
 @Composable
-fun  PageFill(
-    content: @Composable () -> Unit
-) {
-    Box(
-        modifier = Modifier.fillMaxSize(),
-        contentAlignment = Alignment.Center
-    ) { content() }
+fun PageFill(
+    modifier: Modifier = Modifier,
+    alignment: Alignment = Alignment.Center,
+    content: @Composable androidx.compose.foundation.layout.BoxScope.() -> Unit
+) {
+    Box(
+        modifier = modifier.fillMaxSize(),
+        contentAlignment = alignment,
+        content = content
+    )
 }
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (2)

27-28: Remove unused experimental opt-ins.

This composable doesn’t reference APIs gated by ExperimentalFoundationApi or ExperimentalMaterial3ExpressiveApi. Keeping the opt-in widens the experimental surface area unnecessarily.

Apply this diff:

-@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 fun TaskCard(

And remove the corresponding imports:

-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi

29-35: Add sensible defaults for modifier/shape and make reorderIcon optional.

Improves ergonomics and reduces boilerplate at call sites without breaking existing usages.

Apply this diff:

 fun TaskCard(
     task: Task,
     dragState: Boolean = false,
-    reorderIcon: @Composable () -> Unit,
-    shape: Shape,
-    modifier: Modifier
+    reorderIcon: @Composable () -> Unit = {},
+    shape: Shape = CardDefaults.shape,
+    modifier: Modifier = Modifier
 )
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)

46-67: Provide a graceful pre-Android 12 (API < 31) fallback for the gated preview.

On older devices the content area is blank under the “Unlock Plus” button. Consider showing the content with an overlay scrim so users understand what’s being gated.

Apply this diff:

             if (!isUserSubscribed) {
                 Box(
                     contentAlignment = Alignment.Center,
                     modifier = Modifier
                         .height(200.dp)
                         .fillMaxWidth()
                 ) {
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-                        Box(
-                            modifier = Modifier
-                                .matchParentSize()
-                                .blur(
-                                    radius = 10.dp,
-                                    edgeTreatment = BlurredEdgeTreatment.Unbounded
-                                )
-                        ) { content() }
-                    }
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        Box(
+                            modifier = Modifier
+                                .matchParentSize()
+                                .blur(
+                                    radius = 10.dp,
+                                    edgeTreatment = BlurredEdgeTreatment.Unbounded
+                                )
+                        ) { content() }
+                    } else {
+                        // Fallback: show content with a dimmed scrim
+                        Box(modifier = Modifier.matchParentSize()) { content() }
+                        Box(
+                            modifier = Modifier
+                                .matchParentSize()
+                                .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.85f))
+                        )
+                    }
 
                     Button(
                         onClick = onPlusClick
                     ) { Text(text = stringResource(R.string.unlock_plus)) }
                 }

Add missing import if needed:

+import androidx.compose.foundation.background
app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt (2)

1-1: Typo in filename: rename to InitialLoading.kt.

Avoids confusion when searching and keeps naming consistent with the composable.

I can open a follow-up PR to rename the file if desired.


15-23: Allow modifier to be passed in for reuse and testability.

Enables callers to set size, background, or testTag without wrapping this composable.

Apply this diff:

 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
-fun InitialLoading() {
-    Box(
-        modifier = Modifier
+fun InitialLoading(modifier: Modifier = Modifier) {
+    Box(
+        modifier = modifier
             .fillMaxSize()
             .background(MaterialTheme.colorScheme.background),
         contentAlignment = Alignment.Center
     ) {
         ContainedLoadingIndicator()
     }
 }
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (3)

172-188: Reduce O(n) scans in day rendering by precomputing a date set.
dayContent currently scans statusList 3x per day cell. Precompute a HashSet for O(1) lookups.

@@
-    val weekState = rememberWeekCalendarState(
+    val weekState = rememberWeekCalendarState(
         startDate = habit.time.toLocalDate().minusMonths(12),
         endDate = LocalDate.now(),
         firstVisibleWeekDate = LocalDate.now(),
         firstDayOfWeek = startingDay
     )
+
+    // Precompute for O(1) membership checks in the calendar
+    val doneDates = remember(statusList) {
+        statusList.map { it.date }.toHashSet()
+    }
@@
-            dayContent = { weekDay ->
-                val done = statusList.any { it.date == weekDay.date }
+            dayContent = { weekDay ->
+                val done = weekDay.date in doneDates
@@
-                                val donePrevious =
-                                    statusList.any { it.date == weekDay.date.minusDays(1) }
-                                val doneAfter =
-                                    statusList.any { it.date == weekDay.date.plusDays(1) }
+                                val donePrevious = weekDay.date.minusDays(1) in doneDates
+                                val doneAfter = weekDay.date.plusDays(1) in doneDates

60-66: Nit: label name mismatch for animateColorAsState.
The debug label says "cardBackground" but this state is for content color.

-    val cardContent by animateColorAsState(
+    val cardContent by animateColorAsState(
         targetValue = when (completed) {
             true -> MaterialTheme.colorScheme.onPrimaryContainer
             else -> MaterialTheme.colorScheme.onSurface
         },
-        label = "cardBackground"
+        label = "cardContent"
     )

225-229: Localize weekday label instead of truncating enum name.
Using getDisplayName honors locale and proper casing.

-                        Text(
-                            text = weekDay.date.dayOfWeek.toString().take(3),
+                        Text(
+                            text = weekDay.date.dayOfWeek.getDisplayName(
+                                java.time.format.TextStyle.SHORT,
+                                java.util.Locale.getDefault()
+                            ),

Add imports if not present:

import java.time.format.TextStyle
import java.util.Locale
app/src/main/java/com/shub39/grit/habits/presentation/util.kt (2)

15-25: Fix naming for consistency: habitStatuses.
Minor readability improvement; positional call sites remain unaffected.

-fun prepareLineChartData(
-    firstDay: DayOfWeek,
-    habitstatuses: List<HabitStatus>
-): List<Double> {
+fun prepareLineChartData(
+    firstDay: DayOfWeek,
+    habitStatuses: List<HabitStatus>
+): List<Double> {
@@
-    val habitCompletionByWeek = habitstatuses
+    val habitCompletionByWeek = habitStatuses

42-68: Optionally align weekday bar order with user’s starting day.
This makes the bar chart match the calendar/week configuration.

-fun prepareWeekDayData(
-    dates: List<LocalDate>,
-    lineColor: Color
-): List<Bars> {
+fun prepareWeekDayData(
+    firstDay: DayOfWeek,
+    dates: List<LocalDate>,
+    lineColor: Color
+): List<Bars> {
@@
-    val weekdayBars = DayOfWeek.entries.map { dayOfWeek ->
+    val orderedDays = DayOfWeek.entries.dropWhile { it != firstDay } +
+        DayOfWeek.entries.takeWhile { it != firstDay }
+    val weekdayBars = orderedDays.map { dayOfWeek ->

Apply at the call site in OverallAnalytics.kt:

-    val weeklyBreakdownData =
-        prepareWeekDayData(state.habitsWithStatuses.values.flatten().map { it.date }, primary)
+    val weeklyBreakdownData = prepareWeekDayData(
+        state.startingDay,
+        state.habitsWithStatuses.values.flatten().map { it.date },
+        primary
+    )
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (1)

77-80: Cache heavy derived datasets with remember.
These recompute on every recomposition; memoize to cut work.

-    val heatMapData = prepareHeatMapData(state.habitsWithStatuses)
-    val weeklyBreakdownData =
-        prepareWeekDayData(state.habitsWithStatuses.values.flatten().map { it.date }, primary)
+    val heatMapData = remember(state.habitsWithStatuses) {
+        prepareHeatMapData(state.habitsWithStatuses)
+    }
+    val weeklyBreakdownData = remember(state.habitsWithStatuses, primary) {
+        // If you adopt the firstDay param change, add it here.
+        prepareWeekDayData(
+            state.habitsWithStatuses.values.flatten().map { it.date },
+            primary
+        )
+    }

If you implement the optional signature change suggested in util.kt, call it as:

prepareWeekDayData(state.startingDay, state.habitsWithStatuses.values.flatten().map { it.date }, primary)
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt (3)

72-76: Localize icon content descriptions (avoid hardcoded English).

Hardcoded contentDescription strings (“Navigate Back”, “Start”, “Done”, “Fail”) hinder i18n and consistency. Prefer stringResource. If keys don’t exist yet, please add them to strings.xml.

-                    Icon(
-                        imageVector = Icons.AutoMirrored.Default.ArrowBack,
-                        contentDescription = "Navigate Back"
-                    )
+                    Icon(
+                        imageVector = Icons.AutoMirrored.Default.ArrowBack,
+                        contentDescription = stringResource(R.string.navigate_back)
+                    )
@@
-                                ExportState.IDLE -> Icon(
-                                    painter = painterResource(R.drawable.round_play_arrow_24),
-                                    contentDescription = "Start"
-                                )
+                                ExportState.IDLE -> Icon(
+                                    painter = painterResource(R.drawable.round_play_arrow_24),
+                                    contentDescription = stringResource(R.string.start)
+                                )
@@
-                                ExportState.EXPORTED -> Icon(
-                                    imageVector = Icons.Default.CheckCircle,
-                                    contentDescription = "Done"
-                                )
+                                ExportState.EXPORTED -> Icon(
+                                    imageVector = Icons.Default.CheckCircle,
+                                    contentDescription = stringResource(R.string.done)
+                                )
@@
-                                    RestoreState.IDLE -> Icon(
-                                        painter = painterResource(R.drawable.round_play_arrow_24),
-                                        contentDescription = "Start"
-                                    )
+                                    RestoreState.IDLE -> Icon(
+                                        painter = painterResource(R.drawable.round_play_arrow_24),
+                                        contentDescription = stringResource(R.string.start)
+                                    )
@@
-                                    RestoreState.RESTORED -> Icon(
-                                        imageVector = Icons.Default.CheckCircle,
-                                        contentDescription = "Done"
-                                    )
+                                    RestoreState.RESTORED -> Icon(
+                                        imageVector = Icons.Default.CheckCircle,
+                                        contentDescription = stringResource(R.string.done)
+                                    )
@@
-                                    RestoreState.FAILURE -> Icon(
-                                        imageVector = Icons.Default.Warning,
-                                        contentDescription = "Fail"
-                                    )
+                                    RestoreState.FAILURE -> Icon(
+                                        imageVector = Icons.Default.Warning,
+                                        contentDescription = stringResource(R.string.fail)
+                                    )

Also applies to: 101-105, 112-116, 149-166


96-105: Allow re-export without forcing a manual reset.

After a successful export, the button remains disabled (only enabled on IDLE). Consider enabling unless EXPORTING so users can export again in the same session.

-                            enabled = state.backupState.exportState == ExportState.IDLE
+                            enabled = state.backupState.exportState != ExportState.EXPORTING

52-56: Persist selection across configuration changes; verify persistable URI permissions.

If the user selects a file and rotates the device, the remembered Uri resets. Use rememberSaveable. Also, ensure that restore logic takes persistable read permission if the Uri needs to be accessed beyond the immediate call.

+import androidx.compose.runtime.saveable.rememberSaveable
@@
-    var uri by remember { mutableStateOf<Uri?>(null) }
+    var uri by rememberSaveable { mutableStateOf<Uri?>(null) }

Would you like me to add a call to contentResolver.takePersistableUriPermission() in the ViewModel/onAction handler if long-lived access is required?

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/AboutLibraries.kt (1)

42-45: Localize the nav icon content description.

Replace the hardcoded “Navigate Back” with a stringResource for i18n consistency.

-                    Icon(
-                        imageVector = Icons.AutoMirrored.Default.ArrowBack,
-                        contentDescription = "Navigate Back"
-                    )
+                    Icon(
+                        imageVector = Icons.AutoMirrored.Default.ArrowBack,
+                        contentDescription = stringResource(R.string.navigate_back)
+                    )
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (4)

368-369: Fix off-by-one: title/description max length is inconsistent with labels.

The button enables at <20 and <50 while the labels consider <=20 and <=50 valid. Align to <= for both.

-                enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50,
+                enabled = newHabitName.isNotBlank() && newHabitName.length <= 20 && newHabitDescription.length <= 50,

85-86: Reduce unnecessary resets of the reorder list state.

Using remember(state) will reset reorderableHabits on any state change, even unrelated ones. Key it to the derived habits list instead.

-    var reorderableHabits by remember(state) { mutableStateOf(habits) }
+    var reorderableHabits by remember(habits) { mutableStateOf(habits) }

240-241: Stronger duplicate detection (trim + case-insensitive).

Prevents accidental duplicates like “Workout” vs “workout” or trailing spaces.

-        val isHabitPresent = { state.habitsWithStatuses.any { it.key.title == newHabitName } }
+        val isHabitPresent = {
+            val normalized = newHabitName.trim()
+            state.habitsWithStatuses.any { it.key.title.equals(normalized, ignoreCase = true) }
+        }

Optionally also trim the title when constructing Habit to keep stored data clean.


130-134: A11y: add contentDescription to the reorder handle icon.

Toggle button content is currently decorative to TalkBack. Provide a localized description.

-                            Icon(
-                                painter = painterResource(R.drawable.baseline_reorder_24),
-                                contentDescription = null
-                            )
+                            Icon(
+                                painter = painterResource(R.drawable.baseline_reorder_24),
+                                contentDescription = stringResource(R.string.reorder_items)
+                            )

If the string key doesn’t exist, please add it.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)

109-109: Minor readability nit: name the key param instead of shadowing with it

Using it twice (itemsIndexed’s lambda and key lambda) hurts readability.

-            itemsIndexed(categories, key = { _, it -> it.id }) { index, category ->
+            itemsIndexed(categories, key = { _, category -> category.id }) { index, category ->
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1)

63-69: Localize hardcoded strings for i18n/accessibility

“Grit”, “Discord”, and “Github” are hardcoded; prefer stringResource for localization and a11y consistency.

Example:

-                Text(
-                    text = "Grit",
+                Text(
+                    text = stringResource(R.string.app_name),
                 )
...
-                    onClick = { uriHandler.openUri("https://discord.gg/nxA2hgtEKf") }
+                    onClick = { uriHandler.openUri("https://discord.gg/nxA2hgtEKf") }
                 ) {
                     Icon(
-                        imageVector = FontAwesomeIcons.Brands.Discord,
-                        contentDescription = "Discord",
+                        imageVector = FontAwesomeIcons.Brands.Discord,
+                        contentDescription = stringResource(R.string.discord),
                     )
                 }
...
                     Icon(
-                        imageVector = FontAwesomeIcons.Brands.Github,
-                        contentDescription = "Github",
+                        imageVector = FontAwesomeIcons.Brands.Github,
+                        contentDescription = stringResource(R.string.github),
                     )

Please confirm R.string.app_name/discord/github exist; I can add them if needed.

Also applies to: 73-93, 100-135

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (2)

94-114: Prefer localized content descriptions for icons

Hardcoded contentDescription strings (“Grit Plus”, “Navigate”, “Backup”, “About Libraries”) should use stringResource to align with the rest of the screen.

Example:

-                        contentDescription = "Grit Plus"
+                        contentDescription = stringResource(R.string.grit_plus)

Also applies to: 244-263, 266-286, 289-304


60-63: Check biometric check timing across config changes

LaunchedEffect(Unit) runs once per composition; if context-dependent availability can change on configuration updates, consider keying on context as well.

-    LaunchedEffect(Unit) {
+    LaunchedEffect(context) {
         onAction(SettingsAction.OnCheckBiometric(context))
     }
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (3)

127-138: Calendar states may not reflect startingDay changes

rememberHeatMapCalendarState/rememberCalendarState are created once; if state.startingDay changes at runtime, the states may not update. Consider keying them on state.startingDay.

-    val heatMapState = rememberHeatMapCalendarState(
+    val heatMapState = remember(state.startingDay) {
+        rememberHeatMapCalendarState(
             startMonth = currentMonth.minusMonths(12),
             endMonth = currentMonth,
             firstVisibleMonth = currentMonth,
             firstDayOfWeek = state.startingDay
-    )
+        )
+    }
-    val calendarState = rememberCalendarState(
+    val calendarState = remember(state.startingDay) {
+        rememberCalendarState(
             startMonth = currentMonth.minusMonths(12),
             endMonth = currentMonth,
             firstVisibleMonth = currentMonth,
             firstDayOfWeek = state.startingDay
-    )
+        )
+    }

241-271: Localize content descriptions for icons

Hardcoded contentDescription strings (“Warning”, “Delete Habit”, “Edit Habit”, “Add Time”) should use stringResource for i18n and a11y.

Example:

-                contentDescription = "Delete Habit"
+                contentDescription = stringResource(R.string.delete_habit)

Also applies to: 421-455


275-285: Persist edit dialog state across config changes (optional)

Using rememberSaveable for editDialog/timePickerDialog/new* fields will preserve state on rotation/process death.

-    if (editDialog) {
-        var newHabitTitle by remember { mutableStateOf(currentHabit.title) }
+    if (editDialog) {
+        var newHabitTitle by rememberSaveable { mutableStateOf(currentHabit.title) }
         ...
-        var timePickerDialog by remember { mutableStateOf(false) }
+        var timePickerDialog by rememberSaveable { mutableStateOf(false) }

Note: add import androidx.compose.runtime.saveable.rememberSaveable.

app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (1)

62-67: Avoid stacking duplicate destinations by using launchSingleTop on navigate.

If users tap repeatedly, the typed destinations can stack in the back stack. Use launchSingleTop to keep a single instance.

-                onNavigateToOverallAnalytics = { navController.navigate(HabitRoutes.OverallAnalytics) },
-                onNavigateToAnalytics = { navController.navigate(HabitRoutes.HabitAnalytics) }
+                onNavigateToOverallAnalytics = {
+                    navController.navigate(HabitRoutes.OverallAnalytics) {
+                        launchSingleTop = true
+                    }
+                },
+                onNavigateToAnalytics = {
+                    navController.navigate(HabitRoutes.HabitAnalytics) {
+                        launchSingleTop = true
+                    }
+                }
app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (2)

73-76: Prevent duplicate subpages by using launchSingleTop in navigate calls.

Navigating from RootPage can push duplicates if tapped repeatedly.

-                onNavigateToLookAndFeel = { navController.navigate(SettingsRoutes.LookAndFeel) },
-                onNavigateToBackup = { navController.navigate(SettingsRoutes.Backup) },
-                onNavigateToAboutLibraries = { navController.navigate(SettingsRoutes.AboutLibraries) },
+                onNavigateToLookAndFeel = {
+                    navController.navigate(SettingsRoutes.LookAndFeel) { launchSingleTop = true }
+                },
+                onNavigateToBackup = {
+                    navController.navigate(SettingsRoutes.Backup) { launchSingleTop = true }
+                },
+                onNavigateToAboutLibraries = {
+                    navController.navigate(SettingsRoutes.AboutLibraries) { launchSingleTop = true }
+                },

103-119: Scope the preview to file only.

Mark Preview() as private to avoid unnecessarily exporting a top-level symbol.

-@Preview
-@Composable
-fun Preview() {
+@Preview
+@Composable
+private fun Preview() {
app/src/main/java/com/shub39/grit/app/Grit.kt (1)

75-87: Use graph startDestination for popUpTo to simplify and harden bottom-bar navigation.

Hard-coding popUpTo based on a setting can be brittle. Using the graph’s start destination is the canonical pattern and works even if the start page changes later.

-    val navigator = { route: Routes ->
-        if (currentRoute != route) {
-            navController.navigate(route) {
-                launchSingleTop = true
-                popUpTo(
-                    when (settingsState.startingPage) {
-                        Pages.Habits -> Routes.HabitsPages
-                        Pages.Tasks -> Routes.TaskPages
-                    }
-                ) { saveState = true }
-                restoreState = true
-            }
-        }
-    }
+    val navigator = { route: Routes ->
+        if (currentRoute != route) {
+            navController.navigate(route) {
+                launchSingleTop = true
+                // pop up to the graph's start destination and preserve state
+                popUpTo(navController.graph.startDestinationId) { saveState = true }
+                restoreState = true
+            }
+        }
+    }

Note: If you prefer typed popUpTo, you can keep the current API but drive the target from a single source of truth, or use findStartDestination().id for equivalent behavior.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

221-223: Stability tweak: derive remember key from category/tasks, not whole state.

remember(state) causes unnecessary resets of reorderableTasks on unrelated state changes.

-                    var reorderableTasks by remember(state) { mutableStateOf(tasks) }
+                    var reorderableTasks by remember(state.currentCategory, tasks) { mutableStateOf(tasks) }

490-494: No-op keyboardActions; either handle IME action or remove.

newTitle.plus("\n") and newTask.plus("\n") have no effect. Prefer onDone with ImeAction.Done, or drop keyboardActions.

-                keyboardOptions = KeyboardOptions.Default.copy(
-                    capitalization = KeyboardCapitalization.Sentences,
-                    imeAction = ImeAction.None
-                ),
-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTitle.plus("\n")
-                    }
-                ),
+                keyboardOptions = KeyboardOptions.Default.copy(
+                    capitalization = KeyboardCapitalization.Sentences,
+                    imeAction = ImeAction.Done
+                ),
+                keyboardActions = KeyboardActions(
+                    onDone = { /* optionally hide keyboard or advance focus */ }
+                ),
-                keyboardOptions = KeyboardOptions.Default.copy(
-                    capitalization = KeyboardCapitalization.Sentences,
-                    imeAction = ImeAction.None
-                ),
-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTask.plus("\n")
-                    }
-                ),
+                keyboardOptions = KeyboardOptions.Default.copy(
+                    capitalization = KeyboardCapitalization.Sentences,
+                    imeAction = ImeAction.Done
+                ),
+                keyboardActions = KeyboardActions(
+                    onDone = { /* optionally hide keyboard or submit */ }
+                ),

Also applies to: 579-582

app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)

27-28: Avoid symbol shadowing between route object and screen composable by aliasing the import

Both the route (TasksRoutes.EditCategories) and the composable screen (EditCategories) share the same simple name, which can be confusing in reviews and refactors. Aliasing the screen import keeps intent crystal clear.

Apply:

-import com.shub39.grit.tasks.presentation.ui.section.EditCategories
+import com.shub39.grit.tasks.presentation.ui.section.EditCategories as EditCategoriesScreen

And update the call site:

-            EditCategories(
+            EditCategoriesScreen(
                 state = state,
                 onAction = onAction,
                 onNavigateBack = { navController.navigateUp() }
             )

Also applies to: 67-73

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee047f and 4123e4a.

📒 Files selected for processing (27)
  • app/src/main/java/com/shub39/grit/app/Grit.kt (4 hunks)
  • app/src/main/java/com/shub39/grit/app/MainActivity.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/ColorPickerDialog.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/GritBottomSheet.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/GritDialog.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/AboutLibraries.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/LookAndFeelPage.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/util.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/component/TasksGuide.kt (0 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (4 hunks)
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/shub39/grit/tasks/presentation/component/TasksGuide.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/app/Grit.kt (3)
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)
  • TasksGraph (40-75)
app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (1)
  • SettingsGraph (43-101)
app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (1)
  • HabitsGraph (46-86)
🔇 Additional comments (25)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)

1-1: Package move looks good.

Matches the components→component reorg across the codebase.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)

1-1: Package move LGTM.

Matches the ui.component reorg; imports will resolve accordingly.

app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)

1-1: Package move LGTM.

Consistent with ui.component reorg; dependent imports in ui.section appear updated.

app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt (1)

1-1: Package move LGTM.

Aligns with components→component consolidation.

app/src/main/java/com/shub39/grit/app/MainActivity.kt (1)

118-126: Confirm UX on biometric cancellation (finishing the Activity).
On user cancel or negative button, you toast and then call finish(). If the intended UX is to keep the app open but locked, consider returning to the lock screen instead of exiting.

app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)

1-1: Package rename looks correct and consistent with the reorg.
No functional changes; call sites should continue to work with the new path.

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt (1)

1-1: Namespace/import migration looks consistent with the project reorg.

The package move and PageFill import update align with the plural→singular component consolidation. No functional changes introduced here.

Also applies to: 37-37

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/AboutLibraries.kt (1)

1-1: Package/import updates look good.

Matches the broader move to settings.ui.section and core.presentation.component.

Also applies to: 22-22

app/src/main/java/com/shub39/grit/core/presentation/component/ColorPickerDialog.kt (1)

1-1: Package move aligns with core component consolidation.

No functional changes; dialog composition and callbacks look correct.

app/src/main/java/com/shub39/grit/core/presentation/component/GritBottomSheet.kt (1)

1-1: Namespace update looks good; API remains stable.

Sheet configuration (max width, skip partial) and Column contract are sensible defaults.

app/src/main/java/com/shub39/grit/core/presentation/component/GritDialog.kt (1)

1-1: Package rename to singular namespace looks good

This aligns with the broader reorg (components → component). No functional changes; API remains stable.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (3)

1-1: Package move to ui.section is consistent with the reorganisation

Matches the new module layout; no API/behavior change introduced here.


52-55: Updated imports to singular component package

GritBottomSheet/GritDialog/PageFill import paths are correct post-move.


239-245: Ignore the UpdateCategory suggestion – AddCategory is already an upsert

The TaskPageAction.AddCategory action calls repo.upsertCategory, and because category.copy(name = name) preserves the existing id, it updates rather than duplicates. There is no existing UpdateCategory or RenameCategory, and introducing one isn’t necessary for correctness.

• In TaskPageAction.kt (lines 7–8), data class AddCategory(val category: Category) is the sole category‐upsert action.
• In TasksViewModel.kt (lines 59–63), is TaskPageAction.AddCategory invokes upsertCategory(action.category), which updates by id.
• If you find the name confusing, you could refactor AddCategoryUpsertCategory in a follow-up change, but it isn’t required now.

Likely an incorrect or invalid review comment.

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1)

1-1: Package path updated to settings.ui.component is consistent

Matches the settings reorg; usage in RootPage is updated accordingly.

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (1)

1-1: Namespace/import updates are correct

PageFill import moved to singular; AboutApp is correctly sourced from settings.ui.component.

Also applies to: 37-38, 40-41

app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (2)

22-29: Rename to HabitsGraph and component/ui.section import migration look consistent.

The API rename + typed navigation setup align with the PR’s re-organization and other graphs. No functional concerns here.

Also applies to: 47-50


139-145: No action required: HabitStatus.id has a default value
The HabitStatus data class declares id: Long = 0, so calling HabitStatus(habitId = habit.id, date = it.date) without supplying an id is valid and will default to 0. No changes are needed here.

app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (1)

20-27: Settings → SettingsGraph migration and component/ui.section imports look good.

Typed routes and PageFill usage are consistent with the rest of the app graphs.

Also applies to: 44-47, 114-117

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/LookAndFeelPage.kt (1)

1-1: Package + component import reorg looks correct.

Also applies to: 77-80

app/src/main/java/com/shub39/grit/app/Grit.kt (1)

36-38: Graph composable adoption (TasksGraph/SettingsGraph/HabitsGraph) is consistent and correct.

Also applies to: 157-161, 166-169, 178-181

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

70-73: Import reorg to component/ui and TaskCard new location looks good.

Also applies to: 79-79

app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (3)

117-129: LGTM: Preview updated to use TasksGraph after rename

The preview references the renamed composable and compiles the happy path interaction. Looks good.


22-22: ✅ Migration Complete — No Remaining Old Imports Found

I reran a repository-wide search for any import com.shub39.grit.core.presentation.components.PageFill references across all Kotlin files (excluding build artifacts) and found zero matches. The singular component.PageFill import is now consistently used throughout the codebase. No further action is needed.


41-45: Rename completed: no stale Tasks references found

Verified via project-wide search that there are

  • no remaining imports of com.shub39.grit.tasks.presentation.Tasks
  • no unqualified calls to Tasks(...)
  • all call sites now reference TasksGraph(...) (in TasksGraph.kt and Grit.kt)

The rename is fully applied—no further changes needed here.

import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.shub39.grit.R
import com.shub39.grit.core.presentation.components.InitialLoading
import com.shub39.grit.core.presentation.component.InitialLoading
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Import path update looks good; run a quick sweep for stale package paths.
The switch to core.presentation.component.InitialLoading aligns with the reorg.

Run this to catch any lingering old imports or misspellings:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Old plural namespace
rg -nP '\bcom\.shub39\.grit\.core\.presentation\.components\b' -g 'app/src/**' || true

# Old habits UI path (moved to ui.component)
rg -nP '\bcom\.shub39\.grit\.habits\.presentation\.component\b' -g 'app/src/**' || true

# Old tasks UI path (moved to ui.component)
rg -nP '\bcom\.shub39\.grit\.tasks\.presentation\.component\b' -g 'app/src/**' || true

# Ensure InitialLoading composable exists, and no leftover misspelling
rg -nP 'fun\s+InitialLoading\s*\(' -g 'app/src/**' || true
rg -nP 'InitialLoding' -g 'app/src/**' || true

Length of output: 488


Rename misspelled file for InitialLoading composable

All legacy import paths have been swept and none remain, but there’s a typo in the filename:

  • app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt
    → Rename to InitialLoading.kt to match the InitialLoading() composable and avoid confusion.
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/app/MainActivity.kt around line 18, the
import references com.shub39.grit.core.presentation.component.InitialLoading but
the actual source file is misspelled as InitialLoding.kt; rename the file
app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoding.kt
to InitialLoading.kt (update filesystem and VCS), and ensure the Kotlin file’s
class/composable name and any package declaration remain unchanged so the import
compiles correctly.

Comment on lines 65 to 66
import sh.calvin.reorderable.rememberReorderableLazyListState
import java.time.DayOfWeek
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing import for draggableHandle causes a compile error.

Modifier.draggableHandle is an extension; without its import, this file won’t compile.

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import sh.calvin.reorderable.rememberReorderableLazyListState
import java.time.DayOfWeek
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import sh.calvin.reorderable.draggableHandle
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt
around lines 65-66, the file is missing the import for the
Modifier.draggableHandle extension which causes a compile error; add the import
line for the extension (import sh.calvin.reorderable.draggableHandle) alongside
the other imports at the top of the file so the draggableHandle modifier
resolves.

import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Preview Theme: seedColor usage looks fine — confirm new Theme API is consistently adopted elsewhere

Setting isMaterialYou = false, seedColor = Color.Red, and paletteStyle = TonalSpot is fine for a deterministic preview. Two quick checks:

  • Ensure all Theme(...) constructor calls across the app are updated to pass the new seedColor (or rely on an intended default).
  • Confirm the Theme implementation actually uses seedColor when isMaterialYou = false so the preview reflects the intended palette.

Minor nit: if preview builds feel heavy, consider using a subtler brand color (or centralize a PreviewThemeDefaults.seedColor) to keep previews consistent.

Also applies to: 105-111


🏁 Script executed:

#!/bin/bash
# Where is Theme(...) constructed?
rg -nP --type=kt -g '!**/build/**' '\bTheme\s*\(' -C2

# Do those call sites include an explicit seedColor?
rg -nP --type=kt -g '!**/build/**' 'seedColor\s*=' -n

Length of output: 165


🏁 Script executed:

#!/bin/bash
# Find all calls to Theme(...) in Kotlin files outside the build directory, with context
rg -n '\bTheme\s*\(' -g '*.kt' -g '!**/build/**' -C2

# Find all explicit seedColor assignments in Kotlin files outside the build directory
rg -n 'seedColor\s*=' -g '*.kt' -g '!**/build/**'

Length of output: 4161


🏁 Script executed:

#!/bin/bash
# Display the contents of GritTheme.kt to verify seedColor usage logic
sed -n '1,200p' app/src/main/java/com/shub39/grit/core/presentation/theme/GritTheme.kt

Length of output: 1213


🏁 Script executed:

#!/bin/bash
# Print the contents of Theme.kt to inspect seedColor and paletteStyle parameters
sed -n '1,200p' app/src/main/java/com/shub39/grit/core/presentation/theme/Theme.kt

Length of output: 575


Preview Theme API adoption verified—update HabitsGraph preview for consistency

I’ve confirmed that the new Theme API is wired up correctly:

  • GritTheme.kt uses theme.seedColor whenever isMaterialYou = false, and only falls back to the system accent when isMaterialYou = true and on Android 12+.
  • All existing preview composers now explicitly pass seedColor and/or set isMaterialYou = false—with one exception below.

Please adjust the HabitsGraph preview so it too renders deterministically:

• File: app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (around lines 113–117)
Change from:

GritTheme(
    theme = Theme(
        appTheme = AppTheme.DARK,
        isMaterialYou = true
    )
) { … }

To something like:

GritTheme(
    theme = Theme(
        appTheme = AppTheme.DARK,
        isMaterialYou = false,
        seedColor = Color.Green,       // or your chosen preview brand color
        paletteStyle = ToneSpot
    )
) { … }

Minor nit: consider centralizing these preview defaults (e.g. PreviewThemeDefaults.seedColor) if you find yourself reusing the same colors.

🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt around
lines 113–117, the preview GritTheme still sets isMaterialYou = true and thus
renders non-deterministically; update the Theme passed to GritTheme to set
isMaterialYou = false, supply a fixed seedColor (e.g. Color.Green or your chosen
preview brand color) and set paletteStyle = ToneSpot so the preview renders
deterministically; optionally extract these preview defaults into a shared
constant (e.g., PreviewThemeDefaults.seedColor) if reused elsewhere.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
gradle/libs.versions.toml (2)

21-21: Jetpack Compose UI = "1.9.0" — verify this version exists and align with Navigation & Material3; consider migrating to the Compose BOM.

Hard-pinning individual Compose artifacts often creates drift. The BOM ensures consistent, supported coordinates across ui, material3, tooling, etc.

Option A — adopt Compose BOM in the version catalog and drop per-artifact versions:

  • Add a BOM alias (version managed once):

    [toml]
    [versions]
    composeBom = "REPLACE_WITH_PUBLISHED_BOM"

    [libraries]
    androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }

  • Remove version.ref from androidx-ui, ui-graphics, ui-tooling, ui-tooling-preview, material3, and navigation-compose, and import the BOM in Gradle:

    dependencies {
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation(libs.androidx.navigation.compose)
    debugImplementation(libs.androidx.ui.tooling)
    }

If you prefer to keep explicit versions, please confirm that androidx.compose.ui:ui:1.9.0 is published and compatible with your Material3 and Navigation versions using the script in the next comment. I can generate an exact BOM version once you confirm the target Compose train.


13-13: Alpha/stable mix: Material3 "1.5.0-alpha02" + Navigation "2.9.3" — double-check train compatibility.

Mixing alpha M3 with different generation Navigation and a possibly out-of-train Compose UI can cause API/ABI conflicts or missing symbols at runtime.

  • Prefer keeping Material3, Compose UI, and Navigation on the same release train (via BOM).
  • If you need an alpha M3 feature (e.g., Theme.seedColor previews), fix the train via BOM and only override the specific artifact if necessary, keeping others within the same snapshot line.

Use the dependency verification script below (next comment) to confirm these versions exist and co-resolve cleanly.

Also applies to: 16-16

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4123e4a and af7dac2.

📒 Files selected for processing (1)
  • gradle/libs.versions.toml (1 hunks)
🔇 Additional comments (3)
gradle/libs.versions.toml (3)

3-5: Third-party dependencies verified and published

All of the updated coordinates have been confirmed in their respective Maven repositories—no missing versions detected. Feel free to proceed with merging.


11-12: Kotlin & KSP Version Verification Complete

  • Verified on Maven Central:
    • org.jetbrains.kotlin:kotlin-gradle-plugin 2.2.10 ✅
    • com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin 2.2.10-2.0.2 ✅
    • com.google.devtools.ksp:symbol-processing-api 2.2.10-2.0.2 ✅

These artifacts are published and the Kotlin 2.2.10 / KSP 2.2.10-2.0.2 pairing is valid. No further changes are required here.


2-2: AGP 8.12.1 Verified and Compatible

Your script confirms that com.android.tools.build:gradle:8.12.1 is published on Google Maven and your Gradle wrapper is set to version 8.13. According to the official Android Gradle plugin release notes, AGP 8.12.x requires Gradle 8.13 as the minimum supported version (developer.android.com). You can safely retain

agp = "8.12.1"

in gradle/libs.versions.toml.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

221-228: Two fixes: initialize by category/tasks and reorder against the working list.

  • remember(state) risks holding stale data if state is referentially stable; key on category/tasks.
  • Reorder callback must mutate from the current working order, not the original tasks snapshot, or indices can drift after the first move.

Apply this diff:

-                    var reorderableTasks by remember(state) { mutableStateOf(tasks) }
+                    var reorderableTasks by remember(category, tasks) { mutableStateOf(tasks) }
@@
-                        rememberReorderableLazyListState(lazyListState) { from, to ->
-                            reorderableTasks = tasks.toMutableList().apply {
-                                add(to.index, removeAt(from.index))
-                            }
-                        }
+                        rememberReorderableLazyListState(lazyListState) { from, to ->
+                            reorderableTasks = reorderableTasks.toMutableList().apply {
+                                add(to.index, removeAt(from.index))
+                            }
+                        }

594-600: Bug: index calculated for the wrong category when adding a task.

If the user selects a different category in the sheet, the new task’s index should be based on newTaskCategoryId, not state.currentCategory. This can cause corrupted ordering in the target category.

Apply this diff:

                     onAction(
                         TaskPageAction.UpsertTask(
                             Task(
                                 categoryId = newTaskCategoryId,
                                 title = newTask,
                                 status = false,
-                                index = state.tasks[state.currentCategory]?.size ?: 0
+                                index = state.tasks.entries
+                                    .firstOrNull { it.key.id == newTaskCategoryId }
+                                    ?.value?.size ?: 0
                             )
                         )
                     )
app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt (1)

6-7: Fix missing Material3 Expressive dependency or remove its usage

It looks like you’re importing ContainedLoadingIndicator and opting into ExperimentalMaterial3ExpressiveApi, but there’s no material3-expressive artifact declared in your version catalog or Gradle scripts. As a result, the imports won’t resolve and the code will fail to compile.

• File app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt – lines 6–7:

-import androidx.compose.material3.ContainedLoadingIndicator
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.CircularProgressIndicator
@@
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@
-    ContainedLoadingIndicator()
+    CircularProgressIndicator()

If you truly need the expressive loading indicator, add the expressive artifact to your version catalog or Gradle dependencies (e.g. androidx.compose.material3:material3-expressive:<version>), otherwise switch to CircularProgressIndicator to avoid the experimental opt-in and missing-dependency issue.

♻️ Duplicate comments (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

270-278: Move completed task using the working list, not the original snapshot.

This recreates the list from tasks, which can discard in-flight reorders and misplace items. Operate on reorderableTasks instead. Also, prefer using the target list’s size when appending to the end to avoid off-by-one coupling to the old list size.

Apply this diff:

-                                                        reorderableTasks =
-                                                            tasks.toMutableList().apply {
-                                                                add(reorderableTasks.size - 1, removeAt(index))
-                                                            }
+                                                        reorderableTasks =
+                                                            reorderableTasks.toMutableList().apply {
+                                                                // remove from current position and append to end
+                                                                add(size, removeAt(index))
+                                                            }
🧹 Nitpick comments (6)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)

166-175: Avoid persisting the original snapshot on category switch.

onClick sends ReorderTasks(tasks.mapIndexed { ... }), which persists the original snapshot, not the user’s working order. Either:

  • Remove this call and rely on drag-stop/complete-tick flows to persist, or
  • Hoist reorderableTasks so it’s available here (or move the chips into the same scope) and persist the working list instead.

If you keep the flush here, ensure it uses the current working list. Otherwise, consider removing it to avoid persisting stale ordering.


490-494: No-op KeyboardActions handlers.

newTitle.plus("\n") / newTask.plus("\n") returns a new string and doesn’t change state. Either assign the result or remove the handler to avoid confusion. Given both text fields are single-line or have ImeAction.None, removing is fine.

Example cleanup:

-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTitle.plus("\n")
-                    }
-                ),
+                // No keyboardActions: single-line editing

Also applies to: 579-581


191-195: Localize content descriptions.

Hardcoded English strings (“Add Category”, “Edit Categories”, “Drag”, “Warning”, “Add”, “Edit”) should use stringResource(...) for i18n consistency with the rest of the screen.

Example:

- contentDescription = "Add Category"
+ contentDescription = stringResource(R.string.add_category)

Apply similarly to the other icons above.

Also applies to: 207-211, 245-247, 344-347, 389-391, 458-460, 546-548

app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt (3)

15-19: Expose a Modifier parameter for composability and reuse.

Standard Compose convention: accept modifier: Modifier = Modifier to let callers control size/placement without forking this composable.

Apply:

-@Composable
-fun InitialLoading() {
+@Composable
+fun InitialLoading(modifier: Modifier = Modifier) {
     Box(
-        modifier = Modifier
-            .fillMaxSize()
-            .background(MaterialTheme.colorScheme.background),
+        modifier = modifier
+            .fillMaxSize()
+            .background(MaterialTheme.colorScheme.background),
         contentAlignment = Alignment.Center
     ) {

22-22: Add basic a11y semantics so TalkBack announces progress.

Annotate the indicator with a polite live region and a descriptive label; this improves accessibility on the initial loading screen.

Apply:

-        ContainedLoadingIndicator()
+        ContainedLoadingIndicator(
+            modifier = Modifier.semantics {
+                contentDescription = "Loading"
+                liveRegion = LiveRegionMode.Polite
+            }
+        )

Add imports (outside the selected range):

import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics

Optionally, source the label from strings for localization and pass it as a parameter.


18-19: Minor theming nit: prefer surface over background for screen containers.

Using colorScheme.surface (or a Surface composable) better aligns with Material 3 tonal elevation. Not required if you intentionally want the raw background color.

Apply:

-            .background(MaterialTheme.colorScheme.background),
+            .background(MaterialTheme.colorScheme.surface),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between af7dac2 and 46e667e.

📒 Files selected for processing (2)
  • app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (4 hunks)
🔇 Additional comments (4)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)

70-73: Imports migration to core.presentation.component looks good.

The move from components → component aligns with the PR’s reorg and keeps call sites consistent.


78-78: TaskCard import from ui.component is consistent with new structure.

This keeps rendering concerns localized under ui.component. LGTM.


249-252: Correct: persist working order on drag stop.

Flushing reorderableTasks.mapIndexed { index to task } on drag end is the right moment to persist the visible order.

app/src/main/java/com/shub39/grit/core/presentation/component/InitialLoading.kt (1)

1-1: No residual “components” references found—singular “component” package confirmed

  • Executed ripgrep scans across the codebase for any imports or package declarations of
    com.shub39.grit.core.presentation.components and found zero matches.
  • All source files now correctly declare and import from
    com.shub39.grit.core.presentation.component.

The rename is consistent and safe to merge.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

223-227: Reorder callback mutates a stale snapshot (tasks) instead of the working list (reorderableTasks)

This will clobber any in-progress reorders and can misplace items. The move operation should start from reorderableTasks.

Apply one of the following diffs:

Minimal fix (keep current add/remove semantics):

-    rememberReorderableLazyListState(lazyListState) { from, to ->
-        reorderableTasks = tasks.toMutableList().apply {
-            add(to.index, removeAt(from.index))
-        }
-    }
+    rememberReorderableLazyListState(lazyListState) { from, to ->
+        val mutable = reorderableTasks.toMutableList()
+        val moved = mutable.removeAt(from.index)
+        // If moving down, adjust target index after removal.
+        val target = if (to.index > from.index) to.index - 1 else to.index
+        mutable.add(target, moved)
+        reorderableTasks = mutable
+    }

Or, if you have/accept a small helper (safer and clearer):

+fun <T> MutableList<T>.move(from: Int, to: Int) {
+    if (from == to) return
+    val item = removeAt(from)
+    add(if (to > from) to - 1 else to, item)
+}
-    rememberReorderableLazyListState(lazyListState) { from, to ->
-        reorderableTasks = tasks.toMutableList().apply {
-            add(to.index, removeAt(from.index))
-        }
-    }
+    rememberReorderableLazyListState(lazyListState) { from, to ->
+        reorderableTasks = reorderableTasks.toMutableList().apply { move(from.index, to.index) }
+    }

268-281: Reinsertion on status toggle uses tasks and mismatched indices

When marking a task completed, the code rebuilds from tasks.toMutableList() and then removes at index from the current reorderableTasks context. That index may not correspond to the same element in tasks, leading to incorrect removals and lost reorders.

Apply this diff:

-                                                if (task.status) {
-                                                    reorderableTasks =
-                                                        tasks.toMutableList().apply {
-                                                            add(
-                                                                reorderableTasks.size - 1,
-                                                                removeAt(index)
-                                                            )
-                                                        }
+                                                if (task.status) {
+                                                    reorderableTasks = reorderableTasks.toMutableList().apply {
+                                                        add(size - 1, removeAt(index))
+                                                    }
                                                     onAction(
                                                         TaskPageAction.ReorderTasks(
                                                             reorderableTasks.mapIndexed { index, task -> index to task })
                                                     )
                                                 }
🧹 Nitpick comments (5)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)

12-14: Nit: fix spacing and conventional type syntax in the signature

Minor polish for consistency with Kotlin/Compose style.

Apply this diff:

-@Composable
-fun  PageFill(
-    content: @Composable (BoxScope.() -> Unit)
-) {
+@Composable
+fun PageFill(
+    content: @Composable BoxScope.() -> Unit,
+) {
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)

220-224: Tighten the remember keys to avoid unnecessary resets of reorderableTasks

remember(state) will reset the working list on any state change (even unrelated to the current list), which can be jarring mid-edit. Scope it to the actual inputs.

Apply this diff:

-                var reorderableTasks by remember(state) { mutableStateOf(tasks) }
+                var reorderableTasks by remember(state.currentCategory, tasks) { mutableStateOf(tasks) }

If you want to persist local reorders until an explicit commit, consider:

var reorderableTasks by remember { mutableStateOf(tasks) }
LaunchedEffect(tasks) { reorderableTasks = tasks }

295-301: Prefer explicit size over padding-only Spacer

Spacer(modifier = Modifier.padding(60.dp)) can be confusing; a height-based spacer communicates intent.

Apply this diff:

-                        } else {
-                            Spacer(modifier = Modifier.padding(60.dp))
-                        }
+                        } else {
+                            Spacer(modifier = Modifier.size(60.dp))
+                        }

If you only need vertical spacing, use height(60.dp).


491-495: No-op KeyboardActions: remove or actually update the state

KeyboardActions(onAny = { newTitle.plus("\n") }) and newTask.plus("\n") do nothing because the result isn’t assigned. Also, with ImeAction.None, this likely never fires.

Apply this diff to remove the dead code:

-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTitle.plus("\n")
-                    }
-                ),
+                // No keyboardActions needed; multiline input handled by default.

and

-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTask.plus("\n")
-                    }
-                ),
+                // No keyboardActions needed; multiline input handled by default.

If your intent was to insert a newline on IME action, set an IME action and update the state: onDone = { newTitle += "\n" }.

Also applies to: 579-583

app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)

55-64: Consider consistent layering: wrap the Tasks graph with PageFill like Habits/Settings

HabitsGraph/SettingsGraph use PageFill { ... } at the graph level, while TasksGraph delegates PageFill to TaskList. Not a bug, but aligning the pattern keeps app scaffolding consistent and leaves sections free to focus on content.

Example:

-@Composable
-fun TasksGraph(
+@Composable
+fun TasksGraph(
     state: TaskPageState,
     onAction: (TaskPageAction) -> Unit
 ) {
-    val navController = rememberNavController()
+    val navController = rememberNavController()
+    PageFill {
       /* NavHost ... */
+    }
 }

If you do this, remove the PageFill wrapper from TaskList to avoid nesting Boxes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 46e667e and 5472ed7.

📒 Files selected for processing (6)
  • app/build.gradle.kts (1 hunks)
  • app/src/main/java/com/shub39/grit/app/Grit.kt (7 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (6 hunks)
  • gradle/libs.versions.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/build.gradle.kts
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/shub39/grit/app/Grit.kt (3)
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)
  • TasksGraph (39-74)
app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (1)
  • SettingsGraph (43-101)
app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (1)
  • HabitsGraph (46-86)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
  • TaskCard (27-88)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)
🔇 Additional comments (11)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)

13-19: Good API upgrade: BoxScope receiver enables alignment and intrinsics within content

Changing content to @Composable BoxScope.() -> Unit is the right move. It unlocks Modifier.align(...) and other BoxScope APIs for callers (e.g., that FAB aligned to BottomEnd in TaskList). Using content = content with contentAlignment is also clean.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

99-104: TopAppBar scroll behavior integration looks correct

Using TopAppBarDefaults.enterAlwaysScrollBehavior() and wiring it via Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection) on the scrolling container is the right pattern for a collapsing bar.


307-337: FAB overlay via BoxScope alignment works well

Placing the FAB with Modifier.align(Alignment.BottomEnd) inside PageFill’s BoxScope is clean and keeps it above the scrolling content. Good use of AnimatedVisibility to gate it by currentCategory.

app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (2)

40-44: Rename to TasksGraph and route structure look good

The sealed routes + NavHost setup is tidy. Swapping in TaskList and navigating to EditCategories through typed routes is straightforward.


104-110: Preview: deterministic theme configuration is appreciated

Using isMaterialYou = false, seedColor = Color.Red, and TonalSpot produces stable previews. Matches recent theming guidance across the project.

app/src/main/java/com/shub39/grit/app/Grit.kt (4)

138-143: RTL-aware padding: nice upgrade

Switching to calculateStartPadding/calculateEndPadding ensures proper insets in RTL locales. Good attention to detail.


97-129: Bottom navigation simplification looks solid

Using NavigationBar without the previous rounded-clip keeps the layout simpler. Icons/labels are set correctly with alwaysShowLabel = false.


154-167: Graph adoption wired correctly

TasksGraph, SettingsGraph, and HabitsGraph are all invoked with the correct states and actions after the renames.


36-39: All old composable references have been removed

I performed a comprehensive code-wide search for any calls or imports of the legacy Tasks, Settings, and Habits composables and found no matches—there are no lingering references to the old names. This item can be closed.

gradle/libs.versions.toml (2)

2-5: LGTM: Dependency version updates look reasonable.

The version bumps appear to be incremental updates that should maintain compatibility. The changes include:

  • AGP 8.11.1→8.12.1 (patch update)
  • Kotlin 2.2.0→2.2.10 (patch update)
  • Material3 1.4.0-alpha17→1.5.0-alpha02 (minor alpha update)
  • Compose 1.8.3→1.9.0 (minor update)
  • Other incremental updates to calendar, composeCharts, purchases, etc.

Also applies to: 11-14, 16-16, 18-18, 21-22


14-14: Reorderable bumped to 3.0.0 – LazyList API unchanged

I’ve reviewed the v3.0.0 release notes and confirmed that the only breaking changes concern the non-lazy containers (ReorderableColumn/ReorderableRow) and grid auto-scroll behavior—neither of which is used in our codebase (github.com). The official v3.0.0 README still shows the exact same rememberReorderableLazyListState + ReorderableItem pattern we use in both TaskList.kt and HabitsList.kt (github.com). No migration changes are needed for our LazyColumn-based reorderable lists.

compose = "1.8.3"
materialIconsCore = "1.7.3"
compose = "1.9.0"
materialIconsExtended = "1.7.8"
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify compatibility of the new material icons extended library.

The addition of materialIconsExtended = "1.7.8" and the corresponding library androidx-material-icons-extended suggests a migration from a previous icons library. The AI summary mentions that material-icons-core was removed.

Also applies to: 27-27


🏁 Script executed:

#!/bin/bash
# Description: Verify that the new material icons extended library is properly used and replaces the old one
# Expected: Find usages of the new library and ensure no references to the old material-icons-core remain

echo "=== Searching for usage of new material icons extended library ==="
rg -n "androidx.material.icons.extended" --type=gradle

echo "=== Searching for any remaining references to old material-icons-core ==="
rg -n "material.icons.core" --type=gradle

echo "=== Checking imports in Kotlin files for material icons ==="
rg -n "import.*material.*icons" --type=kotlin -C2

echo "=== Verifying the library is actually used in build.gradle files ==="
rg -n "libs\.androidx\.material\.icons\.extended" --type=gradle

Length of output: 10406


🏁 Script executed:

#!/bin/bash
# Display content around the materialIconsExtended version and library aliases
FILE="gradle/libs.versions.toml"
if [ -f "$FILE" ]; then
  echo "=== First 100 lines of $FILE ==="
  sed -n '1,100p' "$FILE"
  echo
  echo "=== Searching for 'extended' in $FILE ==="
  rg -n "extended" "$FILE"
  echo
  echo "=== Searching for 'icons' in $FILE ==="
  rg -n "icons" "$FILE"
else
  echo "Error: $FILE not found"
fi

echo
# Search for alias usage in the codebase
echo "=== Searching for 'materialIconsExtended' references in code ==="
rg -n "materialIconsExtended" -C2 .

echo
# Search for dependency inclusion in Gradle build files
echo "=== Searching for 'material-icons-extended' in build scripts ==="
rg -n "material-icons-extended" -C2 --glob="*.gradle*" .

Length of output: 5965


🏁 Script executed:

#!/bin/bash
echo "=== Searching for core alias in build scripts ==="
rg -n "libs\.androidx\.material\.icons\.core" -C2 --glob="*.gradle*"

echo
echo "=== Searching for literal core artifact usage ==="
rg -n "androidx\.compose\.material:material-icons-core" -C2 --glob="*.gradle*"

echo
echo "=== Searching for 'material-icons-core' in all files ==="
rg -n "material-icons-core" -C2 .

Length of output: 548


Action Required: Material Icons Extended alias defined but not applied

The materialIconsExtended = "1.7.8" version and its androidx-material-icons-extended alias are correctly declared in gradle/libs.versions.toml (lines 22–27), but no module-level build files reference this new library. As a result, the extended icon set isn’t actually added to the classpath. To complete the migration:

• gradle/libs.versions.toml (lines 22–27)

  • Alias is present but unused.

• All module-level build.gradle(.kts) files

  • Under dependencies { … }, add:
    dependencies {
    +   implementation(libs.androidx.material.icons.extended)
        // other dependencies…
    }

• Kotlin UI files importing icons

  • If you intend to use icons provided only by the extended library, update imports from the core namespace to the extended one. For example:
    - import androidx.compose.material.icons.filled.Create
    + import androidx.compose.material.icons.extended.filled.Create

• Cleanup old core dependency (if previously declared explicitly)

  • No explicit material-icons-core dependency was found, but verify that you’re not still relying on the legacy core artifact.

Please add the dependency in each module that needs extended icons and update imports accordingly to ensure the new library is actually used.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In gradle/libs.versions.toml around lines 22 to 22, the alias
materialIconsExtended = "1.7.8" is declared but not used; to fix, add the
extended icons dependency to every module that uses Material icons by adding the
corresponding entry under that module's dependencies block referencing the new
alias (e.g., implement/libraries using the libs alias for
androidx-material-icons-extended), update Kotlin Compose imports in UI files
that need extended icons from the core namespace (e.g.,
androidx.compose.material.icons.*) to the extended namespace (e.g.,
androidx.compose.material.icons.extended.*), and remove or stop relying on any
explicit legacy/core material-icons dependency if present so the extended
artifact is actually used.

* updated icons to imagevectors
* used m3 expressive components

Signed-off-by: shub39 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

252-265: Moving a completed task must operate on reorderableTasks, not tasks, and should append to the end clearly.

You’re rebuilding from tasks (original snapshot) which can drop prior reorders; this also mirrors a previously reported issue. Prefer appending via add(element) for clarity.

Apply this diff:

-                                                if (task.status) {
-                                                    reorderableTasks =
-                                                        tasks.toMutableList().apply {
-                                                            add(
-                                                                reorderableTasks.size - 1,
-                                                                removeAt(index)
-                                                            )
-                                                        }
+                                                if (updated.status) {
+                                                    reorderableTasks =
+                                                        reorderableTasks.toMutableList().apply {
+                                                            // move the toggled item to the end
+                                                            add(removeAt(index))
+                                                        }
                                                     onAction(
                                                         TaskPageAction.ReorderTasks(
                                                             reorderableTasks.mapIndexed { index, task -> index to task })
                                                     )
                                                 }

Additionally, to avoid in-place mutation on a list element (better state semantics in Compose), update the toggle to use an immutable copy:

-                                            if (!editState) {
-                                                task.status = !task.status
-
-                                                onAction(TaskPageAction.UpsertTask(task))
+                                            if (!editState) {
+                                                val updated = task.copy(status = !task.status)
+                                                onAction(TaskPageAction.UpsertTask(updated))

Note: This also requires the guard to check updated.status as shown above.


206-211: Reordering uses the immutable snapshot (tasks) instead of the working list (reorderableTasks).

This resets any in-progress order and can misplace items during drag. Use reorderableTasks as the source of truth inside the reorder callback.

Apply this diff:

-                val reorderableListState =
-                    rememberReorderableLazyListState(lazyListState) { from, to ->
-                        reorderableTasks = tasks.toMutableList().apply {
-                            add(to.index, removeAt(from.index))
-                        }
-                    }
+                val reorderableListState =
+                    rememberReorderableLazyListState(lazyListState) { from, to ->
+                        reorderableTasks = reorderableTasks.toMutableList().apply {
+                            add(to.index, removeAt(from.index))
+                        }
+                    }
🧹 Nitpick comments (8)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (8)

168-171: Category change commits the stale order (tasks) instead of the working order (reorderableTasks).

This can drop uncommitted reorders if the user switches categories mid-reorder. Either:

  • Commit from reorderableTasks, or
  • Omit the commit here and rely on the onDragStopped commit.

Given reorderableTasks is scoped inside AnimatedContent, the minimal safe fix is to remove the commit here.

Apply this diff to remove the stale commit:

-                        onAction(TaskPageAction.ReorderTasks(tasks.mapIndexed { index, task -> index to task }))
                         onAction(TaskPageAction.ChangeCategory(category))
                         editState = false

If you prefer to keep the commit on navigation, hoist reorderableTasks to a scope accessible here (e.g., remember keyed by category outside AnimatedContent) and commit from that instead.


246-251: Avoid in-place mutation of task.status; prefer immutable updates.

Directly mutating an element inside a remembered list can lead to subtle recomposition issues. Use copy and dispatch the new instance.

Apply this diff (coordinates with the later move logic fix):

-                                            if (!editState) {
-                                                task.status = !task.status
-
-                                                onAction(TaskPageAction.UpsertTask(task))
+                                            if (!editState) {
+                                                val updated = task.copy(status = !task.status)
+                                                onAction(TaskPageAction.UpsertTask(updated))

And replace subsequent references to task.status in this onClick block with updated.status.


100-111: Minor naming consistency: use scrollBehavior (US spelling) to match the API and reduce cognitive friction.

Purely stylistic, but aligns with TopAppBarDefaults.scrollBehavior property naming across Compose.

Apply this diff:

-    val scrollBehaviour = TopAppBarDefaults.enterAlwaysScrollBehavior()
+    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@@
-        modifier = Modifier
-            .nestedScroll(scrollBehaviour.nestedScrollConnection)
+        modifier = Modifier
+            .nestedScroll(scrollBehavior.nestedScrollConnection)
@@
-            scrollBehavior = scrollBehaviour,
+            scrollBehavior = scrollBehavior,

204-206: Reinitializing reorderableTasks on any state change may be too aggressive.

remember(state) will reset the working list even for unrelated state changes (e.g., counters), potentially interrupting the UX. Consider scoping to category and/or the tasks list identity.

Apply this diff to reduce churn while still reacting to real task-list changes:

-                var reorderableTasks by remember(state) { mutableStateOf(tasks) }
+                var reorderableTasks by remember(category, tasks) { mutableStateOf(tasks) }

If the tasks list identity is stable on content mutations (new list instances on any change), the above will keep reorderableTasks in sync without resetting on unrelated state updates.


146-148: Enable condition can use the local tasks to avoid repeating a nullable map lookup.

Functionally equivalent but simpler and null-safe by construction.

Apply this diff:

-                            enabled = !state.tasks[state.currentCategory].isNullOrEmpty()
+                            enabled = tasks.isNotEmpty()

183-195: Replace hard-coded contentDescription strings with localized stringResources (and mark purely decorative icons as null).

This improves accessibility and i18n. For icons that convey no extra information beyond adjacent text, set contentDescription = null; otherwise use stringResource.

Apply these diffs:

-                    Icon(
-                        imageVector = Icons.Rounded.Add,
-                        contentDescription = "Add Category"
-                    )
+                    Icon(
+                        imageVector = Icons.Rounded.Add,
+                        contentDescription = stringResource(R.string.add_category)
+                    )
-                    Icon(
-                        imageVector = Icons.Rounded.Edit,
-                        contentDescription = "Edit Categories"
-                    )
+                    Icon(
+                        imageVector = Icons.Rounded.Edit,
+                        contentDescription = stringResource(R.string.edit_categories)
+                    )
-                                    Icon(
-                                        imageVector = Icons.Rounded.DragIndicator,
-                                        contentDescription = "Drag",
+                                    Icon(
+                                        imageVector = Icons.Rounded.DragIndicator,
+                                        contentDescription = stringResource(R.string.drag),
                                         modifier = Modifier.draggableHandle(
-            Icon(
-                imageVector = Icons.Rounded.Warning,
-                contentDescription = "Warning",
+            Icon(
+                imageVector = Icons.Rounded.Warning,
+                contentDescription = stringResource(R.string.warning),
-            Icon(
-                imageVector = Icons.Rounded.Add,
-                contentDescription = "Add"
+            Icon(
+                imageVector = Icons.Rounded.Add,
+                contentDescription = stringResource(R.string.add)
-            Icon(
-                imageVector = Icons.Rounded.Edit,
-                contentDescription = "Edit"
+            Icon(
+                imageVector = Icons.Rounded.Edit,
+                contentDescription = stringResource(R.string.edit)

If some of these string resources don’t exist yet (e.g., edit_categories, drag, warning), I can add them to strings.xml. Want me to open a follow-up PR?

Also applies to: 228-231, 330-333, 374-376, 443-446, 531-533


475-479: No-op keyboard action.

newTitle.plus("\n") doesn’t mutate state; the call result is discarded. Either remove keyboardActions or actually update state.

Apply one of:

Option A — remove it:

-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTitle.plus("\n")
-                    }
-                ),
+                // no keyboardActions needed; multi-line input already supports newline

Option B — mutate:

                 keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTitle.plus("\n")
-                    }
+                    onAny = { newTitle += "\n" }
                 ),

563-567: Same no-op keyboard action in Add Task sheet.

The .plus call does not change newTask.

Apply one of:

Option A — remove:

-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTask.plus("\n")
-                    }
-                ),
+                // no keyboardActions needed; multi-line input already supports newline

Option B — mutate:

                 keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTask.plus("\n")
-                    }
+                    onAny = { newTask += "\n" }
                 ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5472ed7 and 7341f1c.

📒 Files selected for processing (1)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
  • TaskCard (27-88)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)

* stashing changes

Signed-off-by: shub39 <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)

161-170: Empty onClick handler for drag handle button.

The FilledTonalIconButton has an empty onClick handler, which doesn't follow Material Design guidelines. Since this is a drag handle that should only respond to drag gestures, consider using a non-interactive visual element instead.

-                               FilledTonalIconButton(
-                                   onClick = {},
-                                   modifier = Modifier.draggableHandle()
-                               ) {
-                                   Icon(
-                                       imageVector = Icons.Rounded.DragIndicator,
-                                       contentDescription = null
-                                   )
-                               }
+                               Icon(
+                                   imageVector = Icons.Rounded.DragIndicator,
+                                   contentDescription = "Drag to reorder",
+                                   modifier = Modifier.draggableHandle()
+                               )
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

475-478: KeyboardActions.onAny implementation has no effect.

The onAny keyboard action attempts to append a newline but doesn't actually modify the state. The expression newTitle.plus("\n") creates a new string without assigning it anywhere.

                keyboardActions = KeyboardActions(
                    onAny = {
-                       newTitle.plus("\n")
+                       newTitle = newTitle.plus("\n")
                    }
                ),

Alternatively, if multiline input isn't needed, consider removing this keyboard action entirely:

-               keyboardActions = KeyboardActions(
-                   onAny = {
-                       newTitle.plus("\n")
-                   }
-               ),

562-566: KeyboardActions.onAny implementation has no effect.

Same issue as above - the keyboard action doesn't actually modify the state.

                keyboardActions = KeyboardActions(
                    onAny = {
-                       newTask.plus("\n")
+                       newTask = newTask.plus("\n")
                    }
                ),

Or remove if multiline isn't needed:

-               keyboardActions = KeyboardActions(
-                   onAny = {
-                       newTask.plus("\n")
-                   }
-               ),
♻️ Duplicate comments (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

207-210: Reordering uses incorrect source list after initial drag.

The reordering operation uses tasks instead of reorderableTasks as the base list, which will reset any in-progress reorders and potentially misplace items when multiple drag operations occur.

                val reorderableListState =
                    rememberReorderableLazyListState(lazyListState) { from, to ->
-                       reorderableTasks = tasks.toMutableList().apply {
+                       reorderableTasks = reorderableTasks.toMutableList().apply {
                            add(to.index, removeAt(from.index))
                        }
                    }
🧹 Nitpick comments (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)

100-120: Verify reorder toggle button visibility logic.

The reorder toggle button is only visible when there are 2+ categories but is disabled when the current category is empty. This creates a confusing UX where users see a disabled toggle with no clear reason. Consider either:

  1. Hiding the toggle when the current category is empty, or
  2. Showing a tooltip explaining why it's disabled
                AnimatedVisibility(
-                   visible = categories.size >= 2
+                   visible = categories.size >= 2 && !state.tasks[state.currentCategory].isNullOrEmpty()
                ) {
                    FilledTonalIconToggleButton(
                        checked = editState,
                        shapes = IconToggleButtonShapes(
                            shape = CircleShape,
                            checkedShape = MaterialTheme.shapes.small,
                            pressedShape = MaterialTheme.shapes.extraSmall,
                        ),
-                       onCheckedChange = { editState = it },
-                       enabled = !state.tasks[state.currentCategory].isNullOrEmpty()
+                       onCheckedChange = { editState = it }
                    ) {
                        Icon(
                            imageVector = Icons.Rounded.Reorder,
                            contentDescription = null
                        )
                    }
                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7341f1c and 109f69c.

📒 Files selected for processing (2)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (6 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
  • TaskCard (27-88)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)
🔇 Additional comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)

61-67: LGTM!

The function signature and return type correctly follow the new pattern with PageFill wrapper, aligning with the broader UI reorganization.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

86-92: LGTM!

The function signature and PageFill wrapper integration align well with the new UI structure.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (4)

82-89: Replace arbitrary delay with deterministic selection of the new “first” category

delay(200) is a race-prone heuristic; it neither guarantees the DB has emitted the new state nor that _state.tasks reflects the intended ordering. Compute the new current category deterministically from action.mapping (lowest index) and update state immediately. Fall back to existing keys if the mapping is partial/empty.

Apply this diff:

-                    delay(200)
-
-                    _state.update {
-                        it.copy(
-                            currentCategory = it.tasks.keys.firstOrNull()
-                        )
-                    }
+                    val newCurrent = action.mapping.minByOrNull { it.first }?.second
+                    _state.update { it.copy(currentCategory = newCurrent ?: it.tasks.keys.firstOrNull()) }

Alternative (if you truly need to wait for the post-DB emission): await the next emission instead of sleeping.

// Suspends until the next tasks snapshot is available after the reorder.
val firstAfterReorder = repo.getTasksFlow().map { it.keys.firstOrNull() }.first()
_state.update { it.copy(currentCategory = firstAfterReorder) }

94-101: Remove sleep and redundant reassignment after delete; it fights with existing state logic

After deleteCategory(action.category), you sleep and then force-select the first key. This competes with deleteCategory(...)’s own update and can cause flicker and incorrect selection. Prefer a single source of truth for this invariant.

Apply this diff:

-                    delay(200)
-
-                    _state.update {
-                        it.copy(
-                            currentCategory = it.tasks.keys.firstOrNull()
-                        )
-                    }

Follow-up: rely on the invariant maintained in observeTasks() (see next comment) and/or in deleteCategory(...).


118-124: Maintain invariant: currentCategory must be null or present in tasks; fix stale-selection bug

You only correct currentCategory when it is null. If the selected category was deleted or renamed, it can become absent yet non-null, leaving state pointing to a non-existent key. Tighten the condition to also handle “not in keys”.

Apply this diff:

-                if (_state.value.currentCategory == null) {
-                    _state.update {
-                        it.copy(
-                            currentCategory = tasks.keys.firstOrNull()
-                        )
-                    }
-                }
+                val current = _state.value.currentCategory
+                if (current == null || current !in tasks.keys) {
+                    _state.update { it.copy(currentCategory = tasks.keys.firstOrNull()) }
+                }

Note: This change eliminates the need for time-based sleeps elsewhere.


1-161: Ensure Category equality depends only on a stable identifier

The tasks: Map<Category, List<Task>> uses Category as the key, but since Category is defined as a Kotlin data class with all four primary‐constructor properties (id, name, index, color), its autogenerated equals/hashCode include mutable fields like name, index, and color. When you reorder (index) or rename (name) a category, new Category instances no longer match the old map keys, breaking lookups and UI filters.

Action items:

  • In app/src/main/java/com/shub39/grit/tasks/domain/Category.kt, override equals and hashCode to use only the immutable id field.
  • Alternatively, separate identity from presentation: keep a plain CategoryId for map keys and a distinct value class (or DTO) for display properties (name, index, color).
  • Update any code that relies on Category equality (e.g., task lookups, current‐category tracking) to use the new identity‐only semantics.
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

587-595: Fix index when adding to a different category.

The new task’s index is derived from state.currentCategory, which is wrong if the user selected another category in the sheet. Compute the size of the target category instead.

-                                index = state.tasks[state.currentCategory]?.size ?: 0
+                                index = run {
+                                    val target = state.tasks.keys.firstOrNull { it.id == newTaskCategoryId }
+                                    state.tasks[target]?.size ?: 0
+                                }
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)

294-301: Description field validates the wrong variable.

Both the label text and isError for the description field reference newHabitName instead of newHabitDescription.

-                label = {
-                    if (newHabitName.length <= 50) {
+                label = {
+                    if (newHabitDescription.length <= 50) {
                         Text(text = stringResource(id = R.string.description))
                     } else {
                         Text(text = stringResource(id = R.string.too_long))
                     }
                 },
-                isError = newHabitName.length > 50
+                isError = newHabitDescription.length > 50
♻️ Duplicate comments (3)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (2)

262-267: Reorder uses the working list, not the original snapshot. Good catch.

The move-on-complete path now starts from reorderableTasks.toMutableList(), preserving any in-progress reorders. This addresses the earlier review concern.


292-292: Spacer height fix looks good.

Spacer(modifier = Modifier.height(60.dp)) correctly reserves space to avoid FAB overlap.

app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)

68-70: Missing import for draggableHandle (compile error persists).

Modifier.draggableHandle(...) is used at Line 167 but its extension import is still missing.

Apply this diff to fix the import:

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle
🧹 Nitpick comments (13)
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (3)

77-81: Batch category reorders atomically in the repo/DAO

Looping upsertCategory(...) for each mapping entry issues N separate writes and N flow emissions. Wrap the reorder in a single repository API executed in a DB transaction (e.g., Room @Transaction) to ensure atomicity and reduce intermediate UI states.

Proposed repository API:

suspend fun reorderCategories(mapping: List<Pair<Int, Category>>)

Then call repo.reorderCategories(action.mapping) here and drop the per-item loop.


151-161: Avoid forcing a non-empty key; prefer safe fallback and let observeTasks repair

Using it.tasks.keys.first() assumes the map is non-empty at that instant. While it currently runs before the repo deletion, using firstOrNull() is safer and aligns with the invariant fix above.

Apply this diff:

-                    currentCategory = it.tasks.keys.first()
+                    currentCategory = it.tasks.keys.firstOrNull()

61-69: UX nit: selecting the “first” category after Add may not be the newly added category

After adding, you set currentCategory to it.tasks.keys.firstOrNull(), which may not be the new category. If the intent is to focus the just-created category, set it explicitly to action.category (or its persisted copy with ID).

Potential tweak:

_state.update { it.copy(currentCategory = action.category /* or persisted copy */) }

Or let the invariant in observeTasks() handle selection if you prefer passive behavior.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (4)

212-221: Stabilize reorderableTasks initialization and syncing.

Using remember(category.id, state.tasks.values) can reset local reorders when unrelated categories change, and keys based on values are brittle. Initialize on category change and explicitly sync when that category’s list updates.

-                var reorderableTasks by remember(category.id, state.tasks.values) { mutableStateOf(
-                    state.tasks[category] ?: emptyList()
-                ) }
+                var reorderableTasks by remember(category.id) {
+                    mutableStateOf(state.tasks[category].orEmpty())
+                }
+                LaunchedEffect(state.tasks[category]) {
+                    // Sync local list when backing data for the current category changes.
+                    reorderableTasks = state.tasks[category].orEmpty()
+                }
                 val reorderableListState =
                     rememberReorderableLazyListState(lazyListState) { from, to ->
                         reorderableTasks = reorderableTasks.toMutableList().apply {
                             add(to.index, removeAt(from.index))
                         }
                     }

To verify no regressions, try: start a drag reorder without dropping, change another category’s tasks (e.g., complete a task there), then return—local reorder state for the active category should remain intact.


481-489: No-op KeyboardActions in edit sheet; switch to a meaningful IME action.

newTitle.plus("\n") doesn’t mutate newTitle, so onAny is a no-op. Recommend using ImeAction.Done and hiding the keyboard (or handling save).

-                keyboardOptions = KeyboardOptions.Default.copy(
-                    capitalization = KeyboardCapitalization.Sentences,
-                    imeAction = ImeAction.None
-                ),
-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTitle.plus("\n")
-                    }
-                ),
+                keyboardOptions = KeyboardOptions.Default.copy(
+                    capitalization = KeyboardCapitalization.Sentences,
+                    imeAction = ImeAction.Done
+                ),
+                keyboardActions = KeyboardActions(
+                    onDone = { keyboardController?.hide() }
+                ),

569-577: No-op KeyboardActions in add sheet; mirror the edit-sheet fix.

Same issue as above—newTask.plus("\n") is ineffective. Use a real IME action.

-                keyboardOptions = KeyboardOptions.Default.copy(
-                    capitalization = KeyboardCapitalization.Sentences,
-                    imeAction = ImeAction.None
-                ),
-                keyboardActions = KeyboardActions(
-                    onAny = {
-                        newTask.plus("\n")
-                    }
-                ),
+                keyboardOptions = KeyboardOptions.Default.copy(
+                    capitalization = KeyboardCapitalization.Sentences,
+                    imeAction = ImeAction.Done
+                ),
+                keyboardActions = KeyboardActions(
+                    onDone = { keyboardController?.hide() }
+                ),

102-108: Minor: smoother FAB visibility threshold.

Using only firstVisibleItemIndex can flicker at the top. Consider also checking firstVisibleItemScrollOffset.

-    val fabVisible by remember {
-        derivedStateOf {
-            lazyListState.firstVisibleItemIndex <= 0
-        }
-    }
+    val fabVisible by remember {
+        derivedStateOf {
+            lazyListState.firstVisibleItemIndex == 0 &&
+                lazyListState.firstVisibleItemScrollOffset < 10
+        }
+    }
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (6)

88-96: Prevent unnecessary resets of the local reordered list.

remember(state) will reset reorderableHabits whenever the state object identity changes (which can be frequent). Scope the memory key to the source of the list instead.

-    var reorderableHabits by remember(state) { mutableStateOf(habits) }
+    var reorderableHabits by remember(state.habitsWithStatuses) { mutableStateOf(habits) }

98-99: Use exitUntilCollapsed for a flexible top app bar (optional).

MediumFlexibleTopAppBar typically pairs better with TopAppBarDefaults.exitUntilCollapsedScrollBehavior(); enterAlways can feel jumpy in nested scroll scenarios.

-    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()

273-282: Align input thresholds with button enablement.

Title label/error use 20 chars as the limit; button uses < 20. This disables the Add button when length is exactly 20. Similarly, use <= 50 for description to match its label/error.

-                enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50,
+                enabled = newHabitName.isNotBlank() && newHabitName.length <= 20 && newHabitDescription.length <= 50,

Also applies to: 377-381


404-405: Prefer explicit Spacer height over padding for vertical spacing.

Spacer(Modifier.padding(vertical = 4.dp)) results in 8.dp of vertical space and is less clear than a fixed height.

-                    Spacer(modifier = Modifier.padding(vertical = 4.dp))
+                    Spacer(modifier = Modifier.height(8.dp))

If you actually intended 4.dp, use Modifier.height(4.dp) instead.


336-357: Optional: Order day chips by the user’s configured starting day.

Currently uses DayOfWeek.entries, which always starts on Monday. Consider rotating the sequence so chips start at state.startingDay, matching the calendar UX.

Example (outside this block for clarity):

val orderedDays = generateSequence(state.startingDay) { it.plus(1).takeIf { next -> next != state.startingDay } }
    .toList()
orderedDays.forEach { dayOfWeek -> /* ToggleButton(...) */ }

360-372: Minor: Index for new item source of truth.

index = reorderableHabits.size will work, but consider using state.habitsWithStatuses.size to avoid any temporary divergence if local reorder state and VM state fall out of sync during recompositions.

-                        index = reorderableHabits.size,
+                        index = state.habitsWithStatuses.size,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5a50ed5 and cea39d9.

📒 Files selected for processing (5)
  • app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (7 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (6 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (8 hunks)
  • app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
  • TaskCard (27-88)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (1)
  • HabitCard (44-235)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)
🔇 Additional comments (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

307-311: Confirm availability of animateFloatingActionButton().

Modifier.animateFloatingActionButton(...) is not part of stable Material3 APIs in older Compose versions. Ensure your dependency bump includes the artifact/version that provides this extension.

If your catalog pins Compose Material3, please verify the function exists by building locally. If unavailable, consider replacing with AnimatedVisibility + animate*AsState or AnimatedContent around the FAB.

app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)

25-41: ✔️ Expressive Material3 API and Reorderable Dependency Verified

  • The version catalog (gradle/libs.versions.toml) pins androidx.compose.material3:material3 to 1.5.0-alpha02, which includes the ExperimentalMaterial3ExpressiveApi and the MediumFlexibleTopAppBar, MediumFloatingActionButton, IconToggleButtonShapes, and ButtonShapes components.
  • The sh.calvin.reorderable:reorderable library is declared in the same catalog, so no missing dependency issues.
  • There is no usage or import of FlowRow in HabitsList.kt, so no additional @OptIn(ExperimentalLayoutApi::class) is required here.

All checks pass; no changes needed.

Comment on lines +141 to +146
LazyColumn(
state = lazyListState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Reordering won’t work without attaching Modifier.reorderable to LazyColumn.

You created the reorderableListState and items are wrapped in ReorderableItem, but the list itself isn’t wired to the drag system. Without Modifier.reorderable(reorderableListState), drag gestures will not produce reordering.

Apply this diff to attach the modifier:

-        LazyColumn(
-            state = lazyListState,
-            modifier = Modifier.fillMaxSize(),
+        LazyColumn(
+            state = lazyListState,
+            modifier = Modifier
+                .fillMaxSize()
+                .reorderable(reorderableListState),
             contentPadding = PaddingValues(16.dp),
             verticalArrangement = Arrangement.spacedBy(8.dp)
         ) {

And add the import (separate from the earlier draggableHandle import):

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.reorderable
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LazyColumn(
state = lazyListState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import sh.calvin.reorderable.reorderable
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt
around lines 141 to 146, the LazyColumn is not wired to the drag/reorder system;
attach Modifier.reorderable(reorderableListState) to the LazyColumn's modifier
chain (e.g., Modifier.fillMaxSize().reorderable(reorderableListState).then(other
modifiers) or add it before contentPadding) so the ReorderableItem and
reorderableListState receive drag gestures, and add the corresponding import for
Modifier.reorderable from the reorderable library (separate from the
draggableHandle import).

Comment on lines +144 to 162
AnimatedVisibility(
visible = state.tasks.values.isNotEmpty()
) {
FilledTonalIconToggleButton(
checked = editState,
shapes = IconToggleButtonShapes(
shape = CircleShape,
checkedShape = MaterialTheme.shapes.small,
pressedShape = MaterialTheme.shapes.extraSmall,
),
onCheckedChange = { editState = it },
enabled = !state.tasks[state.currentCategory].isNullOrEmpty()
) {
Icon(
imageVector = Icons.Rounded.Reorder,
contentDescription = null
)
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Show “reorder” toggle only when the current category has tasks.

Visibility currently keys off the entire map (state.tasks.values.isNotEmpty()), so the toggle can appear even when the selected category is empty. Bind visibility to the currently selected category, consistent with the existing enabled check.

-                AnimatedVisibility(
-                    visible = state.completedTasks.isNotEmpty()
-                ) {
+                AnimatedVisibility(
+                    visible = state.completedTasks.isNotEmpty()
+                ) {
                   ...
-                AnimatedVisibility(
-                    visible = state.tasks.values.isNotEmpty()
-                ) {
+                AnimatedVisibility(
+                    visible = !state.tasks[state.currentCategory].isNullOrEmpty()
+                ) {
                   FilledTonalIconToggleButton(
                     checked = editState,
                     ...
                     enabled = !state.tasks[state.currentCategory].isNullOrEmpty()
                   ) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 144 to 162, the AnimatedVisibility currently checks the whole tasks
map (state.tasks.values.isNotEmpty()), causing the reorder toggle to show even
when the selected category is empty; change the visibility predicate to target
the currently selected category (e.g., check state.tasks[state.currentCategory]
for null/empty) so the toggle only appears when the current category has tasks,
keeping the existing enabled check for redundancy and handling null safely.

Comment on lines +183 to 203
item {
FilledTonalIconButton(
onClick = { showCategoryAddSheet = true },
enabled = !editState
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Add Category"
)
}

item {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
AssistChip(
onClick = { showCategoryAddSheet = true },
enabled = !editState,
colors = AssistChipDefaults.assistChipColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
),
border = AssistChipDefaults.assistChipBorder(false),
label = {
Icon(
painter = painterResource(R.drawable.round_add_24),
contentDescription = "Add Category"
)
}
)

AssistChip(
onClick = onNavigateToEditCategories,
enabled = !editState,
colors = AssistChipDefaults.assistChipColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
),
border = AssistChipDefaults.assistChipBorder(false),
label = {
Icon(
painter = painterResource(R.drawable.baseline_edit_square_24),
contentDescription = "Edit Categories"
)
}
)
}
FilledTonalIconButton(
onClick = onNavigateToEditCategories,
enabled = !editState
) {
Icon(
imageVector = Icons.Rounded.Edit,
contentDescription = "Edit Categories"
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Two buttons inside a single LazyRow item will overlap and bypass spacedBy(). Split them into two items.

Placing multiple composables in one item { ... } for a LazyRow causes them to render within the same item without the row-level spacing. Define separate items (or wrap in a Row) so Arrangement.spacedBy(8.dp) applies.

-            item {
-                FilledTonalIconButton(
-                    onClick = { showCategoryAddSheet = true },
-                    enabled = !editState
-                ) {
-                    Icon(
-                        imageVector = Icons.Rounded.Add,
-                        contentDescription = "Add Category"
-                    )
-                }
-
-                FilledTonalIconButton(
-                    onClick = onNavigateToEditCategories,
-                    enabled = !editState
-                ) {
-                    Icon(
-                        imageVector = Icons.Rounded.Edit,
-                        contentDescription = "Edit Categories"
-                    )
-                }
-            }
+            item {
+                FilledTonalIconButton(
+                    onClick = { showCategoryAddSheet = true },
+                    enabled = !editState
+                ) {
+                    Icon(
+                        imageVector = Icons.Rounded.Add,
+                        contentDescription = "Add Category"
+                    )
+                }
+            }
+            item {
+                FilledTonalIconButton(
+                    onClick = onNavigateToEditCategories,
+                    enabled = !editState
+                ) {
+                    Icon(
+                        imageVector = Icons.Rounded.Edit,
+                        contentDescription = "Edit Categories"
+                    )
+                }
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
item {
FilledTonalIconButton(
onClick = { showCategoryAddSheet = true },
enabled = !editState
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Add Category"
)
}
item {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
AssistChip(
onClick = { showCategoryAddSheet = true },
enabled = !editState,
colors = AssistChipDefaults.assistChipColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
),
border = AssistChipDefaults.assistChipBorder(false),
label = {
Icon(
painter = painterResource(R.drawable.round_add_24),
contentDescription = "Add Category"
)
}
)
AssistChip(
onClick = onNavigateToEditCategories,
enabled = !editState,
colors = AssistChipDefaults.assistChipColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
labelColor = MaterialTheme.colorScheme.onPrimaryContainer
),
border = AssistChipDefaults.assistChipBorder(false),
label = {
Icon(
painter = painterResource(R.drawable.baseline_edit_square_24),
contentDescription = "Edit Categories"
)
}
)
}
FilledTonalIconButton(
onClick = onNavigateToEditCategories,
enabled = !editState
) {
Icon(
imageVector = Icons.Rounded.Edit,
contentDescription = "Edit Categories"
)
}
}
// … other items in the LazyRow …
// Add Category button as its own item
item {
FilledTonalIconButton(
onClick = { showCategoryAddSheet = true },
enabled = !editState
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Add Category"
)
}
}
// Edit Categories button as its own item
item {
FilledTonalIconButton(
onClick = onNavigateToEditCategories,
enabled = !editState
) {
Icon(
imageVector = Icons.Rounded.Edit,
contentDescription = "Edit Categories"
)
}
}
// … following items in the LazyRow …
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 183 to 203, two FilledTonalIconButton composables are placed inside
a single LazyRow item which prevents the LazyRow's Arrangement.spacedBy(...)
from applying and causes overlap; split them into separate LazyRow item { ... }
blocks (one per button) so the row spacing applies, or alternatively wrap them
in a Row and apply proper horizontal spacing inside that Row if you want them
grouped, keeping the same onClick and enabled properties.

Comment on lines +239 to +247
modifier = Modifier.draggableHandle(
onDragStopped = {
onAction(
TaskPageAction.ReorderTasks(
reorderableTasks.mapIndexed { index, task -> index to task })
)
}
)
},
shape = RoundedCornerShape(cardCorners),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
.combinedClickable(
// change status on click
onClick = {
if (!editState) {
task.status = !task.status

onAction(TaskPageAction.UpsertTask(task))

if (task.status) {
reorderableTasks = tasks.toMutableList().apply {
add(tasks.size - 1, removeAt(index))
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing import for draggableHandle extension.

Modifier.draggableHandle is defined in the sh.calvin.reorderable package but isn’t imported here, which will fail compilation.

Add this import near the other sh.calvin.reorderable.* imports:

import sh.calvin.reorderable.draggableHandle
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 239 to 247, the code uses Modifier.draggableHandle but the
extension import is missing; add the import
sh.calvin.reorderable.draggableHandle alongside the other
sh.calvin.reorderable.* imports so the draggableHandle extension resolves and
the file compiles.

Comment on lines +255 to 274
onClick = {
if (!editState) {
task.status = !task.status

onAction(TaskPageAction.UpsertTask(task))

if (task.status) {
reorderableTasks =
reorderableTasks.toMutableList().apply {
add(
reorderableTasks.size - 1,
removeAt(index)
)
}

onAction(TaskPageAction.ReorderTasks(reorderableTasks.mapIndexed { index, task -> index to task }))
}
}
},
// edit on click and hold
onLongClick = {
if (!editState) {
editTask = task
onAction(
TaskPageAction.ReorderTasks(
reorderableTasks.mapIndexed { index, task -> index to task })
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid in-place mutation of Task; emit a copied value and keep local list consistent.

Directly toggling task.status mutates state objects held by Compose, which can lead to stale snapshots. Prefer copy(...), dispatch the update, and update the local list with the new instance.

-                                            if (!editState) {
-                                                task.status = !task.status
-
-                                                onAction(TaskPageAction.UpsertTask(task))
-
-                                                if (task.status) {
-                                                    reorderableTasks =
-                                                        reorderableTasks.toMutableList().apply {
-                                                            add(
-                                                                reorderableTasks.size - 1,
-                                                                removeAt(index)
-                                                            )
-                                                        }
-
-                                                    onAction(
-                                                        TaskPageAction.ReorderTasks(
-                                                            reorderableTasks.mapIndexed { index, task -> index to task })
-                                                    )
-                                                }
-                                            }
+                                            if (!editState) {
+                                                val updated = task.copy(status = !task.status)
+                                                onAction(TaskPageAction.UpsertTask(updated))
+
+                                                if (updated.status) {
+                                                    val next = reorderableTasks.toMutableList()
+                                                    // Move updated item to end
+                                                    next.removeAt(index)
+                                                    next.add(next.size, updated)
+                                                    reorderableTasks = next
+
+                                                    onAction(
+                                                        TaskPageAction.ReorderTasks(
+                                                            reorderableTasks.mapIndexed { idx, t -> idx to t }
+                                                        )
+                                                    )
+                                                }
+                                            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick = {
if (!editState) {
task.status = !task.status
onAction(TaskPageAction.UpsertTask(task))
if (task.status) {
reorderableTasks =
reorderableTasks.toMutableList().apply {
add(
reorderableTasks.size - 1,
removeAt(index)
)
}
onAction(TaskPageAction.ReorderTasks(reorderableTasks.mapIndexed { index, task -> index to task }))
}
}
},
// edit on click and hold
onLongClick = {
if (!editState) {
editTask = task
onAction(
TaskPageAction.ReorderTasks(
reorderableTasks.mapIndexed { index, task -> index to task })
)
}
onClick = {
if (!editState) {
// Emit a fresh copy instead of mutating in-place
val updated = task.copy(status = !task.status)
onAction(TaskPageAction.UpsertTask(updated))
if (updated.status) {
// Rebuild the local list with the new instance moved to the end
val next = reorderableTasks.toMutableList()
next.removeAt(index)
next.add(next.size, updated)
reorderableTasks = next
onAction(
TaskPageAction.ReorderTasks(
reorderableTasks.mapIndexed { idx, t -> idx to t }
)
)
}
}
// ... other onClick handlers
}
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt
around lines 255 to 274, the code mutates task.status in-place which can break
Compose state snapshots; instead create a newTask = task.copy(status =
!task.status), call onAction(TaskPageAction.UpsertTask(newTask)), and replace
the item in the local list(s) (e.g. reorderableTasks = reorderableTasks.map { if
(it == task || it.id == task.id) newTask else it }.toMutableList()) so the UI
sees the new instance; when reordering, use the newTask instance in the moved
position and dispatch ReorderTasks with the updated list.

import com.shub39.grit.tasks.presentation.TaskPageAction
import com.shub39.grit.tasks.presentation.TaskPageState
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid ad‑hoc time-based synchronization; remove delay import once delays are gone

Importing kotlinx.coroutines.delay foreshadows time-based waiting below. These sleeps make state updates nondeterministic and flaky under load or slow I/O.

Apply this once you remove the delay(...) usages:

-import kotlinx.coroutines.delay
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import kotlinx.coroutines.delay
// (remove the now-unused import)
🤖 Prompt for AI Agents
In app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt around line
12, remove the import kotlinx.coroutines.delay and eliminate any ad-hoc
delay(...) usages; instead convert the code to use proper coroutine
synchronization (e.g., suspend functions that return when work is complete, use
MutableStateFlow/LiveData updates, await jobs or use async/await with structured
concurrency, or expose callbacks/completion handlers) and update tests to use
TestCoroutineDispatcher or appropriate idling mechanisms so no explicit sleeps
are needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (1)

317-324: Fix description field validation using the wrong variable

The description field’s label/error conditions read newHabitName.length instead of newHabitDescription.length, leading to incorrect validation UI.

-                label = {
-                    if (newHabitName.length <= 50) {
+                label = {
+                    if (newHabitDescription.length <= 50) {
                         Text(text = stringResource(id = R.string.description))
                     } else {
                         Text(text = stringResource(id = R.string.too_long))
                     }
                 },
-                isError = newHabitName.length > 50
+                isError = newHabitDescription.length > 50
♻️ Duplicate comments (2)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)

75-77: Missing import for draggableHandle still causes a compile error
This was flagged earlier and remains unresolved. Add the import so Modifier.draggableHandle resolves.

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle

159-165: Attach Modifier.reorderable to LazyColumn to enable drag reordering

Without this, drag gestures won’t be dispatched to the list.

-        LazyColumn(
-            state = lazyListState,
-            modifier = Modifier.fillMaxSize(),
+        LazyColumn(
+            state = lazyListState,
+            modifier = Modifier
+                .fillMaxSize()
+                .reorderable(reorderableListState),
             contentPadding = PaddingValues(16.dp),
             verticalArrangement = Arrangement.spacedBy(8.dp)
         ) {

And add the import:

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.reorderable
🧹 Nitpick comments (7)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (4)

16-21: Verify icon availability and align visual style

Double-check that Icons.Rounded.CheckCircleOutline exists in your icon pack. If it doesn’t, compilation will fail. Also consider keeping styles consistent (Outlined+Outlined or Rounded+Rounded) between the incomplete and complete states.

Two safe alternatives:

Option A (Outlined for both):

- import androidx.compose.material.icons.rounded.CheckCircleOutline
+ import androidx.compose.material.icons.outlined.CheckCircleOutline
- imageVector = if (!it) Icons.Outlined.Circle else Icons.Rounded.CheckCircleOutline,
+ imageVector = if (!it) Icons.Outlined.Circle else Icons.Outlined.CheckCircleOutline,

Option B (Rounded for completed):

- import androidx.compose.material.icons.rounded.CheckCircleOutline
+ import androidx.compose.material.icons.rounded.CheckCircle
- imageVector = if (!it) Icons.Outlined.Circle else Icons.Rounded.CheckCircleOutline,
+ imageVector = if (!it) Icons.Outlined.Circle else Icons.Rounded.CheckCircle,

Also applies to: 111-113


63-77: Fix label mismatch in animateColorAsState

The first animateColorAsState drives content color but uses label "cardBackground". Rename to avoid confusion in inspection tools.

-    val cardContent by animateColorAsState(
+    val cardContent by animateColorAsState(
         targetValue = when (completed) {
             true -> MaterialTheme.colorScheme.onPrimaryContainer
             else -> MaterialTheme.colorScheme.onSurface
         },
-        label = "cardBackground"
+        label = "cardContent"
     )

148-151: Avoid repeated O(n) scans; precompute dates and reuse

statusList.any { ... } is executed multiple times per visible day and you re-create a list for countCurrentStreak. Precompute once and use a Set for O(1) membership checks.

+import androidx.compose.runtime.remember
     val weekState = rememberWeekCalendarState(
         startDate = habit.time.toLocalDate().minusMonths(12),
         endDate = LocalDate.now(),
         firstVisibleWeekDate = LocalDate.now(),
         firstDayOfWeek = startingDay
     )
+    // Precompute date collections for performance
+    val dates = remember(statusList) { statusList.map { it.date } }
+    val dateSet = remember(dates) { dates.toHashSet() }
-                        Text(
-                            text = countCurrentStreak(statusList.map { it.date }).toString()
-                        )
+                        Text(text = countCurrentStreak(dates).toString())
-                val done = statusList.any { it.date == weekDay.date }
+                val done = weekDay.date in dateSet
-                                val donePrevious =
-                                    statusList.any { it.date == weekDay.date.minusDays(1) }
-                                val doneAfter =
-                                    statusList.any { it.date == weekDay.date.plusDays(1) }
+                                val donePrevious = (weekDay.date.minusDays(1) in dateSet)
+                                val doneAfter = (weekDay.date.plusDays(1) in dateSet)

Also applies to: 175-193, 222-231


228-232: Localize weekday label

Using dayOfWeek.toString().take(3) yields English, all-caps abbreviations. Prefer localized short names.

+import java.time.format.TextStyle
+import java.util.Locale
-                        Text(
-                            text = weekDay.date.dayOfWeek.toString().take(3),
+                        Text(
+                            text = weekDay.date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
                             style = MaterialTheme.typography.bodySmall,
                             color = if (done) MaterialTheme.colorScheme.onPrimary else cardContent
                         )
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)

401-402: Align max-length rules across UI states

Button enablement uses < while labels use <=. Make them consistent so exactly-20/50-char inputs are allowed.

-                enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50,
+                enabled = newHabitName.isNotBlank() && newHabitName.length <= 20 && newHabitDescription.length <= 50,

95-99: Smooth FAB visibility with offset threshold

Currently it hides as soon as the first item is not index 0. Consider also checking the scroll offset for a smoother experience.

-    val fabVisible by remember {
-        derivedStateOf {
-            lazyListState.firstVisibleItemIndex == 0
-        }
-    }
+    val fabVisible by remember {
+        derivedStateOf {
+            lazyListState.firstVisibleItemIndex == 0 &&
+            lazyListState.firstVisibleItemScrollOffset < 24
+        }
+    }

140-154: Add contentDescription for the reorder toggle for accessibility

The toggle has contentDescription = null, which is not ideal for screen readers.

-                    FilledTonalIconToggleButton(
+                    FilledTonalIconToggleButton(
                         checked = editState,
                         shapes = IconToggleButtonShapes(
                             shape = CircleShape,
                             checkedShape = MaterialTheme.shapes.small,
                             pressedShape = MaterialTheme.shapes.extraSmall,
                         ),
                         onCheckedChange = { editState = it },
                         enabled = state.habitsWithStatuses.isNotEmpty()
                     ) {
-                        Icon(
-                            imageVector = Icons.Rounded.Reorder,
-                            contentDescription = null
-                        )
+                        Icon(
+                            imageVector = Icons.Rounded.Reorder,
+                            contentDescription = "Toggle reorder mode"
+                        )
                     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cea39d9 and 971bbdf.

📒 Files selected for processing (2)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (7 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (1)
  • HabitCard (48-238)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)

317-324: Description field validates the wrong variable.

OutlinedTextField for the description uses newHabitName.length for label/error checks instead of newHabitDescription.length. This breaks validation and UX.

Apply this diff:

-                label = {
-                    if (newHabitName.length <= 50) {
+                label = {
+                    if (newHabitDescription.length <= 50) {
                         Text(text = stringResource(id = R.string.description))
                     } else {
                         Text(text = stringResource(id = R.string.too_long))
                     }
                 },
-                isError = newHabitName.length > 50
+                isError = newHabitDescription.length > 50

401-401: “Add” button can be disabled at valid limits and allows duplicates.

  • Off-by-one: name length 20 and description length 50 should be allowed (consistent with your label/error checks), but the button uses < and disables submission.
  • Duplicates: the button is enabled even when a habit with the same title exists.

Align with the field constraints and prevent duplicates:

-                enabled = newHabitName.isNotBlank() && newHabitName.length < 20 && newHabitDescription.length < 50,
+                enabled = newHabitName.isNotBlank()
+                    && newHabitName.length <= 20
+                    && newHabitDescription.length <= 50
+                    && !isHabitPresent(),
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (1)

255-272: Use the existing ChangeCategory action when editing a category

Calling TaskPageAction.AddCategory in the “Edit Category” sheet will create a new category rather than updating the one being edited. Since you’ve already defined a ChangeCategory(val category: Category) action, dispatch that here to trigger an update (the ViewModel routes both Add and Change to upsertCategory, so ChangeCategory won’t duplicate).

• File: app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt
• Location: replace the onClick block that currently dispatches AddCategory

Suggested diff:

-   onAction(
-       TaskPageAction.AddCategory(
-           category.copy(name = name)
-       )
-   )
+   onAction(
+       TaskPageAction.ChangeCategory(
+           category.copy(name = name)
+       )
+   )

This ensures the existing category is updated rather than a new one added.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

587-595: Wrong index when adding to a non-current category

index = state.tasks[state.currentCategory]?.size ?: 0 uses the current category size even when the user selected a different newTaskCategoryId, causing incorrect placement.

Compute the target index from the chosen category:

-                    onAction(
-                        TaskPageAction.UpsertTask(
-                            Task(
-                                categoryId = newTaskCategoryId,
-                                title = newTask,
-                                status = false,
-                                index = state.tasks[state.currentCategory]?.size ?: 0
-                            )
-                        )
-                    )
+                    val targetCategory = state.tasks.keys.firstOrNull { it.id == newTaskCategoryId }
+                    val targetIndex = targetCategory?.let { state.tasks[it]?.size } ?: 0
+                    onAction(
+                        TaskPageAction.UpsertTask(
+                            Task(
+                                categoryId = newTaskCategoryId,
+                                title = newTask,
+                                status = false,
+                                index = targetIndex
+                            )
+                        )
+                    )
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (1)

122-126: Fix state handling: derived values aren’t remembered, LaunchedEffect reassignments won’t recompose

These locals are plain vars; reassigning them in LaunchedEffect doesn’t trigger recomposition and values reset on every recomposition. Compute them with remember/derivedStateOf (or drop LaunchedEffect entirely).

Apply this diff to remove the bug and simplify:

-    var lineChartData = prepareLineChartData(state.startingDay, statuses)
-    var weekDayData = prepareWeekDayData(statuses.map { it.date }, primary)
-    var currentStreak = countCurrentStreak(statuses.map { it.date })
-    var bestStreak = countBestStreak(statuses.map { it.date })
+    val lineChartData = remember(statuses, state.startingDay) {
+        prepareLineChartData(state.startingDay, statuses)
+    }
+    val weekDayData = remember(statuses, primary) {
+        prepareWeekDayData(statuses.map { it.date }, primary)
+    }
+    val currentStreak = remember(statuses) {
+        countCurrentStreak(statuses.map { it.date })
+    }
+    val bestStreak = remember(statuses) {
+        countBestStreak(statuses.map { it.date })
+    }
@@
-    LaunchedEffect(statuses) {
-        lineChartData = prepareLineChartData(state.startingDay, statuses)
-        currentStreak = countCurrentStreak(statuses.map { it.date })
-        bestStreak = countBestStreak(statuses.map { it.date })
-        weekDayData = prepareWeekDayData(statuses.map { it.date }, primary)
-    }
+    // No LaunchedEffect necessary; values above update when keys change.

Also applies to: 143-148

app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (1)

82-111: Stabilize chart colors: Random colors regenerate on recomposition

Using Random.nextFloat() each composition causes lines to change colors, hurting recognition. Seed by habit id and remember the mapping.

Apply this diff:

-    val weeklyGraphData = state.habitsWithStatuses.entries.map { entry ->
-        val habit = entry.key
-        val statuses = entry.value
-        val color = Color(
-            red = Random.nextFloat(),
-            green = Random.nextFloat(),
-            blue = Random.nextFloat()
-        ).harmonize(primary, true).fixIfDisliked()
-
-        Line(
-            label = habit.title,
-            values = prepareLineChartData(state.startingDay, statuses),
-            color = SolidColor(color),
-            dotProperties = DotProperties(
-                enabled = false,
-                color = SolidColor(color),
-                strokeWidth = 4.dp,
-                radius = 7.dp
-            ),
-            firstGradientFillColor = color.copy(alpha = 0.8f),
-            secondGradientFillColor = Color.Transparent,
-            popupProperties = PopupProperties(
-                enabled = false
-            ),
-            drawStyle = DrawStyle.Stroke(
-                width = 3.dp,
-                strokeStyle = StrokeStyle.Normal
-            )
-        )
-    }
+    val weeklyGraphData = remember(state.habitsWithStatuses.keys, primary) {
+        state.habitsWithStatuses.entries.map { (habit, statuses) ->
+            val seeded = Random(habit.id.hashCode())
+            val color = Color(
+                red = seeded.nextFloat(),
+                green = seeded.nextFloat(),
+                blue = seeded.nextFloat()
+            ).harmonize(primary, true).fixIfDisliked()
+
+            Line(
+                label = habit.title,
+                values = prepareLineChartData(state.startingDay, statuses),
+                color = SolidColor(color),
+                dotProperties = DotProperties(
+                    enabled = false,
+                    color = SolidColor(color),
+                    strokeWidth = 4.dp,
+                    radius = 7.dp
+                ),
+                firstGradientFillColor = color.copy(alpha = 0.8f),
+                secondGradientFillColor = Color.Transparent,
+                popupProperties = PopupProperties(enabled = false),
+                drawStyle = DrawStyle.Stroke(width = 3.dp, strokeStyle = StrokeStyle.Normal)
+            )
+        }
+    }
♻️ Duplicate comments (6)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)

75-77: Missing import for draggableHandle causes a compile error.

Modifier.draggableHandle is referenced but its extension import is absent.

Apply this diff to add the import alongside the other reorderable imports:

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle

159-164: Reordering won’t work without wiring the list with Modifier.reorderable.

You create reorderableListState and wrap items with ReorderableItem, but LazyColumn itself isn’t connected to the drag system. Without Modifier.reorderable(reorderableListState), drag gestures won’t produce item moves.

Apply this diff to attach the modifier:

-        LazyColumn(
-            state = lazyListState,
-            modifier = Modifier.fillMaxSize(),
+        LazyColumn(
+            state = lazyListState,
+            modifier = Modifier
+                .fillMaxSize()
+                .reorderable(reorderableListState),
             contentPadding = PaddingValues(16.dp),
             verticalArrangement = Arrangement.spacedBy(8.dp)
         ) {

And ensure the import exists (separate from the draggableHandle import):

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.reorderable
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (4)

144-162: Show/enable reorder only when the current category has tasks

Visibility uses state.tasks.values.isNotEmpty(), which can be true even when the selected category is empty. Bind to the current category to avoid a dead toggle.

-                AnimatedVisibility(
-                    visible = state.tasks.values.isNotEmpty()
-                ) {
+                AnimatedVisibility(
+                    visible = !state.tasks[state.currentCategory].isNullOrEmpty()
+                ) {
                     FilledTonalIconToggleButton(
                         checked = editState,
                         ...
-                        enabled = !state.tasks[state.currentCategory].isNullOrEmpty()
+                        enabled = !state.tasks[state.currentCategory].isNullOrEmpty()
                     ) {

183-203: Two buttons in one LazyRow item break spacing; split into separate items

Placing both buttons inside a single item { ... } ignores spacedBy(8.dp) and may overlap.

-            item {
-                FilledTonalIconButton(
-                    onClick = { showCategoryAddSheet = true },
-                    enabled = !editState
-                ) {
-                    Icon(
-                        imageVector = Icons.Rounded.Add,
-                        contentDescription = "Add Category"
-                    )
-                }
-
-                FilledTonalIconButton(
-                    onClick = onNavigateToEditCategories,
-                    enabled = !editState
-                ) {
-                    Icon(
-                        imageVector = Icons.Rounded.Edit,
-                        contentDescription = "Edit Categories"
-                    )
-                }
-            }
+            item {
+                FilledTonalIconButton(
+                    onClick = { showCategoryAddSheet = true },
+                    enabled = !editState
+                ) {
+                    Icon(
+                        imageVector = Icons.Rounded.Add,
+                        contentDescription = "Add Category"
+                    )
+                }
+            }
+            item {
+                FilledTonalIconButton(
+                    onClick = onNavigateToEditCategories,
+                    enabled = !editState
+                ) {
+                    Icon(
+                        imageVector = Icons.Rounded.Edit,
+                        contentDescription = "Edit Categories"
+                    )
+                }
+            }

239-247: Missing import for draggableHandle extension (compilation fails)

Modifier.draggableHandle(...) is used, but import sh.calvin.reorderable.draggableHandle is not present.

Add the import near your other reorderable imports:

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle

253-284: Avoid in-place mutation of Task and fix off-by-one when moving completed items

  • Mutating task.status in-place can confuse Compose state and snapshotting.
  • Moving to reorderableTasks.size - 1 places the item second-to-last, not last.

Apply this safer, deterministic update:

-                                            if (!editState) {
-                                                task.status = !task.status
-
-                                                onAction(TaskPageAction.UpsertTask(task))
-
-                                                if (task.status) {
-                                                    reorderableTasks =
-                                                        reorderableTasks.toMutableList().apply {
-                                                            add(
-                                                                reorderableTasks.size - 1,
-                                                                removeAt(index)
-                                                            )
-                                                        }
-
-                                                    onAction(
-                                                        TaskPageAction.ReorderTasks(
-                                                            reorderableTasks.mapIndexed { index, task -> index to task })
-                                                    )
-                                                }
-                                            }
+                                            if (!editState) {
+                                                val updated = task.copy(status = !task.status)
+                                                onAction(TaskPageAction.UpsertTask(updated))
+
+                                                val next = reorderableTasks.toMutableList()
+                                                next.removeAt(index)
+                                                if (updated.status) {
+                                                    // Move to the end
+                                                    next.add(next.size, updated)
+                                                    reorderableTasks = next
+                                                    onAction(
+                                                        TaskPageAction.ReorderTasks(
+                                                            reorderableTasks.mapIndexed { idx, t -> idx to t }
+                                                        )
+                                                    )
+                                                } else {
+                                                    // Keep position but replace instance for Compose
+                                                    next.add(index, updated)
+                                                    reorderableTasks = next
+                                                }
+                                            }
🧹 Nitpick comments (7)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (2)

95-99: Make FAB visibility more robust to partial scroll.

Only checking firstVisibleItemIndex == 0 keeps the FAB visible when the list is partially scrolled. Include the scroll offset for a crisper UX.

-    val fabVisible by remember {
-        derivedStateOf {
-            lazyListState.firstVisibleItemIndex == 0
-        }
-    }
+    val fabVisible by remember {
+        derivedStateOf {
+            lazyListState.firstVisibleItemIndex == 0 &&
+            lazyListState.firstVisibleItemScrollOffset == 0
+        }
+    }

128-133: Localize the “completed” counter using a formatted string.

String concatenation harms localization. Prefer a resources-backed format string with placeholders.

Proposed code (requires a new string resource):

-                    Text(
-                        text = "${state.completedHabits.size}/${state.habitsWithStatuses.size} " + stringResource(
-                            R.string.completed
-                        )
-                    )
+                    Text(
+                        text = stringResource(
+                            R.string.completed_count,
+                            state.completedHabits.size,
+                            state.habitsWithStatuses.size
+                        )
+                    )

Add to res/values/strings.xml:

<string name="completed_count">%1$d/%2$d completed</string>

I can open a follow-up PR to add the resource for all locales.

app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (3)

109-127: Reorder toggle enablement condition is inconsistent with visibility

Visibility requires at least two categories (categories.size >= 2), but enabled = state.tasks.keys.isNotEmpty() only checks that categories exist and says nothing about tasks. If the intent is to allow reordering when there are at least two categories, the enable check should match the visibility (or be omitted).

Align the enablement with visibility:

-                        onCheckedChange = { editState = it },
-                        enabled = state.tasks.keys.isNotEmpty()
+                        onCheckedChange = { editState = it },
+                        enabled = categories.size >= 2

If you actually want the toggle disabled when there are no tasks at all, consider:

enabled = state.tasks.values.any { it.isNotEmpty() }

74-80: Emit ReorderCategories once on drag stop (reduce churn and state thrash)

You dispatch ReorderCategories inside the move callback, firing on every drag frame. Prefer updating local state during drag and dispatching once when the user releases.

  • Keep the local list mutation in the move callback.
  • Move the onAction(TaskPageAction.ReorderCategories(...)) emission to the handle’s onDragStopped.

Apply these diffs:

Within rememberReorderableLazyListState:

 val reorderableListState = rememberReorderableLazyListState(listState) { from, to ->
     categories = categories.toMutableList().apply {
         add(to.index, removeAt(from.index))
     }
-
-    onAction(TaskPageAction.ReorderCategories(categories.mapIndexed { index, category -> index to category }))
 }

At the drag handle:

-                                    FilledTonalIconButton(
-                                        onClick = {},
-                                        modifier = Modifier.draggableHandle()
-                                    ) {
+                                    FilledTonalIconButton(
+                                        onClick = {},
+                                        modifier = Modifier.draggableHandle(
+                                            onDragStopped = {
+                                                onAction(
+                                                    TaskPageAction.ReorderCategories(
+                                                        categories.mapIndexed { idx, c -> idx to c }
+                                                    )
+                                                )
+                                            }
+                                        )
+                                    ) {

Also applies to: 169-179


146-147: Use plurals for task count to handle singular vs plural properly

Text currently concatenates a number with stringResource(R.string.tasks). This will read awkwardly for “1 tasks”.

If you have a plurals resource, prefer:

val count = state.tasks[category]?.size ?: 0
Text(text = pluralStringResource(R.plurals.tasks, count, count))
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

122-125: Use plurals for “items completed” to handle singular correctly

"${state.completedTasks.size} " + stringResource(R.string.items_completed) will read “1 items completed”.

Prefer a plurals resource (example):

val n = state.completedTasks.size
Text(text = pluralStringResource(R.plurals.items_completed, n, n))
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)

42-63: Optional: Provide a graceful pre-Android 12 fallback

On pre-S devices, content is not shown at all. Consider a tinted scrim with a lock icon/short copy instead of an empty area to keep layout consistent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 971bbdf and 512e6c9.

📒 Files selected for processing (7)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (8 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (4 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (5 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (11 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt (7 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskCard.kt (1)
  • TaskCard (27-88)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/HabitsList.kt (3)
app/src/main/java/com/shub39/grit/core/presentation/component/PageFill.kt (1)
  • PageFill (11-20)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitCard.kt (1)
  • HabitCard (49-239)
app/src/main/java/com/shub39/grit/core/presentation/component/Empty.kt (1)
  • Empty (20-45)
🔇 Additional comments (9)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/TaskList.kt (1)

109-126: LGTM: Top bar + nested scroll integration is clean

The enterAlwaysScrollBehavior wiring with LargeFlexibleTopAppBar and nested scroll looks correct and keeps the FAB logic straightforward.

app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt (1)

30-41: LGTM: Card scaffolding and title styling look solid

Using MaterialTheme shapes/typography and primary color for the title is consistent with M3 guidelines.

app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (3)

70-72: LGTM: import path updates align with the reorg

Component and utility imports reflect the new packages; matches usages below.

Also applies to: 79-83


469-473: LGTM: gating WeekDayBreakdown behind subscription via AnalyticsCard

The canSeeContent flag is correctly wired to the paywall action.


514-518: LGTM: CalendarMap paywall wiring

canSeeContent and onPlusClick are consistent with WeekDayBreakdown.

app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (4)

120-131: LGTM: top app bar migration with nested scroll

MediumFlexibleTopAppBar + enterAlways scroll behavior is correctly applied with nestedScroll.

Also applies to: 126-146


182-185: LGTM: WeekDayBreakdown behind paywall

canSeeContent and onPlusClick usage matches the new AnalyticsCard API.


229-233: LGTM: WeeklyGraph paywall + padding

Consistent with WeekDayBreakdown; inner padding improves chart legibility near the card edges.

Also applies to: 255-260


277-279: LGTM: HabitHeatMap spacing and primary-accented headers

Bottom/content padding reduce clipping; using primary for headers/initials improves visual hierarchy.

Also applies to: 288-288, 305-305

Comment on lines +42 to +63
if (!canSeeContent) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.heightIn(max = 300.dp)
.fillMaxWidth()
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Box(
modifier = Modifier
.matchParentSize()
.blur(
radius = 10.dp,
edgeTreatment = BlurredEdgeTreatment.Unbounded
)
) { content() }
}

Button(
onClick = onPlusClick
) { Text(text = stringResource(R.string.unlock_plus)) }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Block interaction and accessibility when content is locked

When canSeeContent is false on API 31+, the blurred content is still present in the tree. TalkBack can read it, and underlying charts/calendars may still receive gestures unless each consumer disables them. Add: (1) a full-size, non-indicating clickable/spacer overlay to consume touches, and (2) clear semantics on the blurred content to avoid a11y leakage.

Apply this diff:

@@
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
@@
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
@@
-import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
@@
         if (!canSeeContent) {
             Box(
                 contentAlignment = Alignment.Center,
                 modifier = Modifier
                     .heightIn(max = 300.dp)
                     .fillMaxWidth()
             ) {
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                     Box(
                         modifier = Modifier
                             .matchParentSize()
                             .blur(
                                 radius = 10.dp,
                                 edgeTreatment = BlurredEdgeTreatment.Unbounded
                             )
-                    ) { content() }
+                            .clearAndSetSemantics { }
+                    ) { content() }
                 }
 
+                // Swallow all pointer input over the locked area and optionally route to paywall
+                Box(
+                    modifier = Modifier
+                        .matchParentSize()
+                        .clickable(
+                            interactionSource = remember { MutableInteractionSource() },
+                            indication = null,
+                            onClick = onPlusClick
+                        ) { }
+                )
+
                 Button(
                     onClick = onPlusClick
                 ) { Text(text = stringResource(R.string.unlock_plus)) }
             }
         } else {
             content()
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/AnalyticsCard.kt
around lines 42–63, when canSeeContent is false the blurred content remains in
the composition and is exposed to TalkBack and input; fix by wrapping the
blurred content with clear semantics (use clearAndSetSemantics {} so it provides
no accessible content) and add a full-size, non-indicating overlay that consumes
all touches (e.g., a matchParentSize modifier with a clickable that uses
indication = null and a fresh MutableInteractionSource, or a pointerInput that
simply consumes events) so underlying charts/calendars no longer receive
gestures. Ensure the overlay is rendered above the blurred content and the
semantics change is applied to the blurred container only.

Comment on lines +169 to +176
AnimatedVisibility(editState) {
FilledTonalIconButton(
onClick = {},
modifier = Modifier.draggableHandle()
) {
Icon(
imageVector = Icons.Rounded.DragIndicator,
contentDescription = null
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing import for draggableHandle leads to compile error

Modifier.draggableHandle() is used but sh.calvin.reorderable.draggableHandle isn’t imported. The file won’t compile.

Apply this diff to add the missing import near your other reorderable imports:

 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import sh.calvin.reorderable.draggableHandle
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AnimatedVisibility(editState) {
FilledTonalIconButton(
onClick = {},
modifier = Modifier.draggableHandle()
) {
Icon(
imageVector = Icons.Rounded.DragIndicator,
contentDescription = null
// … other imports …
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import sh.calvin.reorderable.draggableHandle
// … rest of file …
🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/tasks/presentation/ui/section/EditCategories.kt
around lines 169 to 176, the code calls Modifier.draggableHandle() but the
import sh.calvin.reorderable.draggableHandle is missing which causes a compile
error; add the missing import alongside the other reorderable imports at the top
of the file (import sh.calvin.reorderable.draggableHandle) so
Modifier.draggableHandle() resolves correctly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (1)

523-530: Bug: today shouldn’t be nullable here (used as non-null in comparisons)

Inside CalendarMap you compare day.date with today. With today: LocalDate?, those operators won’t compile. Make today non-null.

-private fun CalendarMap(
+private fun CalendarMap(
     state: HabitPageState,
     onAction: (HabitsPageAction) -> Unit,
     calendarState: CalendarState,
     statuses: List<HabitStatus>,
-    today: LocalDate?,
+    today: LocalDate,
     currentHabit: Habit,
     primary: Color
 )
🧹 Nitpick comments (5)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (5)

126-128: Avoid !! on derived state; handle missing analytics habit gracefully

Current code will crash if analyticsHabitId doesn’t exist in habitsWithStatuses. Prefer a safe lookup with an early-return UX fallback.

Apply this diff to harden lookups:

-    val currentHabit = state.habitsWithStatuses.keys.find { it.id == state.analyticsHabitId }!!
-    val statuses = state.habitsWithStatuses[currentHabit]!!
+    val currentHabit = state.habitsWithStatuses.keys.firstOrNull { it.id == state.analyticsHabitId }
+        ?: run {
+            onNavigateBack()
+            return@PageFill
+        }
+    val statuses = state.habitsWithStatuses[currentHabit].orEmpty()

If navigating back isn’t desired, show an inline error state instead of returning.


129-133: Replace local vars + LaunchedEffect with remember/derivedStateOf for correctness and efficiency

These values are derived from statuses and startingDay. Using plain var + assigning inside LaunchedEffect doesn’t trigger recomposition and duplicates work. Compute once per dependency change.

-    var lineChartData = prepareLineChartData(state.startingDay, statuses)
-    var weekDayData = prepareWeekDayData(statuses.map { it.date }, primary)
-    var currentStreak = countCurrentStreak(statuses.map { it.date })
-    var bestStreak = countBestStreak(statuses.map { it.date })
+    val lineChartData by remember(statuses, state.startingDay) {
+        derivedStateOf { prepareLineChartData(state.startingDay, statuses) }
+    }
+    val weekDayData by remember(statuses, primary) {
+        derivedStateOf { prepareWeekDayData(statuses.map { it.date }, primary) }
+    }
+    val currentStreak by remember(statuses) {
+        derivedStateOf { countCurrentStreak(statuses.map { it.date }) }
+    }
+    val bestStreak by remember(statuses) {
+        derivedStateOf { countBestStreak(statuses.map { it.date }) }
+    }
@@
-    LaunchedEffect(statuses) {
-        lineChartData = prepareLineChartData(state.startingDay, statuses)
-        currentStreak = countCurrentStreak(statuses.map { it.date })
-        bestStreak = countBestStreak(statuses.map { it.date })
-        weekDayData = prepareWeekDayData(statuses.map { it.date }, primary)
-    }
+    // derivedStateOf handles recomputation when dependencies change; no side-effects needed

Add (outside the ranges above), if missing:

import androidx.compose.runtime.derivedStateOf

Also applies to: 150-155


163-213: Minor polish: consider setting containerColor for initial state parity

You set scrolledContainerColor but not containerColor. If surfaces differ, a color jump can occur before scrolling.

-            colors = TopAppBarDefaults.topAppBarColors(
-                scrolledContainerColor = MaterialTheme.colorScheme.surface
-            ),
+            colors = TopAppBarDefaults.topAppBarColors(
+                containerColor = MaterialTheme.colorScheme.surface,
+                scrolledContainerColor = MaterialTheme.colorScheme.surface
+            ),

559-559: Prefer enum comparison over string literal for day position

Comparing day.position.name to "MonthDate" is brittle. Use the enum directly.

-                if (day.position.name == "MonthDate") {
+                if (day.position == DayPosition.MonthDate) {

Add (outside this range), if not already:

import com.kizitonwose.calendar.core.DayPosition

560-604: Optimize date lookups: precompute a Set for O(1) membership

statuses.any { it.date == … } is called repeatedly in dayContent and scales poorly with history size. Use a remembered Set<LocalDate>.

@@
-        HorizontalCalendar(
+        // Precompute for fast membership checks
+        val doneDates = remember(statuses) { statuses.map { it.date }.toHashSet() }
+
+        HorizontalCalendar(
@@
-                    val done = statuses.any { it.date == day.date }
+                    val done = day.date in doneDates
@@
-                                    val donePrevious =
-                                        statuses.any { it.date == day.date.minusDays(1) }
-                                    val doneAfter =
-                                        statuses.any { it.date == day.date.plusDays(1) }
+                                    val donePrevious = day.date.minusDays(1) in doneDates
+                                    val doneAfter = day.date.plusDays(1) in doneDates
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 512e6c9 and c319c9f.

📒 Files selected for processing (2)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (18 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/shub39/grit/habits/presentation/ui/section/OverallAnalytics.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (1)
app/src/main/java/com/shub39/grit/habits/presentation/util.kt (1)
  • formatDateWithOrdinal (92-98)
🔇 Additional comments (6)
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt (6)

24-30: LGTM: import reorg and UI kit refactor look consistent

  • Moved core components to singular package and switched to ListItem and Rounded icon variants cleanly.
  • Opt-ins for ExperimentalMaterial3/Expressive APIs are in place.

Also applies to: 40-44, 50-50, 60-60, 64-64, 77-80, 86-90


157-161: Top app bar nested scroll integration looks good

Using enterAlwaysScrollBehavior() and wiring nestedScroll(scrollBehavior.nestedScrollConnection) at the column level will propagate to the inner LazyColumn. Looks correct with MediumFlexibleTopAppBar.


248-285: LGTM: dialogs/bottom sheet migration

Migration to GritDialog/GritBottomSheet, consistent iconography, input validation, and shape semantics all look solid. Good accessibility via content descriptions.

Also applies to: 295-470


482-485: LGTM: gating WeekDayBreakdown via canSeeContent

Using canSeeContent = state.isUserSubscribed with a plus action to trigger paywall is clear and self-contained.


700-707: Confirm paywall policy: Weekly heatmap isn’t gated, others are

WeeklyBooleanHeatMap lacks canSeeContent gating, while WeekDayBreakdown and CalendarMap are gated. Verify this is intentional.

If this should be gated, mirror the AnalyticsCard usage:

-    AnalyticsCard(
-        title = stringResource(R.string.weekly_progress)
-    ) {
+    AnalyticsCard(
+        title = stringResource(R.string.weekly_progress),
+        canSeeContent = state.isUserSubscribed,
+        onPlusClick = { onAction(HabitsPageAction.OnShowPaywall) }
+    ) {

632-636: Padding/contentPadding updates: LGTM

Consistent horizontal and bottom padding applied to charts/calendars improves visual rhythm and touch targets.

Also applies to: 705-707, 538-542

Comment on lines +705 to 707
modifier = Modifier.padding(bottom = 16.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
monthHeader = {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: today nullable in WeeklyBooleanHeatMap and used in comparisons

Same issue as CalendarMap. Make today non-null; otherwise comparisons and between-style calls won’t compile.

-private fun WeeklyBooleanHeatMap(
+private fun WeeklyBooleanHeatMap(
     heatMapState: HeatMapCalendarState,
     statuses: List<HabitStatus>,
-    today: LocalDate?,
+    today: LocalDate,
     onAction: (HabitsPageAction) -> Unit,
     currentHabit: Habit,
     primary: Color
 ) {

Also consider the same Set optimization used in CalendarMap for done/neighbors:

@@
-        HeatMapCalendar(
+        val doneDates = remember(statuses) { statuses.map { it.date }.toHashSet() }
+        HeatMapCalendar(
@@
-                val done = statuses.any { it.date == day.date }
+                val done = day.date in doneDates

Also applies to: 734-789, 693-699

Comment on lines +801 to +844
ListItem(
colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
headlineColor = MaterialTheme.colorScheme.onPrimaryContainer,
overlineColor = MaterialTheme.colorScheme.onPrimaryContainer,
supportingColor = MaterialTheme.colorScheme.onPrimaryContainer
),
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Card(
shape = MaterialTheme.shapes.extraLarge,
modifier = Modifier.weight(1f),
colors = CardDefaults.cardColors(
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(R.drawable.baseline_flag_circle_24),
contentDescription = "Flag",
modifier = Modifier.size(64.dp)
)

Spacer(modifier = Modifier.height(8.dp))

Text(
text = stringResource(R.string.started_on),
style = MaterialTheme.typography.titleSmall
.clip(
RoundedCornerShape(
topStart = 30.dp,
topEnd = 30.dp,
bottomStart = 10.dp,
bottomEnd = 10.dp
)

Text(
text = formatDateWithOrdinal(currentHabit.time.toLocalDate()),
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyLarge
)

Text(
text = stringResource(
R.string.days_ago_format,
ChronoUnit.DAYS.between(currentHabit.time.toLocalDate(), today)
),
style = MaterialTheme.typography.bodyMedium
)
}
}

Card(
shape = MaterialTheme.shapes.extraLarge,
modifier = Modifier.weight(1f),
colors = CardDefaults.cardColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
leadingContent = {
Icon(
imageVector = Icons.Rounded.FlagCircle,
contentDescription = "Flag",
modifier = Modifier.size(64.dp)
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(R.drawable.round_local_fire_department_24),
contentDescription = "Streak",
modifier = Modifier
.size(64.dp)
)

Text(
text = stringResource(R.string.streak),
style = MaterialTheme.typography.titleSmall
)
},
overlineContent = {
Text(
text = stringResource(R.string.started_on),
)
},
headlineContent = {
Text(
text = formatDateWithOrdinal(currentHabit.time.toLocalDate()),
fontWeight = FontWeight.Bold,
)
},
supportingContent = {
Text(
text = stringResource(
R.string.days_ago_format,
ChronoUnit.DAYS.between(currentHabit.time.toLocalDate(), today)
),
)
}
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: today should be non-null in HabitStats (and is used as such)

ChronoUnit.DAYS.between and text color logic expect a non-null LocalDate. Change the parameter type.

-private fun HabitStats(
+private fun HabitStats(
     currentHabit: Habit,
-    today: LocalDate?,
+    today: LocalDate,
     currentStreak: Int,
     bestStreak: Int
 ) {

The rest of the ListItem refactor looks good—clear hierarchy and appropriate container/foreground colors.

Also applies to: 848-889, 795-800

🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/habits/presentation/ui/section/AnalyticsPage.kt
around lines 795-800, 801-844 and 848-889 update HabitStats so the today
parameter is a non-null LocalDate (remove nullable type), adjust any callers to
pass a non-null LocalDate (derive from current context or default to
LocalDate.now() where appropriate), and update usages inside the component to
treat today as non-null (remove safe-calls or null checks used solely for
today). Ensure all ChronoUnit.DAYS.between and color logic receive the non-null
LocalDate and fix any compiler errors from changed signatures across the
codebase by updating call sites.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (12)
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (2)

108-111: Make icon decorative to avoid duplicate talkback announcements

The button already has visible text. Setting a contentDescription on the icon causes screen readers to read both. Prefer null for purely decorative icons.

Apply:

-                    Icon(
-                        imageVector = Icons.Rounded.Coffee,
-                        contentDescription = "Buy me a coffee",
-                    )
+                    Icon(
+                        imageVector = Icons.Rounded.Coffee,
+                        contentDescription = null,
+                    )

125-129: Google Play icon: make decorative; optional size consistency

  • Set contentDescription = null to avoid duplicate a11y label (text is present).
  • Nit: consider the default 24.dp for consistency with other icons.

Apply:

-                    Icon(
-                        imageVector = FontAwesomeIcons.Brands.GooglePlay,
-                        contentDescription = "Rate On Google Play",
-                        modifier = Modifier.size(20.dp)
-                    )
+                    Icon(
+                        imageVector = FontAwesomeIcons.Brands.GooglePlay,
+                        contentDescription = null,
+                        modifier = Modifier.size(20.dp)
+                    )

Optional consistency tweak:

-                        modifier = Modifier.size(20.dp)
+                        // modifier = Modifier.size(24.dp)
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (10)

63-71: Consider exitUntilCollapsed for a denser settings list

enterAlways keeps the bar expanded and scrolls content under it; exitUntilCollapsed collapses the large bar on scroll, giving more room to settings. UX call, but worth considering here.

Apply:

-    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()

96-99: Decorative leading icon in the “Grit Plus” card

Make icon decorative to avoid duplicated a11y announcements; the text “Grit Plus” already labels the control.

Apply:

-                        Icon(
-                            imageVector = Icons.Rounded.Add,
-                            contentDescription = "Grit Plus",
-                        )
+                        Icon(
+                            imageVector = Icons.Rounded.Add,
+                            contentDescription = null,
+                        )

110-113: Unify arrow style and make it decorative

Use Rounded.ArrowForwardIos to match the rest and set contentDescription = null.

Apply:

-                        Icon(
-                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
-                            contentDescription = "Grit Plus"
-                        )
+                        Icon(
+                            imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
+                            contentDescription = null
+                        )

255-260: Trailing chevron is decorative

ListItem already conveys navigation; make the icon decorative.

Apply:

-                    trailingContent = {
-                        Icon(
-                            imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
-                            contentDescription = "Navigate"
-                        )
-                    },
+                    trailingContent = {
+                        Icon(
+                            imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
+                            contentDescription = null
+                        )
+                    },

263-265: Leading “Palette” icon: decorative

Apply:

-                        Icon(
-                            imageVector = Icons.Rounded.Palette,
-                            contentDescription = "Navigate",
-                        )
+                        Icon(
+                            imageVector = Icons.Rounded.Palette,
+                            contentDescription = null,
+                        )

283-288: Backup row chevron: decorative

Apply:

-                    trailingContent = {
-                        Icon(
-                            imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
-                            contentDescription = "Navigate"
-                        )
-                    },
+                    trailingContent = {
+                        Icon(
+                            imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
+                            contentDescription = null
+                        )
+                    },

291-293: Backup leading icon: decorative

Apply:

-                        Icon(
-                            imageVector = Icons.Rounded.Download,
-                            contentDescription = "Backup",
-                        )
+                        Icon(
+                            imageVector = Icons.Rounded.Download,
+                            contentDescription = null,
+                        )

306-316: Supporting text duplicates the headline

The supportingContent repeats “About libraries.” Either provide a description string or remove the block to reduce noise.

Two options:

  1. Remove the redundant supporting text (safe, no new resources):
-                    supportingContent = {
-                        Text(
-                            text = stringResource(R.string.about_libraries)
-                        )
-                    },
  1. If you have/plan a description resource:
-                    supportingContent = {
-                        Text(
-                            text = stringResource(R.string.about_libraries)
-                        )
-                    },
+                    supportingContent = {
+                        Text(
+                            text = stringResource(R.string.about_libraries_desc)
+                        )
+                    },

319-321: About Libraries leading icon: decorative

Apply:

-                        Icon(
-                            imageVector = Icons.Rounded.Info,
-                            contentDescription = "About Libraries"
-                        )
+                        Icon(
+                            imageVector = Icons.Rounded.Info,
+                            contentDescription = null
+                        )

327-327: Spacer semantics nit

Spacer(modifier = Modifier.padding(60.dp)) is unclear. Prefer explicit size.

Apply:

-                Spacer(modifier = Modifier.padding(60.dp))
+                Spacer(modifier = Modifier.height(60.dp))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c319c9f and c60810c.

📒 Files selected for processing (3)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (4 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsGraph.kt
🔇 Additional comments (3)
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1)

1-1: Package move to settings.ui.component looks consistent with the re-org

No issues from a Compose or ABI standpoint. Call sites importing AboutApp should now target this package.

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (2)

1-1: Package rename to settings.ui.section is clean

Namespace matches the project’s new layout. No functional concerns.


15-20: Verify Compose Material3 and Icons dependencies
It appears your Gradle files do not declare androidx.compose.material3:material3, androidx.compose.material:material-icons-extended, or a Compose BOM, so imports in RootPage.kt (lines 15–20) and usages of LargeFlexibleTopAppBar and auto-mirrored icons at lines 25, 30, 35, 43, and 46 may fail to resolve.

– In your module build.gradle(.kts), ensure you have one of the following in the dependencies block:
implementation("androidx.compose.material3:material3:<version>")
implementation("androidx.compose.material:material-icons-extended:<version>")
• or the Compose BOM:
implementation(platform("androidx.compose:compose-bom:<version>"))

• Nit: You mix Filled.KeyboardArrowRight and Rounded.ArrowForwardIos. For visual consistency, consider standardizing on the Rounded icon set.

Comment on lines +11 to +12
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Coffee
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Switch to Material Icons for Coffee: confirm dependency is present

Using Icons.Rounded.Coffee requires material-icons-extended. Please verify the module includes that dependency (or BOM-managed alias), otherwise this will fail at compile.

Run this to confirm:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search Gradle files for material icons extended and material3 dependencies
rg -nP --glob '!**/build/**' --glob '!**/.gradle/**' \
  '(androidx\.compose\.material:material-icons-extended|androidx\.compose\.material3:material3)' \
  -- pretty

Length of output: 226


Here’s a corrected sweep for any references to the extended icons or Material3 artifacts:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look in all Gradle files for the two dependencies
rg -nP --glob '*.gradle*' \
    '(androidx\.compose\.material:material-icons-extended|androidx\.compose\.material3:material3)' -n .

# Check the version catalog if you’re using one
rg -nP --glob 'libs.versions.toml' \
    '(material-icons-extended|material3)' -n .

Length of output: 147


Add missing Material Icons Extended dependency

Our search found no references to androidx.compose.material:material-icons-extended or the Material3 BOM in your Gradle files or version catalog. Without this dependency, Icons.Rounded.Coffee will fail at compile time.

• In app/build.gradle (or the appropriate module’s Gradle file), add:

implementation "androidx.compose.material:material-icons-extended:<latest-version>"

(or, if you’re using a BOM, include the Material3 or Compose BOM that brings in the extended icons)
• Alternatively, if you manage versions via libs.versions.toml, add an alias for material-icons-extended and reference it in your dependencies block.

Please ensure the correct version is aligned with your Compose libraries and re-run the build.

🤖 Prompt for AI Agents
In
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
around lines 11-12, the code uses Icons.Rounded.Coffee but the project lacks the
Material Icons Extended dependency; add the appropriate dependency to the module
Gradle (e.g., implementation
"androidx.compose.material:material-icons-extended:<matching-version>") or add
an alias in libs.versions.toml and reference it, or include a Compose/Material3
BOM that brings in the extended icons; ensure the version you add matches your
Compose libraries and sync/rebuild.

@shub39 shub39 closed this Aug 23, 2025
@shub39 shub39 reopened this Aug 23, 2025
@shub39 shub39 merged commit 369fc46 into master Aug 23, 2025
2 checks passed
@shub39 shub39 deleted the state-changes branch August 23, 2025 09:56
@coderabbitai coderabbitai bot mentioned this pull request Nov 6, 2025
This was referenced Nov 14, 2025
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