Design tokens: theme-aware palette via CompositionLocal, light theme, shape/elevation/motion tokens, persisted ui.theme pref with Settings toggle

This commit is contained in:
avi 2026-09-21 12:04:29 -05:00
commit f3a84a2333
4 changed files with 240 additions and 72 deletions

View file

@ -151,6 +151,17 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
val libraryFilter: StateFlow<String> = _libraryFilter.asStateFlow() val libraryFilter: StateFlow<String> = _libraryFilter.asStateFlow()
fun setLibraryFilter(f: String) { _libraryFilter.value = f } fun setLibraryFilter(f: String) { _libraryFilter.value = f }
/** UI theme: dark is the signature mode; light is opt-in and
* persisted (an appearance choice, unlike the filter lens). */
private val _themeMode = MutableStateFlow(ThemeMode.DARK)
val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow()
fun setThemeMode(mode: ThemeMode) {
_themeMode.value = mode
scope.launch {
prefs.putString(KEY_THEME, if (mode == ThemeMode.LIGHT) "light" else "dark")
}
}
/** Files the pump is running or has queued this session (by name). */ /** Files the pump is running or has queued this session (by name). */
private val inFlight = mutableSetOf<String>() private val inFlight = mutableSetOf<String>()
@ -180,6 +191,8 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
prefs.getString(KEY_SORT)?.takeIf { it in SORT_ORDERS } prefs.getString(KEY_SORT)?.takeIf { it in SORT_ORDERS }
?.let { _sortOrder.value = it } ?.let { _sortOrder.value = it }
_summarizer.value = prefs.getString(KEY_SUMMARIZER) ?: "auto" _summarizer.value = prefs.getString(KEY_SUMMARIZER) ?: "auto"
_themeMode.value = if (prefs.getString(KEY_THEME) == "light")
ThemeMode.LIGHT else ThemeMode.DARK
loadNotifyState() loadNotifyState()
prefs.getString(KEY_FOLDER)?.let { File(it).takeIf { it.isDirectory } } prefs.getString(KEY_FOLDER)?.let { File(it).takeIf { it.isDirectory } }
?.let { setFolder(it, silent = true) } ?.let { setFolder(it, silent = true) }
@ -2030,6 +2043,7 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
} }
/** Summarizer choice: "auto" | "lan" | "local" (see setSummarizer). */ /** Summarizer choice: "auto" | "lan" | "local" (see setSummarizer). */
const val KEY_SUMMARIZER = "llm.summarizer" const val KEY_SUMMARIZER = "llm.summarizer"
const val KEY_THEME = "ui.theme"
const val KEY_UNSEEN_SUMMARIES = "notify.unseen_summaries" const val KEY_UNSEEN_SUMMARIES = "notify.unseen_summaries"
const val KEY_ANNOUNCED_SUMMARIES = "notify.announced_summaries" const val KEY_ANNOUNCED_SUMMARIES = "notify.announced_summaries"
/** Preferred local summarization model (Ollama). */ /** Preferred local summarization model (Ollama). */

View file

@ -98,7 +98,8 @@ fun main() = application {
state = rememberWindowState(width = 1100.dp, height = 800.dp), state = rememberWindowState(width = 1100.dp, height = 800.dp),
) { ) {
val baseDensity = LocalDensity.current val baseDensity = LocalDensity.current
ShonarTheme { val themeMode by state.themeMode.collectAsState()
ShonarTheme(mode = themeMode) {
Surface( Surface(
Modifier.fillMaxSize().onPreviewKeyEvent { e -> Modifier.fillMaxSize().onPreviewKeyEvent { e ->
if (e.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false if (e.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false

View file

@ -1293,6 +1293,19 @@ fun SettingsScreen(state: DesktopState) {
Text("Settings", style = MaterialTheme.typography.headlineSmall) Text("Settings", style = MaterialTheme.typography.headlineSmall)
} }
Text("Local engine: $url", style = MaterialTheme.typography.bodySmall) Text("Local engine: $url", style = MaterialTheme.typography.bodySmall)
SectionHeader("Appearance")
Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2),
verticalAlignment = Alignment.CenterVertically) {
val mode = state.themeMode.collectAsState().value
PillChip(selected = mode == ThemeMode.DARK,
onClick = { state.setThemeMode(ThemeMode.DARK) },
label = "Dark")
PillChip(selected = mode == ThemeMode.LIGHT,
onClick = { state.setThemeMode(ThemeMode.LIGHT) },
label = "Light")
}
HelperText("Dark is the app's native look; light mirrors the same " +
"hierarchy on paper tones. Applied instantly.")
SectionHeader("Recordings folder") SectionHeader("Recordings folder")
Row(verticalAlignment = Alignment.CenterVertically, Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {

View file

@ -1,5 +1,7 @@
package com.shonar.desktop package com.shonar.desktop
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -15,7 +17,9 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -29,56 +33,146 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
/** /**
* SHONAR Desktop design system — the "deep-sea sonar" language from the * SHONAR Desktop design system — a calm, dark-first workspace language:
* reference design: near-black navy page, one elevated surface for all * a deep cool-navy page, one elevated surface for all cards/fields,
* cards/fields, hairline borders, a single mint-teal accent used with * hairline borders, and a single mint-teal accent reserved for active
* restraint (active controls, live status, selection), and a soft teal * states, live status, and primary actions. A light theme mirrors the
* glow reserved for the selected pill. * same hierarchy on paper tones (light accent darkened for contrast).
* *
* Everything visual routes through [Shonar] tokens or the MaterialTheme * Every visual value routes through [Shonar] tokens or the MaterialTheme
* roles derived from them — screens must not hardcode colors, radii, or * roles derived from them — screens must not hardcode colors, radii, or
* spacing. Reusable primitives live in Components.kt. * spacing. Reusable primitives live in Components.kt.
* *
* Theming: colors resolve through [Shonar.LocalPalette], so all token
* names keep working in both modes; [ShonarTheme] swaps the palette and
* the derived [MaterialTheme.colorScheme]. Spacing, shapes, type scale,
* and motion are theme-independent.
*
* Status semantics map to scheme roles app-wide: * Status semantics map to scheme roles app-wide:
* done -> primary (teal), failed -> error (red), queued/running -> * done -> primary (teal), failed -> error (red), queued/running ->
* onSurfaceVariant text with a primary progress bar. * onSurfaceVariant text with a primary progress bar.
*/ */
/** A complete set of theme-dependent colors. Both [DarkPalette] and
* [LightPalette] provide every slot; nothing outside this file should
* hardcode a color. */
data class ShonarPalette(
val background: Color,
val surface: Color,
val surfaceAlt: Color,
val border: Color,
val accent: Color,
val accentStrong: Color,
val accentGlow: Color,
val inkOnAccent: Color,
val textPrimary: Color,
val textSecondary: Color,
val textMuted: Color,
val error: Color,
val onError: Color,
)
/** The signature near-black cool-navy workspace. */
private val DarkPalette = ShonarPalette(
background = Color(0xFF0A0E15), // page: near-black cool navy
surface = Color(0xFF121A25), // cards, fields, pills
surfaceAlt = Color(0xFF0E151F), // pressed / hover / inset
border = Color(0xFF1E2A38), // hairline on dark
accent = Color(0xFF5EEAD4), // mint-teal: icons, selection
accentStrong = Color(0xFF4FD1C5), // filled-accent hover depth
accentGlow = Color(0x405EEAD4), // soft halo (~25% mint)
inkOnAccent = Color(0xFF06231F), // text on filled accent
textPrimary = Color(0xFFE8EEF4),
textSecondary = Color(0xFF8A97A8),
textMuted = Color(0xFF6B7686),
error = Color(0xFFF27272), // red readable on navy
onError = Color(0xFF1A0B0D),
)
/** Light mode: cool paper tones, same three-step surface ramp
* (page < card < inset). The accent darkens to teal-700 so accent
* text on paper meets AA contrast; filled accent takes white ink. */
private val LightPalette = ShonarPalette(
background = Color(0xFFF4F6F9), // page: cool paper
surface = Color(0xFFFFFFFF), // cards, fields, pills
surfaceAlt = Color(0xFFEAEFF5), // pressed / hover / inset
border = Color(0xFFD5DDE7), // hairline on light
accent = Color(0xFF0F766E), // deep teal: readable on paper
accentStrong = Color(0xFF115E59), // filled-accent hover depth
accentGlow = Color(0x330E9488), // soft halo (~20% teal)
inkOnAccent = Color(0xFFF6FFFD), // text on filled accent
textPrimary = Color(0xFF18222E),
textSecondary = Color(0xFF52606F),
textMuted = Color(0xFF7A8695),
error = Color(0xFFBA1A1A), // AA red on white
onError = Color(0xFFFFF8F7),
)
/** Which palette the app renders in. Persisted as "dark" / "light". */
enum class ThemeMode { DARK, LIGHT }
object Shonar { object Shonar {
// ---- color tokens ---- // ---- palette routing ----
val Background = Color(0xFF0A0E15) // page: near-black cool navy /** Active palette; [ShonarTheme] provides the mode's instance.
val Surface = Color(0xFF121A25) // cards, fields, pills * Defaults to dark so previews/strays get the signature look. */
val SurfaceAlt = Color(0xFF0E151F) // pressed / hover / inset val LocalPalette = staticCompositionLocalOf { DarkPalette }
val Border = Color(0xFF1E2A38) // hairline on dark
val Accent = Color(0xFF5EEAD4) // mint-teal: icons, selection // ---- color tokens (theme-aware: readable inside any composable) ----
val AccentStrong = Color(0xFF4FD1C5) // filled-accent hover depth val Background: Color @Composable get() = LocalPalette.current.background
val AccentGlow = Color(0x405EEAD4) // soft halo (~25% mint) val Surface: Color @Composable get() = LocalPalette.current.surface
val InkOnAccent = Color(0xFF06231F) // text on filled accent val SurfaceAlt: Color @Composable get() = LocalPalette.current.surfaceAlt
val TextPrimary = Color(0xFFE8EEF4) val Border: Color @Composable get() = LocalPalette.current.border
val TextSecondary = Color(0xFF8A97A8) val Accent: Color @Composable get() = LocalPalette.current.accent
val TextMuted = Color(0xFF6B7686) val AccentStrong: Color @Composable get() = LocalPalette.current.accentStrong
val Error = Color(0xFFF27272) // red readable on navy val AccentGlow: Color @Composable get() = LocalPalette.current.accentGlow
val OnError = Color(0xFF1A0B0D) val InkOnAccent: Color @Composable get() = LocalPalette.current.inkOnAccent
val TextPrimary: Color @Composable get() = LocalPalette.current.textPrimary
val TextSecondary: Color @Composable get() = LocalPalette.current.textSecondary
val TextMuted: Color @Composable get() = LocalPalette.current.textMuted
val Error: Color @Composable get() = LocalPalette.current.error
val OnError: Color @Composable get() = LocalPalette.current.onError
// ---- shape tokens ---- // ---- shape tokens ----
val RadiusCard = 14.dp // cards + fields val RadiusCard = 14.dp // cards + fields
val RadiusButton = 12.dp // rectangular buttons val RadiusButton = 12.dp // rectangular buttons
val RadiusPanel = 16.dp // sidebar / large panels
val ShapeCard = RoundedCornerShape(RadiusCard) val ShapeCard = RoundedCornerShape(RadiusCard)
val ShapeButton = RoundedCornerShape(RadiusButton) val ShapeButton = RoundedCornerShape(RadiusButton)
val ShapePanel = RoundedCornerShape(RadiusPanel)
val ShapePill = RoundedCornerShape(50) // pills + round controls val ShapePill = RoundedCornerShape(50) // pills + round controls
// ---- border + elevation tokens ----
val BorderW = 1.dp // hairline everywhere
val FocusW = 2.dp // keyboard focus ring
// Restrained card lift: cards read mostly by border, the shadow is
// barely-there separation, never a floating card.
val ElevationCard = 2.dp
// ---- spacing scale (4dp rhythm; compact but comfortable) ---- // ---- spacing scale (4dp rhythm; compact but comfortable) ----
val Space1 = 4.dp val Space1 = 4.dp
val Space2 = 8.dp val Space2 = 8.dp
val Space3 = 12.dp val Space3 = 12.dp
val Space4 = 16.dp val Space4 = 16.dp
val Space5 = 20.dp // page padding val Space5 = 20.dp // page padding
val CardPadding = 14.dp val CardPadding = 14.dp
val RowGap = 8.dp // gap between list cards val RowGap = 8.dp // gap between list cards
val ControlH = 40.dp // standard button height val ControlH = 40.dp // standard button height
val RailW = 64.dp // collapsed sidebar (icon rail)
val SidebarW = 232.dp // expanded sidebar
// ---- icon sizing ---- // ---- icon sizing ----
val IconSm = 18.dp val IconSm = 18.dp
val IconMd = 20.dp val IconMd = 20.dp
// ---- motion (ease-out for entrances, in-out for state swaps) ----
object Motion {
val InstantMs = 80 // pressed feedback
val FastMs = 140 // hover/focus/selection
val NormalMs = 220 // panels, theme crossfade
val SlowMs = 320 // sidebar collapse/expand
val EaseOut: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
val EaseInOut: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
}
} }
private val ShonarShapes = Shapes( private val ShonarShapes = Shapes(
@ -89,78 +183,120 @@ private val ShonarShapes = Shapes(
extraLarge = RoundedCornerShape(20.dp), extraLarge = RoundedCornerShape(20.dp),
) )
// Typography hierarchy per the reference: page title 22/700 wide // Typography hierarchy: page title 22/700 wide tracking; section 16/600;
// tracking; section 16/600; item title 15/600; body 14; metadata 13 // item title 15/600; body 14; metadata 13 muted; labels 12–13. Line
// muted; labels 12–13. Line heights are explicit everywhere: the // heights are explicit everywhere: the summary is the product's reading
// summary is the product's reading surface and 1.0 leading read as a // surface and 1.0 leading read as a dense wall; each tier separates by
// dense wall; each tier now separates by size AND rhythm, not weight // size AND rhythm, not weight alone. Colors are NOT set here — the
// alone. // MaterialTheme roles carry them so both palettes work; screens pick
// tiers, not ink.
private val ShonarTypography = Typography( private val ShonarTypography = Typography(
headlineSmall = TextStyle( headlineSmall = TextStyle(
fontSize = 22.sp, fontWeight = FontWeight.Bold, fontSize = 22.sp, fontWeight = FontWeight.Bold,
letterSpacing = 0.5.sp, lineHeight = 28.sp, letterSpacing = 0.5.sp, lineHeight = 28.sp,
color = Shonar.TextPrimary,
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontSize = 17.sp, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, fontWeight = FontWeight.SemiBold,
lineHeight = 24.sp, color = Shonar.TextPrimary, lineHeight = 24.sp,
), ),
titleSmall = TextStyle( titleSmall = TextStyle(
fontSize = 15.sp, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
letterSpacing = 0.2.sp, lineHeight = 22.sp, letterSpacing = 0.2.sp, lineHeight = 22.sp,
color = Shonar.TextPrimary,
), ),
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontSize = 16.sp, lineHeight = 25.sp, color = Shonar.TextPrimary, fontSize = 16.sp, lineHeight = 25.sp,
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontSize = 14.sp, lineHeight = 21.sp, color = Shonar.TextPrimary, fontSize = 14.sp, lineHeight = 21.sp,
), ),
bodySmall = TextStyle( bodySmall = TextStyle(
fontSize = 13.sp, lineHeight = 18.sp, color = Shonar.TextSecondary, fontSize = 13.sp, lineHeight = 18.sp,
), ),
labelLarge = TextStyle( labelLarge = TextStyle(
fontSize = 14.sp, fontWeight = FontWeight.Medium, lineHeight = 18.sp, fontSize = 14.sp, fontWeight = FontWeight.Medium, lineHeight = 18.sp,
), ),
labelMedium = TextStyle( labelMedium = TextStyle(
fontSize = 13.sp, fontWeight = FontWeight.Medium, lineHeight = 17.sp, fontSize = 13.sp, fontWeight = FontWeight.Medium, lineHeight = 17.sp,
color = Shonar.TextSecondary,
), ),
labelSmall = TextStyle( labelSmall = TextStyle(
fontSize = 12.sp, lineHeight = 16.sp, color = Shonar.TextMuted, fontSize = 12.sp, lineHeight = 16.sp,
), ),
) )
/** Derive the full Material role set from a palette so anything still
* reading MaterialTheme.colorScheme follows the active theme. */
private fun schemeFor(p: ShonarPalette) =
if (p === DarkPalette) ShonarDarkScheme else ShonarLightScheme
private val ShonarDarkScheme = darkColorScheme( private val ShonarDarkScheme = darkColorScheme(
primary = Shonar.AccentStrong, primary = DarkPalette.accentStrong,
onPrimary = Shonar.InkOnAccent, onPrimary = DarkPalette.inkOnAccent,
primaryContainer = Shonar.AccentGlow, primaryContainer = DarkPalette.accentGlow,
onPrimaryContainer = Shonar.Accent, onPrimaryContainer = DarkPalette.accent,
secondary = Shonar.Accent, secondary = DarkPalette.accent,
onSecondary = Shonar.InkOnAccent, onSecondary = DarkPalette.inkOnAccent,
secondaryContainer = Shonar.SurfaceAlt, secondaryContainer = DarkPalette.surfaceAlt,
onSecondaryContainer = Shonar.TextSecondary, onSecondaryContainer = DarkPalette.textSecondary,
background = Shonar.Background, background = DarkPalette.background,
onBackground = Shonar.TextPrimary, onBackground = DarkPalette.textPrimary,
surface = Shonar.Surface, surface = DarkPalette.surface,
onSurface = Shonar.TextPrimary, onSurface = DarkPalette.textPrimary,
surfaceVariant = Shonar.SurfaceAlt, surfaceVariant = DarkPalette.surfaceAlt,
onSurfaceVariant = Shonar.TextSecondary, onSurfaceVariant = DarkPalette.textSecondary,
error = Shonar.Error, error = DarkPalette.error,
onError = Shonar.OnError, onError = DarkPalette.onError,
outline = Shonar.Border, outline = DarkPalette.border,
outlineVariant = Shonar.Border, outlineVariant = DarkPalette.border,
) )
/** Wrap the app content in the SHONAR dark sonar design system. */ private val ShonarLightScheme = lightColorScheme(
primary = LightPalette.accent,
onPrimary = LightPalette.inkOnAccent,
primaryContainer = LightPalette.accentGlow,
onPrimaryContainer = LightPalette.accentStrong,
secondary = LightPalette.accent,
onSecondary = LightPalette.inkOnAccent,
secondaryContainer = LightPalette.surfaceAlt,
onSecondaryContainer = LightPalette.textSecondary,
background = LightPalette.background,
onBackground = LightPalette.textPrimary,
surface = LightPalette.surface,
onSurface = LightPalette.textPrimary,
surfaceVariant = LightPalette.surfaceAlt,
onSurfaceVariant = LightPalette.textSecondary,
error = LightPalette.error,
onError = LightPalette.onError,
outline = LightPalette.border,
outlineVariant = LightPalette.border,
)
/** Wrap the app content in the SHONAR design system in [mode]
* (dark is the signature default). */
@Composable @Composable
fun ShonarTheme(content: @Composable () -> Unit) { fun ShonarTheme(
MaterialTheme( mode: ThemeMode = ThemeMode.DARK,
colorScheme = ShonarDarkScheme, content: @Composable () -> Unit,
shapes = ShonarShapes, ) {
typography = ShonarTypography, val palette = if (mode == ThemeMode.LIGHT) LightPalette else DarkPalette
content = content, // Muted tiers carry secondary/muted ink per palette (call sites may
) // still override with an explicit color).
val typography = androidx.compose.runtime.remember(palette) {
ShonarTypography.copy(
bodySmall = ShonarTypography.bodySmall.copy(color = palette.textSecondary),
labelMedium = ShonarTypography.labelMedium.copy(color = palette.textSecondary),
labelSmall = ShonarTypography.labelSmall.copy(color = palette.textMuted),
)
}
androidx.compose.runtime.CompositionLocalProvider(
Shonar.LocalPalette provides palette,
) {
MaterialTheme(
colorScheme = schemeFor(palette),
shapes = ShonarShapes,
typography = typography,
content = content,
)
}
} }
/** /**
@ -170,11 +306,14 @@ fun ShonarTheme(content: @Composable () -> Unit) {
*/ */
@Composable @Composable
fun SonarLogo(size: Dp = 32.dp, modifier: Modifier = Modifier) { fun SonarLogo(size: Dp = 32.dp, modifier: Modifier = Modifier) {
// Canvas draws run outside composition: hoist palette reads to locals.
val accent = Shonar.Accent
val accentStrong = Shonar.AccentStrong
Canvas(modifier = modifier.size(size)) { Canvas(modifier = modifier.size(size)) {
val s = this.size.minDimension / 108f val s = this.size.minDimension / 108f
fun c(x: Float, y: Float) = Offset(x * s, y * s) fun c(x: Float, y: Float) = Offset(x * s, y * s)
// Center dot: r=6 at (54,54) // Center dot: r=6 at (54,54)
drawCircle(Shonar.Accent, radius = 6f * s, center = c(54f, 54f)) drawCircle(accent, radius = 6f * s, center = c(54f, 54f))
// Upper-right quarter arcs (teal), centered on the dot. // Upper-right quarter arcs (teal), centered on the dot.
val arcStyle = Stroke(width = 4f * s, cap = StrokeCap.Round) val arcStyle = Stroke(width = 4f * s, cap = StrokeCap.Round)
fun arc(color: Color, startDeg: Float, boxTopLeft: Float) { fun arc(color: Color, startDeg: Float, boxTopLeft: Float) {
@ -186,11 +325,11 @@ fun SonarLogo(size: Dp = 32.dp, modifier: Modifier = Modifier) {
style = arcStyle, style = arcStyle,
) )
} }
arc(Shonar.Accent, -90f, 38f) arc(accent, -90f, 38f)
arc(Shonar.Accent, -90f, 26f) arc(accent, -90f, 26f)
// Lower-left quarter arcs (teal-dark). // Lower-left quarter arcs (teal-dark).
arc(Shonar.AccentStrong, 90f, 38f) arc(accentStrong, 90f, 38f)
arc(Shonar.AccentStrong, 90f, 26f) arc(accentStrong, 90f, 26f)
} }
} }
@ -199,7 +338,8 @@ fun SonarLogo(size: Dp = 32.dp, modifier: Modifier = Modifier) {
@Composable @Composable
fun SonarLogoTile(modifier: Modifier = Modifier, size: Dp = 40.dp) { fun SonarLogoTile(modifier: Modifier = Modifier, size: Dp = 40.dp) {
Surface( Surface(
modifier = modifier.size(size).border(1.dp, Shonar.Border, Shonar.ShapeCard), modifier = modifier.size(size)
.border(Shonar.BorderW, Shonar.Border, Shonar.ShapeCard),
color = Shonar.Surface, color = Shonar.Surface,
shape = Shonar.ShapeCard, shape = Shonar.ShapeCard,
) { ) {