TASK-042: colored status badges for issue/PR state (열림/닫힘/병합됨)

Replaces the plain-text 열림/닫힘/병합됨 label on issue/PR list rows and
detail headers with a compact colored badge, reusing the VisibilityBadge
pattern TASK-039 established for 공개/비공개. OPEN reuses the app's green
primaryContainer, CLOSED reuses the baseline errorContainer, MERGED reuses
the teal tertiaryContainer -- all existing theme roles, so dark mode is
handled automatically with no new hardcoded colors.

Found while verifying issue/PR body rendering for real content (confirmed
correct, no bug) during the design-review pass. Live-verified on-device in
both light and dark mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
최민영 2026-08-29 22:40:33 +09:00
parent 39cbad522e
commit 471456258b
3 changed files with 190 additions and 4 deletions

View File

@ -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

View File

@ -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.

View File

@ -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.