package com.example.aiapp import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import dev.snipme.highlights.model.SyntaxTheme /** * Catppuccin Mocha, as published in `catppuccin/palette`. * * Named rather than used as literals at the point of need, so the mapping below reads as the * decision it is -- "a card is Surface 0" -- and so a value can be checked against the upstream * palette without reading the layout that uses it. */ private object Mocha { val Rosewater = Color(0xFFF5E0DC) val Mauve = Color(0xFFCBA6F7) val Red = Color(0xFFF38BA8) val Peach = Color(0xFFFAB387) val Yellow = Color(0xFFF9E2AF) val Green = Color(0xFFA6E3A1) val Teal = Color(0xFF94E2D5) val Sky = Color(0xFF89DCEB) val Blue = Color(0xFF89B4FA) val Lavender = Color(0xFFB4BEFE) val Text = Color(0xFFCDD6F4) val Subtext0 = Color(0xFFA6ADC8) val Overlay0 = Color(0xFF6C7086) val Surface2 = Color(0xFF585B70) val Surface1 = Color(0xFF45475A) val Surface0 = Color(0xFF313244) val Base = Color(0xFF1E1E2E) val Mantle = Color(0xFF181825) val Crust = Color(0xFF11111B) } /** * The app's colour scheme: Catppuccin Mocha mapped onto Material's roles. * * Copied from dev-updater rather than shared, which is a deliberate line: wg-app-link is the *link* * -- the tunnel, the pinned CA, enrollment -- and a palette is not that. The two apps looking alike * is a preference, not a contract, and the moment one wants a different accent the shared version * becomes a thing to fight rather than a thing to use. * * The mapping that matters is the surface ladder. Mocha names its darks in order -- Crust, Mantle, * Base, Surface 0, Surface 1 -- and Material asks for the same thing under different names, so the * page is Base, a component's outlined card stays Base beside it, and a project's card is Surface * 0: one visible step up, which is the whole of what the nesting has to say. * * Accents on this palette are light, so anything filled with one takes Crust for its text rather * than the near-white the roles default to. */ val AiAppColors = darkColorScheme( primary = Mocha.Mauve, onPrimary = Mocha.Crust, primaryContainer = Mocha.Surface1, onPrimaryContainer = Mocha.Mauve, secondary = Mocha.Lavender, onSecondary = Mocha.Crust, secondaryContainer = Mocha.Surface1, onSecondaryContainer = Mocha.Lavender, tertiary = Mocha.Rosewater, onTertiary = Mocha.Crust, background = Mocha.Base, onBackground = Mocha.Text, surface = Mocha.Base, onSurface = Mocha.Text, surfaceVariant = Mocha.Surface0, onSurfaceVariant = Mocha.Subtext0, surfaceContainerLowest = Mocha.Crust, surfaceContainerLow = Mocha.Mantle, surfaceContainer = Mocha.Base, surfaceContainerHigh = Mocha.Surface0, surfaceContainerHighest = Mocha.Surface0, inverseSurface = Mocha.Text, inverseOnSurface = Mocha.Base, inversePrimary = Mocha.Mauve, outline = Mocha.Overlay0, outlineVariant = Mocha.Surface2, error = Mocha.Red, onError = Mocha.Crust, errorContainer = Mocha.Surface1, onErrorContainer = Mocha.Red, scrim = Mocha.Crust, ) /** * What a session is doing, said in colour. * * Here rather than beside each screen that shows a status. These were separate literals in two * other files -- an amber, a green and a red picked off Material's defaults -- so the same state * was a slightly different colour depending which screen you looked at, and none of them belonged * to this palette at all. A colour that carries meaning is part of the scheme, not a value typed * where it happened to be needed. */ val runningColor: Color @Composable get() = Mocha.Green /** * "This went wrong on its own": a session that fell over. * * The scheme's error colour, and deliberately not "the same red as a destructive button" even * though it is the same red. They are the same red for different reasons, and a state is not an * action -- nothing here is a button. */ val failedColor: Color @Composable get() = MaterialTheme.colorScheme.error /** * About the session rather than about the task: a command, and the compaction one of them starts. * * Its own colour because it is its own kind of work. Everything else a session does is progress * through what was asked of it; this is the session acting on itself -- rewriting what it * remembers, taking a new name -- and none of it appears in the transcript as an answer to * anything. A reader who has learned that blue means "not stuck, but not replying to you either" * has learned the thing that distinguishes it from a session that has hung. */ val commandColor: Color @Composable get() = Mocha.Blue /** * A clear: the conversation taken out of what the session is given. * * Red because of what it does, not because anything went wrong -- somebody asked for this, and a * deliberate choice is not a problem to report. It is the same red as [failedColor] and [stopColor] * for a third reason, which is worth naming rather than collapsing: this is neither a fault nor a * button, it is the mark left where something was taken away. The reader never has to tell the * three apart, because no two of them can appear as the same kind of thing. */ val clearedColor: Color @Composable get() = Mocha.Red /** Waiting on a person: a question, a permission, a turn that is theirs. */ val awaitingColor: Color @Composable get() = Mocha.Peach /** Approaching a limit -- still fine, worth seeing. */ val warningColor: Color @Composable get() = Mocha.Yellow /** * The fill of a progress bar that is only reporting how far along something is. * * Blue because a bar like this reports a quantity rather than a verdict, and the scheme's primary * made it the loudest thing on a screen the reader opened to do something else. A download, or a * compaction, has no limit to be near: it finishes. Only a bar measuring a *quota* escalates, and * that one is [quotaColor]. */ val progressColor: Color @Composable get() = Mocha.Blue /** * The fill of a bar measuring how much of a quota is gone: blue, then yellow, then red. * * One function rather than the same `when` written beside each bar, because the whole point of * colouring by consequence is that the reader learns the step once -- two bars showing the same 80% * in different colours teaches nothing except that the colour cannot be trusted. It reads as a * difference in degree, which is all colour can carry: the states that differ in *kind* from this * -- a window nobody could read, a machine that meters nothing -- are said in words elsewhere, * because a reader has no way to tell those from an ordinary low number by colour alone. * * [percent] is the API's own 0-100 rather than a fraction, so callers pass what the server sent * without each converting it first and one of them getting it wrong by a factor of a hundred. */ @Composable fun quotaColor(percent: Double): Color = when { percent >= OVER_LIMIT_PERCENT -> overLimitColor percent >= WARNING_PERCENT -> warningColor else -> progressColor } /** Close enough to the limit to be worth seeing before starting something big. */ private const val WARNING_PERCENT = 75.0 /** Close enough that the next turn may be the one that is refused. */ private const val OVER_LIMIT_PERCENT = 90.0 /** * Catppuccin Mocha as a syntax theme, for the highlighter used on a tool call's input. * * Here with the rest of the palette rather than beside the code that highlights: a library's own * theme would otherwise be the one surface in the app whose colours came from somewhere else, and * the accents below are the same ones every other coloured thing already uses. */ fun catppuccinSyntax(): SyntaxTheme = SyntaxTheme( key = "catppuccin-mocha", code = Mocha.Text.toArgb(), keyword = Mocha.Mauve.toArgb(), string = Mocha.Green.toArgb(), literal = Mocha.Peach.toArgb(), comment = Mocha.Overlay0.toArgb(), metadata = Mocha.Yellow.toArgb(), multilineComment = Mocha.Overlay0.toArgb(), punctuation = Mocha.Subtext0.toArgb(), mark = Mocha.Sky.toArgb(), ) /** * A link. Blue is what a link is on every Catppuccin surface, and the one colour to leave alone. */ val linkColor: Color @Composable get() = Mocha.Blue /** Past a limit. The scheme's error colour, for the reason [failedColor] gives. */ val overLimitColor: Color @Composable get() = MaterialTheme.colorScheme.error /** * The composer's buttons, coloured by what pressing one does rather than by where it sits. * * Green makes something happen now, blue makes it happen later, orange takes back what is in * flight, red ends the process. The near-collisions with the states above are deliberate and worth * naming rather than collapsing: [runningColor] is green because a session is working, * [failedColor] is red because one fell over, [awaitingColor] is the same orange because a session * is waiting on somebody -- those are *states*, and these are *actions*. A reader never has to tell * them apart, because nothing here is a state and nothing there is pressable. */ val sendColor: Color @Composable get() = Mocha.Green /** Sending while a turn runs: the message waits rather than starting one. See [sendColor]. */ val queueColor: Color @Composable get() = Mocha.Blue /** * Interrupting the running turn: the work stops and the session stays. * * Orange rather than red because of how much it takes: only what is in flight. The process is still * there holding the conversation, and the next message starts a turn as though nothing had * happened. Red is spent on [stopColor], which is the same button in the same place when what it * would end is the session's process. */ val pauseColor: Color @Composable get() = Mocha.Peach /** Ending the session's process -- the one button here that takes something away. */ val stopColor: Color @Composable get() = Mocha.Red /** * Starting the process again, on the conversation it left. * * The same green as [sendColor] on purpose: both mean "this happens now", and they are never the * same button -- the process button only offers to start when there is nothing running to stop. */ val startColor: Color @Composable get() = Mocha.Green /** * A filled button in one of the action colours above. * * The content colour is stated here beside the fill rather than inherited. A semantic colour has to * carry its own contrast: these fills are fixed whatever the surface under them does, so the theme * will not change to rescue a foreground that stops being readable on one of them. Crust is what * every accent on this palette takes, which is the same reason `onPrimary` is Crust above. */ @Composable fun actionButtonColors(fill: Color): ButtonColors = ButtonDefaults.buttonColors(containerColor = fill, contentColor = Mocha.Crust)