diff --git a/.gitignore b/.gitignore index 0a826448..843774f1 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,5 @@ ClaudeReport.md docs/brand/visual-identity.md docs/brand/asset-system.md docs/brand/visual-identity.md +docs/brand/asset-system.md +ClaudeBrandingReview.md diff --git a/ClaudeBrandingReview.md b/ClaudeBrandingReview.md index a0a5cc73..bad7458b 100644 --- a/ClaudeBrandingReview.md +++ b/ClaudeBrandingReview.md @@ -99,8 +99,9 @@ Legend: ✅ on-brand / no art needed · ➕ add/þwire art (prompt below) · Priority order = the moments a user feels most. Each is self-contained after you paste the House Style block first. **A1 · Pairing success celebration** — *1:1, transparent bg.* -> Scene: two equal heart-halves (soft pink + soft lavender) sliding together into one whole heart at the center, with -> a gentle burst of small floating hearts and petals around it. Convey "you're connected now." Calm, joyful, no text. +> Scene: the Closer mark resolving into place — a soft-pink upper C-arc and a lavender lower sweep curving together to +> enclose a heart-shaped space with a small keyhole at its center — with a gentle burst of small floating hearts and +> petals around it. Convey "you're connected now." Calm, joyful, no text. (White keyhole if on a dark background.) **A2 · Answer history empty state** — *1:1.* > Scene: a small soft journal/photo-album, slightly open, with two paired cards tucked inside and a few faint floating @@ -151,7 +152,7 @@ Priority order = the moments a user feels most. Each is self-contained after you > alarm imagery. **G-set · Notification + relationship glyphs** — *single-color vector, square, legible at 24 dp.* (One prompt each:) -> Simple single-color flat glyph in the Closer style, no text, no background: **heart-of-two-halves**, **paired sealed +> Simple single-color flat glyph in the Closer style, no text, no background: **the Closer C-heart-keyhole mark**, **paired sealed > cards**, **daily card**, **sealed answer (card with small lock)**, **memory capsule**, **date-card with heart**, > **quiet-hours moon**, **couple-premium (heart + small crown/spark, tasteful)**, **export-data**, **delete-account**. > Export in lavender for in-app and as high-contrast monochrome for the platform notification glyph. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc60fab8..6de58705 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -131,6 +131,7 @@ dependencies { implementation(composeBom) implementation("androidx.core:core-ktx:1.15.0") + implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") implementation("androidx.activity:activity-compose:1.9.3") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87559a64..4a8ef50d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:name=".MainActivity" android:exported="true" android:windowSoftInputMode="adjustResize" - android:theme="@style/Theme.Closer"> + android:theme="@style/Theme.Closer.Splash"> diff --git a/app/src/main/java/app/closer/MainActivity.kt b/app/src/main/java/app/closer/MainActivity.kt index ef501675..46f21d18 100644 --- a/app/src/main/java/app/closer/MainActivity.kt +++ b/app/src/main/java/app/closer/MainActivity.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope @@ -62,7 +63,21 @@ class MainActivity : AppCompatActivity() { private val pendingDeepLink = mutableStateOf(null) override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) + // Gentle scale-up + fade exit, handing off to the animated in-app logo loader. + splashScreen.setOnExitAnimationListener { provider -> + provider.iconView.animate() + .scaleX(1.15f) + .scaleY(1.15f) + .setDuration(300L) + .start() + provider.view.animate() + .alpha(0f) + .setDuration(300L) + .withEndAction { provider.remove() } + .start() + } maybeRequestNotificationPermission() registerFcmToken() pendingDeepLink.value = deepLinkRouteFromIntent(intent) diff --git a/app/src/main/java/app/closer/ui/components/LoadingState.kt b/app/src/main/java/app/closer/ui/components/LoadingState.kt index d2530821..3fc31548 100644 --- a/app/src/main/java/app/closer/ui/components/LoadingState.kt +++ b/app/src/main/java/app/closer/ui/components/LoadingState.kt @@ -4,29 +4,43 @@ import app.closer.R import app.closer.ui.theme.closerCardColor import android.provider.Settings import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image +import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.imageResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp @Composable @@ -68,6 +82,15 @@ fun CloserHeartLoader( CloserMarkLoader(modifier = modifier, size = size) } +/** Below this size we use a lightweight inline spinner instead of the full app-icon chip. */ +private val ChipLoaderMinSize = 40.dp + +/** + * The brand loader. At prominent sizes it renders the **white-keyhole logo on an aubergine app-icon + * chip** with a soft fill that rises through the mark (the "charging the connection" motion). At small + * inline sizes (e.g. inside buttons) it falls back to a lightweight indeterminate arc tinted to the + * current content color, so it never shows a dark square on a colored button. Honors "remove animations". + */ @Composable fun CloserMarkLoader( modifier: Modifier = Modifier, @@ -81,21 +104,48 @@ fun CloserMarkLoader( 1f ) == 0f } + if (size >= ChipLoaderMinSize) { + CloserMarkChipLoader(modifier = modifier, size = size, reducedMotion = reducedMotion) + } else { + CloserInlineSpinner( + modifier = modifier, + size = size, + color = LocalContentColor.current, + reducedMotion = reducedMotion + ) + } +} + +@Composable +private fun CloserMarkChipLoader( + modifier: Modifier, + size: Dp, + reducedMotion: Boolean +) { val transition = rememberInfiniteTransition(label = "closerMarkLoader") - val animatedPulse = transition.animateFloat( - initialValue = 0.96f, - targetValue = 1.04f, + val fill = if (reducedMotion) 1f else transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Restart + ), + label = "closerMarkFill" + ).value + val pulse = if (reducedMotion) 1f else transition.animateFloat( + initialValue = 0.97f, + targetValue = 1.03f, animationSpec = infiniteRepeatable( animation = tween(durationMillis = 900, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "closerMarkPulse" - ) - val pulse = if (reducedMotion) 1f else animatedPulse.value + ).value - Image( - painter = painterResource(R.drawable.closer_mark_loader), - contentDescription = null, + // White-keyhole mark (already safe-zone padded) drawn over the aubergine app-icon chip. + val mark = ImageBitmap.imageResource(R.drawable.closer_launcher_foreground) + + Canvas( modifier = modifier .size(size) .graphicsLayer { @@ -103,5 +153,88 @@ fun CloserMarkLoader( scaleY = pulse } .clearAndSetSemantics {} - ) + ) { + val w = this.size.width + val h = this.size.height + val corner = CornerRadius(w * 0.22f, h * 0.22f) + // Aubergine gradient chip (matches ic_launcher_background palette). + drawRoundRect( + brush = Brush.linearGradient( + colors = listOf(Color(0xFF5A2F74), Color(0xFF3A1D4D), Color(0xFF24122F)), + start = Offset(0f, 0f), + end = Offset(w, h) + ), + cornerRadius = corner + ) + val src = IntSize(mark.width, mark.height) + val dst = IntSize(w.toInt(), h.toInt()) + // Ghost of the full mark… + drawImage( + image = mark, + srcOffset = IntOffset.Zero, + srcSize = src, + dstOffset = IntOffset.Zero, + dstSize = dst, + alpha = 0.16f + ) + // …then the fill rises bottom→top. + clipRect(top = h * (1f - fill)) { + drawImage( + image = mark, + srcOffset = IntOffset.Zero, + srcSize = src, + dstOffset = IntOffset.Zero, + dstSize = dst, + alpha = 1f + ) + } + // Faint ring so the dark chip separates on dark backgrounds. + drawRoundRect( + color = Color.White.copy(alpha = 0.10f), + cornerRadius = corner, + style = Stroke(width = 1.dp.toPx()) + ) + } +} + +@Composable +private fun CloserInlineSpinner( + modifier: Modifier, + size: Dp, + color: Color, + reducedMotion: Boolean +) { + val angle = if (reducedMotion) 0f else rememberInfiniteTransition(label = "closerInlineSpinner") + .animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 900, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "closerInlineAngle" + ).value + + Canvas( + modifier = modifier + .size(size) + .clearAndSetSemantics {} + ) { + val stroke = this.size.minDimension * 0.12f + val inset = stroke / 2f + val d = this.size.minDimension - stroke + rotate(degrees = angle) { + drawArc( + brush = Brush.sweepGradient( + colors = listOf(color.copy(alpha = 0.12f), color) + ), + startAngle = 0f, + sweepAngle = 270f, + useCenter = false, + topLeft = Offset(inset, inset), + size = Size(d, d), + style = Stroke(width = stroke, cap = StrokeCap.Round) + ) + } + } } diff --git a/app/src/main/java/app/closer/ui/debug/ArtPreviewScreen.kt b/app/src/main/java/app/closer/ui/debug/ArtPreviewScreen.kt index 86f6c252..da606194 100644 --- a/app/src/main/java/app/closer/ui/debug/ArtPreviewScreen.kt +++ b/app/src/main/java/app/closer/ui/debug/ArtPreviewScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import app.closer.R import app.closer.ui.components.CelebrationOverlay import app.closer.ui.components.CloserActionButton +import app.closer.ui.components.CloserMarkLoader import app.closer.ui.components.CloserButtonStyle import app.closer.ui.components.CloserCard import app.closer.ui.settings.SettingsMuted @@ -84,6 +85,16 @@ fun ArtPreviewScreen(onNavigate: (String) -> Unit = {}) { Image(painterResource(R.drawable.particle_petal), null, Modifier.size(56.dp)) } } + ArtCard("Loading mark (animated)") { + androidx.compose.foundation.layout.Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CloserMarkLoader(size = 76.dp) // chip + rising fill + CloserMarkLoader(size = 40.dp) // chip threshold + CloserMarkLoader(size = 24.dp) // inline arc + } + } CloserActionButton( label = "Play the celebration", diff --git a/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt b/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt index 73b5e9d5..0943e4c5 100644 --- a/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/PairingSuccessScreen.kt @@ -203,19 +203,25 @@ fun PairingSuccessScreen( .size(60.dp) .zIndex(2f) .align(Alignment.Center) + .scale(pulse) .clip(CircleShape) .background(MaterialTheme.colorScheme.background) - .padding(5.dp) - .clip(CircleShape) - .background(Color(0xFFFFF8FC)), + .padding(4.dp) + .clip(CircleShape), contentAlignment = Alignment.Center ) { + // White-keyhole app-icon chip: aubergine gradient + the brand mark. Image( - painter = painterResource(R.drawable.closer_mark_loader), + painter = painterResource(R.drawable.ic_launcher_background), contentDescription = null, - modifier = Modifier - .size(38.dp) - .scale(pulse) + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize() + ) + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.matchParentSize() ) } } diff --git a/app/src/main/res/drawable-nodpi/closer_mark_loader.png b/app/src/main/res/drawable-nodpi/closer_mark_loader.png deleted file mode 100644 index cddf19b2..00000000 Binary files a/app/src/main/res/drawable-nodpi/closer_mark_loader.png and /dev/null differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..6b4881cf --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + + #FF24122F + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b4e91603..8783d6bd 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,4 +1,13 @@ diff --git a/docs/brand/asset-system.md b/docs/brand/asset-system.md index a5a1b714..8108ebfe 100644 --- a/docs/brand/asset-system.md +++ b/docs/brand/asset-system.md @@ -97,18 +97,17 @@ masters (do not ship them directly); ship the exports and the wired Android rast | `docs/brand/sources/closer-approved-icon-square.png` | Square 1024 master | iOS `AppIcon` 1024, store 1024 | | `docs/brand/sources/closer-mark.svg` | Vector master of the mark | Source for all PNG exports | | `docs/brand/sources/closer-mark-transparent-keyhole-white.png` | Transparent mark, white keyhole (for dark aubergine/purple) | **Source of** `drawable-nodpi/closer_launcher_foreground.png` (adaptive fg over the aubergine bg) + the Auth/Onboarding logo tile (dark surface) | -| `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png` | Transparent mark, dark keyhole (for light/blush surfaces) | **Source of** `drawable-nodpi/closer_mark_loader.png` (all loaders + the PairingSuccess hero badge on blush) | +| `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png` | Transparent mark, dark keyhole (for light/blush surfaces) | Logo on light/blush placements + web/docs exports (no in-app wiring today — in-app loaders now use the white-keyhole chip) | | `docs/brand/sources/closer-mark-transparent-keyhole-black.png` | Mono/max-contrast mark | Print/legal, max-contrast light | | `docs/brand/exports/logo/transparent-keyhole-aubergine/closer-mark-{24..1024}.png` | Sized PNGs, dark keyhole | Web/email/social on light, docs, favicons | | `docs/brand/exports/logo/transparent-keyhole-white/closer-mark-{24..1024}.png` | Sized PNGs, white keyhole | Same, on dark surfaces | | `docs/brand/exports/logo/transparent-keyhole-black/closer-mark-{24..1024}.png` | Sized PNGs, mono | One-color/print/footer | | `docs/brand/exports/logo/social/closer-mark-social-{1024,2048}.png` | Square social avatars / OG | Social profile + open-graph square | | `docs/brand/exports/logo/app-icon/closer-app-icon-512.png` | Play listing icon | `docs/store/app-icon-512.png` (Play Console) | -| `app/src/main/res/drawable-nodpi/closer_launcher_foreground.png` | Adaptive **foreground** (white-keyhole mark, safe-zone) | `drawable/ic_launcher_foreground.xml` → `mipmap-anydpi-*/ic_launcher*.xml`; also the Auth/Onboarding logo tile | +| `app/src/main/res/drawable-nodpi/closer_launcher_foreground.png` | Adaptive **foreground** (white-keyhole mark, safe-zone) | `drawable/ic_launcher_foreground.xml` → `mipmap-anydpi-*/ic_launcher*.xml`; Auth/Onboarding logo tile; **`CloserMarkLoader` chip + `PairingSuccess` hero** (white-keyhole logo on the aubergine app-icon chip) | | `app/src/main/res/drawable/ic_launcher_background.xml` | Adaptive **background** (aubergine gradient) | `mipmap-anydpi-*/ic_launcher*.xml` | | `app/src/main/res/drawable-nodpi/closer_launcher_monochrome.png` | Themed-icon **monochrome** (grayscale+alpha) | `drawable/ic_launcher_monochrome.xml` → `mipmap-anydpi-v33/*` | | `app/src/main/res/drawable-nodpi/ic_notification_closer.png` | Notification small icon (white+alpha silhouette) | `NotificationHelper`, `PartnerNotificationManager` `setSmallIcon` | -| `app/src/main/res/drawable-nodpi/closer_mark_loader.png` | Full-color transparent mark | `CloserMarkLoader` (all loaders), `PairingSuccessScreen` hero, `AuthLogoMark`/Onboarding tile | Note: the section-1 rows `closer-mark-on-dark.svg` / `closer-mark-on-light.svg` and the lockup/favicon rows are still **to-build** — the on-disk equivalents today are the `transparent-keyhole-white` (dark) diff --git a/docs/brand/visual-identity.md b/docs/brand/visual-identity.md index 36cca7c2..51c34821 100644 --- a/docs/brand/visual-identity.md +++ b/docs/brand/visual-identity.md @@ -42,8 +42,11 @@ Use the keyhole color to support contrast while keeping the official icon visual the black-keyhole variant only when maximum contrast is needed. - **Notification small icon:** use the single-color platform glyph; do not use the full-color launcher art. -- **Loading mark:** use the aubergine-keyhole variant on light/card surfaces so it stays calmer than - the launcher icon. +- **Loading mark:** the in-app loader is the **white-keyhole logo on an aubergine app-icon chip** with a + soft fill that rises through the mark (a "charging" loop). Below ~40dp (e.g. inside buttons) it falls + back to a lightweight indeterminate arc tinted to the surrounding content color. +- **Cold-launch splash:** aubergine background + the white-keyhole app icon (static, system-masked), with + a scale/fade exit that hands off to the in-app loading mark above. ## Core colors