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:
parent
90f6f1b786
commit
f3a84a2333
4 changed files with 240 additions and 72 deletions
|
|
@ -151,6 +151,17 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
|
|||
val libraryFilter: StateFlow<String> = _libraryFilter.asStateFlow()
|
||||
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). */
|
||||
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 }
|
||||
?.let { _sortOrder.value = it }
|
||||
_summarizer.value = prefs.getString(KEY_SUMMARIZER) ?: "auto"
|
||||
_themeMode.value = if (prefs.getString(KEY_THEME) == "light")
|
||||
ThemeMode.LIGHT else ThemeMode.DARK
|
||||
loadNotifyState()
|
||||
prefs.getString(KEY_FOLDER)?.let { File(it).takeIf { it.isDirectory } }
|
||||
?.let { setFolder(it, silent = true) }
|
||||
|
|
@ -2030,6 +2043,7 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
|
|||
}
|
||||
/** Summarizer choice: "auto" | "lan" | "local" (see setSummarizer). */
|
||||
const val KEY_SUMMARIZER = "llm.summarizer"
|
||||
const val KEY_THEME = "ui.theme"
|
||||
const val KEY_UNSEEN_SUMMARIES = "notify.unseen_summaries"
|
||||
const val KEY_ANNOUNCED_SUMMARIES = "notify.announced_summaries"
|
||||
/** Preferred local summarization model (Ollama). */
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ fun main() = application {
|
|||
state = rememberWindowState(width = 1100.dp, height = 800.dp),
|
||||
) {
|
||||
val baseDensity = LocalDensity.current
|
||||
ShonarTheme {
|
||||
val themeMode by state.themeMode.collectAsState()
|
||||
ShonarTheme(mode = themeMode) {
|
||||
Surface(
|
||||
Modifier.fillMaxSize().onPreviewKeyEvent { e ->
|
||||
if (e.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
||||
|
|
|
|||
|
|
@ -1293,6 +1293,19 @@ fun SettingsScreen(state: DesktopState) {
|
|||
Text("Settings", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
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")
|
||||
Row(verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
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.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -15,7 +17,9 @@ import androidx.compose.material3.Surface
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
|
@ -29,56 +33,146 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* SHONAR Desktop design system — the "deep-sea sonar" language from the
|
||||
* reference design: near-black navy page, one elevated surface for all
|
||||
* cards/fields, hairline borders, a single mint-teal accent used with
|
||||
* restraint (active controls, live status, selection), and a soft teal
|
||||
* glow reserved for the selected pill.
|
||||
* SHONAR Desktop design system — a calm, dark-first workspace language:
|
||||
* a deep cool-navy page, one elevated surface for all cards/fields,
|
||||
* hairline borders, and a single mint-teal accent reserved for active
|
||||
* states, live status, and primary actions. A light theme mirrors the
|
||||
* 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
|
||||
* 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:
|
||||
* done -> primary (teal), failed -> error (red), queued/running ->
|
||||
* 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 {
|
||||
// ---- color tokens ----
|
||||
val Background = Color(0xFF0A0E15) // page: near-black cool navy
|
||||
val Surface = Color(0xFF121A25) // cards, fields, pills
|
||||
val SurfaceAlt = Color(0xFF0E151F) // pressed / hover / inset
|
||||
val Border = Color(0xFF1E2A38) // hairline on dark
|
||||
val Accent = Color(0xFF5EEAD4) // mint-teal: icons, selection
|
||||
val AccentStrong = Color(0xFF4FD1C5) // filled-accent hover depth
|
||||
val AccentGlow = Color(0x405EEAD4) // soft halo (~25% mint)
|
||||
val InkOnAccent = Color(0xFF06231F) // text on filled accent
|
||||
val TextPrimary = Color(0xFFE8EEF4)
|
||||
val TextSecondary = Color(0xFF8A97A8)
|
||||
val TextMuted = Color(0xFF6B7686)
|
||||
val Error = Color(0xFFF27272) // red readable on navy
|
||||
val OnError = Color(0xFF1A0B0D)
|
||||
// ---- palette routing ----
|
||||
/** Active palette; [ShonarTheme] provides the mode's instance.
|
||||
* Defaults to dark so previews/strays get the signature look. */
|
||||
val LocalPalette = staticCompositionLocalOf { DarkPalette }
|
||||
|
||||
// ---- color tokens (theme-aware: readable inside any composable) ----
|
||||
val Background: Color @Composable get() = LocalPalette.current.background
|
||||
val Surface: Color @Composable get() = LocalPalette.current.surface
|
||||
val SurfaceAlt: Color @Composable get() = LocalPalette.current.surfaceAlt
|
||||
val Border: Color @Composable get() = LocalPalette.current.border
|
||||
val Accent: Color @Composable get() = LocalPalette.current.accent
|
||||
val AccentStrong: Color @Composable get() = LocalPalette.current.accentStrong
|
||||
val AccentGlow: Color @Composable get() = LocalPalette.current.accentGlow
|
||||
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 ----
|
||||
val RadiusCard = 14.dp // cards + fields
|
||||
val RadiusButton = 12.dp // rectangular buttons
|
||||
val RadiusCard = 14.dp // cards + fields
|
||||
val RadiusButton = 12.dp // rectangular buttons
|
||||
val RadiusPanel = 16.dp // sidebar / large panels
|
||||
val ShapeCard = RoundedCornerShape(RadiusCard)
|
||||
val ShapeButton = RoundedCornerShape(RadiusButton)
|
||||
val ShapePanel = RoundedCornerShape(RadiusPanel)
|
||||
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) ----
|
||||
val Space1 = 4.dp
|
||||
val Space2 = 8.dp
|
||||
val Space3 = 12.dp
|
||||
val Space4 = 16.dp
|
||||
val Space5 = 20.dp // page padding
|
||||
val Space5 = 20.dp // page padding
|
||||
val CardPadding = 14.dp
|
||||
val RowGap = 8.dp // gap between list cards
|
||||
val ControlH = 40.dp // standard button height
|
||||
val RowGap = 8.dp // gap between list cards
|
||||
val ControlH = 40.dp // standard button height
|
||||
val RailW = 64.dp // collapsed sidebar (icon rail)
|
||||
val SidebarW = 232.dp // expanded sidebar
|
||||
|
||||
// ---- icon sizing ----
|
||||
val IconSm = 18.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(
|
||||
|
|
@ -89,78 +183,120 @@ private val ShonarShapes = Shapes(
|
|||
extraLarge = RoundedCornerShape(20.dp),
|
||||
)
|
||||
|
||||
// Typography hierarchy per the reference: page title 22/700 wide
|
||||
// tracking; section 16/600; item title 15/600; body 14; metadata 13
|
||||
// muted; labels 12–13. Line heights are explicit everywhere: the
|
||||
// summary is the product's reading surface and 1.0 leading read as a
|
||||
// dense wall; each tier now separates by size AND rhythm, not weight
|
||||
// alone.
|
||||
// Typography hierarchy: page title 22/700 wide tracking; section 16/600;
|
||||
// item title 15/600; body 14; metadata 13 muted; labels 12–13. Line
|
||||
// heights are explicit everywhere: the summary is the product's reading
|
||||
// surface and 1.0 leading read as a dense wall; each tier separates by
|
||||
// size AND rhythm, not weight alone. Colors are NOT set here — the
|
||||
// MaterialTheme roles carry them so both palettes work; screens pick
|
||||
// tiers, not ink.
|
||||
private val ShonarTypography = Typography(
|
||||
headlineSmall = TextStyle(
|
||||
fontSize = 22.sp, fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 0.5.sp, lineHeight = 28.sp,
|
||||
color = Shonar.TextPrimary,
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontSize = 17.sp, fontWeight = FontWeight.SemiBold,
|
||||
lineHeight = 24.sp, color = Shonar.TextPrimary,
|
||||
lineHeight = 24.sp,
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
|
||||
letterSpacing = 0.2.sp, lineHeight = 22.sp,
|
||||
color = Shonar.TextPrimary,
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontSize = 16.sp, lineHeight = 25.sp, color = Shonar.TextPrimary,
|
||||
fontSize = 16.sp, lineHeight = 25.sp,
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontSize = 14.sp, lineHeight = 21.sp, color = Shonar.TextPrimary,
|
||||
fontSize = 14.sp, lineHeight = 21.sp,
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontSize = 13.sp, lineHeight = 18.sp, color = Shonar.TextSecondary,
|
||||
fontSize = 13.sp, lineHeight = 18.sp,
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontSize = 14.sp, fontWeight = FontWeight.Medium, lineHeight = 18.sp,
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontSize = 13.sp, fontWeight = FontWeight.Medium, lineHeight = 17.sp,
|
||||
color = Shonar.TextSecondary,
|
||||
),
|
||||
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(
|
||||
primary = Shonar.AccentStrong,
|
||||
onPrimary = Shonar.InkOnAccent,
|
||||
primaryContainer = Shonar.AccentGlow,
|
||||
onPrimaryContainer = Shonar.Accent,
|
||||
secondary = Shonar.Accent,
|
||||
onSecondary = Shonar.InkOnAccent,
|
||||
secondaryContainer = Shonar.SurfaceAlt,
|
||||
onSecondaryContainer = Shonar.TextSecondary,
|
||||
background = Shonar.Background,
|
||||
onBackground = Shonar.TextPrimary,
|
||||
surface = Shonar.Surface,
|
||||
onSurface = Shonar.TextPrimary,
|
||||
surfaceVariant = Shonar.SurfaceAlt,
|
||||
onSurfaceVariant = Shonar.TextSecondary,
|
||||
error = Shonar.Error,
|
||||
onError = Shonar.OnError,
|
||||
outline = Shonar.Border,
|
||||
outlineVariant = Shonar.Border,
|
||||
primary = DarkPalette.accentStrong,
|
||||
onPrimary = DarkPalette.inkOnAccent,
|
||||
primaryContainer = DarkPalette.accentGlow,
|
||||
onPrimaryContainer = DarkPalette.accent,
|
||||
secondary = DarkPalette.accent,
|
||||
onSecondary = DarkPalette.inkOnAccent,
|
||||
secondaryContainer = DarkPalette.surfaceAlt,
|
||||
onSecondaryContainer = DarkPalette.textSecondary,
|
||||
background = DarkPalette.background,
|
||||
onBackground = DarkPalette.textPrimary,
|
||||
surface = DarkPalette.surface,
|
||||
onSurface = DarkPalette.textPrimary,
|
||||
surfaceVariant = DarkPalette.surfaceAlt,
|
||||
onSurfaceVariant = DarkPalette.textSecondary,
|
||||
error = DarkPalette.error,
|
||||
onError = DarkPalette.onError,
|
||||
outline = DarkPalette.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
|
||||
fun ShonarTheme(content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colorScheme = ShonarDarkScheme,
|
||||
shapes = ShonarShapes,
|
||||
typography = ShonarTypography,
|
||||
content = content,
|
||||
)
|
||||
fun ShonarTheme(
|
||||
mode: ThemeMode = ThemeMode.DARK,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val palette = if (mode == ThemeMode.LIGHT) LightPalette else DarkPalette
|
||||
// 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
|
||||
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)) {
|
||||
val s = this.size.minDimension / 108f
|
||||
fun c(x: Float, y: Float) = Offset(x * s, y * s)
|
||||
// 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.
|
||||
val arcStyle = Stroke(width = 4f * s, cap = StrokeCap.Round)
|
||||
fun arc(color: Color, startDeg: Float, boxTopLeft: Float) {
|
||||
|
|
@ -186,11 +325,11 @@ fun SonarLogo(size: Dp = 32.dp, modifier: Modifier = Modifier) {
|
|||
style = arcStyle,
|
||||
)
|
||||
}
|
||||
arc(Shonar.Accent, -90f, 38f)
|
||||
arc(Shonar.Accent, -90f, 26f)
|
||||
arc(accent, -90f, 38f)
|
||||
arc(accent, -90f, 26f)
|
||||
// Lower-left quarter arcs (teal-dark).
|
||||
arc(Shonar.AccentStrong, 90f, 38f)
|
||||
arc(Shonar.AccentStrong, 90f, 26f)
|
||||
arc(accentStrong, 90f, 38f)
|
||||
arc(accentStrong, 90f, 26f)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +338,8 @@ fun SonarLogo(size: Dp = 32.dp, modifier: Modifier = Modifier) {
|
|||
@Composable
|
||||
fun SonarLogoTile(modifier: Modifier = Modifier, size: Dp = 40.dp) {
|
||||
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,
|
||||
shape = Shonar.ShapeCard,
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue