### 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>
192 lines
6.7 KiB
Kotlin
192 lines
6.7 KiB
Kotlin
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,
|
|
val name: String,
|
|
val avatarColor: Color,
|
|
val isActive: Boolean
|
|
)
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun ProfileSwitchSheet(
|
|
profiles: List<ProfileSwitchItem>,
|
|
onProfileSelected: (Long) -> Unit,
|
|
onDismiss: () -> Unit
|
|
) {
|
|
var confirmTarget by remember { mutableStateOf<ProfileSwitchItem?>(null) }
|
|
|
|
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()
|
|
.clickable {
|
|
if (!profile.isActive) confirmTarget = profile
|
|
else onDismiss()
|
|
}
|
|
.padding(vertical = 14.dp, horizontal = 4.dp),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(14.dp)
|
|
) {
|
|
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) {
|
|
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 },
|
|
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()
|
|
},
|
|
colors = ButtonDefaults.textButtonColors(contentColor = AccentColor)
|
|
) {
|
|
Text("Switch", fontFamily = InstrumentSans)
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(
|
|
onClick = { confirmTarget = null },
|
|
colors = ButtonDefaults.textButtonColors(contentColor = FgMutedColor)
|
|
) {
|
|
Text("Cancel", fontFamily = InstrumentSans)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|