feat(components): icon info card, onboarding headline and language row header
IconInfoCard for the detail/edit cards, OnboardingScaffold headline/supporting plus OnboardingPrimaryButton, and header/predictiveBack on LanguagePickerRow.
This commit is contained in:
@@ -43,6 +43,17 @@ are no binary releases; "version" tracks the shared `version` in the root build
|
||||
switch. `GroupedHint` is the muted text beside the cards (settings hints,
|
||||
picker descriptions), and `ExpandIcon` the turning chevron, now also used by
|
||||
`InstructionSteps`.
|
||||
- **`components`** — `IconInfoCard`, the tonal icon-plus-content card behind
|
||||
Calendula's event detail and edit screens (optionally clickable, a top-aligned
|
||||
icon for multi-line fields, a `header` line the icon centres on).
|
||||
- **`components`** — `OnboardingScaffold` takes `headline` and `supporting`,
|
||||
the centred title and muted explanation most steps open `body` with; `body`
|
||||
now defaults to empty. `OnboardingPrimaryButton` is the full-width 56dp
|
||||
call-to-action, with an optional trailing icon. Existing calls are unchanged.
|
||||
- **`components`** — `LanguagePickerRow` takes `header` and `predictiveBack`,
|
||||
forwarded to its `OptionPicker`, so an app with a "help translate" row above
|
||||
the languages can drop its own copy of the row. Both default to today's
|
||||
behaviour.
|
||||
|
||||
- **`identity`** — a pure-black (AMOLED) dark theme. `ColorScheme.toPureBlack()`
|
||||
sets `background`, `surface`, `surfaceDim` and `surfaceContainerLowest` to
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package de.jeanlucmakiola.floret.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* A free-standing info card: a tonal container with [icon] in a 24dp gutter on
|
||||
* the left and [content] beside it — a detail screen's "where / when / repeats"
|
||||
* cards, or the same fields on an edit form.
|
||||
*
|
||||
* [onClick] makes the whole card tappable. [iconAlignment] Top with an
|
||||
* [iconTopPadding] lines the icon up with a multi-line field's first line. A
|
||||
* tall card passes its first line as [header]: the icon centres on that, and
|
||||
* [content] continues below it on the same text column.
|
||||
*/
|
||||
@Composable
|
||||
fun IconInfoCard(
|
||||
icon: ImageVector,
|
||||
iconContentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
iconTint: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
onClick: (() -> Unit)? = null,
|
||||
iconAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
iconTopPadding: Dp = 0.dp,
|
||||
header: (@Composable ColumnScope.() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
val color = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
val inner: @Composable () -> Unit = {
|
||||
if (header != null) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CardIcon(icon, iconContentDescription, iconTint, Modifier)
|
||||
Spacer(Modifier.width(IconGap))
|
||||
Column(Modifier.weight(1f), content = header)
|
||||
}
|
||||
Column(Modifier.padding(start = IconSize + IconGap), content = content)
|
||||
}
|
||||
} else {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = iconAlignment) {
|
||||
CardIcon(icon, iconContentDescription, iconTint, Modifier.padding(top = iconTopPadding))
|
||||
Spacer(Modifier.width(IconGap))
|
||||
Column(Modifier.weight(1f), content = content)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
Surface(onClick = onClick, color = color, shape = shape, modifier = modifier.fillMaxWidth()) { inner() }
|
||||
} else {
|
||||
Surface(color = color, shape = shape, modifier = modifier.fillMaxWidth()) { inner() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardIcon(icon: ImageVector, contentDescription: String?, tint: Color, modifier: Modifier) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = tint,
|
||||
modifier = modifier.size(IconSize),
|
||||
)
|
||||
}
|
||||
|
||||
private val IconSize = 24.dp
|
||||
private val IconGap = 16.dp
|
||||
@@ -19,6 +19,8 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -30,6 +32,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -66,6 +69,10 @@ object OnboardingSpace {
|
||||
* the scrolling content, for a step whose content is taller than the screen: a
|
||||
* pinned bar there eats the room the content needs and hides the fact that
|
||||
* there is more of it below.
|
||||
*
|
||||
* [headline] and [supporting] are the step's centred title and muted
|
||||
* explanation, drawn at the top of [body] — the block nearly every step opens
|
||||
* with. [body] then continues below them, and may be left out entirely.
|
||||
*/
|
||||
@Composable
|
||||
fun OnboardingScaffold(
|
||||
@@ -77,7 +84,9 @@ fun OnboardingScaffold(
|
||||
topSpacing: Dp = OnboardingSpace.xl,
|
||||
contentPadding: Dp = OnboardingSpace.md,
|
||||
scrollingActions: Boolean = false,
|
||||
body: @Composable ColumnScope.() -> Unit,
|
||||
headline: String? = null,
|
||||
supporting: String? = null,
|
||||
body: @Composable ColumnScope.() -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
@@ -120,6 +129,22 @@ fun OnboardingScaffold(
|
||||
Spacer(Modifier.height(topSpacing))
|
||||
hero()
|
||||
Spacer(Modifier.height(OnboardingSpace.lg))
|
||||
if (headline != null) {
|
||||
Text(
|
||||
text = headline,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
if (headline != null && supporting != null) Spacer(Modifier.height(12.dp))
|
||||
if (supporting != null) {
|
||||
Text(
|
||||
text = supporting,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
body()
|
||||
// No inset of its own: scrolling actions sit inside the content
|
||||
// column, so they line up with whatever is above them.
|
||||
@@ -135,6 +160,37 @@ fun OnboardingScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The step's main call to action for [OnboardingScaffold]'s `actions`: a
|
||||
* full-width 56dp filled button with a `titleMedium` label, and an optional
|
||||
* [trailingIcon] (e.g. a forward arrow) after it.
|
||||
*/
|
||||
@Composable
|
||||
fun OnboardingPrimaryButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
trailingIcon: ImageVector? = null,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = modifier.fillMaxWidth().height(56.dp),
|
||||
contentPadding = if (trailingIcon != null) {
|
||||
ButtonDefaults.ButtonWithIconContentPadding
|
||||
} else {
|
||||
ButtonDefaults.ContentPadding
|
||||
},
|
||||
) {
|
||||
Text(text = text, style = MaterialTheme.typography.titleMedium)
|
||||
if (trailingIcon != null) {
|
||||
Spacer(Modifier.width(OnboardingSpace.xs))
|
||||
Icon(imageVector = trailingIcon, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The call-to-action stack, wherever the shell has put it. */
|
||||
@Composable
|
||||
private fun ActionsColumn(
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
||||
import androidx.annotation.XmlRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -111,7 +112,8 @@ fun AboutCard(
|
||||
* [AppLanguage]. The choice is mirrored locally so the row updates instantly
|
||||
* before the activity recreation lands. [autoLabel] names the follow-the-system
|
||||
* option (the `null` tag); [localesConfig] is the app's `res/xml/locales_config`;
|
||||
* [leading] is the row's icon.
|
||||
* [leading] is the row's icon. [header] and [predictiveBack] are forwarded to the
|
||||
* picker's [OptionPicker] (e.g. a "help translate" row above the languages).
|
||||
*/
|
||||
@Composable
|
||||
fun LanguagePickerRow(
|
||||
@@ -119,6 +121,8 @@ fun LanguagePickerRow(
|
||||
title: String,
|
||||
autoLabel: String,
|
||||
@XmlRes localesConfig: Int,
|
||||
header: (@Composable ColumnScope.() -> Unit)? = null,
|
||||
predictiveBack: Boolean = false,
|
||||
leading: @Composable () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -148,6 +152,8 @@ fun LanguagePickerRow(
|
||||
AppLanguage.apply(context, it)
|
||||
},
|
||||
onDismiss = { showDialog = false },
|
||||
header = header,
|
||||
predictiveBack = predictiveBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user