fix: derive all theme colors from web CSS files (OKLCH → P3 RGB)

Amber blobs were cold (blue/indigo) but web uses warm (gold/coral/honey/teal).
Mono had slate chroma but web is pure achromatic (zero chroma).
Aurora dark primary was magenta but web swaps to gold in dark mode.
All four themes now pixel-match the web app's OKLCH values.
This commit is contained in:
Bruno Deanoz 2026-06-21 14:41:23 +02:00
parent f1c6a88c4c
commit 4d71475995
7 changed files with 107 additions and 81 deletions

View file

@ -50,7 +50,7 @@ import androidx.compose.ui.unit.dp
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.BlobAmber
import dev.kaizen.app.ui.theme.BlobBlue
import dev.kaizen.app.ui.theme.BlobCoral
import dev.kaizen.app.ui.theme.BlobTeal
import dev.kaizen.app.ui.theme.DITHER_ALPHA
import dev.kaizen.app.ui.theme.rememberDitherBrush
@ -81,7 +81,7 @@ fun MeshBackground(content: @Composable BoxScope.() -> Unit) {
},
) {
Blob(BlobAmber, 360.dp, (-60f + drift * 30f).dp, (-40f + drift * 24f).dp, 0.30f * factor)
Blob(BlobBlue, 380.dp, (170f - drift * 44f).dp, (150f + drift * 40f).dp, 0.24f * factor)
Blob(BlobCoral, 380.dp, (170f - drift * 44f).dp, (150f + drift * 40f).dp, 0.24f * factor)
Blob(BlobTeal, 300.dp, (30f + drift * 50f).dp, (560f - drift * 36f).dp, 0.22f * factor)
// Vignette: darken the edges so the center comes forward (dark mode only).
if (dark) {

View file

@ -38,8 +38,10 @@ val LightText = p3(0.102f, 0.122f, 0.161f) // ≈ #1A1F29
val LightMuted = p3(0.357f, 0.392f, 0.439f) // ≈ #5B6470
val LightStroke = p3(0f, 0f, 0f, 0.08f) // outline
// ── Background blobs ───────────────────────────────────────────────────────
val BlobAmber = p3(0.878f, 0.635f, 0.243f) // ≈ Amber
val BlobBlue = p3(0.235f, 0.373f, 0.839f) // ≈ #3C5FD6
val BlobTeal = p3(0.169f, 0.714f, 0.651f) // ≈ #2BB6A6
val BlobIndigo = p3(0.290f, 0.290f, 0.690f) // 4th blob for Amber theme
// ── Background blobs (Amber theme defaults, derived from amber.css) ────────
// These are the light-mode defaults. Theme-specific blobs come from
// LocalKaizenAccent.current.blob1..4 once the theme system is wired.
val BlobAmber = p3(0.992f, 0.767f, 0.282f) // oklch(0.85 0.15 83) gold
val BlobCoral = p3(1.000f, 0.562f, 0.230f) // oklch(0.78 0.18 50) coral
val BlobHoney = p3(0.917f, 0.853f, 0.471f) // oklch(0.88 0.12 100) honey
val BlobTeal = p3(0.461f, 0.885f, 0.908f) // oklch(0.85 0.10 200) teal accent

View file

@ -6,27 +6,28 @@ import androidx.compose.ui.graphics.colorspace.ColorSpaces
private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3)
// Derived from kaizen/app/themes/amber.css — warm blobs (gold, coral, honey, teal accent)
val AmberTheme = KaizenThemeSpec(
id = KaizenThemeId.AMBER,
label = "Amber",
dark = KaizenAccentScheme(
primary = p3(0.878f, 0.635f, 0.243f), // Amber
primaryDeep = p3(0.784f, 0.525f, 0.165f),
onPrimary = p3(0.106f, 0.071f, 0.024f),
ring = p3(0.878f, 0.635f, 0.243f),
blob1 = p3(0.878f, 0.635f, 0.243f), // Amber
blob2 = p3(0.235f, 0.373f, 0.839f), // Blue
blob3 = p3(0.290f, 0.290f, 0.690f), // Indigo
blob4 = p3(0.169f, 0.714f, 0.651f), // Teal
),
light = KaizenAccentScheme(
primary = p3(0.784f, 0.525f, 0.165f), // AmberDeep (better contrast on white)
primaryDeep = p3(0.784f, 0.525f, 0.165f),
onPrimary = p3(1f, 1f, 1f),
ring = p3(0.878f, 0.635f, 0.243f),
blob1 = p3(0.878f, 0.635f, 0.243f),
blob2 = p3(0.235f, 0.373f, 0.839f),
blob3 = p3(0.290f, 0.290f, 0.690f),
blob4 = p3(0.169f, 0.714f, 0.651f),
primary = p3(0.811f, 0.608f, 0.126f), // oklch(0.72 0.14 83)
primaryDeep = p3(0.811f, 0.608f, 0.126f),
onPrimary = p3(0.015f, 0.013f, 0.009f), // oklch(0.10 0.005 83)
ring = p3(0.811f, 0.608f, 0.126f),
blob1 = p3(0.992f, 0.767f, 0.282f), // oklch(0.85 0.15 83) gold
blob2 = p3(1.000f, 0.562f, 0.230f), // oklch(0.78 0.18 50) coral
blob3 = p3(0.917f, 0.853f, 0.471f), // oklch(0.88 0.12 100) honey
blob4 = p3(0.461f, 0.885f, 0.908f), // oklch(0.85 0.10 200) teal accent
),
dark = KaizenAccentScheme(
primary = p3(0.862f, 0.658f, 0.201f), // oklch(0.76 0.14 83)
primaryDeep = p3(0.811f, 0.608f, 0.126f),
onPrimary = p3(0.015f, 0.013f, 0.009f), // oklch(0.10 0.005 83)
ring = p3(0.862f, 0.658f, 0.201f),
blob1 = p3(0.619f, 0.394f, 0.000f), // oklch(0.55 0.16 83) deep gold
blob2 = p3(0.628f, 0.087f, 0.000f), // oklch(0.45 0.18 40) deep coral
blob3 = p3(0.404f, 0.407f, 0.084f), // oklch(0.50 0.10 110) olive
blob4 = p3(0.000f, 0.405f, 0.436f), // oklch(0.45 0.12 200) deep teal
),
)

View file

@ -6,27 +6,30 @@ import androidx.compose.ui.graphics.colorspace.ColorSpaces
private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3)
// Derived from kaizen/app/themes/aurora.css
// Light: purple primary, blobs = purple/gold/green/teal
// Dark: GOLD primary (swaps!), blobs = deep purple/gold/green/teal
val AuroraTheme = KaizenThemeSpec(
id = KaizenThemeId.AURORA,
label = "Aurora",
dark = KaizenAccentScheme(
primary = p3(0.850f, 0.310f, 0.750f), // Magenta P3
primaryDeep = p3(0.680f, 0.220f, 0.620f),
onPrimary = p3(0.106f, 0.024f, 0.090f),
ring = p3(0.850f, 0.310f, 0.750f),
blob1 = p3(0.850f, 0.310f, 0.750f), // Magenta
blob2 = p3(0.169f, 0.714f, 0.800f), // Cyan
blob3 = p3(0.545f, 0.361f, 0.859f), // Violet
blob4 = p3(0.520f, 0.850f, 0.310f), // Lime (accent)
),
light = KaizenAccentScheme(
primary = p3(0.490f, 0.270f, 0.690f), // Violet-600 P3
primaryDeep = p3(0.490f, 0.270f, 0.690f),
onPrimary = p3(1f, 1f, 1f),
ring = p3(0.850f, 0.310f, 0.750f),
blob1 = p3(0.850f, 0.310f, 0.750f),
blob2 = p3(0.169f, 0.714f, 0.800f),
blob3 = p3(0.545f, 0.361f, 0.859f),
blob4 = p3(0.520f, 0.850f, 0.310f),
primary = p3(0.724f, 0.328f, 0.812f), // oklch(0.62 0.20 320) purple
primaryDeep = p3(0.724f, 0.328f, 0.812f),
onPrimary = p3(1.000f, 0.979f, 1.000f), // oklch(0.99 0.01 320)
ring = p3(0.724f, 0.328f, 0.812f),
blob1 = p3(0.855f, 0.540f, 0.926f), // oklch(0.75 0.16 320) purple
blob2 = p3(0.931f, 0.738f, 0.291f), // oklch(0.82 0.14 85) gold
blob3 = p3(0.433f, 0.822f, 0.455f), // oklch(0.78 0.16 145) green
blob4 = p3(0.227f, 0.808f, 0.837f), // oklch(0.78 0.12 200) teal
),
dark = KaizenAccentScheme(
primary = p3(0.890f, 0.684f, 0.156f), // oklch(0.78 0.15 85) GOLD (swapped!)
primaryDeep = p3(0.724f, 0.328f, 0.812f),
onPrimary = p3(0.034f, 0.020f, 0.003f), // oklch(0.12 0.02 85)
ring = p3(0.890f, 0.684f, 0.156f),
blob1 = p3(0.495f, 0.141f, 0.569f), // oklch(0.45 0.18 320) deep purple
blob2 = p3(0.600f, 0.405f, 0.000f), // oklch(0.55 0.15 85) deep gold
blob3 = p3(0.000f, 0.459f, 0.000f), // oklch(0.48 0.18 145) deep green
blob4 = p3(0.000f, 0.410f, 0.445f), // oklch(0.45 0.13 200) deep teal
),
)

View file

@ -6,27 +6,28 @@ import androidx.compose.ui.graphics.colorspace.ColorSpaces
private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3)
// Derived from kaizen/app/themes/blue.css — cool blobs (sky, cyan, indigo, warm peach accent)
val BlauTheme = KaizenThemeSpec(
id = KaizenThemeId.BLAU,
label = "Blau",
dark = KaizenAccentScheme(
primary = p3(0.337f, 0.600f, 0.988f), // Sky-500 P3
primaryDeep = p3(0.243f, 0.420f, 0.855f),
onPrimary = p3(0.024f, 0.047f, 0.106f),
ring = p3(0.337f, 0.600f, 0.988f),
blob1 = p3(0.235f, 0.373f, 0.839f), // Blue
blob2 = p3(0.290f, 0.290f, 0.690f), // Indigo
blob3 = p3(0.169f, 0.714f, 0.800f), // Cyan
blob4 = p3(0.878f, 0.635f, 0.243f), // Amber (contrast)
),
light = KaizenAccentScheme(
primary = p3(0.243f, 0.310f, 0.730f), // Indigo-600 P3
primaryDeep = p3(0.243f, 0.310f, 0.730f),
onPrimary = p3(1f, 1f, 1f),
ring = p3(0.337f, 0.600f, 0.988f),
blob1 = p3(0.235f, 0.373f, 0.839f),
blob2 = p3(0.290f, 0.290f, 0.690f),
blob3 = p3(0.169f, 0.714f, 0.800f),
blob4 = p3(0.878f, 0.635f, 0.243f),
primary = p3(0.168f, 0.513f, 0.962f), // oklch(0.62 0.19 257)
primaryDeep = p3(0.168f, 0.513f, 0.962f),
onPrimary = p3(0.971f, 0.989f, 1.000f), // oklch(0.99 0.01 257)
ring = p3(0.168f, 0.513f, 0.962f),
blob1 = p3(0.464f, 0.725f, 1.000f), // oklch(0.78 0.15 257) sky
blob2 = p3(0.000f, 0.766f, 0.953f), // oklch(0.75 0.16 220) cyan
blob3 = p3(0.648f, 0.551f, 1.000f), // oklch(0.72 0.18 290) indigo
blob4 = p3(0.973f, 0.763f, 0.519f), // oklch(0.85 0.10 70) peach accent
),
dark = KaizenAccentScheme(
primary = p3(0.276f, 0.592f, 1.000f), // oklch(0.68 0.18 257)
primaryDeep = p3(0.168f, 0.513f, 0.962f),
onPrimary = p3(0.010f, 0.023f, 0.051f), // oklch(0.12 0.02 257)
ring = p3(0.276f, 0.592f, 1.000f),
blob1 = p3(0.000f, 0.307f, 0.713f), // oklch(0.45 0.18 257) deep sky
blob2 = p3(0.000f, 0.360f, 0.533f), // oklch(0.42 0.16 220) deep cyan
blob3 = p3(0.305f, 0.111f, 0.660f), // oklch(0.40 0.20 290) deep indigo
blob4 = p3(0.580f, 0.315f, 0.000f), // oklch(0.50 0.14 70) amber accent
),
)

View file

@ -6,27 +6,28 @@ import androidx.compose.ui.graphics.colorspace.ColorSpaces
private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3)
// Derived from kaizen/app/themes/mono.css — achromatic, zero chroma, subtle warm blob4 accent
val MonoTheme = KaizenThemeSpec(
id = KaizenThemeId.MONO,
label = "Mono",
dark = KaizenAccentScheme(
primary = p3(0.878f, 0.894f, 0.918f), // Slate-100
primaryDeep = p3(0.710f, 0.741f, 0.784f),
onPrimary = p3(0.063f, 0.082f, 0.118f),
ring = p3(0.710f, 0.741f, 0.784f),
blob1 = p3(0.710f, 0.741f, 0.784f), // Slate-300
blob2 = p3(0.580f, 0.620f, 0.680f), // Slate-400
blob3 = p3(0.820f, 0.845f, 0.878f), // Slate-200
blob4 = p3(0.600f, 0.580f, 0.560f), // Warm gray
),
light = KaizenAccentScheme(
primary = p3(0.188f, 0.224f, 0.286f), // Slate-800
primaryDeep = p3(0.188f, 0.224f, 0.286f),
onPrimary = p3(1f, 1f, 1f),
ring = p3(0.420f, 0.455f, 0.510f),
blob1 = p3(0.710f, 0.741f, 0.784f),
blob2 = p3(0.580f, 0.620f, 0.680f),
blob3 = p3(0.820f, 0.845f, 0.878f),
blob4 = p3(0.600f, 0.580f, 0.560f),
primary = p3(0.104f, 0.104f, 0.104f), // oklch(0.22 0 255) near-black
primaryDeep = p3(0.104f, 0.104f, 0.104f),
onPrimary = p3(0.987f, 0.987f, 0.987f), // oklch(0.99 0 255) near-white
ring = p3(0.104f, 0.104f, 0.104f),
blob1 = p3(0.806f, 0.806f, 0.806f), // oklch(0.85 0 255) light gray
blob2 = p3(0.681f, 0.681f, 0.681f), // oklch(0.75 0 255) mid gray
blob3 = p3(0.896f, 0.896f, 0.896f), // oklch(0.92 0 255) off-white
blob4 = p3(0.796f, 0.765f, 0.714f), // oklch(0.82 0.02 80) subtle warm
),
dark = KaizenAccentScheme(
primary = p3(0.896f, 0.896f, 0.896f), // oklch(0.92 0 255) near-white
primaryDeep = p3(0.806f, 0.806f, 0.806f),
onPrimary = p3(0.022f, 0.022f, 0.022f), // oklch(0.12 0 255) near-black
ring = p3(0.896f, 0.896f, 0.896f),
blob1 = p3(0.229f, 0.229f, 0.229f), // oklch(0.35 0 255) dark gray
blob2 = p3(0.334f, 0.334f, 0.334f), // oklch(0.45 0 255) mid-dark gray
blob3 = p3(0.179f, 0.179f, 0.179f), // oklch(0.30 0 255) charcoal
blob4 = p3(0.304f, 0.277f, 0.234f), // oklch(0.40 0.02 80) subtle warm
),
)

View file

@ -1,7 +1,25 @@
package dev.kaizen.app
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import dev.kaizen.app.ui.theme.*
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.BlobAmber
import dev.kaizen.app.ui.theme.BlobCoral
import dev.kaizen.app.ui.theme.BlobHoney
import dev.kaizen.app.ui.theme.BlobTeal
import dev.kaizen.app.ui.theme.DarkMuted
import dev.kaizen.app.ui.theme.DarkStroke
import dev.kaizen.app.ui.theme.DarkText
import dev.kaizen.app.ui.theme.LightBackground
import dev.kaizen.app.ui.theme.LightMuted
import dev.kaizen.app.ui.theme.LightStroke
import dev.kaizen.app.ui.theme.LightSurface
import dev.kaizen.app.ui.theme.LightSurfaceVariant
import dev.kaizen.app.ui.theme.LightText
import dev.kaizen.app.ui.theme.Obsidian
import dev.kaizen.app.ui.theme.ObsidianElevated
import dev.kaizen.app.ui.theme.ObsidianInput
import dev.kaizen.app.ui.theme.OnAmber
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@ -41,7 +59,7 @@ class ColorPipelineTest {
@Test
fun `blob colors are in DisplayP3`() {
listOf(BlobAmber, BlobBlue, BlobTeal, BlobIndigo).forEach { color ->
listOf(BlobAmber, BlobCoral, BlobHoney, BlobTeal).forEach { color ->
assertEquals(ColorSpaces.DisplayP3, color.colorSpace)
}
}