diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt b/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt index 4aeceae..004f63a 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button @@ -26,6 +27,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text @@ -37,6 +39,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.ImeAction @@ -155,7 +158,7 @@ private fun ModeTabs(tab: WorkItemsTab, onShowIssues: () -> Unit, onShowPullRequ private fun IssueRow(issue: IssueSummary, onSelected: () -> Unit) { Column(Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = onSelected).padding(horizontal = 16.dp, vertical = 12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Text(stringResource(issue.state.labelResource), style = MaterialTheme.typography.bodySmall) + StatusBadge(issue.state) Text("#${issue.number}", style = MaterialTheme.typography.bodySmall) } Text(issue.title, style = MaterialTheme.typography.titleMedium, maxLines = 2) @@ -168,7 +171,7 @@ private fun IssueRow(issue: IssueSummary, onSelected: () -> Unit) { private fun PullRequestRow(pr: PullRequestSummary, onSelected: () -> Unit) { Column(Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = onSelected).padding(horizontal = 16.dp, vertical = 12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Text(stringResource(pr.state.labelResource), style = MaterialTheme.typography.bodySmall) + StatusBadge(pr.state) Text("#${pr.number}", style = MaterialTheme.typography.bodySmall) } Text(pr.title, style = MaterialTheme.typography.titleMedium, maxLines = 2) @@ -182,7 +185,7 @@ private fun IssueDetailView(state: WorkItemsUiState.IssueDetailContent) { LazyColumn(Modifier.fillMaxSize()) { item { Column(Modifier.fillMaxWidth().padding(16.dp)) { - Text(stringResource(state.detail.state.labelResource), style = MaterialTheme.typography.bodySmall) + StatusBadge(state.detail.state, Modifier.padding(bottom = 4.dp)) Text(state.detail.title, style = MaterialTheme.typography.titleLarge) Text(stringResource(R.string.workitems_author, state.detail.author.label()), style = MaterialTheme.typography.bodySmall) if (state.detail.labels.isNotEmpty()) { @@ -208,7 +211,7 @@ private fun PullRequestDetailView(state: WorkItemsUiState.PullRequestDetailConte LazyColumn(Modifier.fillMaxSize()) { item { Column(Modifier.fillMaxWidth().padding(16.dp)) { - Text(stringResource(state.detail.state.labelResource), style = MaterialTheme.typography.bodySmall) + StatusBadge(state.detail.state, Modifier.padding(bottom = 4.dp)) Text(state.detail.title, style = MaterialTheme.typography.titleLarge) Text(stringResource(R.string.workitems_author, state.detail.author.label()), style = MaterialTheme.typography.bodySmall) Text(stringResource(R.string.workitems_pr_refs, state.detail.baseRef, state.detail.headRef), style = MaterialTheme.typography.bodySmall) @@ -416,6 +419,50 @@ private val PullRequestState.labelResource: Int PullRequestState.MERGED -> R.string.workitems_pr_merged } +// 열림/닫힘/병합됨 used to render as a plain bodySmall line indistinguishable in weight from the +// "#12" issue number next to it -- easy to miss when scanning a list. This reuses the same compact +// colored-Surface badge treatment RepositoryListScreen's VisibilityBadge already established for +// 공개/비공개, borrowing existing theme roles rather than inventing new colors: OPEN gets the app's +// green primaryContainer (this app's one "affirmative" accent, same one VisibilityBadge uses for a +// public repository), CLOSED gets the baseline Material3 errorContainer (matches how "closed"/ +// "removed" states are treated elsewhere per Theme.kt's own doc comment), and MERGED -- a third, +// visually distinct state GitHub/Gitea conventionally colors purple, which this theme has no role +// for -- gets the teal tertiaryContainer instead of introducing an unthemed one-off color. +@Composable +private fun StatusBadge(state: IssueState, modifier: Modifier = Modifier) { + val (container, content) = when (state) { + IssueState.OPEN -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer + IssueState.CLOSED -> MaterialTheme.colorScheme.errorContainer to MaterialTheme.colorScheme.onErrorContainer + } + StatusBadge(stringResource(state.labelResource), container, content, modifier) +} + +@Composable +private fun StatusBadge(state: PullRequestState, modifier: Modifier = Modifier) { + val (container, content) = when (state) { + PullRequestState.OPEN -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer + PullRequestState.CLOSED -> MaterialTheme.colorScheme.errorContainer to MaterialTheme.colorScheme.onErrorContainer + PullRequestState.MERGED -> MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onTertiaryContainer + } + StatusBadge(stringResource(state.labelResource), container, content, modifier) +} + +@Composable +private fun StatusBadge(label: String, containerColor: Color, contentColor: Color, modifier: Modifier = Modifier) { + Surface( + color = containerColor, + contentColor = contentColor, + shape = RoundedCornerShape(4.dp), + modifier = modifier, + ) { + Text( + label, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + ) + } +} + private val ReviewState.labelResource: Int get() = when (this) { ReviewState.PENDING -> R.string.workitems_review_pending diff --git a/docs/development-log.md b/docs/development-log.md index 987ce7a..bd0ce33 100644 --- a/docs/development-log.md +++ b/docs/development-log.md @@ -845,3 +845,45 @@ This is the append-only implementation and handoff record shared by Codex and Cl detail views with real content, file viewers, extended dark-mode coverage) from where it was paused for this bug fix. - Next owner: Claude, continuing per the standing direction. + +## 2026-08-29 (continued) — TASK-042: colored status badges for issue/PR state + +- Agent and role: Claude, direct continuation of the same live session, per "쭉 이어서 개발이랑 디자인 + 진행해줄래?" — resuming the paused design-review pass named as next work in TASK-041's log entry. +- Investigation: first re-verified the review item flagged as unresolved at the pause point — whether + issue/PR body text renders correctly for real content (the one PR viewed earlier happened to have no + body). Opened the real "PostActivated 관련 추가 필요" issue (multi-paragraph Korean body plus an + embedded C# code block) and confirmed it renders correctly end to end — no bug found there. While + doing that, noticed the 열림/닫힘/병합됨 state label next to it (and next to every issue/PR, in both + list rows and detail headers) was a plain, uncolored `bodySmall` line, no more visually prominent + than the `#8` issue number beside it — inconsistent with the colored `VisibilityBadge` treatment + TASK-039 already gave 공개/비공개 on the repository list. +- Commit/diff: on `claude/task-015-notifications`; `feature/workitems/WorkItemsScreen.kt` gained a + `StatusBadge` composable (two thin state-typed overloads sharing one rendering implementation) + replacing the four plain-text state-label call sites (`IssueRow`, `PullRequestRow`, + `IssueDetailView`, `PullRequestDetailView`); `tasks/042-workitem-status-badges.md`. +- How it was implemented: reused existing Material3 theme roles rather than inventing new colors — + OPEN gets `primaryContainer` (the same green `VisibilityBadge` uses for a public repository), CLOSED + gets the baseline `errorContainer` `Theme.kt` already reserves for closed/removed states, and MERGED + (a third state GitHub/Gitea conventionally color purple, which this theme has no role for) gets the + teal `tertiaryContainer` instead. Both light and dark mode are handled automatically since no new + hardcoded color was introduced. +- Files changed: `feature/workitems/WorkItemsScreen.kt`, `tasks/042-workitem-status-badges.md`. +- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` + BUILD SUCCESSFUL, all existing unit tests passing unmodified (a pure presentation change with no new + branching logic beyond the enums' own exhaustive `when`s), 0 lint errors. +- Live verification: installed over wireless `adb`. RobotAutomationDevelopment-Dept/OC5-PilotLine- + RectangleTypeWelder PR tab: confirmed a teal "병합됨" badge on two merged PRs and a green "열림" badge + on the third, side by side (screenshot); opened the merged PR's detail view and confirmed the same + teal badge in the header (screenshot). Repeated with `adb shell cmd uimode night yes` — both badge + colors keep good contrast against the dark surface (screenshot) — then restored `night no`. +- Review findings and resolutions: None from a separate review pass. +- Known limitations: `NotificationsScreen.kt` renders its own separate PR/이슈 상태 column as plain + text — the same visual gap, in a different screen, not touched in this pass to keep it scoped to + what was actually being reviewed. Did not visually confirm the red CLOSED badge with this fix + installed (no real closed issue/PR was opened live this session) — low risk, since the color mapping + reuses the same `errorContainer` role already used unconditionally elsewhere in the app. +- Next recommended task: continue the design-review pass — the text/image/CSV/PDF file viewers (not + yet reviewed this session), and consider extending today's status-badge fix to + `NotificationsScreen.kt` for consistency. +- Next owner: Claude, continuing per the standing direction. diff --git a/tasks/042-workitem-status-badges.md b/tasks/042-workitem-status-badges.md new file mode 100644 index 0000000..4ea0bc6 --- /dev/null +++ b/tasks/042-workitem-status-badges.md @@ -0,0 +1,97 @@ +# TASK-042: Colored status badges for issue/PR state (열림/닫힘/병합됨) + +Status: implemented; all required checks pass; live-confirmed on the human product owner's device +Owner: Claude +Reviewer: Codex + +## Outcome + +Design-review follow-up (continuing the "앱의 아이콘부터 해서 전체적으로 확인해보고 부족한 부분 고쳐줘" +pass from TASK-039). While verifying that issue/PR body text renders correctly for real content +(it does — confirmed against a real issue with a multi-paragraph body and an embedded code block), +noticed the 열림/닫힘/병합됨 state label next to each issue/PR — in both the list row and the detail +header — rendered as a plain, uncolored `bodySmall` line, visually no more prominent than the +`#12` issue number sitting right next to it. Gitea's own web UI (and GitHub's, which established the +convention) colors this label so it reads at a glance while scanning a list. Replaced the plain text +with a compact colored badge, reusing the `VisibilityBadge` pattern TASK-039 already established for +공개/비공개 on the repository list. + +## Scope + +- Included: `feature/workitems/WorkItemsScreen.kt` — new private `StatusBadge` composable (two + overloads, one per state enum, sharing one color-agnostic rendering implementation) replacing the + four plain-`Text` state-label call sites: `IssueRow`, `PullRequestRow`, `IssueDetailView`, + `PullRequestDetailView`. +- Excluded: `NotificationsScreen.kt`'s own PR/이슈 state labels (a separate screen/composable, out of + scope for this pass — noted below as a known limitation, not silently left inconsistent). + `ReviewState` labels (승인/변경 요청/etc. on `ReviewRow`) — these are a different kind of status + with no obvious two/three-color GitHub/Gitea convention to borrow, left as plain text. + +## Design notes + +- **Reused existing theme roles instead of introducing new colors.** OPEN gets + `primaryContainer`/`onPrimaryContainer` — the same green `VisibilityBadge` already uses for a + public repository's affirmative state. CLOSED gets `errorContainer`/`onErrorContainer` — the + baseline Material3 red `Theme.kt` deliberately left untouched for exactly this kind of + closed/removed state. MERGED — a third state GitHub/Gitea conventionally color purple, which this + app's green/teal-accented theme has no role for — gets `tertiaryContainer`/`onTertiaryContainer` + (teal) rather than a one-off hardcoded color with no theme backing (and no dark-mode variant of its + own to get wrong). +- **One rendering composable, two small color-mapping overloads.** `IssueState` and `PullRequestState` + are different enums (`PullRequestState` has the extra MERGED case), so a single `when` couldn't + cover both — instead each enum gets its own thin `StatusBadge(state, modifier)` overload that maps + to a container/content color pair and delegates to the same private `StatusBadge(label, container, + content, modifier)` for the actual `Surface` + `Text` rendering, keeping the visual treatment + (rounded 4dp corner, `labelSmall`, 6dp/2dp padding) defined in exactly one place. + +## Acceptance criteria + +- [x] Issue list rows show a colored badge (not plain text) for 열림/닫힘. +- [x] PR list rows show a colored badge for 열림/닫힘/병합됨. +- [x] Issue detail header shows the same colored badge as its list row. +- [x] PR detail header shows the same colored badge as its list row. +- [x] Colors are pulled from the existing Material3 theme (no new hardcoded colors), so dark mode is + handled automatically. +- [x] Build, unit tests, and lint pass. +- [x] Live-confirmed on the human product owner's device: PR list showing 병합됨 (teal) ×2 and 열림 + (green) ×1 side by side; PR detail header showing the same teal 병합됨 badge; both re-confirmed + unchanged under forced dark mode. + +## Required checks + +```text +./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug +``` + +## Evidence + +- `feature/workitems/WorkItemsScreen.kt`: `StatusBadge` composable (two state-typed overloads + + one shared rendering implementation) added; `IssueRow`, `PullRequestRow`, `IssueDetailView`, + `PullRequestDetailView` each call it in place of a plain `Text(stringResource(state.labelResource))`. +- `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug`: + BUILD SUCCESSFUL, all unit tests passing (no test changes needed — this is a pure presentation + change with no new branching logic to cover beyond what the enum `when`s already exhaustively + check at compile time), 0 lint errors. +- Live device: installed over wireless `adb`. Notifications → PostActivated 관련 추가 필요 이슈 detail: + confirmed the issue body (multi-paragraph Korean text plus an indented C# code block) renders + correctly end to end — this was the original investigation trigger, and it surfaced no bug on its + own. RobotAutomationDevelopment-Dept/OC5-PilotLine-RectangleTypeWelder PR tab: #6/#5 both show a + teal "병합됨" badge, #4 shows a green "열림" badge (screenshot); opened #6's detail view and confirmed + the same teal badge appears in the header (screenshot). Forced dark mode + (`adb shell cmd uimode night yes`) and reconfirmed the same PR list — both badge colors keep good + contrast against the dark surface (screenshot) — then restored `night no`. + +## Known limitations + +- `NotificationsScreen.kt` still renders its own PR/이슈 상태 column as plain text — a second, + separate location with the same visual gap this task fixed on the work-items screens. Not touched + here to keep this change scoped to what was actually being reviewed; worth a follow-up pass. +- Did not find a real CLOSED (닫힘) issue/PR with this fix installed to visually confirm the red badge + live — the CLOSED branch was exercised only via the existing 닫힘 rows seen in `NotificationsScreen` + (a different, unfixed screen) prior to this change. The color mapping itself + (`errorContainer`/`onErrorContainer`) is the same theme role used unconditionally elsewhere in the + app, so this is a low-risk gap, not left silently assumed away. + +## Review findings + +None yet — pending Codex/human review.