refactor(commons): extract CodeSnippet + EcashMint renderers to commons

First pilot of the note-ui-commons extraction plan
(commons/plans/2026-07-16-note-ui-commons-extraction.md, Tier 0). These
two event-kind renderers take plain quartz events with no AccountViewModel,
INav, or R.string, so they move to commons/ui/note/ almost verbatim:

- EcashMintCard: RenderCashuMint / RenderFedimint / RenderMintRecommendation
  move wholesale (already past the seam — the NoteCompose dispatcher hands
  them a decoded event). NoteCompose + ThreadFeedView imports re-pointed.
- CodeSnippetCard: the feed preview and thread header layouts move to
  commons; the Android app keeps a thin RenderCodeSnippetEvent(Note) entry
  that decodes the Note and calls the shared card.
- NoteBorders: shared QuoteBorder/SmallBorder/StdHorzSpacer/StdVertSpacer/
  subtleBorder/replyModifier theme primitives in commons so the migrated
  cards share one visual vocabulary. Mirrors the Android ui/theme values;
  the Android copies are deleted once every renderer that uses them moves.

Desktop now renders the identical cards, so the two front ends can't drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gmrt3jwYPDJ38MJGJ6GaNr
This commit is contained in:
Claude
2026-07-17 22:47:37 +00:00
parent 90acafe389
commit 870aeb4b99
6 changed files with 330 additions and 222 deletions
@@ -63,6 +63,9 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill
import com.vitorpamplona.amethyst.commons.ui.note.QuietMark
import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint
import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint
import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
@@ -127,7 +130,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarCollectionEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarRSVPEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderCashuMint
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderChat
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessage
@@ -137,7 +139,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderClassifieds
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderCommunity
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
import com.vitorpamplona.amethyst.ui.note.types.RenderFedimint
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
import com.vitorpamplona.amethyst.ui.note.types.RenderFundraiser
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
@@ -156,7 +157,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderMintRecommendation
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicPlaylist
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse
@@ -20,223 +20,19 @@
*/
package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.TextStyle
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.vitorpamplona.amethyst.commons.ui.note.CodeSnippetCard
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent
/**
* Entry point for a NIP-C0 code snippet in the feed: decodes the [Note] and hands the
* event to the shared [CodeSnippetCard] in commons. The thread header renders through
* commons' `RenderCodeSnippetHeaderForThread` directly.
*/
@Composable
fun RenderCodeSnippetEvent(baseNote: Note) {
val noteEvent = baseNote.event as? CodeSnippetEvent ?: return
val name =
remember(noteEvent) {
noteEvent.snippetName()
?: noteEvent.language()?.let { "$it snippet" }
?: "Code Snippet"
}
val language = remember(noteEvent) { noteEvent.language() ?: noteEvent.extension() }
val description = remember(noteEvent) { noteEvent.snippetDescription() }
val codePreview =
remember(noteEvent) {
noteEvent.content
.lines()
.take(5)
.joinToString("\n")
}
Column(MaterialTheme.colorScheme.replyModifier) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
language?.let {
Spacer(modifier = StdHorzSpacer)
Box(
modifier =
Modifier
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 6.dp, vertical = 2.dp),
) {
Text(
text = it,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
description?.ifBlank { null }?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = StdVertSpacer)
}
Box(
modifier =
Modifier
.fillMaxWidth()
.padding(10.dp)
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f))
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), QuoteBorder)
.padding(10.dp),
) {
Text(
text = codePreview,
style =
TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
),
maxLines = 5,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
fun RenderCodeSnippetHeaderForThread(noteEvent: CodeSnippetEvent) {
val name =
remember(noteEvent) {
noteEvent.snippetName()
?: noteEvent.language()?.let { "$it snippet" }
?: "Code Snippet"
}
val language = remember(noteEvent) { noteEvent.language() ?: noteEvent.extension() }
val description = remember(noteEvent) { noteEvent.snippetDescription() }
val license = remember(noteEvent) { noteEvent.license() }
val runtime = remember(noteEvent) { noteEvent.runtime() }
val deps = remember(noteEvent) { noteEvent.deps() }
Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
language?.let {
Spacer(modifier = StdHorzSpacer)
Box(
modifier =
Modifier
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 8.dp, vertical = 3.dp),
) {
Text(
text = it,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
description?.ifBlank { null }?.let {
Spacer(modifier = StdVertSpacer)
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth(),
)
}
Spacer(modifier = StdVertSpacer)
Box(
modifier =
Modifier
.fillMaxWidth()
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f))
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), QuoteBorder)
.padding(12.dp),
) {
Text(
text = noteEvent.content,
style =
TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
),
)
}
license?.let {
Spacer(modifier = StdVertSpacer)
Text(
text = "License: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
runtime?.let {
Text(
text = "Runtime: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (deps.isNotEmpty()) {
Text(
text = "Deps: ${deps.joinToString(", ")}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
CodeSnippetCard(noteEvent)
}
@@ -80,6 +80,10 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint
import com.vitorpamplona.amethyst.commons.ui.note.RenderCodeSnippetHeaderForThread
import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint
import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation
import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -160,13 +164,10 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderBirdDetection
import com.vitorpamplona.amethyst.ui.note.types.RenderBirdex
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderCashuMint
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderChat
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetHeaderForThread
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
import com.vitorpamplona.amethyst.ui.note.types.RenderFedimint
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
import com.vitorpamplona.amethyst.ui.note.types.RenderFundraiser
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
@@ -181,7 +182,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderMintRecommendation
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicPlaylist
import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
@@ -0,0 +1,241 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.note
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.TextStyle
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.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent
/** Compact feed preview for a NIP-C0 code snippet: name, language pill, description, first lines. */
@Composable
fun CodeSnippetCard(noteEvent: CodeSnippetEvent) {
val name =
remember(noteEvent) {
noteEvent.snippetName()
?: noteEvent.language()?.let { "$it snippet" }
?: "Code Snippet"
}
val language = remember(noteEvent) { noteEvent.language() ?: noteEvent.extension() }
val description = remember(noteEvent) { noteEvent.snippetDescription() }
val codePreview =
remember(noteEvent) {
noteEvent.content
.lines()
.take(5)
.joinToString("\n")
}
Column(MaterialTheme.colorScheme.replyModifier) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
language?.let {
Spacer(modifier = StdHorzSpacer)
Box(
modifier =
Modifier
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 6.dp, vertical = 2.dp),
) {
Text(
text = it,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
description?.ifBlank { null }?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = StdVertSpacer)
}
Box(
modifier =
Modifier
.fillMaxWidth()
.padding(10.dp)
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f))
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), QuoteBorder)
.padding(10.dp),
) {
Text(
text = codePreview,
style =
TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
),
maxLines = 5,
overflow = TextOverflow.Ellipsis,
)
}
}
}
/** Full snippet header shown at the top of a thread: full code plus license/runtime/deps metadata. */
@Composable
fun RenderCodeSnippetHeaderForThread(noteEvent: CodeSnippetEvent) {
val name =
remember(noteEvent) {
noteEvent.snippetName()
?: noteEvent.language()?.let { "$it snippet" }
?: "Code Snippet"
}
val language = remember(noteEvent) { noteEvent.language() ?: noteEvent.extension() }
val description = remember(noteEvent) { noteEvent.snippetDescription() }
val license = remember(noteEvent) { noteEvent.license() }
val runtime = remember(noteEvent) { noteEvent.runtime() }
val deps = remember(noteEvent) { noteEvent.deps() }
Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
language?.let {
Spacer(modifier = StdHorzSpacer)
Box(
modifier =
Modifier
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 8.dp, vertical = 3.dp),
) {
Text(
text = it,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
description?.ifBlank { null }?.let {
Spacer(modifier = StdVertSpacer)
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth(),
)
}
Spacer(modifier = StdVertSpacer)
Box(
modifier =
Modifier
.fillMaxWidth()
.clip(QuoteBorder)
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f))
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), QuoteBorder)
.padding(12.dp),
) {
Text(
text = noteEvent.content,
style =
TextStyle(
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
),
)
}
license?.let {
Spacer(modifier = StdVertSpacer)
Text(
text = "License: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
runtime?.let {
Text(
text = "Runtime: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (deps.isNotEmpty()) {
Text(
text = "Deps: ${deps.joinToString(", ")}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note.types
package com.vitorpamplona.amethyst.commons.ui.note
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
@@ -41,9 +41,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.theme
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
// Shared border geometry and spacing for the event-kind renderers that are being
// pulled down from `amethyst/ui/note` into commons. These mirror the values in the
// Android app's `ui/theme/Shape.kt` so the two front ends can't drift; the Android
// copies become dead code as each renderer moves and are deleted at the end of the
// migration.
/** Rounded corner used for quoted/embedded note bodies (15dp). */
val QuoteBorder = RoundedCornerShape(15.dp)
/** Rounded corner used for small inline pills/chips (7dp). */
val SmallBorder = RoundedCornerShape(7.dp)
/** 5dp horizontal spacer used between inline elements. */
val StdHorzSpacer = Modifier.width(5.dp)
/** 5dp vertical spacer used between stacked elements. */
val StdVertSpacer = Modifier.height(5.dp)
/**
* Faint hairline border color for cards and quoted bodies. Matches the Android app's
* `subtleBorder`: onSurface at 5% (light) / 12% (dark) alpha.
*/
val ColorScheme.subtleBorder: Color
get() = onSurface.copy(alpha = if (isLight) 0.05f else 0.12f)
/**
* The wrapping modifier for a quoted/reply body: top padding, full width, clipped to
* [QuoteBorder] with a [subtleBorder] hairline. Mirrors the Android `replyModifier`.
*/
@Suppress("ModifierFactoryExtensionFunction")
val ColorScheme.replyModifier: Modifier
get() =
Modifier
.padding(top = 5.dp)
.fillMaxWidth()
.clip(QuoteBorder)
.border(1.dp, subtleBorder, QuoteBorder)