fix: give every tappable element a real target, and every icon a decided description
Phase 3, second step, of docs/material-design-conformance.md. Two accessibility rules the
tree had no way to hold: M3's 48x48dp touch target and 44x44dp pointer target, and its
requirement that a decorative visual be *annotated* as decorative rather than merely left
undescribed.
**Nineteen `.clickable` chains had no minimum size, and three were text-sized.**
`ArticleCard` and `LiveStreamCardContent` each make an author's name tappable -- a
`labelMedium`, around 16dp tall -- and `LinkPreview` does the same to a `bodyLarge` url
with 2dp of vertical padding. The other sixteen are cards, rows and full-screen boxes that
are already far larger.
`minimumInteractiveComponentSize()` is applied to all nineteen rather than to the three,
because it is a no-op on anything already 48dp and that makes the rule checkable by a
script instead of by measuring. Worth being precise about what it does, since the modifier
is easy to describe wrongly: it reserves 48x48dp of **layout**, not of touch handling --
touch expansion happens at the input layer regardless. Layout is what keeps adjacent
targets from overlapping, what satisfies M3's 8dp separation, and what a mouse pointer on
the desktop build actually has to land on.
**`Clickable.kt` had it built in and moved house.** The vendored ACINQ helper defaults to
`RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as its
content -- and its call sites wrap a 20dp emoji and a row of wallet text. It now applies
the modifier unconditionally, before `.padding(internalPadding)`, since a size modifier
after it would re-impose the smaller constraint.
It also stopped declaring `package com.machankura.compose.ui.composable.widgets.buttons`
while living under `press/mantra/`. That is the second of the three package namespaces the
UI was spread across; `Type.kt` was the first.
**Eighteen `contentDescription = null` were indistinguishable from eighteen oversights.**
`null` is the *correct* API -- M3 asks that decorative visuals be "annotated as decorative
in order to hide them in code", and null is how that annotation is spelled in Compose. The
problem is that it reads identically whether somebody decided or never looked.
So `Decorative` is introduced -- a `String?` that is null -- and fifteen sites now say
`contentDescription = Decorative`. Same bytes, same behaviour, and the difference between
a decision and a gap is now visible in the source and countable by the audit. Each of the
fifteen has adjacent text saying what the icon says: a lock beside "Private to Ada", a
check beside "The group has a shared key.", an icon inside a button whose label is right
there.
**Three were not decorative and now carry their state.**
- `DkgRitualScreen`'s participant list -- a filled or empty circle beside each member.
The name says who; only the icon says whether they have contributed. Now "Contributed"
/ "Not yet contributed".
- `DkgRitualScreen`'s round header -- the title says which round and the count says how
far along; only the icon says whether it finished. Now "Complete" / "In progress".
- `ProposalListScreen`'s leading icon, which is the one this commit could not have left
alone: the previous commit took the red away from the failure state on the highlighted
card, because `error` is 2.67:1 there. The shape is now the only cue a sighted user
gets and the description is the only cue anyone else gets. Now "Awaiting your
signature" / "Signed" / "Failed" / "Waiting on others".
Descriptions follow M3's rule -- name the purpose, not the picture, and never the role.
"Contributed", not "green check", and never "Contributed icon", since the role is added
automatically and a screen reader would say it twice.
**Two new checks, replacing one that was asking the wrong question.**
`docs/scripts/m3-touch-targets.py` finds `.clickable` chains with no minimum size,
including chains broken across two lines. The audit used to count `.clickable` outright,
which is not a defect count: a clickable `Card` is fine and a clickable `Text` is not, and
only the modifier tells them apart. The audit also now separates `contentDescription =
null` (untriaged, budget 0) from `Decorative` (decided, reported at 15).
Both budgets ratcheted to 0, dated in the file.
**Tests.** 944 pass, 595 jvm over 72 classes and 349 android over 44, unchanged -- these
are layout and semantics properties, and this repo has no Compose UI test infrastructure to
assert them against a running composition. What stands in for it is the two scripts, which
check the property that *can* be checked statically: that the modifier and the decision are
present at every site. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh
--check` exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,7 @@ import press.mantra.compose.ui.view.model.ChatRoomMessagingViewModel
|
||||
import press.mantra.compose.ui.view.state.ChatRoomMessagingUIState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -222,7 +223,7 @@ fun ChatRoomMessagingScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
contentDescription = Decorative,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
/**
|
||||
* The frame every ChillDKG approval screen sits in: the view model, the states
|
||||
@@ -137,7 +138,7 @@ internal fun DkgApprovalScaffold(
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.spacing.space500))
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
contentDescription = Decorative,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -76,6 +76,7 @@ import fr.acinq.phoenix.data.ActiveWallet
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
/**
|
||||
* The shared-key ceremony: a ChillDKG ritual run across the group's NIP-17
|
||||
@@ -165,7 +166,7 @@ fun DkgRitualScreen(
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.Key, contentDescription = null)
|
||||
Icon(Icons.Default.Key, contentDescription = Decorative)
|
||||
Text(
|
||||
text = when (pending) {
|
||||
DkgApprovalStep.HOST_KEY -> "Review and join"
|
||||
@@ -333,7 +334,7 @@ private fun RitualProgress(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.ErrorOutline, contentDescription = null)
|
||||
Icon(Icons.Default.ErrorOutline, contentDescription = Decorative)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.relatedGap)) {
|
||||
Text("The ceremony was abandoned.", style = MaterialTheme.typography.titleSmall)
|
||||
session.failureReason?.let {
|
||||
@@ -404,7 +405,7 @@ private fun RitualProgress(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = null)
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = Decorative)
|
||||
Text("The group has a shared key.", style = MaterialTheme.typography.titleSmall)
|
||||
}
|
||||
|
||||
@@ -473,7 +474,7 @@ private fun RitualProgress(
|
||||
if (isActionPending) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp))
|
||||
} else {
|
||||
Icon(Icons.Default.Groups, contentDescription = null)
|
||||
Icon(Icons.Default.Groups, contentDescription = Decorative)
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.spacing.space100))
|
||||
Text(text = "Create the #admins group")
|
||||
}
|
||||
@@ -526,13 +527,19 @@ private fun RitualRoster(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Not decorative: the name beside it says who, and only the
|
||||
// icon says whether they have contributed.
|
||||
Icon(
|
||||
imageVector = if (publicKey in hostKeyParticipants) {
|
||||
Icons.Default.CheckCircle
|
||||
} else {
|
||||
Icons.Default.RadioButtonUnchecked
|
||||
},
|
||||
contentDescription = null
|
||||
contentDescription = if (publicKey in hostKeyParticipants) {
|
||||
"Contributed"
|
||||
} else {
|
||||
"Not yet contributed"
|
||||
}
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -572,9 +579,11 @@ private fun RitualStep(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Not decorative: the title says which round, the count says how far
|
||||
// along, and only the icon says whether it has finished.
|
||||
Icon(
|
||||
imageVector = if (isDone) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked,
|
||||
contentDescription = null
|
||||
contentDescription = if (isDone) "Complete" else "In progress"
|
||||
)
|
||||
Text(text = title, style = MaterialTheme.typography.titleSmall)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
@@ -57,6 +57,7 @@ import press.mantra.compose.ui.theme.TorchTheme
|
||||
import press.mantra.compose.ui.view.model.FrostSigningViewModel
|
||||
import press.mantra.compose.ui.view.state.FrostSigningUIState
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
/**
|
||||
* One signing session, and the member's decision about it.
|
||||
@@ -275,7 +276,7 @@ fun FrostSigningScreen(
|
||||
!frostSigningViewModel.isActionPending.value,
|
||||
onClick = { frostSigningViewModel.approve(onNavigateBack) }
|
||||
) {
|
||||
Icon(Icons.Default.Draw, contentDescription = null)
|
||||
Icon(Icons.Default.Draw, contentDescription = Decorative)
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.spacing.space125))
|
||||
Text("Sign")
|
||||
}
|
||||
|
||||
@@ -270,7 +270,14 @@ private fun ProposalCard(
|
||||
Icons.Default.ErrorOutline
|
||||
else -> Icons.Default.HourglassEmpty
|
||||
},
|
||||
contentDescription = null,
|
||||
// Carries the stage on its own, and carries it alone on the
|
||||
// highlighted card where the failure state has no colour to spare.
|
||||
contentDescription = when {
|
||||
proposal.awaitsYou -> "Awaiting your signature"
|
||||
proposal.session.stage == FrostSigningStage.COMPLETE -> "Signed"
|
||||
proposal.session.stage == FrostSigningStage.FAILED -> "Failed"
|
||||
else -> "Waiting on others"
|
||||
},
|
||||
tint = when {
|
||||
// `error` is 2.67:1 on the highlighted card, so on that one the
|
||||
// failure is carried by the icon shape and the supporting line
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -176,6 +177,7 @@ fun SearchScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable {
|
||||
onNavigateToSearchResult.invoke(
|
||||
SearchResultRoute(
|
||||
@@ -218,6 +220,7 @@ fun SearchScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable {
|
||||
onNavigateToSearchResult.invoke(
|
||||
SearchResultRoute(
|
||||
@@ -257,6 +260,7 @@ fun SearchScreen(
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable {
|
||||
onNavigateToProfile.invoke(
|
||||
NostrEventDetailRoute(
|
||||
@@ -362,6 +366,7 @@ fun SearchScreen(
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable {
|
||||
onNavigateToSearchResult.invoke(
|
||||
SearchResultRoute(
|
||||
|
||||
@@ -56,6 +56,7 @@ import fr.acinq.phoenix.data.ActiveWallet
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
/**
|
||||
* Last step of group creation: convenient (one admin) or robust (everyone
|
||||
@@ -345,7 +346,7 @@ private fun ChatRoomTypeCard(
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
contentDescription = Decorative
|
||||
)
|
||||
|
||||
Text(
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -192,7 +193,8 @@ private fun ChunkTranslationRow(
|
||||
// there is no translation yet, the original text is shown greyed out as
|
||||
// a placeholder. It stays plain text, laid out like the original cell
|
||||
// beside it, rather than a button with its own shape and padding.
|
||||
rightModifier = Modifier.clickable(onClick = onClick),
|
||||
rightModifier = Modifier.minimumInteractiveComponentSize()
|
||||
.clickable(onClick = onClick),
|
||||
right = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package press.mantra.compose.ui.composable.widgets
|
||||
|
||||
/**
|
||||
* An icon or image that adds nothing a screen reader user would miss.
|
||||
*
|
||||
* `contentDescription = null` is the correct Compose API for this -- M3 asks that
|
||||
* decorative visuals be "annotated as decorative in order to hide them in code", and null
|
||||
* is how that annotation is spelled. The problem with `null` is not what it does; it is
|
||||
* that it looks identical whether somebody decided the icon was decorative or never
|
||||
* thought about it, and 18 of them in this tree were indistinguishable.
|
||||
*
|
||||
* `contentDescription = Decorative` compiles to the same null and says which it was. It is
|
||||
* also greppable, so docs/scripts/m3-audit.sh can count the ones still to triage.
|
||||
*
|
||||
* **Use it when the adjacent text already says what the icon says** -- a lock beside
|
||||
* "Private to Ada", a check beside "The group has a shared key.", an icon inside a button
|
||||
* whose label is right there.
|
||||
*
|
||||
* **Do not use it for an icon carrying state the text does not repeat**: a filled-versus-
|
||||
* empty circle beside a member's name, a status icon in a list row. Those get a real
|
||||
* description, and M3's rule for writing one is to name the purpose rather than the
|
||||
* picture, and never to include the role -- "Contributed", not "green check", and never
|
||||
* "Contributed icon".
|
||||
*/
|
||||
@Suppress("MayBeConstant") // a `const` cannot be nullable, and null is the point
|
||||
val Decorative: String? = null
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.machankura.compose.ui.composable.widgets.buttons
|
||||
package press.mantra.compose.ui.composable.widgets.buttons
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Indication
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -38,6 +39,21 @@ import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
|
||||
/**
|
||||
* A tappable surface with no chrome of its own -- vendored from ACINQ's phoenix app.
|
||||
*
|
||||
* Two things were changed when it moved into `press.mantra` from the `com.machankura`
|
||||
* package it was still declaring:
|
||||
*
|
||||
* - **`minimumInteractiveComponentSize()` is applied unconditionally.** Its defaults are
|
||||
* `RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as
|
||||
* whatever is inside it, and the call sites here wrap a 20dp emoji and a row of wallet
|
||||
* text. The modifier reserves 48x48dp of *layout* -- touch expansion happens at the
|
||||
* input layer regardless -- which is what keeps adjacent targets from overlapping and
|
||||
* is what a pointer on desktop has to hit.
|
||||
* - The modifier ordering. It comes before `.padding(internalPadding)`, since a size
|
||||
* modifier after it would re-impose the smaller constraint.
|
||||
*/
|
||||
@Composable
|
||||
fun Clickable(
|
||||
onClick: () -> Unit,
|
||||
@@ -61,6 +77,7 @@ fun Clickable(
|
||||
border = border,
|
||||
contentColor = contentColor,
|
||||
modifier = modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.clip(shape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -53,7 +54,8 @@ private fun ArticleCard(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.spacing.space75)
|
||||
.then(
|
||||
if (onArticleClick != null) Modifier.clickable { onArticleClick(30023, author, dTag) }
|
||||
if (onArticleClick != null) Modifier.minimumInteractiveComponentSize()
|
||||
.clickable { onArticleClick(30023, author, dTag) }
|
||||
else Modifier
|
||||
)
|
||||
) {
|
||||
@@ -141,7 +143,8 @@ private fun ArticleCard(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = if (onProfileClick != null) {
|
||||
Modifier.clickable { onProfileClick(author) }
|
||||
Modifier.minimumInteractiveComponentSize()
|
||||
.clickable { onProfileClick(author) }
|
||||
} else Modifier
|
||||
)
|
||||
if (publishedAt != null) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil3.compose.AsyncImage
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
|
||||
@Composable
|
||||
@@ -64,6 +65,7 @@ fun FullScreenImageViewer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.scrim)
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -49,6 +50,7 @@ internal fun ImageWithContextMenu(meta: MediaMeta, onFullScreen: () -> Unit) {
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable { loaded = true },
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import fr.acinq.lightning.payment.Bolt11Invoice
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
@Composable
|
||||
internal fun LightningInvoiceCard(
|
||||
@@ -109,7 +110,7 @@ internal fun LightningInvoiceCard(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Bolt,
|
||||
contentDescription = null,
|
||||
contentDescription = Decorative,
|
||||
tint = primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -25,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import io.ktor.http.Url
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
@Composable
|
||||
internal fun LinkPreview(url: String) {
|
||||
@@ -49,13 +51,14 @@ internal fun LinkPreview(url: String) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.spacing.space50)
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable { uriHandler.openUri(url) }
|
||||
) {
|
||||
Column {
|
||||
data.image?.let { imageUrl ->
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = null,
|
||||
contentDescription = Decorative,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -120,6 +123,7 @@ internal fun LinkPreview(url: String) {
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.padding(vertical = MaterialTheme.spacing.space25)
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable { uriHandler.openUri(url) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -47,7 +48,8 @@ private fun LiveStreamCard(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.spacing.space75)
|
||||
.then(
|
||||
if (onLiveStreamClick != null) Modifier.clickable {
|
||||
if (onLiveStreamClick != null) Modifier.minimumInteractiveComponentSize()
|
||||
.clickable {
|
||||
onLiveStreamClick(author, dTag, segmentRelayHints.firstOrNull())
|
||||
}
|
||||
else Modifier
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -149,7 +150,8 @@ internal fun LiveStreamCardContent(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = if (onProfileClick != null) {
|
||||
Modifier.clickable { onProfileClick(author) }
|
||||
Modifier.minimumInteractiveComponentSize()
|
||||
.clickable { onProfileClick(author) }
|
||||
} else Modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -73,7 +74,8 @@ fun QuotedNote(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
Modifier.clickable { effectiveNoteClick(localQuotedNostrEvent.nostrEvent.id) }
|
||||
Modifier.minimumInteractiveComponentSize()
|
||||
.clickable { effectiveNoteClick(localQuotedNostrEvent.nostrEvent.id) }
|
||||
)
|
||||
) {
|
||||
localQuotedNostrEvent.RenderNotePreview(
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -103,6 +104,7 @@ fun TextNoteEventDetail(
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable {
|
||||
onNavigateToWriteAReply.invoke(
|
||||
localNostrEvent.nostrEvent.id
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -41,7 +42,8 @@ fun press.mantra.compose.database.model.intermdiate.LocalNostrEvent.ListView(
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = MaterialTheme.spacing.space125
|
||||
).clickable(
|
||||
).minimumInteractiveComponentSize()
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = {
|
||||
onNavigateToEvent.invoke(profile?.nostrEventId ?: nostrEvent.id)
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -54,7 +55,8 @@ internal fun TextNoteFeedItem(
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.clickable(
|
||||
modifier = Modifier.minimumInteractiveComponentSize()
|
||||
.clickable(
|
||||
onClick = {
|
||||
onNavigateToEvent.invoke(localInReplyToNostrEvent.inReplyToRelation.inReplyToNostrEventId)
|
||||
}
|
||||
@@ -81,7 +83,8 @@ internal fun TextNoteFeedItem(
|
||||
) {
|
||||
if (profile != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable(
|
||||
modifier = Modifier.fillMaxWidth().minimumInteractiveComponentSize()
|
||||
.clickable(
|
||||
onClick = {
|
||||
onNavigateToEvent.invoke(profile.nostrEventId)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.machankura.compose.ui.composable.widgets.buttons.Clickable
|
||||
import press.mantra.compose.ui.composable.widgets.buttons.Clickable
|
||||
import press.mantra.compose.ui.composable.widgets.dialogs.ModalBottomSheet
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.machankura.compose.ui.composable.widgets.buttons.Clickable
|
||||
import press.mantra.compose.ui.composable.widgets.buttons.Clickable
|
||||
import fr.acinq.phoenix.data.UserWallet
|
||||
import fr.acinq.phoenix.data.WalletId
|
||||
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
|
||||
|
||||
@@ -47,6 +47,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -92,6 +93,7 @@ import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
|
||||
class ChatMessageListViewModel(
|
||||
initialChatMessageListUIState: ChatMessageListUIState,
|
||||
@@ -650,7 +652,7 @@ class ChatMessageListViewModel(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
contentDescription = Decorative,
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -724,7 +726,7 @@ class ChatMessageListViewModel(
|
||||
Text("Reply privately to ${nameFor(localChatMessage.chatMessage.senderPublicKey)}")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
Icon(Icons.Default.Lock, contentDescription = Decorative)
|
||||
},
|
||||
onClick = {
|
||||
participantFor(localChatMessage.chatMessage.senderPublicKey)
|
||||
@@ -864,7 +866,7 @@ private fun ProposalsAwaitingYouNotice(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Draw,
|
||||
contentDescription = null
|
||||
contentDescription = Decorative
|
||||
)
|
||||
|
||||
Column(
|
||||
@@ -930,7 +932,7 @@ private fun PrivateMessageNotice(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
contentDescription = Decorative,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -1029,6 +1031,7 @@ private fun RitualNotice(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = MaterialTheme.spacing.space250, vertical = MaterialTheme.spacing.space125),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
|
||||
@@ -1036,7 +1039,7 @@ private fun RitualNotice(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
contentDescription = Decorative,
|
||||
tint = tint
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user