Port Agendula's list editor and palette to calendars (#287)
This commit is contained in:
+28
-15
@@ -1,22 +1,35 @@
|
|||||||
package de.jeanlucmakiola.calendula.data.calendar
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google-Calendar-style palette; ARGB ints for a raw `CALENDAR_COLOR` /
|
* The colours offered when creating or editing a calendar; ARGB ints for a raw
|
||||||
* `EVENT_COLOR`. The named entries exist for callers that need one specific
|
* `CALENDAR_COLOR` / `EVENT_COLOR`.
|
||||||
* hue (the managed special-dates calendars), so they can't drift from the
|
*
|
||||||
* swatches offered in the colour picker.
|
* Hues rather than final fills: every surface that draws a calendar colour runs
|
||||||
|
* it through the app's own tone pass first, so these are chosen to stay
|
||||||
|
* distinguishable after it. Shared with Agendula, whose list palette these are —
|
||||||
|
* the two apps are the same family and a calendar and a task list picked from
|
||||||
|
* different-looking sets read as different products.
|
||||||
|
*
|
||||||
|
* The named entries exist for callers that need one specific hue (the managed
|
||||||
|
* special-dates calendars), so they can't drift from the swatches on offer.
|
||||||
*/
|
*/
|
||||||
object CalendarColorPalette {
|
object CalendarColorPalette {
|
||||||
val Red = 0xFFD50000.toInt()
|
val Mauve = 0xFF7A5C6B.toInt()
|
||||||
val Orange = 0xFFE67C00.toInt()
|
val Red = 0xFFD7484A.toInt()
|
||||||
val Amber = 0xFFF6BF26.toInt()
|
val Orange = 0xFFE8743B.toInt()
|
||||||
val Green = 0xFF33B679.toInt()
|
val Amber = 0xFFE0A32E.toInt()
|
||||||
val DarkGreen = 0xFF0B8043.toInt()
|
val Olive = 0xFF7CA83E.toInt()
|
||||||
val Blue = 0xFF039BE5.toInt()
|
val Green = 0xFF35A06A.toInt()
|
||||||
val Indigo = 0xFF3F51B5.toInt()
|
val Teal = 0xFF19938C.toInt()
|
||||||
val Purple = 0xFF8E24AA.toInt()
|
val Cyan = 0xFF2A9BC4.toInt()
|
||||||
val Graphite = 0xFF616161.toInt()
|
val Blue = 0xFF3C74C8.toInt()
|
||||||
|
val Indigo = 0xFF6A5CC0.toInt()
|
||||||
|
val Purple = 0xFF9455B8.toInt()
|
||||||
|
val Pink = 0xFFC94F8E.toInt()
|
||||||
|
|
||||||
/** The full palette, in swatch-row order. */
|
/** The full palette, in swatch-grid order. */
|
||||||
val all: List<Int> = listOf(Red, Orange, Amber, Green, DarkGreen, Blue, Indigo, Purple, Graphite)
|
val all: List<Int> = listOf(
|
||||||
|
Mauve, Red, Orange, Amber, Olive, Green,
|
||||||
|
Teal, Cyan, Blue, Indigo, Purple, Pink,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,11 +97,23 @@ import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
|||||||
import de.jeanlucmakiola.floret.identity.collapseExit
|
import de.jeanlucmakiola.floret.identity.collapseExit
|
||||||
import de.jeanlucmakiola.floret.identity.expandEnter
|
import de.jeanlucmakiola.floret.identity.expandEnter
|
||||||
import de.jeanlucmakiola.floret.identity.predictiveBack
|
import de.jeanlucmakiola.floret.identity.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
|
||||||
import de.jeanlucmakiola.floret.components.GroupedListInset
|
import de.jeanlucmakiola.floret.components.GroupedListInset
|
||||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||||
|
import de.jeanlucmakiola.floret.components.GroupedSurface
|
||||||
|
|
||||||
/** Sentinel [editorId] meaning "the editor is composing a new calendar". */
|
/** Sentinel [editorId] meaning "the editor is composing a new calendar". */
|
||||||
private const val NEW_CALENDAR_ID = Long.MIN_VALUE
|
private const val NEW_CALENDAR_ID = Long.MIN_VALUE
|
||||||
@@ -343,7 +355,13 @@ private fun CalendarsList(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
private const val SWATCHES_PER_ROW = 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or edit a local calendar: a name field over the palette of calendar
|
||||||
|
* colours, on the family's full-screen sheet with the commit in its title bar.
|
||||||
|
* The same sheet Agendula edits a task list in, so the two read as one product.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CalendarEditor(
|
internal fun CalendarEditor(
|
||||||
sessionKey: Int,
|
sessionKey: Int,
|
||||||
@@ -360,121 +378,48 @@ internal fun CalendarEditor(
|
|||||||
var color by rememberSaveable(sessionKey) { mutableStateOf(initialColor) }
|
var color by rememberSaveable(sessionKey) { mutableStateOf(initialColor) }
|
||||||
var description by rememberSaveable(sessionKey) { mutableStateOf(initialDescription) }
|
var description by rememberSaveable(sessionKey) { mutableStateOf(initialDescription) }
|
||||||
var confirmDelete by remember { mutableStateOf(false) }
|
var confirmDelete by remember { mutableStateOf(false) }
|
||||||
val dark = isSystemInDarkTheme()
|
val focusRequester = remember { FocusRequester() }
|
||||||
val soften = LocalSoftenColors.current
|
// A new calendar opens with the keyboard up: naming it is the whole task.
|
||||||
|
LaunchedEffect(isNew) { if (isNew) focusRequester.requestFocus() }
|
||||||
|
|
||||||
Scaffold(
|
val valid = name.isNotBlank()
|
||||||
modifier = Modifier
|
val commit = { if (valid) onSave(name.trim(), color, description.trim().ifEmpty { null }) }
|
||||||
.predictiveBack(onBack = onClose)
|
|
||||||
.fillMaxSize()
|
FullScreenPicker(
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
title = stringResource(
|
||||||
topBar = {
|
if (isNew) R.string.calendars_new_title else R.string.calendars_edit_title,
|
||||||
TopAppBar(
|
),
|
||||||
title = {
|
onDismiss = onClose,
|
||||||
Text(
|
predictiveBack = true,
|
||||||
stringResource(
|
actions = {
|
||||||
if (isNew) R.string.calendars_new_title
|
Button(
|
||||||
else R.string.calendars_edit_title,
|
onClick = commit,
|
||||||
),
|
enabled = valid,
|
||||||
)
|
modifier = Modifier.padding(end = 12.dp),
|
||||||
},
|
) { Text(stringResource(R.string.event_edit_save)) }
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onClose) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Close,
|
|
||||||
contentDescription = stringResource(R.string.event_edit_close),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
if (!isNew) {
|
|
||||||
// Disabled rather than hidden while the special-dates
|
|
||||||
// sync owns this calendar; the card below says why.
|
|
||||||
IconButton(
|
|
||||||
onClick = { confirmDelete = true },
|
|
||||||
enabled = !deleteLocked,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Delete,
|
|
||||||
contentDescription = stringResource(R.string.event_detail_delete),
|
|
||||||
tint = if (deleteLocked) {
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Filled save button, matching the event editor's top bar.
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
onSave(name.trim(), color, description.trim().ifEmpty { null })
|
|
||||||
},
|
|
||||||
enabled = name.isNotBlank(),
|
|
||||||
modifier = Modifier.padding(end = 12.dp),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.event_edit_save))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) {
|
||||||
Column(
|
if (deleteLocked) {
|
||||||
modifier = Modifier
|
ManagedNote()
|
||||||
.padding(innerPadding)
|
Spacer(Modifier.height(20.dp))
|
||||||
.fillMaxSize()
|
}
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
CalendarNameField(
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
name = name,
|
||||||
) {
|
color = color,
|
||||||
if (deleteLocked) {
|
focusRequester = focusRequester,
|
||||||
EditorCard(
|
onNameChange = { name = it },
|
||||||
icon = Icons.Default.Info,
|
onImeAction = commit,
|
||||||
iconTint = MaterialTheme.colorScheme.onSurfaceVariant,
|
)
|
||||||
iconAtTop = true,
|
|
||||||
) {
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
EditorSectionLabel(stringResource(R.string.calendars_color_label))
|
||||||
text = stringResource(R.string.calendars_managed_delete_locked),
|
CalendarColorGrid(selected = color, onSelect = { color = it })
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
Spacer(Modifier.height(20.dp))
|
||||||
)
|
EditorSectionLabel(stringResource(R.string.calendars_description_hint))
|
||||||
}
|
GroupedSurface(position = Position.Alone, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
}
|
Box(Modifier.fillMaxWidth().heightIn(min = 72.dp).padding(16.dp)) {
|
||||||
EditorCard(icon = Icons.Default.CalendarMonth, iconTint = eventAccent(color, dark, soften)) {
|
|
||||||
InlineTextField(
|
|
||||||
value = name,
|
|
||||||
onValueChange = { name = it },
|
|
||||||
placeholder = stringResource(R.string.calendars_name_label),
|
|
||||||
textStyle = MaterialTheme.typography.titleLarge,
|
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
EditorCard(
|
|
||||||
icon = Icons.Default.Palette,
|
|
||||||
iconTint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
iconAtTop = true,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.calendars_color_label),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(12.dp))
|
|
||||||
ColorSwatchRow(
|
|
||||||
colors = CalendarColorPalette.all,
|
|
||||||
selected = color,
|
|
||||||
onSelect = { color = it },
|
|
||||||
dark = dark,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
EditorCard(
|
|
||||||
icon = Icons.AutoMirrored.Filled.Notes,
|
|
||||||
iconTint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
iconAtTop = true,
|
|
||||||
) {
|
|
||||||
InlineTextField(
|
InlineTextField(
|
||||||
value = description,
|
value = description,
|
||||||
onValueChange = { description = it },
|
onValueChange = { description = it },
|
||||||
@@ -485,20 +430,21 @@ internal fun CalendarEditor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isNew && !deleteLocked) {
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
DeleteCalendarRow(onClick = { confirmDelete = true })
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmDelete) {
|
if (confirmDelete) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { confirmDelete = false },
|
onDismissRequest = { confirmDelete = false },
|
||||||
title = { Text(stringResource(R.string.calendars_delete_confirm_title)) },
|
title = { Text(stringResource(R.string.calendars_delete_confirm_title)) },
|
||||||
text = {
|
text = { Text(stringResource(R.string.calendars_delete_confirm_message, initialName)) },
|
||||||
Text(stringResource(R.string.calendars_delete_confirm_message, initialName))
|
|
||||||
},
|
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = { confirmDelete = false; onDelete() }) {
|
||||||
confirmDelete = false
|
|
||||||
onDelete()
|
|
||||||
}) {
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.event_detail_delete),
|
stringResource(R.string.event_detail_delete),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
@@ -514,6 +460,178 @@ internal fun CalendarEditor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The name, with the chosen colour beside it so the two read as one thing. */
|
||||||
|
@Composable
|
||||||
|
private fun CalendarNameField(
|
||||||
|
name: String,
|
||||||
|
color: Int,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
onNameChange: (String) -> Unit,
|
||||||
|
onImeAction: () -> Unit,
|
||||||
|
) {
|
||||||
|
val dark = isSystemInDarkTheme()
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
GroupedSurface(position = Position.Alone, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().heightIn(min = 72.dp).padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CalendarMonth,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = eventAccent(color, dark, soften),
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
InlineTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = onNameChange,
|
||||||
|
placeholder = stringResource(R.string.calendars_name_label),
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
onImeAction = onImeAction,
|
||||||
|
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The palette as two rows of round swatches; the chosen one carries a check.
|
||||||
|
*
|
||||||
|
* Drawn through the app's own tone pass rather than raw, so a swatch is the
|
||||||
|
* colour the calendar will actually appear in — the user's "soften colours"
|
||||||
|
* preference included.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CalendarColorGrid(selected: Int, onSelect: (Int) -> Unit) {
|
||||||
|
val dark = isSystemInDarkTheme()
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
GroupedSurface(position = Position.Alone, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
CalendarColorPalette.all.chunked(SWATCHES_PER_ROW).forEach { row ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
row.forEach { swatch ->
|
||||||
|
ColorSwatch(
|
||||||
|
fill = eventAccent(swatch, dark, soften),
|
||||||
|
selected = swatch == selected,
|
||||||
|
onClick = { onSelect(swatch) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Keeps a short final row's swatches the size a full row's
|
||||||
|
// are, rather than stretching them across the width.
|
||||||
|
repeat(SWATCHES_PER_ROW - row.size) { Spacer(Modifier.weight(1f)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColorSwatch(
|
||||||
|
fill: Color,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val label = stringResource(R.string.calendars_color_label)
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(fill)
|
||||||
|
// selectable, not clickable: the swatch carries its chosen state in
|
||||||
|
// semantics, so the check is decoration rather than the only cue.
|
||||||
|
.selectable(selected = selected, role = Role.RadioButton, onClick = onClick)
|
||||||
|
.semantics { contentDescription = label },
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (selected) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (fill.luminance() > 0.5f) Color.Black else Color.White,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Said in the sheet rather than on a disabled button: the sync owns this one. */
|
||||||
|
@Composable
|
||||||
|
private fun ManagedNote() {
|
||||||
|
GroupedSurface(
|
||||||
|
position = Position.Alone,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().heightIn(min = 64.dp).padding(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Info, contentDescription = null)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.calendars_managed_delete_locked),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeleteCalendarRow(onClick: () -> Unit) {
|
||||||
|
GroupedSurface(
|
||||||
|
position = Position.Alone,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
onClick = onClick,
|
||||||
|
color = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().heightIn(min = 64.dp).padding(horizontal = 20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.event_detail_delete),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EditorSectionLabel(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(start = 28.dp, end = 28.dp, bottom = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The row's supporting line: the states that make this calendar behave unlike a
|
* The row's supporting line: the states that make this calendar behave unlike a
|
||||||
* plain writable one (#76), then its own description.
|
* plain writable one (#76), then its own description.
|
||||||
@@ -556,36 +674,6 @@ private fun EnableSwitch(
|
|||||||
private fun dimIf(disabled: Boolean): Modifier =
|
private fun dimIf(disabled: Boolean): Modifier =
|
||||||
if (disabled) Modifier.alpha(0.38f) else Modifier
|
if (disabled) Modifier.alpha(0.38f) else Modifier
|
||||||
|
|
||||||
/** Tonal field card matching the event editor's design (icon + content). */
|
|
||||||
@Composable
|
|
||||||
private fun EditorCard(
|
|
||||||
icon: ImageVector,
|
|
||||||
iconTint: Color,
|
|
||||||
iconAtTop: Boolean = false,
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = if (iconAtTop) Alignment.Top else Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = iconTint,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = if (iconAtTop) 2.dp else 0.dp)
|
|
||||||
.size(24.dp),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
Column(modifier = Modifier.weight(1f)) { content() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One collapsible calendar group rendered as a connected card. The header row is
|
* One collapsible calendar group rendered as a connected card. The header row is
|
||||||
|
|||||||
Reference in New Issue
Block a user