25 KiB
25 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02.1 | 02 | execute | 1 |
|
|
true |
|
|
|
Purpose: Every later plan in this phase (and every later phase) reads from these tokens. Get the API and values right now.
Output: Six files in composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-CONTEXT.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-UI-SPEC.md @.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md @composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt Current `RecipeTheme.kt` (analog — to be rewritten while preserving the MaterialTheme wrapper):private val LightColors = lightColorScheme(primary = Color(0xFF3B6939))
private val DarkColors = darkColorScheme(primary = Color(0xFFA2D597))
@Composable
fun RecipeTheme(content: @Composable () -> Unit) {
val colors = if (isSystemInDarkTheme()) DarkColors else LightColors
MaterialTheme(colorScheme = colors, content = content)
}
Legacy consumers (must keep working — DO NOT break):
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt — reads
MaterialTheme.colorScheme.surface,MaterialTheme.typography.displaySmall, etc. - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt — reads
MaterialTheme.colorScheme.surface,MaterialTheme.typography.headlineSmall. - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/SplashScreen.kt — likely reads MaterialTheme.
UI-SPEC § Color (verbatim hex values):
- background light=#F7F5F1, dark=#0F1113
- surface light=#FFFFFF, dark=#1A1D21
- surfaceGlass light=#FFFFFF @ 60% alpha, dark=#1A1D21 @ 55% alpha
- content light=#0F1113, dark=#F1EFEA
- contentMuted light=#6B6E73, dark=#9AA0A6
- accent light=#D97757, dark=#E48A6E
- separator light=#E5E1DA, dark=#2A2D31
- borderCard light=#E5E1DA @ 60% alpha, dark=#FFFFFF @ 8% alpha
- destructive light=#C0392B, dark=#E57368
UI-SPEC § Typography:
- display: 28sp, FontWeight.SemiBold (W600), lineHeight 34sp, letterSpacing -0.2sp
- title: 20sp, FontWeight.SemiBold, lineHeight 24sp, letterSpacing 0sp
- body: 16sp, FontWeight.Normal (W400), lineHeight 24sp, letterSpacing 0sp
- label: 13sp, FontWeight.SemiBold, lineHeight 16sp, letterSpacing 0.1sp
UI-SPEC § Spacing (rev 1):
- xs=4dp, sm=8dp, lg=16dp, xl=24dp, 2xl=32dp, 3xl=48dp
UI-SPEC § Glass (defaults consumed by GlassSurface):
- Dock pill corner radius: 28dp (height 56dp), collapsed 22dp (height 44dp)
- Search pill / floating button: 22dp (height 44dp)
- Border: 1dp borderCard
- Shadow (light): y=8dp, blur=24dp, alpha=12%; (dark): no shadow
- Blur radius (Liquid+Haze): 24dp initial
File 1 — `RecipeColors.kt`:
```kotlin
package dev.ulfrx.recipe.ui.theme
import androidx.compose.ui.graphics.Color
/**
* Semantic color tokens (UI-SPEC § Color, CONTEXT D-14, D-15).
* Values are locked; do not introduce raw hex in screen code.
*/
public data class RecipeColors(
val background: Color,
val surface: Color,
val surfaceGlass: Color,
val content: Color,
val contentMuted: Color,
val accent: Color,
val separator: Color,
val borderCard: Color,
val destructive: Color,
)
public val LightRecipeColors: RecipeColors = RecipeColors(
background = Color(0xFFF7F5F1),
surface = Color(0xFFFFFFFF),
surfaceGlass = Color(0xFFFFFFFF).copy(alpha = 0.60f),
content = Color(0xFF0F1113),
contentMuted = Color(0xFF6B6E73),
accent = Color(0xFFD97757),
separator = Color(0xFFE5E1DA),
borderCard = Color(0xFFE5E1DA).copy(alpha = 0.60f),
destructive = Color(0xFFC0392B),
)
public val DarkRecipeColors: RecipeColors = RecipeColors(
background = Color(0xFF0F1113),
surface = Color(0xFF1A1D21),
surfaceGlass = Color(0xFF1A1D21).copy(alpha = 0.55f),
content = Color(0xFFF1EFEA),
contentMuted = Color(0xFF9AA0A6),
accent = Color(0xFFE48A6E),
separator = Color(0xFF2A2D31),
borderCard = Color(0xFFFFFFFF).copy(alpha = 0.08f),
destructive = Color(0xFFE57368),
)
```
File 2 — `RecipeTypography.kt`:
```kotlin
package dev.ulfrx.recipe.ui.theme
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
/**
* Typography tokens (UI-SPEC § Typography). System default font family
* (SF Pro on iOS, Roboto on Android) for v1.
*/
public data class RecipeTypography(
val display: TextStyle,
val title: TextStyle,
val body: TextStyle,
val label: TextStyle,
)
public val DefaultRecipeTypography: RecipeTypography = RecipeTypography(
display = TextStyle(
fontFamily = FontFamily.Default,
fontSize = 28.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 34.sp,
letterSpacing = (-0.2).sp,
),
title = TextStyle(
fontFamily = FontFamily.Default,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 24.sp,
letterSpacing = 0.sp,
),
body = TextStyle(
fontFamily = FontFamily.Default,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
lineHeight = 24.sp,
letterSpacing = 0.sp,
),
label = TextStyle(
fontFamily = FontFamily.Default,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 16.sp,
letterSpacing = 0.1.sp,
),
)
```
File 3 — `RecipeSpacing.kt`:
```kotlin
package dev.ulfrx.recipe.ui.theme
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Spacing scale (UI-SPEC § Spacing rev 1: 4 / 8 / 16 / 24 / 32 / 48).
* `xxl` and `xxxl` map to UI-SPEC's `2xl` / `3xl` because Kotlin identifiers
* cannot start with a digit. Tokens are referenced by these property names
* in screen code; UI-SPEC token names (`2xl`/`3xl`) are the documented contract.
*/
public data class RecipeSpacing(
val xs: Dp,
val sm: Dp,
val lg: Dp,
val xl: Dp,
val xxl: Dp,
val xxxl: Dp,
)
public val DefaultRecipeSpacing: RecipeSpacing = RecipeSpacing(
xs = 4.dp,
sm = 8.dp,
lg = 16.dp,
xl = 24.dp,
xxl = 32.dp,
xxxl = 48.dp,
)
```
File 4 — `RecipeShapes.kt`:
```kotlin
package dev.ulfrx.recipe.ui.theme
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Shape tokens (UI-SPEC § Glass — corner radii for chrome elements).
*/
public data class RecipeShapes(
val dockExpanded: Dp,
val dockCollapsed: Dp,
val searchPill: Dp,
val floatingButton: Dp,
)
public val DefaultRecipeShapes: RecipeShapes = RecipeShapes(
dockExpanded = 28.dp,
dockCollapsed = 22.dp,
searchPill = 22.dp,
floatingButton = 22.dp,
)
```
File 5 — `RecipeGlass.kt`:
```kotlin
package dev.ulfrx.recipe.ui.theme
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Glass surface defaults (UI-SPEC § Glass / Layout).
* Consumed by GlassSurface (plan 02.1-03) and the dock / search pill /
* floating button (plan 02.1-05).
*/
public data class RecipeGlass(
val borderWidth: Dp,
val shadowOffsetY: Dp,
val shadowBlur: Dp,
val shadowAlphaLight: Float,
val shadowAlphaDark: Float,
val blurRadius: Dp,
)
public val DefaultRecipeGlass: RecipeGlass = RecipeGlass(
borderWidth = 1.dp,
shadowOffsetY = 8.dp,
shadowBlur = 24.dp,
shadowAlphaLight = 0.12f,
shadowAlphaDark = 0.0f,
blurRadius = 24.dp,
)
```
./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q
- All 5 files exist under `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/`
- `grep -c 'data class RecipeColors' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns 1
- `grep -c '0xFFF7F5F1' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns 1 (exact light background hex)
- `grep -c '0xFF0F1113' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns at least 2 (dark background + light content)
- `grep -c '0xFFD97757' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt` returns 1 (light accent)
- `grep -c '28.sp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt` returns 1 (display fontSize)
- `grep -c '13.sp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt` returns 1 (label fontSize)
- `grep -c 'xxl = 32.dp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt` returns 1
- `grep -c 'xxxl = 48.dp' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt` returns 1
- No file imports material3: `grep -rn 'androidx.compose.material3' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeColors.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTypography.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeSpacing.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeShapes.kt composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeGlass.kt` returns no matches
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0
Five token files compile cleanly under iOS source set; values match UI-SPEC verbatim; no Material 3 imports leaked into the new token layer.
Task 2: Rewrite RecipeTheme.kt — CompositionLocals + system-following light/dark + MaterialTheme wrapper preserved
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt (current shape — `LightColors`/`DarkColors` Material 3 schemes + `MaterialTheme(...)` wrapper)
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/LoginScreen.kt (consumer — `MaterialTheme.colorScheme.surface`, `MaterialTheme.typography.displaySmall`. Both must keep resolving.)
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt (consumer — `MaterialTheme.typography.headlineSmall`)
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt (caller of `RecipeTheme { ... }` at the root)
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Open Question 3 (lines 686-690 — locks the dual-theme decision)
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § RecipeTheme.kt (rewritten) lines 126-148
- `RecipeTheme(content)` composable selects light/dark via `isSystemInDarkTheme()` (D-15).
- Wraps content in `MaterialTheme(colorScheme = ..., content = { CompositionLocalProvider(...) { content() } })` — auth screens read MaterialTheme, new screens read RecipeTheme, both compose simultaneously.
- Five `CompositionLocal` sentinels declared: `LocalRecipeColors`, `LocalRecipeTypography`, `LocalRecipeSpacing`, `LocalRecipeShapes`, `LocalRecipeGlass`. All use `staticCompositionLocalOf` (read-only invariants). Defaults throw with a helpful message when accessed outside `RecipeTheme { ... }`.
- `object RecipeTheme` exposes 5 properties (`colors`, `typography`, `spacing`, `shapes`, `glass`) as `@Composable @ReadOnlyComposable get()` accessors mirroring `MaterialTheme` idiom.
Replace `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` with:
```kotlin
package dev.ulfrx.recipe.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
/**
* Recipe theme entry point (CONTEXT D-14, D-15).
*
* Wraps a Material 3 [MaterialTheme] so the legacy auth screens
* (LoginScreen / PostLoginPlaceholderScreen / SplashScreen) continue to
* resolve `MaterialTheme.colorScheme.*` / `MaterialTheme.typography.*`
* (RESEARCH § Open Question 3). New code reads `RecipeTheme.colors.*`,
* `RecipeTheme.typography.*`, etc.
*/
private val LegacyMaterialLightColors = lightColorScheme(primary = LightRecipeColors.accent)
private val LegacyMaterialDarkColors = darkColorScheme(primary = DarkRecipeColors.accent)
public val LocalRecipeColors: androidx.compose.runtime.ProvidableCompositionLocal<RecipeColors> =
staticCompositionLocalOf { error("RecipeColors accessed outside RecipeTheme { }") }
public val LocalRecipeTypography: androidx.compose.runtime.ProvidableCompositionLocal<RecipeTypography> =
staticCompositionLocalOf { error("RecipeTypography accessed outside RecipeTheme { }") }
public val LocalRecipeSpacing: androidx.compose.runtime.ProvidableCompositionLocal<RecipeSpacing> =
staticCompositionLocalOf { error("RecipeSpacing accessed outside RecipeTheme { }") }
public val LocalRecipeShapes: androidx.compose.runtime.ProvidableCompositionLocal<RecipeShapes> =
staticCompositionLocalOf { error("RecipeShapes accessed outside RecipeTheme { }") }
public val LocalRecipeGlass: androidx.compose.runtime.ProvidableCompositionLocal<RecipeGlass> =
staticCompositionLocalOf { error("RecipeGlass accessed outside RecipeTheme { }") }
@Composable
public fun RecipeTheme(content: @Composable () -> Unit) {
val dark = isSystemInDarkTheme()
val recipeColors = if (dark) DarkRecipeColors else LightRecipeColors
val materialColors = if (dark) LegacyMaterialDarkColors else LegacyMaterialLightColors
MaterialTheme(colorScheme = materialColors) {
CompositionLocalProvider(
LocalRecipeColors provides recipeColors,
LocalRecipeTypography provides DefaultRecipeTypography,
LocalRecipeSpacing provides DefaultRecipeSpacing,
LocalRecipeShapes provides DefaultRecipeShapes,
LocalRecipeGlass provides DefaultRecipeGlass,
content = content,
)
}
}
public object RecipeTheme {
public val colors: RecipeColors
@Composable @ReadOnlyComposable get() = LocalRecipeColors.current
public val typography: RecipeTypography
@Composable @ReadOnlyComposable get() = LocalRecipeTypography.current
public val spacing: RecipeSpacing
@Composable @ReadOnlyComposable get() = LocalRecipeSpacing.current
public val shapes: RecipeShapes
@Composable @ReadOnlyComposable get() = LocalRecipeShapes.current
public val glass: RecipeGlass
@Composable @ReadOnlyComposable get() = LocalRecipeGlass.current
}
```
Notes:
- The `RecipeTheme` composable function and the `object RecipeTheme` coexist in Kotlin (function vs declaration in same package).
- `MaterialTheme(colorScheme = materialColors)` keeps the auth-screen path working using a thin wrapper of Recipe's accent — the auth screens never relied on a specific Material primary; they only used `surface` (which `lightColorScheme(primary = ...)` provides via Material defaults) and typography defaults.
- DO NOT remove the existing import `androidx.compose.foundation.isSystemInDarkTheme` style; replicate the file structure shown above verbatim.
./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q
- `grep -c 'staticCompositionLocalOf' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 5
- `grep -c 'CompositionLocalProvider' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1
- `grep -c 'MaterialTheme(colorScheme' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1
- `grep -c 'public object RecipeTheme' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 1
- `grep -c '@ReadOnlyComposable' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns 5
- `grep -c 'isSystemInDarkTheme' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` returns at least 1
- Legacy auth screens still compile (regression check): `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0 (will fail if MaterialTheme wrapper accidentally removed)
- `./gradlew :composeApp:check -q` does not introduce new test failures
RecipeTheme.kt exposes five CompositionLocals + a `RecipeTheme` object with `@Composable @ReadOnlyComposable` accessors, all under a preserved `MaterialTheme(...)` wrapper so legacy auth screens keep resolving Material symbols. Whole composeApp still compiles for iosSimulatorArm64.
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` exits 0
- `./gradlew :composeApp:commonTest -q` exits 0 (no regression in existing Phase 2 tests)
- All 6 theme files exist; no Material 3 imports leak into the 5 token files
- Legacy auth screens unchanged on disk (verified by `git diff --name-only composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/` shows nothing in this plan's diff)
<success_criteria>
- Token data classes exist with values exactly matching UI-SPEC.
RecipeTheme { ... }provides all five CompositionLocals AND wrapsMaterialTheme(...)(Phase 2 auth screens unaffected).- New code can read
RecipeTheme.colors.background,RecipeTheme.typography.title,RecipeTheme.spacing.lg, etc., from any@Composabledescendant. - composeApp builds cleanly for iOS simulator and Phase 2 test suite stays green. </success_criteria>