Implement major visual overhaul and brand identity transition to "Aubergine Nocturne" style across all screens.

### Design & Theming
- Implement a fixed dark editorial theme with a palette of deep aubergine, warm copper (Accent), and cream (Text) using OKLCH-to-sRGB color tokens.
- Integrate premium typography using Google Fonts:
    - **Cormorant Garamond:** Editorial display serif for headlines and status labels.
    - **Instrument Sans:** Clean UI sans for body text and actions.
    - **JetBrains Mono:** Monospaced typeface for dates, counters, and ledger-style labels.
- Replace standard Material3 components with custom "Aubergine" styled components (cards, chips, and dividers).

### Calendar & Insights
- Redesign the Calendar grid with refined "bubble" day cells, high-contrast period indicators, and a horizontal "Today" footer.
- Add a new `PhaseArc` visualization in Cycle Insights to show the current phase progress within the overall cycle length.
- Enhance `CycleHistoryChart` with dashed average lines and active-state highlighting for the latest cycle.
- Refine the cycle status banner with a pulsing `GlowDot` and detailed phase descriptions.

### Day Detail & Health Trends
- Reorganize Day Detail into numbered sections (Cycle, Symptoms, Notes, Intimacy).
- Redesign the symptom logging interface with expanded category drawers and custom rating boxes.
- Transform Health Trends with a sparkline-based condition frequency view and a mono-styled range selector.
- Update `AddIntimacyForm` to follow the new typography and color scheme within its modal bottom sheet.

### Onboarding & Settings
- Rewrite the Onboarding flow to feature an editorial "Welcome" step with a monogram header and private-ledger tagline.
- Overhaul Settings with custom toggle tabs for preferences and a redesigned profile management card.

### Domain & Logic
- Update `CyclePredictionEngine` to include logic for `avgPeriodLength` and a more precise phase mapping algorithm that eliminates gaps between cycle windows.
- Refine `CycleRepository` to set `cycleLength` only when a new cycle starts, rather than at period end, to ensure accurate start-to-next-start calculations.
- Add `ui-text-google-fonts` dependency to support the new typeface requirements.

Signed-off-by: whitlocktech <whitlocktech@gmail.com>
This commit is contained in:
2026-05-23 03:41:32 -05:00
parent 2105cf861c
commit 27403525a8
15 changed files with 2562 additions and 1091 deletions

View File

@@ -1,13 +1,19 @@
package com.hsdiary.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.hsdiary.ui.theme.*
data class ProfileSwitchItem(
val id: Long,
@@ -25,14 +31,47 @@ fun ProfileSwitchSheet(
) {
var confirmTarget by remember { mutableStateOf<ProfileSwitchItem?>(null) }
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 32.dp)) {
Text(
text = "Switch Profile",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 16.dp)
ModalBottomSheet(
onDismissRequest = onDismiss,
containerColor = SurfaceColor,
dragHandle = {
Box(
modifier = Modifier
.padding(top = 12.dp)
.size(width = 36.dp, height = 4.dp)
.background(BorderSoftColor, RoundedCornerShape(2.dp))
)
}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 18.dp)
.padding(bottom = 28.dp)
) {
Spacer(Modifier.height(4.dp))
// Title
Text(
text = "Switch diary",
fontFamily = CormorantGaramond,
fontStyle = FontStyle.Italic,
fontSize = 20.sp,
color = FgColor
)
val lastOpened = "last opened just now"
Text(
text = "${profiles.size} profiles · $lastOpened".uppercase(),
fontFamily = JetBrainsMono,
fontSize = 10.sp,
color = FgSubtleColor,
letterSpacing = 1.2.sp,
modifier = Modifier.padding(top = 2.dp, bottom = 14.dp)
)
// Profile rows
profiles.forEach { profile ->
HorizontalDivider(color = BorderSoftColor, thickness = 0.5.dp)
Row(
modifier = Modifier
.fillMaxWidth()
@@ -40,38 +79,112 @@ fun ProfileSwitchSheet(
if (!profile.isActive) confirmTarget = profile
else onDismiss()
}
.padding(vertical = 12.dp),
.padding(vertical = 14.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
horizontalArrangement = Arrangement.spacedBy(14.dp)
) {
AvatarDot(name = profile.name, avatarColor = profile.avatarColor)
Text(
text = profile.name,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
AvatarDot(
name = profile.name,
avatarColor = profile.avatarColor,
size = 36
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = profile.name,
fontFamily = CormorantGaramond,
fontStyle = FontStyle.Italic,
fontSize = 17.sp,
color = FgColor
)
Text(
text = if (profile.isActive) "CURRENTLY OPEN" else "TAP TO SWITCH",
fontFamily = JetBrainsMono,
fontSize = 10.sp,
color = FgFaintColor,
letterSpacing = 1.sp,
modifier = Modifier.padding(top = 2.dp)
)
}
if (profile.isActive) {
Badge { Text("Active") }
Box(
modifier = Modifier
.background(Color.Transparent, RoundedCornerShape(999.dp))
.border(1.dp, AccentFaintColor, RoundedCornerShape(999.dp))
.padding(horizontal = 8.dp, vertical = 3.dp)
) {
Text(
"ACTIVE",
fontFamily = JetBrainsMono,
fontSize = 9.sp,
color = AccentColor,
letterSpacing = 1.2.sp
)
}
}
}
}
Spacer(Modifier.height(14.dp))
// Add profile button — outline style
Box(
modifier = Modifier
.fillMaxWidth()
.border(1.dp, BorderColor, RoundedCornerShape(999.dp))
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "+ Add a profile",
fontFamily = InstrumentSans,
fontSize = 13.sp,
color = FgColor
)
}
}
}
// Confirm switch dialog
confirmTarget?.let { target ->
AlertDialog(
onDismissRequest = { confirmTarget = null },
title = { Text("Switch Profile") },
text = { Text("Switch to ${target.name}?") },
containerColor = SurfaceColor,
title = {
Text(
"Switch diary",
fontFamily = CormorantGaramond,
fontStyle = FontStyle.Italic,
fontSize = 20.sp,
color = FgColor
)
},
text = {
Text(
"Switch to ${target.name}?",
fontFamily = InstrumentSans,
fontSize = 14.sp,
color = FgMutedColor
)
},
confirmButton = {
TextButton(onClick = {
onProfileSelected(target.id)
confirmTarget = null
onDismiss()
}) { Text("Switch") }
TextButton(
onClick = {
onProfileSelected(target.id)
confirmTarget = null
onDismiss()
},
colors = ButtonDefaults.textButtonColors(contentColor = AccentColor)
) {
Text("Switch", fontFamily = InstrumentSans)
}
},
dismissButton = {
TextButton(onClick = { confirmTarget = null }) { Text("Cancel") }
TextButton(
onClick = { confirmTarget = null },
colors = ButtonDefaults.textButtonColors(contentColor = FgMutedColor)
) {
Text("Cancel", fontFamily = InstrumentSans)
}
}
)
}