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()
|
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). */
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
|
|
||||||
|
|
@ -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,43 +33,121 @@ 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
|
||||||
|
|
@ -75,10 +157,22 @@ object Shonar {
|
||||||
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(
|
||||||
|
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(
|
MaterialTheme(
|
||||||
colorScheme = ShonarDarkScheme,
|
colorScheme = schemeFor(palette),
|
||||||
shapes = ShonarShapes,
|
shapes = ShonarShapes,
|
||||||
typography = ShonarTypography,
|
typography = typography,
|
||||||
content = content,
|
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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue