brand: update app icon, iOS assets, Android drawables, brand docs (Pass H)

This commit is contained in:
null 2026-06-25 14:34:27 -05:00
parent 450ddccd16
commit 334cb079fa
71 changed files with 95 additions and 310 deletions

View File

@ -56,7 +56,7 @@ object NotificationHelper {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
val notification = NotificationCompat.Builder(context, channelId) val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_launcher_foreground) .setSmallIcon(R.drawable.ic_notification_closer)
.setContentTitle(title) .setContentTitle(title)
.setContentText(body) .setContentText(body)
.setAutoCancel(true) .setAutoCancel(true)

View File

@ -131,7 +131,7 @@ class PartnerNotificationManager @Inject constructor(
) )
val notification = NotificationCompat.Builder(context, type.channelId) val notification = NotificationCompat.Builder(context, type.channelId)
.setSmallIcon(R.drawable.ic_launcher_foreground) .setSmallIcon(R.drawable.ic_notification_closer)
.setContentTitle(type.title) .setContentTitle(type.title)
.setContentText(type.body) .setContentText(type.body)
.setAutoCancel(true) .setAutoCancel(true)

View File

@ -1,5 +1,6 @@
package app.closer.ui.components package app.closer.ui.components
import app.closer.R
import app.closer.ui.theme.closerCardColor import app.closer.ui.theme.closerCardColor
import android.provider.Settings import android.provider.Settings
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
@ -8,7 +9,7 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -20,13 +21,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@ -67,6 +64,14 @@ fun LoadingState(
fun CloserHeartLoader( fun CloserHeartLoader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: Dp = 76.dp size: Dp = 76.dp
) {
CloserMarkLoader(modifier = modifier, size = size)
}
@Composable
fun CloserMarkLoader(
modifier: Modifier = Modifier,
size: Dp = 76.dp
) { ) {
val context = LocalContext.current val context = LocalContext.current
val reducedMotion = remember { val reducedMotion = remember {
@ -76,16 +81,7 @@ fun CloserHeartLoader(
1f 1f
) == 0f ) == 0f
} }
val transition = rememberInfiniteTransition(label = "closerHeartLoader") val transition = rememberInfiniteTransition(label = "closerMarkLoader")
val animatedFill = transition.animateFloat(
initialValue = 0.08f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart
),
label = "closerHeartFill"
)
val animatedPulse = transition.animateFloat( val animatedPulse = transition.animateFloat(
initialValue = 0.96f, initialValue = 0.96f,
targetValue = 1.04f, targetValue = 1.04f,
@ -93,38 +89,13 @@ fun CloserHeartLoader(
animation = tween(durationMillis = 900, easing = FastOutSlowInEasing), animation = tween(durationMillis = 900, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
), ),
label = "closerHeartPulse" label = "closerMarkPulse"
) )
val fillProgress = if (reducedMotion) 1f else animatedFill.value
val pulse = if (reducedMotion) 1f else animatedPulse.value val pulse = if (reducedMotion) 1f else animatedPulse.value
val shadowPath = remember {
PathParser().parsePathString(
"M54,89C48,82 25,65 20,50C15,35 23,22 37,22C45,22 51,26 54,33C57,26 63,22 71,22C85,22 93,35 88,50C83,65 60,82 54,89Z"
).toPath()
}
val leftPath = remember {
PathParser().parsePathString(
"M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35Z"
).toPath()
}
val rightPath = remember {
PathParser().parsePathString(
"M54,85C59,79 81,62 86,48C91,35 84,24 71,24C63,24 57,28 54,35Z"
).toPath()
}
val leftHighlight = remember {
PathParser().parsePathString(
"M27,42C28,32 34,27 42,27C48,27 52,30 54,35L54,41C47,36 37,36 27,42Z"
).toPath()
}
val rightHighlight = remember {
PathParser().parsePathString(
"M54,35C57,30 62,27 69,27C78,27 84,32 85,42C75,36 65,36 54,41Z"
).toPath()
}
Canvas( Image(
painter = painterResource(R.drawable.closer_mark_loader),
contentDescription = null,
modifier = modifier modifier = modifier
.size(size) .size(size)
.graphicsLayer { .graphicsLayer {
@ -132,22 +103,5 @@ fun CloserHeartLoader(
scaleY = pulse scaleY = pulse
} }
.clearAndSetSemantics {} .clearAndSetSemantics {}
) { )
val scaleX = this.size.width / 108f
val scaleY = this.size.height / 108f
withTransform({
scale(scaleX = scaleX, scaleY = scaleY, pivot = Offset.Zero)
}) {
drawPath(shadowPath, color = Color(0xFF24122F).copy(alpha = 0.10f))
drawPath(leftPath, color = Color(0xFFF7C8E4).copy(alpha = 0.22f))
drawPath(rightPath, color = Color(0xFFD9B8FF).copy(alpha = 0.22f))
clipRect(top = 108f * (1f - fillProgress), bottom = 108f) {
drawPath(leftPath, color = Color(0xFFF7C8E4))
drawPath(rightPath, color = Color(0xFFD9B8FF))
drawPath(leftHighlight, color = Color(0xFFFFF4FA).copy(alpha = 0.68f))
drawPath(rightHighlight, color = Color(0xFFF3E8FF).copy(alpha = 0.52f))
}
}
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,33 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:src="@drawable/closer_launcher_foreground"
android:height="108dp" android:gravity="fill" />
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#24122F"
android:fillAlpha="0.38"
android:pathData="M54,89C48,82 25,65 20,50C15,35 23,22 37,22C45,22 51,26 54,33C57,26 63,22 71,22C85,22 93,35 88,50C83,65 60,82 54,89Z" />
<path
android:fillColor="#F7C8E4"
android:pathData="M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35Z" />
<path
android:fillColor="#D9B8FF"
android:pathData="M54,85C59,79 81,62 86,48C91,35 84,24 71,24C63,24 57,28 54,35Z" />
<path
android:fillColor="#FFF4FA"
android:fillAlpha="0.68"
android:pathData="M27,42C28,32 34,27 42,27C48,27 52,30 54,35L54,41C47,36 37,36 27,42Z" />
<path
android:fillColor="#F3E8FF"
android:fillAlpha="0.52"
android:pathData="M54,35C57,30 62,27 69,27C78,27 84,32 85,42C75,36 65,36 54,41Z" />
<path
android:fillColor="#6D2B55"
android:fillAlpha="0.22"
android:pathData="M22,48C29,62 45,75 54,85L54,89C45,79 27,65 22,48Z" />
<path
android:fillColor="#56306F"
android:fillAlpha="0.22"
android:pathData="M86,48C79,62 63,75 54,85L54,89C63,79 81,65 86,48Z" />
</vector>

View File

@ -1,10 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:src="@drawable/closer_launcher_monochrome"
android:height="108dp" android:gravity="fill" />
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35C57,28 63,24 71,24C84,24 91,35 86,48C81,62 59,79 54,85Z" />
</vector>

View File

@ -4,8 +4,8 @@ This is the working artwork asset set for Closer. It keeps the existing purple/p
and the 2D pastel couple illustration style, but gives every visual surface a clearer job. and the 2D pastel couple illustration style, but gives every visual surface a clearer job.
The brand should feel like a private ritual for two people: warm, quiet, equal, and intentional. The brand should feel like a private ritual for two people: warm, quiet, equal, and intentional.
The heart remains the compact brand mark. The couple artwork should become the primary visual The compact brand mark is now the approved Closer C-heart-keyhole. The couple artwork should remain
language anywhere there is enough space to show a human moment. the primary visual language anywhere there is enough space to show a human moment.
## Current Artwork Review ## Current Artwork Review
@ -33,19 +33,21 @@ Avoid:
Keep: Keep:
- `docs/store/sources/app-icon.svg` as the source of the compact heart mark. - `docs/brand/sources/closer-approved-icon-source.png` as the visual source of truth for the compact
C-heart-keyhole mark.
- `docs/store/sources/app-icon.svg` as the source wrapper for the launcher/store icon.
- `iphone/Closer/Resources/illustration-couple-*.png` as the human brand style. - `iphone/Closer/Resources/illustration-couple-*.png` as the human brand style.
- `iphone/Closer/Resources/pack-art-*.png` as the category/pack art direction. - `iphone/Closer/Resources/pack-art-*.png` as the category/pack art direction.
- `iphone/Closer/Resources/particle-heart.png` and `particle-petal.png` for celebration moments. - `iphone/Closer/Resources/particle-heart.png` and `particle-petal.png` for celebration moments.
Improve: Improve:
- The heart is strong for launchers and tiny UI, but too generic when used alone at larger sizes. - The mark is strong for launchers, auth, loaders, notification glyphs, and small privacy seals. Use
Use couple illustrations for onboarding, paywall, empty states, store screenshots, and web/social. couple illustrations for onboarding, paywall, empty states, store screenshots, and web/social.
- Android currently has the launcher vectors but not the larger illustration library. Mirror the - Android currently has the launcher vectors but not the larger illustration library. Mirror the
iOS resources into Android when those screens start using raster art. iOS resources into Android when those screens start using raster art.
- Store feature graphics should show the heart plus one illustrated/private ritual scene, not only - Store feature graphics should show the C-heart-keyhole plus one illustrated/private ritual scene,
cards and symbols. not only cards and symbols.
### Notification Artwork ### Notification Artwork
@ -63,18 +65,20 @@ This section is only about the artwork used to represent notifications, not noti
| Asset | Use | Source / Target | | Asset | Use | Source / Target |
| --- | --- | --- | | --- | --- | --- |
| Primary app mark | Launcher, favicon, small brand moments | `docs/store/sources/app-icon.svg` | | Primary app mark | Launcher, favicon, small brand moments | `docs/brand/sources/closer-approved-icon-source.png` |
| Adaptive foreground | Android launcher layer | `app/src/main/res/drawable/ic_launcher_foreground.xml` | | Adaptive foreground | Android launcher layer | `app/src/main/res/drawable/ic_launcher_foreground.xml` |
| Adaptive background | Android launcher layer | `app/src/main/res/drawable/ic_launcher_background.xml` | | Adaptive background | Android launcher layer | `app/src/main/res/drawable/ic_launcher_background.xml` |
| Monochrome mark | Android themed icon, single-color use | `app/src/main/res/drawable/ic_launcher_monochrome.xml` | | Monochrome mark | Android themed icon, single-color use | `app/src/main/res/drawable/ic_launcher_monochrome.xml` |
| Notification glyph | Android/iOS notification art direction | Needed: monochrome heart/paired-card source | | Notification glyph | Android notification small icon | `app/src/main/res/drawable-nodpi/ic_notification_closer.png` |
| Wordmark lockup | Website, store hero, press kit | Needed: `docs/brand/sources/closer-lockup.svg` | | Wordmark lockup | Website, store hero, press kit | Needed: `docs/brand/sources/closer-lockup.svg` |
| Horizontal logo | Email header, social headers | Needed: SVG + PNG exports | | Horizontal logo | Email header, social headers | Needed: SVG + PNG exports |
| One-color logo | Legal docs, monochrome print, dark/light footer | Needed: SVG exports | | One-color logo | Legal docs, monochrome print, dark/light footer | Needed: SVG exports |
| Favicon set | Website/browser/PWA | Needed: 16, 32, 48, 180, 192, 512 px | | Favicon set | Website/browser/PWA | Needed: 16, 32, 48, 180, 192, 512 px |
Logo rule: the heart mark should mean "two equal people meeting in the middle." Do not split it, Logo rule: the mark should mean "Closer is the private space around your relationship." Keep the
rotate it, add faces, add text inside it, or use it as a reaction emoji. approved pink upper C, lavender lower sweep, heart-shaped inner space, and true keyhole. Do not
redraw it as a generic `C`, turn the keyhole into a heart, add a key or lock shackle, add faces, add
text inside it, or use it as a reaction emoji.
### 2. App Icons ### 2. App Icons
@ -86,9 +90,8 @@ rotate it, add faces, add text inside it, or use it as a reaction emoji.
Current status: Current status:
- Android and Play icon assets exist. - Android, Play, and iOS AppIcon assets exist and should be regenerated from the approved source
- iOS has an asset catalog folder but no visible app icon set in the checked file list. Add the image after any future mark change.
full iOS app icon set before TestFlight/App Store review.
### 3. Illustration Library ### 3. Illustration Library
@ -158,7 +161,7 @@ These should be simple single-color vectors that work at 20-32 dp/pt.
| Asset | Spec | Direction | | Asset | Spec | Direction |
| --- | --- | --- | | --- | --- | --- |
| Play feature graphic | 1024 x 500 PNG | Heart + one private ritual illustration + short promise | | Play feature graphic | 1024 x 500 PNG | C-heart-keyhole + one private ritual illustration + short promise |
| Play screenshots | Up to 8 phone screenshots | Use product screens; omit login unless testing trust | | Play screenshots | Up to 8 phone screenshots | Use product screens; omit login unless testing trust |
| App Store screenshots | iPhone 6.7", 6.5", 5.5" as needed | Mirror Play story | | App Store screenshots | iPhone 6.7", 6.5", 5.5" as needed | Mirror Play story |
| Social open graph | 1200 x 630 | Couple illustration + "A private space for two." | | Social open graph | 1200 x 630 | Couple illustration + "A private space for two." |
@ -234,10 +237,9 @@ iphone/Closer/Resources/
## Priority Build List ## Priority Build List
1. Keep the current heart mark, but create wordmark/horizontal/one-color SVG lockups. 1. Build wordmark/horizontal/one-color lockups around the approved C-heart-keyhole.
2. Add the missing iOS AppIcon set. 2. Mirror illustration PNGs into Android and start using them in onboarding, empty states, paywall,
3. Mirror illustration PNGs into Android and start using them in onboarding, empty states, paywall,
and store screenshots. and store screenshots.
4. Rework the Play feature graphic to include one couple/private-ritual illustration. 3. Rework the Play feature graphic to include one couple/private-ritual illustration.
5. Add the new notification and privacy illustrations listed above. 4. Add the new notification and privacy illustrations listed above.
6. Build a small custom glyph set for privacy/reveal/date/capsule concepts. 5. Build a small custom glyph set for privacy/reveal/date/capsule concepts.

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1254" height="1254" viewBox="0 0 1254 1254" role="img" aria-label="Closer C-heart-keyhole mark">
<image href="docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png" width="1254" height="1254" preserveAspectRatio="xMidYMid meet"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@ -11,14 +11,21 @@ requirements, see `docs/brand/asset-system.md`.
## Brand mark ## Brand mark
The mark is one heart formed by two equal halves. Pink and lavender represent two people meeting at The mark is the approved Closer `C-heart-keyhole`: a soft pink upper `C`, lavender lower sweep,
the center; neither side visually dominates. Keep the mark intact and do not separate, rotate, add a heart-shaped inner space, and a true centered keyhole. Pink and lavender represent two people
text inside, or place it directly over a busy image. meeting in one private space; the keyhole represents trust and privacy.
- Master mark source: `docs/brand/sources/closer-approved-icon-source.png`
- Transparent mark source: `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png`
- Launcher source: `docs/store/sources/app-icon.svg` - Launcher source: `docs/store/sources/app-icon.svg`
- Android adaptive layers: `app/src/main/res/drawable/ic_launcher_*` - Android adaptive layers: `app/src/main/res/drawable/ic_launcher_*`
- Android notification glyph: `app/src/main/res/drawable-nodpi/ic_notification_closer.png`
- Minimum clear space: one quarter of the mark's width on all sides. - Minimum clear space: one quarter of the mark's width on all sides.
- Minimum digital size: 24 px. At small sizes, use the solid monochrome mark. - Minimum digital size: 24 px. At small sizes, use the solid monochrome C-heart-keyhole glyph.
Keep the mark visually faithful to the approved artwork. Do not redraw it as a generic `C`, close
the aperture into an `O`, add a padlock shackle, turn the keyhole into a heart, add a separate key,
or place text inside the icon.
## Core colors ## Core colors
@ -64,8 +71,9 @@ when the couple key is unavailable. These claims describe deployed behavior.
- Store graphics and screenshots should use the same purple/pink palette as the product. - Store graphics and screenshots should use the same purple/pink palette as the product.
- Lead with privacy and mutual connection before feature volume. - Lead with privacy and mutual connection before feature volume.
- The Play feature graphic should show the heart mark, the primary promise, and compact product cues - The Play feature graphic should show the C-heart-keyhole mark, the primary promise, and compact
for private reveals, two-person use, and daily rituals. Do not turn it into a feature checklist. product cues for private reveals, two-person use, and daily rituals. Do not turn it into a feature
checklist.
- Do not show intimate answer content, real email addresses, invite codes, or notification tokens. - Do not show intimate answer content, real email addresses, invite codes, or notification tokens.
- Use clean demo data and crop out development indicators before publishing. - Use clean demo data and crop out development indicators before publishing.
- Re-export `docs/store/app-icon-512.png` and `docs/store/feature-graphic-1024x500.png` from the - Re-export `docs/store/app-icon-512.png` and `docs/store/feature-graphic-1024x500.png` from the

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 477 KiB

View File

@ -1,17 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="Closer app icon">
<defs> <image href="docs/brand/sources/closer-approved-icon-square.png" width="512" height="512" preserveAspectRatio="xMidYMid slice"/>
<filter id="shadow" x="-30%" y="-30%" width="160%" height="170%">
<feDropShadow dx="0" dy="18" stdDeviation="14" flood-color="#24122F" flood-opacity=".42"/>
</filter>
</defs>
<rect width="512" height="512" fill="#4A235F"/>
<path d="M0 0h512v198C411 236 314 232 222 207 124 180 52 125 0 64Z" fill="#B98AF4" opacity=".24"/>
<path d="M0 354c92-52 192-45 296-15 93 27 157 37 216 1v172H0Z" fill="#24122F" opacity=".36"/>
<path d="M378 0h134v512H352c6-74-5-148-34-219-34-82-27-171 60-293Z" fill="#B98AF4" opacity=".10"/>
<g filter="url(#shadow)">
<path d="M256 402c-25-28-130-111-153-177-24-62 13-116 73-116 38 0 65 19 80 49Z" fill="#F7C8E4"/>
<path d="M256 402c25-28 130-111 153-177 24-62-13-116-73-116-38 0-65 19-80 49Z" fill="#D9B8FF"/>
<path d="M122 199c7-45 36-69 78-69 27 0 46 11 56 28v28c-44-25-89-21-134 13Z" fill="#FFF4FA" opacity=".68"/>
<path d="M256 158c14-18 38-28 67-28 42 0 70 24 77 69-49-34-97-38-144-13Z" fill="#F3E8FF" opacity=".52"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 266 B

View File

@ -13,17 +13,7 @@
<path d="M0 372c144-62 292-57 449-14 142 39 259 43 358 7 70-25 142-33 217-13v148H0Z" fill="#180C20" opacity=".34"/> <path d="M0 372c144-62 292-57 449-14 142 39 259 43 358 7 70-25 142-33 217-13v148H0Z" fill="#180C20" opacity=".34"/>
<path d="M900 0h124v500H820c31-76 34-154 8-236C800 174 812 88 900 0Z" fill="#F7C8E4" opacity=".08"/> <path d="M900 0h124v500H820c31-76 34-154 8-236C800 174 812 88 900 0Z" fill="#F7C8E4" opacity=".08"/>
<g transform="translate(68 74)"> <image x="68" y="74" width="312" height="312" href="docs/brand/sources/closer-approved-icon-square.png" preserveAspectRatio="xMidYMid slice" filter="url(#markShadow)"/>
<rect x="0" y="0" width="312" height="312" rx="72" fill="#4A235F"/>
<path d="M0 0h312v120C250 143 192 141 136 126 76 109 32 76 0 38Z" fill="#B98AF4" opacity=".24"/>
<path d="M0 216c56-32 117-28 181-9 57 16 96 23 131 1v104H0Z" fill="#180C20" opacity=".28"/>
<g transform="translate(0 -4)" filter="url(#markShadow)">
<path d="M156 246c-15-17-79-68-93-109-15-38 8-71 45-71 23 0 40 12 48 30Z" fill="#F7C8E4"/>
<path d="M156 246c15-17 79-68 93-109 15-38-8-71-45-71-23 0-40 12-48 30Z" fill="#D9B8FF"/>
<path d="M74 121c4-28 22-42 48-42 16 0 28 7 34 17v17c-27-15-54-13-82 8Z" fill="#FFF4FA" opacity=".68"/>
<path d="M156 96c9-11 23-17 41-17 26 0 43 14 47 42-30-21-59-23-88-8Z" fill="#F3E8FF" opacity=".52"/>
</g>
</g>
<text x="408" y="142" fill="#FFF8FC" font-family="DejaVu Sans" font-size="72" font-weight="700">Closer</text> <text x="408" y="142" fill="#FFF8FC" font-family="DejaVu Sans" font-size="72" font-weight="700">Closer</text>
<text x="412" y="198" fill="#F7C8E4" font-family="DejaVu Sans" font-size="26" font-weight="600">A private space</text> <text x="412" y="198" fill="#F7C8E4" font-family="DejaVu Sans" font-size="26" font-weight="600">A private space</text>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -7,7 +7,7 @@ struct LoadingView: View {
var body: some View { var body: some View {
VStack(spacing: CloserSpacing.lg) { VStack(spacing: CloserSpacing.lg) {
CloserHeartLoader() CloserMarkLoader()
Text(message) Text(message)
.font(CloserFont.callout) .font(CloserFont.callout)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
@ -17,167 +17,44 @@ struct LoadingView: View {
} }
struct CloserHeartLoader: View { struct CloserHeartLoader: View {
var size: CGFloat = 76
var body: some View {
CloserMarkLoader(size: size)
}
}
struct CloserMarkLoader: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var fillProgress: CGFloat = 0.08
@State private var isPulsing = false @State private var isPulsing = false
var size: CGFloat = 76 var size: CGFloat = 76
var body: some View { var body: some View {
ZStack { Image("closer-mark-loader")
CloserHeartShape() .resizable()
.fill( .scaledToFit()
LinearGradient( .frame(width: size, height: size)
colors: [ .scaleEffect(reduceMotion ? 1 : (isPulsing ? 1.04 : 0.96))
Color(hex: "F7C8E4").opacity(0.22), .accessibilityHidden(true)
Color(hex: "D9B8FF").opacity(0.22) .onAppear {
], guard !reduceMotion else {
startPoint: .leading, isPulsing = false
endPoint: .trailing return
) }
)
HStack(spacing: 0) { withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) {
Color(hex: "F7C8E4") isPulsing = true
Color(hex: "D9B8FF") }
} }
.mask(CloserHeartShape()) .onChange(of: reduceMotion) { _, newValue in
.mask(alignment: .bottom) { if newValue {
Rectangle() isPulsing = false
.frame(height: size * (reduceMotion ? 1 : fillProgress)) }
} }
CloserHeartHighlightShape(side: .left)
.fill(Color(hex: "FFF4FA").opacity(reduceMotion ? 0.68 : 0.68 * fillProgress))
CloserHeartHighlightShape(side: .right)
.fill(Color(hex: "F3E8FF").opacity(reduceMotion ? 0.52 : 0.52 * fillProgress))
}
.frame(width: size, height: size)
.scaleEffect(reduceMotion ? 1 : (isPulsing ? 1.04 : 0.96))
.accessibilityHidden(true)
.onAppear {
guard !reduceMotion else {
fillProgress = 1
isPulsing = false
return
}
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: false)) {
fillProgress = 1
}
withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) {
isPulsing = true
}
}
.onChange(of: reduceMotion) { _, newValue in
if newValue {
fillProgress = 1
isPulsing = false
}
}
} }
} }
private struct CloserHeartShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: point(54, 85, in: rect))
path.addCurve(
to: point(22, 48, in: rect),
control1: point(49, 79, in: rect),
control2: point(27, 62, in: rect)
)
path.addCurve(
to: point(37, 24, in: rect),
control1: point(17, 35, in: rect),
control2: point(24, 24, in: rect)
)
path.addCurve(
to: point(54, 35, in: rect),
control1: point(45, 24, in: rect),
control2: point(51, 28, in: rect)
)
path.addCurve(
to: point(71, 24, in: rect),
control1: point(57, 28, in: rect),
control2: point(63, 24, in: rect)
)
path.addCurve(
to: point(86, 48, in: rect),
control1: point(84, 24, in: rect),
control2: point(91, 35, in: rect)
)
path.addCurve(
to: point(54, 85, in: rect),
control1: point(81, 62, in: rect),
control2: point(59, 79, in: rect)
)
path.closeSubpath()
return path
}
}
private struct CloserHeartHighlightShape: Shape {
enum Side {
case left
case right
}
let side: Side
func path(in rect: CGRect) -> Path {
var path = Path()
switch side {
case .left:
path.move(to: point(27, 42, in: rect))
path.addCurve(
to: point(42, 27, in: rect),
control1: point(28, 32, in: rect),
control2: point(34, 27, in: rect)
)
path.addCurve(
to: point(54, 35, in: rect),
control1: point(48, 27, in: rect),
control2: point(52, 30, in: rect)
)
path.addLine(to: point(54, 41, in: rect))
path.addCurve(
to: point(27, 42, in: rect),
control1: point(47, 36, in: rect),
control2: point(37, 36, in: rect)
)
path.closeSubpath()
case .right:
path.move(to: point(54, 35, in: rect))
path.addCurve(
to: point(69, 27, in: rect),
control1: point(57, 30, in: rect),
control2: point(62, 27, in: rect)
)
path.addCurve(
to: point(85, 42, in: rect),
control1: point(78, 27, in: rect),
control2: point(84, 32, in: rect)
)
path.addCurve(
to: point(54, 41, in: rect),
control1: point(75, 36, in: rect),
control2: point(65, 36, in: rect)
)
path.closeSubpath()
}
return path
}
}
private func point(_ x: CGFloat, _ y: CGFloat, in rect: CGRect) -> CGPoint {
CGPoint(
x: rect.minX + rect.width * (x / 108),
y: rect.minY + rect.height * (y / 108)
)
}
// MARK: - Error View // MARK: - Error View
struct ErrorView: View { struct ErrorView: View {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB