# Jetpack Compose Components Patterns

Full code examples backing the guidance in `SKILL.md`. Headings mirror the
SKILL.md sections.

## Contents

- [Two-Layer Design Tokens](#two-layer-design-tokens)
- [Bridging Tokens to Material 3](#bridging-tokens-to-material-3)
- [Component Catalog](#component-catalog)

## Two-Layer Design Tokens

Layer 1 is a machine-generated raw-token source: one file, never hand-edited,
the single source of truth for every primitive value. Layer 2 is a hand-authored
semantic wrapper: `@Immutable` data classes that name each role and reference the
raw tokens, never a literal hex or `sp`. Each semantic model exposes a `light()`
and a `dark()` variant.

```kotlin
// Layer 1: generated, do not edit. Produced by the token codegen pipeline.
object RawTokens {
    val neutral0 = Color(0xFFFFFFFF)
    val neutral900 = Color(0xFF1C1B1F)
    val neutral1000 = Color(0xFF000000)
    val red600 = Color(0xFFBA1A1A)
    val red200 = Color(0xFFFFB4AB)
    val fontSizeBody = 16.sp
    val lineHeightBody = 24.sp
    val fontSizeTitle = 22.sp
    val lineHeightTitle = 28.sp
}
```

```kotlin
// Layer 2: hand-authored semantic wrapper.
@Immutable
data class AppColors(
    val background: Color,
    val onBackground: Color,
    val brandPrimary: Color,
    val brandOnPrimary: Color,
    val surface: Color,
    val danger: Color,
) {
    companion object {
        fun light() = AppColors(
            background = RawTokens.neutral0,
            onBackground = RawTokens.neutral900,
            brandPrimary = RawTokens.red600,
            brandOnPrimary = RawTokens.neutral0,
            surface = RawTokens.neutral0,
            danger = RawTokens.red600,
        )

        fun dark() = AppColors(
            background = RawTokens.neutral1000,
            onBackground = RawTokens.neutral0,
            brandPrimary = RawTokens.red200,
            brandOnPrimary = RawTokens.neutral900,
            surface = RawTokens.neutral1000,
            danger = RawTokens.red200,
        )
    }
}

@Immutable
data class AppTypography(
    val body: TextStyle,
    val title: TextStyle,
) {
    companion object {
        fun default() = AppTypography(
            body = TextStyle(
                fontSize = RawTokens.fontSizeBody,
                lineHeight = RawTokens.lineHeightBody,
            ),
            title = TextStyle(
                fontSize = RawTokens.fontSizeTitle,
                lineHeight = RawTokens.lineHeightTitle,
                fontWeight = FontWeight.Medium,
            ),
        )
    }
}
```

The codegen pipeline that produces Layer 1 is a separate concern; see the
`android-design-tokens-codegen` skill. This skill covers the consumption side
only.

## Bridging Tokens to Material 3

Bridge the semantic tokens to both worlds. Select an M3
`light`/`darkColorScheme` built from the tokens so stock Material components
(`Button`, `TopAppBar`, `Card`) inherit the brand colors, and expose the richer
token set through `CompositionLocalProvider`. Read the rich set through a
`Theme.colors` / `Theme.typography` accessor marked `@ReadOnlyComposable`.

```kotlin
val LocalAppColors = staticCompositionLocalOf { AppColors.light() }
val LocalAppTypography = staticCompositionLocalOf { AppTypography.default() }

private fun AppColors.toMaterialColorScheme(darkTheme: Boolean): ColorScheme {
    val base = if (darkTheme) darkColorScheme() else lightColorScheme()
    return base.copy(
        primary = brandPrimary,
        onPrimary = brandOnPrimary,
        background = background,
        onBackground = onBackground,
        surface = surface,
        error = danger,
    )
}

private fun AppTypography.toMaterialTypography(): Typography =
    Typography(bodyLarge = body, titleLarge = title)

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) {
    val colors = if (darkTheme) AppColors.dark() else AppColors.light()
    val typography = AppTypography.default()

    CompositionLocalProvider(
        LocalAppColors provides colors,
        LocalAppTypography provides typography,
    ) {
        MaterialTheme(
            colorScheme = colors.toMaterialColorScheme(darkTheme),
            typography = typography.toMaterialTypography(),
            content = content,
        )
    }
}

object Theme {
    val colors: AppColors
        @Composable @ReadOnlyComposable get() = LocalAppColors.current
    val typography: AppTypography
        @Composable @ReadOnlyComposable get() = LocalAppTypography.current
}
```

Stock Material components read `MaterialTheme.colorScheme`; custom components read
the richer set:

```kotlin
@Composable
fun PriceTag(text: String) {
    Text(
        text = text,
        color = Theme.colors.brandPrimary,
        style = Theme.typography.title,
    )
}
```

## Component Catalog

A component-catalog module (for example Showkase) renders every annotated
composable and color/typography token in a browsable in-app gallery. Keep the
annotation cheap and universal so it can sit on previews across every module, but
gate the KSP processor behind a build flag so the browser codegen costs nothing
on a normal build.

```kotlin
// A preview, annotated for the catalog. The annotation dependency is universal.
@ShowkaseComposable(name = "InfoCard", group = "Cards")
@Preview
@Composable
private fun InfoCardCatalog() {
    AppTheme { InfoCard(/* sample slots */) }
}
```

```kotlin
// ui module build.gradle.kts
dependencies {
    implementation(libs.showkase.annotation) // cheap, always present
    if (providers.gradleProperty("enableShowkase").isPresent) {
        ksp(libs.showkase.processor)          // browser codegen only when asked
    }
}
```

```kotlin
// The catalog entry point, compiled only when the processor ran.
class CatalogActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { ShowkaseBrowser(getMetadata()) }
    }
}
```

Build the catalog on demand with `./gradlew :app:installDebug -PenableShowkase`;
routine builds skip the processor entirely.
