TASK-045: sky blue + white redesign, UbiSam logo

Rebuilds the Material3 light/dark palette around a sky-blue primary, adds the UBGiteaTopBar wordmark to the repository list, and recolors the launcher icon background to match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
최민영 2026-09-28 13:17:31 +09:00
parent 39be1b71dc
commit 71c8457578
8 changed files with 365 additions and 90 deletions

View File

@ -51,6 +51,7 @@ import com.ubisam.ubgitea.R
import com.ubisam.ubgitea.domain.organizations.OrganizationSummary
import com.ubisam.ubgitea.domain.model.RepositoryCoordinates
import com.ubisam.ubgitea.ui.SimplePaginationFooter
import com.ubisam.ubgitea.ui.components.UBGiteaTopBar
import com.ubisam.ubgitea.ui.formatInstant
@Composable
@ -77,7 +78,10 @@ fun RepositoryListScreen(
onViewSettings: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(modifier = modifier.fillMaxSize()) { padding ->
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = { UBGiteaTopBar() },
) { padding ->
when (state) {
RepositoryListUiState.SignedOut -> MessageState(
stringResource(R.string.repositories_sign_in_description),

View File

@ -0,0 +1,110 @@
package com.ubisam.ubgitea.ui.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.ubisam.ubgitea.ui.theme.UbiSamLogoBlue
import com.ubisam.ubgitea.ui.theme.UbiSamLogoGray
/**
* App-wide header: the UbiSam wordmark, small, top-left, on every top-level screen. Nested screens
* (settings, notifications, branch detail, ...) keep the app's existing system-back-gesture
* navigation as-is -- this bar is deliberately just a branding element, not a navigation bar, so it
* carries no back button and doesn't need to know about any screen's back stack.
*/
@Composable
fun UBGiteaTopBar(modifier: Modifier = Modifier, actions: @Composable RowScope.() -> Unit = {}) {
Surface(color = MaterialTheme.colorScheme.surface, modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
UBGiteaLogo()
Spacer(Modifier.weight(1f))
actions()
}
}
}
@Composable
fun UBGiteaLogo(modifier: Modifier = Modifier) {
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Ubi",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = UbiSamLogoGray,
)
Text(
text = "Sam",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = UbiSamLogoBlue,
)
Spacer(Modifier.width(5.dp))
Box(
modifier = Modifier
.size(18.dp)
.clip(RoundedCornerShape(6.dp))
.background(UbiSamLogoBlue),
contentAlignment = Alignment.Center,
) {
SignalMark(color = Color.White, modifier = Modifier.size(11.dp))
}
}
}
// Stand-in for the real logo's signal/RSS mark, drawn rather than imported so the redesign needs
// no new image asset: a dot plus two quarter-arcs, all centered on the bottom-left corner of the
// box, matching the mark in the real UbiSam wordmark. Swap for an actual drawable resource if/when
// the source image file is available.
@Composable
private fun SignalMark(color: Color, modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
val corner = Offset(0f, size.height)
val stroke = size.minDimension * 0.22f
drawCircle(
color = color,
radius = size.minDimension * 0.14f,
center = Offset(size.minDimension * 0.14f, size.height - size.minDimension * 0.14f),
)
listOf(0.55f, 0.9f).forEach { fraction ->
val radius = size.minDimension * fraction
drawArc(
color = color,
startAngle = -90f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(corner.x - radius, corner.y - radius),
size = Size(radius * 2f, radius * 2f),
style = Stroke(width = stroke, cap = StrokeCap.Round),
)
}
}
}

View File

@ -2,59 +2,63 @@ package com.ubisam.ubgitea.ui.theme
import androidx.compose.ui.graphics.Color
// Gitea's own logo/brand green is #609926 (verified against Gitea's source:
// web_src/css/themes/theme-gitea-light.css's --color-logo). The Material3 primary role below is a
// deeper tone of that same hue rather than the exact logo value, purely for contrast: white text
// on #609926 sits around a 3.4:1 contrast ratio, under WCAG's 4.5:1 threshold for normal-size
// button labels. Every other color here is a hand-built, standard-shaped Material3 tonal
// palette (primary/secondary/tertiary, each with a container + on-* pair, plus neutrals) grown
// from that same green family, so the whole app reads as one deliberate "Gitea green" theme
// instead of Compose's unthemed default (Material3's baseline purple, which is what every screen
// in this app looked like before this file existed with real values).
// Sky-blue + white redesign (superseding the earlier "Gitea green" theme -- see git history for
// that palette if it's ever needed again). Primary is a deep-enough sky blue (Tailwind's sky-700
// family) that white text on it clears WCAG's 4.5:1 threshold for normal-size button labels, while
// still reading unmistakably as "sky blue" rather than navy. Background is a near-white with a
// faint blue cast rather than stark #FFFFFF, so white cards/surfaces still have visible separation
// from the page. Every other role here is a hand-built, standard-shaped Material3 tonal palette
// (primary/secondary/tertiary, each with a container + on-* pair, plus neutrals) grown from that
// same blue family, so the whole app reads as one deliberate sky-blue theme.
// Light theme
val GreenPrimaryLight = Color(0xFF3B6A1F)
val OnGreenPrimaryLight = Color(0xFFFFFFFF)
val GreenPrimaryContainerLight = Color(0xFFBCEF8B)
val OnGreenPrimaryContainerLight = Color(0xFF0C2000)
val SageSecondaryLight = Color(0xFF55624A)
val OnSageSecondaryLight = Color(0xFFFFFFFF)
val SageSecondaryContainerLight = Color(0xFFD9E7C8)
val OnSageSecondaryContainerLight = Color(0xFF131F0B)
val TealTertiaryLight = Color(0xFF386661)
val OnTealTertiaryLight = Color(0xFFFFFFFF)
val TealTertiaryContainerLight = Color(0xFFBBECE5)
val OnTealTertiaryContainerLight = Color(0xFF00201D)
val BackgroundLight = Color(0xFFFCFDF6)
val OnBackgroundLight = Color(0xFF1A1C16)
val SurfaceVariantLight = Color(0xFFDFE4D3)
val OnSurfaceVariantLight = Color(0xFF43483C)
val OutlineLight = Color(0xFF73796B)
val OutlineVariantLight = Color(0xFFC3C8B8)
val InverseSurfaceLight = Color(0xFF2F312A)
val InverseOnSurfaceLight = Color(0xFFF1F1E9)
val InversePrimaryLight = Color(0xFFA1D474)
val SkyPrimaryLight = Color(0xFF0369A1)
val OnSkyPrimaryLight = Color(0xFFFFFFFF)
val SkyPrimaryContainerLight = Color(0xFFBAE6FD)
val OnSkyPrimaryContainerLight = Color(0xFF082F49)
val SlateSecondaryLight = Color(0xFF4C6B82)
val OnSlateSecondaryLight = Color(0xFFFFFFFF)
val SlateSecondaryContainerLight = Color(0xFFD3E4F0)
val OnSlateSecondaryContainerLight = Color(0xFF0F2A3A)
val CyanTertiaryLight = Color(0xFF006874)
val OnCyanTertiaryLight = Color(0xFFFFFFFF)
val CyanTertiaryContainerLight = Color(0xFFA6EEF2)
val OnCyanTertiaryContainerLight = Color(0xFF001F24)
val BackgroundLight = Color(0xFFF7FBFF)
val OnBackgroundLight = Color(0xFF10202B)
val SurfaceVariantLight = Color(0xFFDCE8EF)
val OnSurfaceVariantLight = Color(0xFF41505A)
val OutlineLight = Color(0xFF71818C)
val OutlineVariantLight = Color(0xFFC0D0D9)
val InverseSurfaceLight = Color(0xFF1D333D)
val InverseOnSurfaceLight = Color(0xFFECF3F8)
val InversePrimaryLight = Color(0xFF8ED0FA)
// Dark theme — tones flip per Material3 convention (lighter primary for legibility on a dark
// Dark theme -- tones flip per Material3 convention (lighter primary for legibility on a dark
// surface; the light theme's primary becomes the dark theme's inversePrimary, and vice versa).
val GreenPrimaryDark = Color(0xFFA1D474)
val OnGreenPrimaryDark = Color(0xFF1B3700)
val GreenPrimaryContainerDark = Color(0xFF2C4F0A)
val OnGreenPrimaryContainerDark = Color(0xFFBCEF8B)
val SageSecondaryDark = Color(0xFFBDCBAB)
val OnSageSecondaryDark = Color(0xFF28331E)
val SageSecondaryContainerDark = Color(0xFF3E4A32)
val OnSageSecondaryContainerDark = Color(0xFFD9E7C8)
val TealTertiaryDark = Color(0xFFA0D0C9)
val OnTealTertiaryDark = Color(0xFF003733)
val TealTertiaryContainerDark = Color(0xFF1E4E49)
val OnTealTertiaryContainerDark = Color(0xFFBBECE5)
val BackgroundDark = Color(0xFF12140F)
val OnBackgroundDark = Color(0xFFE2E3D9)
val SurfaceVariantDark = Color(0xFF43483C)
val OnSurfaceVariantDark = Color(0xFFC3C8B8)
val OutlineDark = Color(0xFF8D9285)
val OutlineVariantDark = Color(0xFF43483C)
val InverseSurfaceDark = Color(0xFFE2E3D9)
val InverseOnSurfaceDark = Color(0xFF2F312A)
val InversePrimaryDark = Color(0xFF3B6A1F)
val SkyPrimaryDark = Color(0xFF8ED0FA)
val OnSkyPrimaryDark = Color(0xFF00344D)
val SkyPrimaryContainerDark = Color(0xFF024C6E)
val OnSkyPrimaryContainerDark = Color(0xFFBAE6FD)
val SlateSecondaryDark = Color(0xFFB4CADD)
val OnSlateSecondaryDark = Color(0xFF1C3444)
val SlateSecondaryContainerDark = Color(0xFF34495C)
val OnSlateSecondaryContainerDark = Color(0xFFD3E4F0)
val CyanTertiaryDark = Color(0xFF83D2DB)
val OnCyanTertiaryDark = Color(0xFF00363D)
val CyanTertiaryContainerDark = Color(0xFF004F58)
val OnCyanTertiaryContainerDark = Color(0xFFA6EEF2)
val BackgroundDark = Color(0xFF0B141B)
val OnBackgroundDark = Color(0xFFE3EDF3)
val SurfaceVariantDark = Color(0xFF41505A)
val OnSurfaceVariantDark = Color(0xFFC0D0D9)
val OutlineDark = Color(0xFF8B9AA4)
val OutlineVariantDark = Color(0xFF41505A)
val InverseSurfaceDark = Color(0xFFE3EDF3)
val InverseOnSurfaceDark = Color(0xFF1D333D)
val InversePrimaryDark = Color(0xFF0369A1)
// UbiSam wordmark colors -- fixed brand colors independent of light/dark theme (a logo should look
// the same in both modes, unlike the tonal palette above).
val UbiSamLogoGray = Color(0xFF5B6472)
val UbiSamLogoBlue = Color(0xFF0EA5E9)

View File

@ -6,25 +6,25 @@ import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
// Gitea-green Material3 color schemes (see Color.kt for the individual values and how they were
// derived from Gitea's actual logo green). Every role not set explicitly here — error/onError/
// errorContainer/onErrorContainer, most notably, kept as Material3's own baseline red — is left at
// the corresponding lightColorScheme()/darkColorScheme() default rather than reinvented, since
// nothing about this request was about changing what "error" or "removed" looks like, only about
// branding the app around Gitea's own color.
// Sky-blue Material3 color schemes (see Color.kt for the individual values and how they were
// derived). Every role not set explicitly here -- error/onError/errorContainer/onErrorContainer,
// most notably, kept as Material3's own baseline red -- is left at the corresponding
// lightColorScheme()/darkColorScheme() default rather than reinvented, since nothing about this
// request was about changing what "error" or "removed" looks like, only about the app's own brand
// color.
private val LightColorScheme = lightColorScheme(
primary = GreenPrimaryLight,
onPrimary = OnGreenPrimaryLight,
primaryContainer = GreenPrimaryContainerLight,
onPrimaryContainer = OnGreenPrimaryContainerLight,
secondary = SageSecondaryLight,
onSecondary = OnSageSecondaryLight,
secondaryContainer = SageSecondaryContainerLight,
onSecondaryContainer = OnSageSecondaryContainerLight,
tertiary = TealTertiaryLight,
onTertiary = OnTealTertiaryLight,
tertiaryContainer = TealTertiaryContainerLight,
onTertiaryContainer = OnTealTertiaryContainerLight,
primary = SkyPrimaryLight,
onPrimary = OnSkyPrimaryLight,
primaryContainer = SkyPrimaryContainerLight,
onPrimaryContainer = OnSkyPrimaryContainerLight,
secondary = SlateSecondaryLight,
onSecondary = OnSlateSecondaryLight,
secondaryContainer = SlateSecondaryContainerLight,
onSecondaryContainer = OnSlateSecondaryContainerLight,
tertiary = CyanTertiaryLight,
onTertiary = OnCyanTertiaryLight,
tertiaryContainer = CyanTertiaryContainerLight,
onTertiaryContainer = OnCyanTertiaryContainerLight,
background = BackgroundLight,
onBackground = OnBackgroundLight,
surface = BackgroundLight,
@ -39,18 +39,18 @@ private val LightColorScheme = lightColorScheme(
)
private val DarkColorScheme = darkColorScheme(
primary = GreenPrimaryDark,
onPrimary = OnGreenPrimaryDark,
primaryContainer = GreenPrimaryContainerDark,
onPrimaryContainer = OnGreenPrimaryContainerDark,
secondary = SageSecondaryDark,
onSecondary = OnSageSecondaryDark,
secondaryContainer = SageSecondaryContainerDark,
onSecondaryContainer = OnSageSecondaryContainerDark,
tertiary = TealTertiaryDark,
onTertiary = OnTealTertiaryDark,
tertiaryContainer = TealTertiaryContainerDark,
onTertiaryContainer = OnTealTertiaryContainerDark,
primary = SkyPrimaryDark,
onPrimary = OnSkyPrimaryDark,
primaryContainer = SkyPrimaryContainerDark,
onPrimaryContainer = OnSkyPrimaryContainerDark,
secondary = SlateSecondaryDark,
onSecondary = OnSlateSecondaryDark,
secondaryContainer = SlateSecondaryContainerDark,
onSecondaryContainer = OnSlateSecondaryContainerDark,
tertiary = CyanTertiaryDark,
onTertiary = OnCyanTertiaryDark,
tertiaryContainer = CyanTertiaryContainerDark,
onTertiaryContainer = OnCyanTertiaryContainerDark,
background = BackgroundDark,
onBackground = OnBackgroundDark,
surface = BackgroundDark,

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- App icon background layer. This app had no custom launcher icon at all until now (Android's
own default was showing), so this is a solid fill in the same green already used as this
app's Material 3 primary color (ui/theme/Color.kt's GreenPrimaryLight), so the home-screen
icon and the in-app theme read as the same deliberate color, not two different greens. -->
<!-- App icon background layer: a solid fill in the same sky blue used as this app's Material 3
primary color (ui/theme/Color.kt's SkyPrimaryLight), so the home-screen icon and the in-app
theme read as the same deliberate color rather than drifting apart after the sky-blue redesign
(this used to match the earlier "Gitea green" theme's primary before that changed). -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3B6A1F"
android:fillColor="#0369A1"
android:pathData="M0,0h108v108h-108z" />
</vector>

View File

@ -1012,3 +1012,51 @@ This is the append-only implementation and handoff record shared by Codex and Cl
whether biometric reauthentication is required — is a product decision for the human product owner,
not something to build unprompted. No further design-review gaps currently queued.
- Next owner: Claude, continuing per the standing direction.
## 2026-09-08 — TASK-045: Sky blue + white redesign, UbiSam logo
- Agent and role: Claude, feature implementer, at the human product owner's explicit request to
overhaul the app's visual design ("현재 UI/UX를 전체적으로 갈아엎고싶다... 하늘색과 하얀색으로
구성하고 싶어"), with a small UbiSam logo top-left. This revises TASK-039's "Gitea green" visual
identity decision rather than starting from an unset one.
- Commit/diff: working tree in the main checkout (`D:/Android/UBGitea`, branch
`claude/task-015-notifications-main`), not yet committed; new `tasks/045-sky-blue-redesign.md`.
- Features implemented: App-wide Material3 light/dark theme rebuilt around a sky-blue primary with a
near-white background; a small UbiSam wordmark (drawn, not an image asset) in the top-left of the
저장소 목록 (root/home) screen; launcher icon background recolored to match.
- How it was implemented: `ui/theme/Color.kt`/`Theme.kt` rebuilt with the same shaped
primary/secondary/tertiary + container/on-* + neutral roles TASK-039 used, just re-derived from a
sky-blue seed (`SkyPrimaryLight` `#0369A1`, chosen deep enough for WCAG 4.5:1 white-text contrast).
New `ui/components/UBGiteaTopBar.kt` adds `UBGiteaTopBar`/`UBGiteaLogo`, with the logo's small
signal/RSS-mark badge hand-drawn via `Canvas`/`drawArc` rather than adding a Material Icons
dependency (this app has none today). The logo was deliberately added only to the root screen and
only as branding, not a navigation bar — the other six screens have no back-button UI today (system
back gesture only, with non-trivial existing tab/pager back-interception logic in
`BranchDetailScreen`), and retrofitting a context-aware header into them would be a real navigation
behavior change, not a color/branding pass, so it was left out. Those six screens still pick up the
new color theme automatically since none hardcode colors outside `MaterialTheme.colorScheme`.
`ic_launcher_background.xml`'s fill was updated from `#3B6A1F` to `#0369A1` to match.
- Files changed: `ui/theme/Color.kt`, `ui/theme/Theme.kt`, new `ui/components/UBGiteaTopBar.kt`,
`feature/repositories/RepositoryListScreen.kt` (topBar wiring), `res/drawable/ic_launcher_background.xml`,
`docs/product.md`, `tasks/045-sky-blue-redesign.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug`
BUILD SUCCESSFUL — all existing unit tests passing unmodified (no behavior touched by this task),
0 lint errors.
- Live verification: not yet done. Wireless `adb pair` failed five consecutive times across three
freshly issued pairing codes with `protocol fault (couldn't read status message)`, on a PC that
could otherwise ping the phone with 0% packet loss — pointing at a local firewall/security-software
block on the TLS pairing handshake, not an expired code. Diagnosis paused at the human product
owner's direction ("일단 보류"); USB cable connection was proposed as the way to bypass this and is
still pending.
- Review findings and resolutions: None from a separate review pass yet.
- Known limitations: not yet confirmed on the human product owner's physical device (see above); the
logo is a hand-drawn recreation of the reference image, not the actual asset file, since it was only
shared inline in chat and not as a file this agent could read; six of seven screens get the new
theme automatically but no logo/branded header, by design.
- Next recommended task: get the change onto the product owner's device (USB, once available) to
confirm the redesign looks right; if they still want the exact reference logo image rather than the
drawn recreation, get it as a saved file and swap it into `UBGiteaLogo`. Diagnosing the wireless-adb
firewall block remains open but was explicitly deprioritized by the product owner.
- Next owner: Claude, continuing per the standing direction; human product owner for the device
verification and the logo-asset decision.

View File

@ -49,9 +49,9 @@ The first release is read-oriented. Creating, merging, deleting, or administerin
- Android application ID: `com.ubisam.ubgitea` (see ADR-001)
- Minimum supported version: Android 9, API 28 (see ADR-001)
- Visual identity, icon, and color palette: sky blue + white, with the UbiSam wordmark small in the top-left of the root screen (see TASK-045)
- Repositories are cached for offline browsing at the list level only (branches/files inside a repository are not cached); see TASK-044
## Open product decisions
- Visual identity, icon, and color palette
- Whether biometric reauthentication is required
- Whether repositories must be cached for offline browsing

View File

@ -0,0 +1,109 @@
# TASK-045: Sky blue + white redesign, UbiSam logo
Status: implemented; all required checks pass; awaiting live device confirmation (wireless adb
pairing is currently broken on the human product owner's PC -- see Known limitations)
Owner: Claude
Reviewer: Codex
## Outcome
Revises `docs/product.md`'s "Visual identity, icon, and color palette" decision (originally closed
by TASK-039 with a "Gitea green" theme), at the human product owner's fresh, explicit request
("현재 UI/UX를 전체적으로 갈아엎고싶다... 하늘색과 하얀색으로 구성하고 싶어"). The app's Material 3
theme moves from that earlier green palette to a sky-blue + white one, and the UbiSam wordmark now
appears small in the top-left of the root (저장소 목록) screen, matching the reference logo image the
product owner shared.
## Scope
- Included: `ui/theme/Color.kt` and `Theme.kt` (full light/dark Material3 tonal palette rebuilt
around a sky-blue primary); a new shared `UBGiteaTopBar`/`UBGiteaLogo` composable; wiring the top
bar into `RepositoryListScreen` (the app's root/home screen); the launcher icon background color
updated to match the new primary.
- Excluded (deliberately): adding the logo or a visible top bar to the other six screens (settings,
notifications, work items, branch detail/files, history), and adding visible back-arrow buttons
anywhere. Those screens have no back-button UI today -- they rely entirely on the system back
gesture, with non-trivial existing logic around exactly when that gesture is intercepted (see
`BranchDetailScreen`'s tab/pager comments). Retrofitting a header with a back action into that
would mean threading current-tab-aware back callbacks through screens that don't expose that
today -- a real behavior change, not a color/branding pass, so it was left out of this task. Those
screens still get the new sky-blue theme automatically, since none of them hardcode colors outside
`MaterialTheme.colorScheme`.
- Excluded (pending an asset): the logo is currently a hand-drawn Compose recreation (`UBGiteaLogo`
in `ui/components/UBGiteaTopBar.kt`) -- grey "Ubi" + blue "Sam" text plus a small rounded badge
with a drawn signal/RSS mark -- reproducing the reference image's colors and layout without an
image asset. If the actual PNG/SVG is provided later, it should replace this with a real
`Image(painterResource(...))`.
## Design notes
- **Primary is a deep-enough sky blue for contrast, not the literal reference-image blue**, same
reasoning as the previous "Gitea green" theme: `SkyPrimaryLight` (`#0369A1`) is chosen so white
text/icons on it clear WCAG's 4.5:1 threshold, while lighter tones (`SkyPrimaryContainerLight`,
`InversePrimaryLight`) carry the brighter, more literal sky-blue feel.
- **Background is a near-white with a faint blue cast (`#F7FBFF`), not pure white**, so white
cards/surfaces still have visible separation from the page -- pure white-on-white would flatten
the whole UI.
- **The logo appears only on the root screen.** An app wordmark conventionally belongs on the
top-level/home screen; nested screens showing the same static logo would be redundant chrome that
doesn't help navigation, and (per Scope above) adding a context-aware header to those screens is a
separate, larger change than what was asked for here.
- **No new icon library dependency.** The app currently has zero Material Icons usage (an established
convention -- see the launcher icon's own comment about "no icon library"); the small signal/RSS
mark inside the logo badge is drawn with `Canvas`/`drawArc` rather than pulling in
`material-icons-core` for one glyph.
- **Launcher icon background recolored to match**, so the home-screen icon and in-app theme read as
the same deliberate color rather than drifting apart (this exact reasoning -- and this exact file --
already existed for the old green theme; this task only changes the fill value and comment).
## Acceptance criteria
- [x] App-wide Material3 light and dark color schemes use a sky-blue primary with a near-white
background, replacing the old green theme, with no other role (error, etc.) touched.
- [x] The 저장소 목록 (root) screen shows a small UbiSam wordmark top-left.
- [x] No behavior change to navigation, back handling, or any non-visual logic anywhere in the app.
- [x] Launcher icon background color matches the new theme's primary.
- [x] Build, unit tests, and lint pass.
- [ ] Live-confirmed on the human product owner's device -- blocked, see Known limitations.
## Required checks
```text
./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug
```
## Evidence
- `ui/theme/Color.kt`: full sky-blue light/dark palette (`SkyPrimary*`, `SlateSecondary*`,
`CyanTertiary*`, background/surface/outline/inverse roles), plus fixed `UbiSamLogoGray`/
`UbiSamLogoBlue` brand constants independent of light/dark mode.
- `ui/theme/Theme.kt`: `LightColorScheme`/`DarkColorScheme` rebuilt from the new values; structure
otherwise unchanged from the previous theme file.
- New `ui/components/UBGiteaTopBar.kt`: `UBGiteaTopBar` (a plain branding bar, not a Material3
`TopAppBar`, deliberately without a back button -- see Scope), `UBGiteaLogo`, and a private
`SignalMark` Canvas composable.
- `feature/repositories/RepositoryListScreen.kt`: `Scaffold`'s `topBar` now renders
`UBGiteaTopBar()`.
- `res/drawable/ic_launcher_background.xml`: fill color changed from `#3B6A1F` to `#0369A1`.
- `docs/product.md`: "Visual identity, icon, and color palette" and "Whether repositories must be
cached for offline browsing" (already shipped by TASK-044, previously left undocumented as
resolved) moved from Open to Product decisions.
- `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug`:
BUILD SUCCESSFUL, all existing unit tests passing unmodified (no behavior touched), 0 lint errors.
## Known limitations
- **Not yet confirmed on the human product owner's physical device.** Wireless `adb` pairing failed
five consecutive times with `protocol fault (couldn't read status message)` across three freshly
issued pairing codes, on a PC that could otherwise ping the phone with 0% loss -- pointing at a
local firewall/security-software block on the TLS pairing handshake specifically, not an expired
code or a network-reachability problem. Diagnosis was paused at the product owner's direction
("일단 보류"); a USB cable connection was suggested as the way to bypass this and hasn't been tried
yet.
- The logo is a hand-drawn recreation, not the actual reference image file (see Scope).
- Six of the app's seven screens get the new color theme automatically but no logo/branded header --
intentional, see Scope.
## Review findings
None yet -- pending Codex/human review.