Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
11221e0
Add agent project guidance
ohmzi May 19, 2026
360baad
Implement elastic top spacing for the first pinned row in the iOS tim…
ohmzi May 19, 2026
c04df7f
Implement elastic header and row spacing for the iOS timeline screens.
ohmzi May 19, 2026
00c9248
Implement elastic top insets for the mode tabs in the iOS Calendar sc…
ohmzi May 19, 2026
18a3346
Refine calendar day cell interactions in the Android Compose client.
ohmzi May 19, 2026
757832b
Refactor calendar view mode selection to use native segmented control…
ohmzi May 19, 2026
1711b81
Refactor calendar title and mode tab layout in the iOS SwiftUI client.
ohmzi May 19, 2026
48f7c34
Redesign the calendar view mode tabs and refine the mini-calendar UI …
ohmzi May 19, 2026
769e870
Refine the calendar UI and task timeline interactions across Android …
ohmzi May 19, 2026
0f97122
Implement interactive pop gesture and refine `TodoListScreen` UI for …
ohmzi May 19, 2026
e232389
Enable interactive swipe-back gestures in the iOS app root view.
ohmzi May 19, 2026
dfa0274
Refine interactive pop gesture reliability and navigation controller …
ohmzi May 19, 2026
7c3504e
Implement a custom header for the task creation sheet in the iOS Swif…
ohmzi May 19, 2026
d770224
Refactor and unify calendar card metrics for both iOS and Android cli…
ohmzi May 19, 2026
7ac5918
Refactor the task creation and list settings UI on iOS and enhance th…
ohmzi May 19, 2026
7266225
Refine the calendar UI for both Android (Compose) and iOS (SwiftUI) t…
ohmzi May 19, 2026
c00acdc
Refine the calendar UI for both Android (Compose) and iOS (SwiftUI) t…
ohmzi May 19, 2026
d3e90ed
Refine the UI for the "Create List" sheet and todo timeline on both i…
ohmzi May 19, 2026
f27a364
Revert "Refine the UI for the "Create List" sheet and todo timeline o…
ohmzi May 19, 2026
8906f64
Refactor task and list creation sheets for Android (Compose) and iOS …
ohmzi May 19, 2026
0620a18
Refine the UI for task and list creation sheets on both Android and iOS.
ohmzi May 19, 2026
5bd234d
Refactor the task creation sheet in the iOS SwiftUI client to replace…
ohmzi May 19, 2026
a7ff04b
Refactor `CreateTaskSheet` internal selector naming.
ohmzi May 20, 2026
c5a5fc9
Implement list creation timestamps and synchronize list ordering acro…
ohmzi May 20, 2026
18af7c1
Implement custom settings UI and enhance timeline section animations …
ohmzi May 20, 2026
31fe67b
Refactor timeline section expansion and collapse logic across Android…
ohmzi May 20, 2026
16813cb
Refactor `TodoListScreen` to remove empty placeholders in the timeline.
ohmzi May 20, 2026
558b098
Implement collapsing top bar navigation for Settings and App Version …
ohmzi May 20, 2026
00a0496
Remove item restoration functionality from the Completed screen in th…
ohmzi May 20, 2026
7fae1bb
Refactor task due date string formatting to use positional arguments.
ohmzi May 20, 2026
6012f8f
Refine task creation and completed screens across Android and iOS cli…
ohmzi May 20, 2026
cb876e8
Enhance `SettingsScreen` header animations and scroll behavior for th…
ohmzi May 20, 2026
6931a25
Improve keyboard and focus management in `CreateTaskBottomSheet`.
ohmzi May 20, 2026
0df0d28
Refactor the settings screen UI implementation from a `ScrollView` to…
ohmzi May 20, 2026
4bcfaf2
Refactor `SettingsScreen` and the version release view in the iOS Swi…
ohmzi May 20, 2026
802d68c
Refactor `SettingsScreen` and the version release view in the iOS Swi…
ohmzi May 20, 2026
af33bce
Refine top bar action button styling and apply circular chrome to key…
ohmzi May 20, 2026
75364dd
Update the Home screen search UI to support full-width expansion on i…
ohmzi May 20, 2026
0703a4e
Update the Home screen search UI to support full-width expansion on i…
ohmzi May 20, 2026
41fb153
Adjust search input focus delay for iOS and Android.
ohmzi May 20, 2026
6a745ff
Refactor the home screen search bar for the iOS and Android clients.
ohmzi May 20, 2026
43469b5
Refine the presentation detent and height management logic for the Cr…
ohmzi May 20, 2026
511496a
Adjust the maximum height for the "Create List" sheet in the iOS Swif…
ohmzi May 20, 2026
7f3676a
Implement dynamic height adjustment for the task creation sheet in th…
ohmzi May 20, 2026
fb5adb3
Refine layout and background styling for the "Create List" sheet in t…
ohmzi May 20, 2026
dae975d
Refine layout and background styling for the "Create List" sheet in t…
ohmzi May 20, 2026
bfbc10f
Refine the checkmark color on the iOS Completed screen.
ohmzi May 20, 2026
82a01b1
Implement standard bottom sheet sizing for iOS 18+ in the SwiftUI cli…
ohmzi May 20, 2026
3c5e5fd
Revert "Implement standard bottom sheet sizing for iOS 18+ in the Swi…
ohmzi May 20, 2026
448bbd2
Improve dynamic height handling and animations for "Create Task" and …
ohmzi May 20, 2026
9d2caba
Improve dynamic height handling and animations for "Create Task" and …
ohmzi May 20, 2026
2039f2e
Replace `ModalBottomSheet` with a custom animated `Dialog` for the li…
ohmzi May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# T'Day Agent Guide

This file is the working agreement for AI agents contributing to T'Day. Read it with `README.md`, `docs/ARCHITECTURE.md`, `docs/CODING_STANDARDS.md`, and `docs/TESTING.md`.

## Project Shape

T'Day is a private, self-hosted personal task planner with:

- `tday-web/`: Vite, React, TypeScript, Tailwind, i18next.
- `tday-backend/`: Ktor, Exposed, Flyway, PostgreSQL, JWE sessions.
- `shared/`: Kotlin Multiplatform DTOs, enums, and validators consumed by backend, Android, and iOS.
- `android-compose/`: Native Android app using Kotlin, Jetpack Compose, Hilt, Retrofit, offline cache and sync.
- `ios-swiftUI/`: Native iOS app using SwiftUI, SwiftData, Observation, URLSession, Keychain/cookie handling.

The native mobile apps should feel like one product expressed through two platform-native implementations.

## How To Work In This Repo

- Start by checking `git status --short --branch`. The worktree may already contain user changes.
- Never revert or overwrite user work unless explicitly asked.
- Avoid destructive git commands. Do not use `git reset --hard` or `git checkout --` to clean up.
- Prefer small, focused changes. Do not opportunistically refactor unrelated modules.
- When the user asks for implementation, implement it, verify it, then report clearly.
- When the user asks for a PR, push the active branch and open/update the PR they requested.
- When resolving merge conflicts into an outdated base, prefer the active/latest branch behavior unless the user explicitly says otherwise.

## Git And Attribution

- Commits should be authored as the user's GitHub identity:
- `user.name=ohmzi`
- `user.email=6551272+ohmzi@users.noreply.github.com`
- Check the local git config before committing if attribution matters.
- Do not add AI trailers or tool attribution to commit messages.
- Do not use `--no-verify` to bypass hooks. Fix the hook or the commit message instead.
- Keep commit messages short and human, for example `Refine Android calendar paging polish`.

## Cross-Platform UX Rule

Any user-facing mobile change on Android or iOS should trigger a quick parity check on the other platform.

Before finishing a mobile UI task, ask:

- Does Android and iOS expose the same feature surface?
- Do labels, task counts, date rules, empty states, and disabled states match?
- Do navigation rules match, including lower bounds and edge cases?
- Does the interaction feel platform-native on each OS?
- Is one platform now clearly nicer? If yes, bring the other platform up to the same product quality.

Do not blindly copy implementation details across platforms. Copy behavior, interaction rules, information architecture, and visual intent while using native APIs and established local patterns.

## Calendar UX Contract

The calendar is a cross-platform feature and should stay behaviorally aligned across Android and iOS.

Core rules:

- Modes are `Month`, `Week`, and `Day`.
- Default mode is `Month`.
- Week starts on Sunday.
- Navigation cannot go before the current month.
- The top-right calendar button jumps to today without changing the active mode.
- The FAB creates a task using the currently selected calendar date.
- Task counts come from pending scheduled items grouped by local start-of-day.
- Day and week task counts cap display at `9+`.
- The task section title stays in the form `Tasks due EEE, MMM d`.

Interaction rules:

- Swipe, chevron buttons, and Today jumps should use the same horizontal paging motion.
- Headers should stay anchored. In month view, the month title and weekday row should not slide with the date grid. In week/day view, the period header should not slide with the date content.
- Animate only the changing calendar content, then commit the selected date/period after the page settles.
- Avoid fade-heavy redraws for calendar paging. It should feel like a native pager, not a full card replacement.
- Prevent rapid repeated taps from stacking broken page transitions.
- If jumping a long distance back to today, animate toward the target in the correct direction and settle directly on today.

Relevant files:

- iOS: `ios-swiftUI/Tday/Feature/Calendar/CalendarScreen.swift`
- Android: `android-compose/app/src/main/java/com/ohmz/tday/compose/feature/calendar/CalendarScreen.kt`

## Mobile UI Direction

T'Day is a task app, not a marketing site. Mobile screens should feel quiet, useful, and polished.

- Prefer direct usable UI over explanatory copy.
- Keep controls discoverable through familiar icons, clear labels, and expected placement.
- Use rounded typography and the existing soft, focused visual language.
- Keep cards purposeful. Do not nest decorative cards.
- Use haptics where the surrounding code already does for primary button actions.
- Text must fit in compact mobile layouts without overlap or truncation that hides meaning.
- Empty states should be calm and short.
- Preserve dark mode.

## Design Tokens And Strings

- Web strings live in i18next locale files.
- Android strings live in `android-compose/app/src/main/res/values/strings.xml`.
- iOS strings should follow the current local SwiftUI patterns until a broader localization layer exists.
- Prefer theme and dimension tokens over inline colors or magic sizes.
- If a new semantic color or repeated dimension is needed, add it to the platform's theme/token layer instead of scattering literals.
- Existing feature-scoped constants can remain when they are already part of that feature's local style, but do not expand hardcoded styling casually.

## Architecture Expectations

Backend:

- Keep request/response contracts aligned with `shared/` models when possible.
- Services return typed errors and avoid leaking internals.
- Preserve tenant isolation in all data access.

Android:

- Use MVVM with `@HiltViewModel`, `StateFlow`, repositories, and app services.
- Keep mutable state private and expose read-only state.
- Respect offline-first cache/sync behavior.
- Use Compose idioms and Material 3.

iOS:

- Use SwiftUI, Observation, SwiftData, and URLSession patterns already present in the app.
- Keep feature code inside `Feature/<Name>/` unless it is truly shared.
- Prefer small local helpers before creating broad abstractions.

Web:

- Use React Query for server state.
- Use the shared API client, not raw backend `fetch` calls from components.
- Use Tailwind semantic tokens and locale keys.

## Verification Commands

Run the smallest meaningful verification for the change, then broaden when risk is higher.

Common commands:

```bash
# Web
cd tday-web && npm run lint
cd tday-web && npm run test

# Backend
./gradlew :tday-backend:test

# Android
cd android-compose && ./gradlew :app:compileDebugKotlin
cd android-compose && ./gradlew :app:testDebugUnitTest

# iOS
xcodebuild test -project ios-swiftUI/TdayApp.xcodeproj -scheme Tday -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6'
```

For mobile UI changes, prefer running the platform build even when there are no dedicated UI tests. If a simulator/device visual check is practical, do it.

## Review Checklist Before Final Response

- `git status --short --branch` is clean or only contains intentional uncommitted changes.
- User-facing behavior is aligned across Android and iOS when the feature exists on both.
- Strings, colors, and dimensions follow the project conventions.
- No secrets, build outputs, dependency folders, or generated artifacts were added.
- Tests/builds relevant to the touched area were run, or skipped with a clear reason.
- If committed, the commit author is `ohmzi <6551272+ohmzi@users.noreply.github.com>`.
- If pushed/opened as a PR, report the branch, commit, and PR URL.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Tday/

| Document | Purpose |
|----------|---------|
| [`AGENTS.md`](AGENTS.md) | AI agent workflow, git expectations, and cross-platform UX parity rules |
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Developer setup, conventions, PR process |
| [`SECURITY.md`](SECURITY.md) | Security practices and responsible disclosure |
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | System design, domain boundaries, data flow |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,6 @@ fun TdayApp() {
uiState = uiState,
onBack = { navController.popBackStack() },
onRefresh = viewModel::refresh,
onUncomplete = viewModel::uncomplete,
onDelete = { item ->
viewModel.delete(item) {
showTaskDeletedToast()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data class CachedListRecord(
val iconKey: String? = null,
val todoCount: Int = 0,
val updatedAtEpochMs: Long = 0L,
val createdAtEpochMs: Long = 0L,
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,20 @@ internal fun listToCache(list: ListSummary): CachedListRecord {
iconKey = list.iconKey,
todoCount = list.todoCount,
updatedAtEpochMs = list.updatedAt?.toEpochMilli() ?: 0L,
createdAtEpochMs = list.createdAt?.toEpochMilli() ?: 0L,
)
}

internal fun orderListsLikeWeb(lists: List<CachedListRecord>): List<CachedListRecord> {
if (lists.none { it.createdAtEpochMs > 0L }) return lists
return lists.withIndex()
.sortedWith(
compareByDescending<IndexedValue<CachedListRecord>> { it.value.createdAtEpochMs }
.thenBy { it.index },
)
.map { it.value }
}

internal fun listFromCache(
cache: CachedListRecord,
todoCountOverride: Int,
Expand All @@ -82,6 +93,11 @@ internal fun listFromCache(
} else {
null
},
createdAt = if (cache.createdAtEpochMs > 0L) {
Instant.ofEpochMilli(cache.createdAtEpochMs)
} else {
null
},
)
}

Expand Down Expand Up @@ -168,6 +184,7 @@ internal fun mapListDto(dto: ListDto, iconFallback: String? = null): ListSummary
iconKey = dto.iconKey ?: iconFallback,
todoCount = dto.todoCount,
updatedAt = parseOptionalInstant(dto.updatedAt),
createdAt = parseOptionalInstant(dto.createdAt),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,11 @@ val MIGRATION_1_2 = object : Migration(1, 2) {
)
}
}

val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE `cached_lists` ADD COLUMN `createdAtEpochMs` INTEGER NOT NULL DEFAULT 0",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object DatabaseModule {
TdayDatabase::class.java,
"tday_offline_cache.db",
)
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.allowMainThreadQueries()
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ data class CachedListEntity(
val iconKey: String?,
val todoCount: Int,
val updatedAtEpochMs: Long,
val createdAtEpochMs: Long,
)

@Entity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ fun CachedListRecord.toEntity() = CachedListEntity(
iconKey = iconKey,
todoCount = todoCount,
updatedAtEpochMs = updatedAtEpochMs,
createdAtEpochMs = createdAtEpochMs,
)

fun CachedListEntity.toRecord() = CachedListRecord(
Expand All @@ -52,6 +53,7 @@ fun CachedListEntity.toRecord() = CachedListRecord(
iconKey = iconKey,
todoCount = todoCount,
updatedAtEpochMs = updatedAtEpochMs,
createdAtEpochMs = createdAtEpochMs,
)

fun CachedCompletedRecord.toEntity() = CachedCompletedEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import androidx.room.RoomDatabase
PendingMutationEntity::class,
SyncMetadataEntity::class,
],
version = 2,
version = 3,
exportSchema = false,
)
abstract class TdayDatabase : RoomDatabase() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import com.ohmz.tday.compose.core.data.SecureConfigStore
import com.ohmz.tday.compose.core.data.cache.LOCAL_LIST_PREFIX
import com.ohmz.tday.compose.core.data.cache.OfflineCacheManager
import com.ohmz.tday.compose.core.data.cache.listFromCache
import com.ohmz.tday.compose.core.data.cache.orderListsLikeWeb
import com.ohmz.tday.compose.core.data.cache.parseOptionalInstant
import com.ohmz.tday.compose.core.data.isLikelyUnrecoverableMutationError
import com.ohmz.tday.compose.core.data.requireApiBody
import com.ohmz.tday.compose.core.data.sync.SyncManager
import com.ohmz.tday.compose.core.model.CreateListRequest
import com.ohmz.tday.compose.core.model.ListSummary
import com.ohmz.tday.compose.core.model.UpdateListRequest
import com.ohmz.tday.compose.core.model.capitalizeFirstListLetter
import com.ohmz.tday.compose.core.data.cache.parseOptionalInstant
import com.ohmz.tday.compose.core.network.TdayApiService
import java.util.UUID
import javax.inject.Inject
Expand Down Expand Up @@ -55,6 +56,7 @@ class ListRepository @Inject constructor(
color = color,
iconKey = iconKey,
todoCount = 0,
createdAtEpochMs = timestampMs,
updatedAtEpochMs = timestampMs,
)
state.copy(
Expand Down Expand Up @@ -84,7 +86,10 @@ class ListRepository @Inject constructor(
).list
}.onSuccess { createdList ->
if (createdList == null) return@onSuccess
val createdAt = parseOptionalInstant(createdList.updatedAt)?.toEpochMilli() ?: timestampMs
val createdAt =
parseOptionalInstant(createdList.createdAt)?.toEpochMilli() ?: timestampMs
val updatedAt =
parseOptionalInstant(createdList.updatedAt)?.toEpochMilli() ?: timestampMs
cacheManager.updateOfflineState { state ->
val remapped = replaceLocalListId(
state = state,
Expand All @@ -100,7 +105,8 @@ class ListRepository @Inject constructor(
color = createdList.color,
iconKey = createdList.iconKey ?: list.iconKey,
todoCount = todoCount,
updatedAtEpochMs = createdAt,
updatedAtEpochMs = updatedAt,
createdAtEpochMs = createdAt,
)
} else {
list
Expand Down Expand Up @@ -240,7 +246,7 @@ class ListRepository @Inject constructor(
.filterNot { it.completed }
.groupingBy { it.listId }
.eachCount()
return state.lists.map {
return orderListsLikeWeb(state.lists).map {
listFromCache(cache = it, todoCountOverride = todoCountsByList[it.id] ?: 0)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.ohmz.tday.compose.core.data.cache.listToCache
import com.ohmz.tday.compose.core.data.cache.mapCompletedDto
import com.ohmz.tday.compose.core.data.cache.mapListDto
import com.ohmz.tday.compose.core.data.cache.mapTodoDto
import com.ohmz.tday.compose.core.data.cache.orderListsLikeWeb
import com.ohmz.tday.compose.core.data.cache.todoMergeKey
import com.ohmz.tday.compose.core.data.cache.todoToCache
import com.ohmz.tday.compose.core.data.isLikelyConnectivityIssue
Expand All @@ -28,8 +29,8 @@ import com.ohmz.tday.compose.core.model.CreateTodoRequest
import com.ohmz.tday.compose.core.model.DeleteTodoRequest
import com.ohmz.tday.compose.core.model.ListSummary
import com.ohmz.tday.compose.core.model.TodoCompleteRequest
import com.ohmz.tday.compose.core.model.TodoItem
import com.ohmz.tday.compose.core.model.TodoInstanceUpdateRequest
import com.ohmz.tday.compose.core.model.TodoItem
import com.ohmz.tday.compose.core.model.TodoPrioritizeRequest
import com.ohmz.tday.compose.core.model.TodoUncompleteRequest
import com.ohmz.tday.compose.core.model.UpdateListRequest
Expand Down Expand Up @@ -584,9 +585,11 @@ class SyncManager @Inject constructor(
.filterNot { it.completed }
.groupingBy { it.listId }
.eachCount()
val normalizedLists = mergedLists.map {
it.copy(todoCount = todoCountByList[it.id] ?: 0)
}
val normalizedLists = orderListsLikeWeb(
mergedLists.map {
it.copy(todoCount = todoCountByList[it.id] ?: 0)
},
)

val dataMergedState = localState.copy(
todos = mergedTodos,
Expand Down
Loading
Loading