refactor(settings): make the month style picker a live preview (#38, #53)

The first version of this picker invented its own visual language — free-
standing bordered cards with a hand-rolled check — which matched nothing else
in the app. It is now the family's standard shape: FullScreenPicker with
PickerDescription and connected GroupedRows, tonal highlight and the shared
SelectedCheck.

Above the rows sits a live, scaled-down Month view that changes as you pick a
style. It renders the *real* grid composables rather than a drawing of them, so
the preview cannot drift from what it depicts: MonthGrid, ContinuousMonthGrid,
SplitMonthGrid and SplitDayPane are now internal rather than private, and the
sample month runs through the same layoutMonthWeeks/layoutCalendarWeek the live
views use. The month, today's position, week start, colour softening and clock
format are all real; only the events are stand-ins, since a settings screen has
no business querying the provider for a thumbnail.

Selecting applies immediately and leaves the picker open — closing on tap would
hide the very thing the screen is for. Back exits, as in the App name picker.

Scaling note worth keeping: Modifier.requiredSize looks like the way to force a
full-viewport measurement, but it *centres* content that overflows the incoming
constraints, which left only the grid's bottom-right corner inside the clip. A
layout modifier that measures at Constraints.fixed and reports the scaled size
has no overflow to align and no dependence on parent alignment.

PickerDescription is internal now rather than duplicated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 17:34:16 +02:00
parent 81dcbbdce7
commit 94a82c2f6c
5 changed files with 339 additions and 256 deletions

View File

@@ -198,7 +198,7 @@ private fun CustomReminderEditor(
/** A short explanatory paragraph shown under a picker's title, above the rows. */
@Composable
private fun PickerDescription(text: String) {
internal fun PickerDescription(text: String) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,

View File

@@ -454,7 +454,7 @@ private fun MonthTopBar(
}
@Composable
private fun WeekdayHeader(weekStart: DayOfWeek, showWeekNumbers: Boolean) {
internal fun WeekdayHeader(weekStart: DayOfWeek, showWeekNumbers: Boolean) {
val locale = currentLocale()
val days = remember(weekStart, locale) {
(0 until 7).map { offset ->
@@ -503,7 +503,7 @@ private const val MAX_EVENT_ROWS = 3
private val CONTINUOUS_ROW_HEIGHT = 112.dp
@Composable
private fun MonthGrid(
internal fun MonthGrid(
state: MonthUiState.Success,
showWeekNumbers: Boolean,
onOpenDay: (LocalDate) -> Unit,
@@ -545,7 +545,7 @@ private fun MonthGrid(
* duplication #38 asks to be rid of.
*/
@Composable
private fun ContinuousMonthGrid(
internal fun ContinuousMonthGrid(
state: ContinuousMonthUiState.Success,
listState: LazyListState,
showWeekNumbers: Boolean,
@@ -682,7 +682,7 @@ private const val SPLIT_MAX_DOTS = 3
* the layout. The full Day view stays one tap away on the pane's date header.
*/
@Composable
private fun SplitMonthGrid(
internal fun SplitMonthGrid(
state: MonthUiState.Success,
selected: LocalDate,
showWeekNumbers: Boolean,
@@ -812,7 +812,7 @@ private fun SplitDots(events: List<EventInstance>, dark: Boolean) {
* the Week and Agenda headers (#37).
*/
@Composable
private fun SplitDayPane(
internal fun SplitDayPane(
date: LocalDate,
today: LocalDate,
events: List<EventInstance>,

View File

@@ -0,0 +1,268 @@
package de.jeanlucmakiola.calendula.ui.month
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.domain.EventInstance
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.YearMonth
import kotlinx.datetime.atTime
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
import kotlin.math.roundToInt
/**
* A live, scaled-down Month view in a given [MonthViewStyle], for the settings
* chooser.
*
* It renders the *real* grid composables rather than a drawing of them, so the
* preview cannot drift from the thing it depicts: change a cell's shape or an
* event bar's colour and every preview follows automatically. The trick is
* [requiredSize] — it ignores the incoming constraints, so the grid lays itself
* out at a full phone width and a plausible viewport height, and a
* [graphicsLayer] scale shrinks the finished layout into the card.
*
* The month, today's position and the week start are all real; only the events
* are stand-ins, since the settings screen has no business querying the
* provider for a thumbnail.
*/
@Composable
internal fun MonthStylePreview(
style: MonthViewStyle,
weekStart: DayOfWeek,
height: Dp,
modifier: Modifier = Modifier,
) {
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val scale = height.value / VIRTUAL_HEIGHT.value
val zone = remember { TimeZone.currentSystemDefault() }
val today = remember(zone) { Clock.System.now().toLocalDateTime(zone).date }
val sample = remember(today, weekStart, zone) { sampleMonthState(today, weekStart, zone) }
Box(
modifier = modifier
.clipToBounds()
.background(MaterialTheme.colorScheme.surface)
// A preview is a picture, not a control: swallow touches before the
// grid's own clickables see them, and give screen readers one label
// instead of six weeks of day cells.
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
awaitPointerEvent(PointerEventPass.Initial).changes
.forEach { it.consume() }
}
}
}
.clearAndSetSemantics { }
// Measure the grid at a full phone viewport but report the scaled
// size, so the node occupies exactly what it draws.
//
// Modifier.requiredSize would be the obvious way to force the larger
// measurement, but it *centres* content that overflows the incoming
// constraints — which pushed the grid to a negative offset and left
// only its bottom-right corner inside the clip.
.layout { measurable, _ ->
val fullWidth = screenWidth.roundToPx()
val fullHeight = VIRTUAL_HEIGHT.roundToPx()
val placeable = measurable.measure(Constraints.fixed(fullWidth, fullHeight))
layout((fullWidth * scale).roundToInt(), (fullHeight * scale).roundToInt()) {
placeable.place(0, 0)
}
}
.graphicsLayer {
scaleX = scale
scaleY = scale
transformOrigin = TransformOrigin(0f, 0f)
},
) {
Column(Modifier.fillMaxSize()) {
WeekdayHeader(weekStart = weekStart, showWeekNumbers = false)
when (style) {
MonthViewStyle.Paged -> MonthGrid(
state = sample.month,
showWeekNumbers = false,
onOpenDay = {},
)
MonthViewStyle.Continuous -> ContinuousMonthGrid(
state = sample.continuous,
listState = rememberLazyListState(
initialFirstVisibleItemIndex =
weekIndexOf(LocalDate(today.year, today.month, 1), weekStart),
),
showWeekNumbers = false,
onOpenDay = {},
)
MonthViewStyle.Split -> {
SplitMonthGrid(
state = sample.month,
selected = today,
showWeekNumbers = false,
onSelectDay = {},
)
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
SplitDayPane(
date = today,
today = today,
events = sample.month.instancesByDay[today].orEmpty(),
zone = zone,
onOpenDay = {},
onEventClick = {},
onCreateEvent = {},
modifier = Modifier.fillMaxWidth().height(SPLIT_PANE_HEIGHT),
)
}
}
}
}
}
/**
* The viewport the preview pretends to be: a phone's width by the height a
* calendar view gets under the top bar. Scaling from a fixed height keeps the
* three styles comparable — each shows the same slice of screen.
*/
private val VIRTUAL_HEIGHT = 440.dp
private val SPLIT_PANE_HEIGHT = 170.dp
private class SampleMonth(
val month: MonthUiState.Success,
val continuous: ContinuousMonthUiState.Success,
)
/**
* A month's worth of stand-in events, laid out through the same
* [layoutMonthWeeks] / [layoutCalendarWeek] the live views use — so the preview
* exercises the real span, lane and overflow logic rather than approximating it.
*
* Colours are raw ARGB on purpose: that is what the provider hands out for an
* event, so a token here would misrepresent what the grid actually renders.
*/
private fun sampleMonthState(
today: LocalDate,
weekStart: DayOfWeek,
zone: TimeZone,
): SampleMonth {
val ym = YearMonth(today.year, today.month)
val first = LocalDate(ym.year, ym.month, 1)
val events = sampleEvents(first, today, zone)
val weeks = layoutMonthWeeks(ym, weekStart, events, zone)
val month = MonthUiState.Success(
month = ym,
today = today,
weeks = weeks,
instancesByDay = instancesByDay(weeks.flatMap { it.days }, events, zone),
zone = zone,
)
// Enough weeks either side of today for the continuous list to fill the
// viewport and still have somewhere to scroll.
val centre = weekIndexOf(today, weekStart)
val window = (centre - 8)..(centre + 8)
val continuous = ContinuousMonthUiState.Success(
today = today,
weeksByIndex = window.associateWith { index ->
val days = (0 until 7).map {
weekStartForIndex(index, weekStart).plus(it, DateTimeUnit.DAY)
}
layoutCalendarWeek(days, events, zone)
},
weekStart = weekStart,
)
return SampleMonth(month, continuous)
}
private val SAMPLE_COLORS = listOf(
0xFF3F7BD4.toInt(),
0xFFCE5B4C.toInt(),
0xFF4E9A6A.toInt(),
0xFF8A63C7.toInt(),
)
private fun sampleEvents(
firstOfMonth: LocalDate,
today: LocalDate,
zone: TimeZone,
): List<EventInstance> {
var id = 0L
fun next() = ++id
fun timed(day: LocalDate, hour: Int, length: Int, colorIndex: Int) = EventInstance(
instanceId = next(),
eventId = id,
calendarId = 1L,
title = SAMPLE_TITLES[(id.toInt() - 1) % SAMPLE_TITLES.size],
start = day.atTime(hour, 0).toInstant(zone),
end = day.atTime(hour + length, 0).toInstant(zone),
isAllDay = false,
color = SAMPLE_COLORS[colorIndex % SAMPLE_COLORS.size],
location = null,
)
fun allDay(from: LocalDate, days: Int, colorIndex: Int) = EventInstance(
instanceId = next(),
eventId = id,
calendarId = 1L,
title = SAMPLE_TITLES[(id.toInt() - 1) % SAMPLE_TITLES.size],
// All-day events sit at UTC midnights with an exclusive end.
start = from.atTime(0, 0).toInstant(TimeZone.UTC),
end = from.plus(days, DateTimeUnit.DAY).atTime(0, 0).toInstant(TimeZone.UTC),
isAllDay = true,
color = SAMPLE_COLORS[colorIndex % SAMPLE_COLORS.size],
location = null,
)
// Spread across the month so most weeks carry something, with one multi-day
// bar to show a span bridging cells and a busy today for the split pane.
return buildList {
add(allDay(firstOfMonth.plus(9, DateTimeUnit.DAY), days = 3, colorIndex = 2))
add(timed(firstOfMonth.plus(1, DateTimeUnit.DAY), 9, 1, 0))
add(timed(firstOfMonth.plus(4, DateTimeUnit.DAY), 14, 2, 1))
add(timed(firstOfMonth.plus(7, DateTimeUnit.DAY), 11, 1, 3))
add(timed(firstOfMonth.plus(15, DateTimeUnit.DAY), 10, 1, 0))
add(timed(firstOfMonth.plus(18, DateTimeUnit.DAY), 16, 1, 2))
add(timed(firstOfMonth.plus(22, DateTimeUnit.DAY), 8, 2, 1))
add(timed(firstOfMonth.plus(25, DateTimeUnit.DAY), 13, 1, 3))
// Today, so the split pane's list and the grid's dots both have content.
add(timed(today, 9, 1, 0))
add(timed(today, 12, 1, 1))
add(timed(today, 15, 2, 3))
}
}
private val SAMPLE_TITLES = listOf(
"Standup",
"Lunch",
"Review",
"Gym",
"Call",
"Workshop",
"Dentist",
"Trip",
)

View File

@@ -1,297 +1,109 @@
package de.jeanlucmakiola.calendula.ui.settings
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.calendula.ui.common.PickerDescription
import de.jeanlucmakiola.calendula.ui.month.MonthStylePreview
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.month.descriptionRes
import de.jeanlucmakiola.calendula.ui.month.labelRes
import de.jeanlucmakiola.floret.components.FullScreenPicker
import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.SelectedCheck
import de.jeanlucmakiola.floret.components.positionOf
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
import kotlinx.datetime.DayOfWeek
/**
* The Month view style chooser (#38, #53). Each option carries a schematic of the
* layout it produces rather than a bare label — the three differ in *shape*, which
* a word like "Continuous" doesn't convey on its own.
* The Month view style chooser (#38, #53).
*
* Tapping applies immediately and the picker stays open, matching the App name
* picker: the change is worth seeing land before leaving.
* A live, scaled-down Month view sits above the options and changes as you pick
* one, so the choice is made by looking rather than by reading "Continuous" and
* guessing. Selecting therefore applies immediately and leaves the picker open —
* closing on tap would hide the very thing the screen is for. Back exits, the
* same as the App name picker, which stays open for the same reason.
*
* Below the preview it is the family's standard picker shape: connected grouped
* rows with a tonal highlight and [SelectedCheck] on the current one.
*/
@Composable
internal fun MonthViewStylePicker(
selected: MonthViewStyle,
weekStart: DayOfWeek,
onSelect: (MonthViewStyle) -> Unit,
onDismiss: () -> Unit,
) {
val options = MonthViewStyle.entries
val reduceMotion = rememberReduceMotion()
FullScreenPicker(
title = stringResource(R.string.settings_month_view_style),
onDismiss = onDismiss,
predictiveBack = true,
) {
Text(
text = stringResource(R.string.settings_month_view_style_summary),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
)
Spacer(Modifier.height(24.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.selectableGroup(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
MonthViewStyle.entries.forEach { style ->
MonthStyleOptionCard(
style = style,
selected = style == selected,
onClick = { onSelect(style) },
)
}
}
}
}
/**
* One selectable style: its schematic, name and a line on what it does. Selection
* reads three ways over — border weight, container tint and a check — so it never
* rests on colour alone.
*/
@Composable
private fun MonthStyleOptionCard(
style: MonthViewStyle,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val shape = RoundedCornerShape(24.dp)
val borderColor = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.outlineVariant
}
val containerColor = if (selected) {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
}
Row(
modifier = modifier
.fillMaxWidth()
.clip(shape)
.background(containerColor)
.border(width = if (selected) 2.dp else 1.dp, color = borderColor, shape = shape)
.selectable(selected = selected, role = Role.RadioButton, onClick = onClick)
.padding(all = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
MonthStyleSchematic(style)
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(style.labelRes),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = stringResource(style.descriptionRes),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
// Selection indicator: a filled check when active, an empty ring otherwise.
Box(
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
.background(if (selected) MaterialTheme.colorScheme.primary else Color.Transparent)
.then(
if (selected) {
Modifier
} else {
Modifier.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
},
),
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.height(PREVIEW_HEIGHT),
contentAlignment = Alignment.Center,
) {
if (selected) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(16.dp),
)
Crossfade(
targetState = selected,
animationSpec = if (reduceMotion) snap() else tween(durationMillis = 250),
label = "month-style-preview",
) { shown ->
// The preview renders the real grid at phone size and scales it
// down, so its own corners are square — the frame rounds it.
Box(
modifier = Modifier
.clip(PREVIEW_SHAPE)
.background(MaterialTheme.colorScheme.surface)
.clipToBounds(),
) {
MonthStylePreview(
style = shown,
weekStart = weekStart,
height = PREVIEW_HEIGHT,
)
}
}
}
}
}
private val SCHEMATIC_WIDTH = 64.dp
private val SCHEMATIC_HEIGHT = 60.dp
private val SCHEMATIC_SHAPE = RoundedCornerShape(10.dp)
/**
* A miniature of the layout, drawn from plain boxes on theme tokens — small
* enough to read as an icon, literal enough that the three are told apart at a
* glance: pages sit side by side, the continuous stream runs off both edges, the
* split style stacks a dotted grid over a list.
*/
@Composable
private fun MonthStyleSchematic(style: MonthViewStyle, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(width = SCHEMATIC_WIDTH, height = SCHEMATIC_HEIGHT)
.clip(SCHEMATIC_SHAPE)
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
.clipToBounds(),
contentAlignment = Alignment.Center,
) {
when (style) {
MonthViewStyle.Paged -> PagedSchematic()
MonthViewStyle.Continuous -> ContinuousSchematic()
MonthViewStyle.Split -> SplitSchematic()
}
}
}
/** Two pages side by side, the second peeking in — a swipe away. */
@Composable
private fun PagedSchematic() {
Row(
modifier = Modifier.fillMaxWidth().padding(start = 6.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
SchematicGrid(rows = 4, modifier = Modifier.weight(1f))
// The next page, cropped by the container — the swipe affordance.
SchematicGrid(rows = 4, modifier = Modifier.width(20.dp), dim = true)
}
}
/** One column of weeks running off both edges: no page breaks, no repeated days. */
@Composable
private fun ContinuousSchematic() {
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
// Six rows in a 60dp box overflow deliberately: the stream is clipped top
// and bottom rather than fitting neatly, which is the whole idea.
repeat(6) { index ->
SchematicWeekRow(dim = index == 0 || index == 5)
}
}
}
/** A squished dotted grid over the selected day's list. */
@Composable
private fun SplitSchematic() {
Column(
modifier = Modifier.fillMaxWidth().padding(6.dp),
verticalArrangement = Arrangement.spacedBy(5.dp),
) {
SchematicGrid(rows = 3, dotted = true)
// The day pane: two stand-in event rows.
repeat(2) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.clip(RoundedCornerShape(3.dp))
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.55f)),
PickerDescription(stringResource(R.string.settings_month_view_style_summary))
options.forEachIndexed { index, style ->
val isSelected = style == selected
GroupedRow(
title = stringResource(style.labelRes),
summary = stringResource(style.descriptionRes),
position = positionOf(index, options.size),
selected = isSelected,
trailing = if (isSelected) {
{ SelectedCheck() }
} else {
null
},
// Applies straight away; the preview above is the confirmation,
// so there is nothing to dismiss for.
onClick = { onSelect(style) },
)
}
}
}
/** [rows] week rows of seven cells; [dotted] shrinks the cells to event dots. */
@Composable
private fun SchematicGrid(
rows: Int,
modifier: Modifier = Modifier,
dim: Boolean = false,
dotted: Boolean = false,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(if (dotted) 4.dp else 3.dp),
) {
repeat(rows) {
if (dotted) SchematicDotRow(dim) else SchematicWeekRow(dim)
}
}
}
/** A week as seven filled cells. */
@Composable
private fun SchematicWeekRow(dim: Boolean = false, modifier: Modifier = Modifier) {
val color = MaterialTheme.colorScheme.onSurfaceVariant
.copy(alpha = if (dim) 0.18f else 0.38f)
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
repeat(7) {
Box(
modifier = Modifier
.weight(1f)
.height(5.dp)
.clip(RoundedCornerShape(1.5.dp))
.background(color),
)
}
}
}
/** A week as seven event dots — the split style's compact grid. */
@Composable
private fun SchematicDotRow(dim: Boolean = false, modifier: Modifier = Modifier) {
val color = MaterialTheme.colorScheme.onSurfaceVariant
.copy(alpha = if (dim) 0.18f else 0.38f)
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(7) {
Box(
modifier = Modifier
.weight(1f)
.height(3.dp)
.clip(CircleShape)
.background(color),
)
}
}
}
private val PREVIEW_HEIGHT = 240.dp
private val PREVIEW_SHAPE = RoundedCornerShape(12.dp)

View File

@@ -104,6 +104,7 @@ import de.jeanlucmakiola.floret.reminders.reminderOverrideFor
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
import de.jeanlucmakiola.calendula.domain.EventFormField
import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType
import de.jeanlucmakiola.calendula.qs.NewEventTileService
@@ -915,6 +916,8 @@ private fun ViewsScreen(
if (showMonthStyle) {
MonthViewStylePicker(
selected = state.monthViewStyle,
// The preview is a real grid, so it uses the real week start too.
weekStart = state.weekStart.resolveFirstDay(currentLocale()),
onSelect = viewModel::setMonthViewStyle,
onDismiss = { showMonthStyle = false },
)