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.Amber
import dev.kaizen.app.ui.theme.AmberDeep import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.BlobAmber 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.BlobTeal
import dev.kaizen.app.ui.theme.DITHER_ALPHA import dev.kaizen.app.ui.theme.DITHER_ALPHA
import dev.kaizen.app.ui.theme.rememberDitherBrush 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(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) 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). // Vignette: darken the edges so the center comes forward (dark mode only).
if (dark) { 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 LightMuted = p3(0.357f, 0.392f, 0.439f) // ≈ #5B6470
val LightStroke = p3(0f, 0f, 0f, 0.08f) // outline val LightStroke = p3(0f, 0f, 0f, 0.08f) // outline
// ── Background blobs ─────────────────────────────────────────────────────── // ── Background blobs (Amber theme defaults, derived from amber.css) ────────
val BlobAmber = p3(0.878f, 0.635f, 0.243f) // ≈ Amber // These are the light-mode defaults. Theme-specific blobs come from
val BlobBlue = p3(0.235f, 0.373f, 0.839f) // ≈ #3C5FD6 // LocalKaizenAccent.current.blob1..4 once the theme system is wired.
val BlobTeal = p3(0.169f, 0.714f, 0.651f) // ≈ #2BB6A6 val BlobAmber = p3(0.992f, 0.767f, 0.282f) // oklch(0.85 0.15 83) gold
val BlobIndigo = p3(0.290f, 0.290f, 0.690f) // 4th blob for Amber theme 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 = private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3) 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( val AmberTheme = KaizenThemeSpec(
id = KaizenThemeId.AMBER, id = KaizenThemeId.AMBER,
label = "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( light = KaizenAccentScheme(
primary = p3(0.784f, 0.525f, 0.165f), // AmberDeep (better contrast on white) primary = p3(0.811f, 0.608f, 0.126f), // oklch(0.72 0.14 83)
primaryDeep = p3(0.784f, 0.525f, 0.165f), primaryDeep = p3(0.811f, 0.608f, 0.126f),
onPrimary = p3(1f, 1f, 1f), onPrimary = p3(0.015f, 0.013f, 0.009f), // oklch(0.10 0.005 83)
ring = p3(0.878f, 0.635f, 0.243f), ring = p3(0.811f, 0.608f, 0.126f),
blob1 = p3(0.878f, 0.635f, 0.243f), blob1 = p3(0.992f, 0.767f, 0.282f), // oklch(0.85 0.15 83) gold
blob2 = p3(0.235f, 0.373f, 0.839f), blob2 = p3(1.000f, 0.562f, 0.230f), // oklch(0.78 0.18 50) coral
blob3 = p3(0.290f, 0.290f, 0.690f), blob3 = p3(0.917f, 0.853f, 0.471f), // oklch(0.88 0.12 100) honey
blob4 = p3(0.169f, 0.714f, 0.651f), 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 = private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3) 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( val AuroraTheme = KaizenThemeSpec(
id = KaizenThemeId.AURORA, id = KaizenThemeId.AURORA,
label = "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( light = KaizenAccentScheme(
primary = p3(0.490f, 0.270f, 0.690f), // Violet-600 P3 primary = p3(0.724f, 0.328f, 0.812f), // oklch(0.62 0.20 320) purple
primaryDeep = p3(0.490f, 0.270f, 0.690f), primaryDeep = p3(0.724f, 0.328f, 0.812f),
onPrimary = p3(1f, 1f, 1f), onPrimary = p3(1.000f, 0.979f, 1.000f), // oklch(0.99 0.01 320)
ring = p3(0.850f, 0.310f, 0.750f), ring = p3(0.724f, 0.328f, 0.812f),
blob1 = p3(0.850f, 0.310f, 0.750f), blob1 = p3(0.855f, 0.540f, 0.926f), // oklch(0.75 0.16 320) purple
blob2 = p3(0.169f, 0.714f, 0.800f), blob2 = p3(0.931f, 0.738f, 0.291f), // oklch(0.82 0.14 85) gold
blob3 = p3(0.545f, 0.361f, 0.859f), blob3 = p3(0.433f, 0.822f, 0.455f), // oklch(0.78 0.16 145) green
blob4 = p3(0.520f, 0.850f, 0.310f), 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 = private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3) 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( val BlauTheme = KaizenThemeSpec(
id = KaizenThemeId.BLAU, id = KaizenThemeId.BLAU,
label = "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( light = KaizenAccentScheme(
primary = p3(0.243f, 0.310f, 0.730f), // Indigo-600 P3 primary = p3(0.168f, 0.513f, 0.962f), // oklch(0.62 0.19 257)
primaryDeep = p3(0.243f, 0.310f, 0.730f), primaryDeep = p3(0.168f, 0.513f, 0.962f),
onPrimary = p3(1f, 1f, 1f), onPrimary = p3(0.971f, 0.989f, 1.000f), // oklch(0.99 0.01 257)
ring = p3(0.337f, 0.600f, 0.988f), ring = p3(0.168f, 0.513f, 0.962f),
blob1 = p3(0.235f, 0.373f, 0.839f), blob1 = p3(0.464f, 0.725f, 1.000f), // oklch(0.78 0.15 257) sky
blob2 = p3(0.290f, 0.290f, 0.690f), blob2 = p3(0.000f, 0.766f, 0.953f), // oklch(0.75 0.16 220) cyan
blob3 = p3(0.169f, 0.714f, 0.800f), blob3 = p3(0.648f, 0.551f, 1.000f), // oklch(0.72 0.18 290) indigo
blob4 = p3(0.878f, 0.635f, 0.243f), 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 = private fun p3(r: Float, g: Float, b: Float): Color =
Color(red = r, green = g, blue = b, colorSpace = ColorSpaces.DisplayP3) 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( val MonoTheme = KaizenThemeSpec(
id = KaizenThemeId.MONO, id = KaizenThemeId.MONO,
label = "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( light = KaizenAccentScheme(
primary = p3(0.188f, 0.224f, 0.286f), // Slate-800 primary = p3(0.104f, 0.104f, 0.104f), // oklch(0.22 0 255) near-black
primaryDeep = p3(0.188f, 0.224f, 0.286f), primaryDeep = p3(0.104f, 0.104f, 0.104f),
onPrimary = p3(1f, 1f, 1f), onPrimary = p3(0.987f, 0.987f, 0.987f), // oklch(0.99 0 255) near-white
ring = p3(0.420f, 0.455f, 0.510f), ring = p3(0.104f, 0.104f, 0.104f),
blob1 = p3(0.710f, 0.741f, 0.784f), blob1 = p3(0.806f, 0.806f, 0.806f), // oklch(0.85 0 255) light gray
blob2 = p3(0.580f, 0.620f, 0.680f), blob2 = p3(0.681f, 0.681f, 0.681f), // oklch(0.75 0 255) mid gray
blob3 = p3(0.820f, 0.845f, 0.878f), blob3 = p3(0.896f, 0.896f, 0.896f), // oklch(0.92 0 255) off-white
blob4 = p3(0.600f, 0.580f, 0.560f), 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 package dev.kaizen.app
import androidx.compose.ui.graphics.colorspace.ColorSpaces 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.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
@ -41,7 +59,7 @@ class ColorPipelineTest {
@Test @Test
fun `blob colors are in DisplayP3`() { 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) assertEquals(ColorSpaces.DisplayP3, color.colorSpace)
} }
} }