docs: add compose development guidelines and skills

Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-07-07 16:55:43 +02:00
parent ad914067a9
commit fd4fff2c86
44 changed files with 10509 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
# Material 3 Theming & Components
## TL;DR Defaults
| Concern | Default |
|---|---|
| Theme entry point | `MaterialTheme(colorScheme, typography, shapes)` wrapping app content |
| Dynamic color | Enable on Android 12+; fall back to brand `ColorScheme` on older APIs |
| Dark/light | Follow system via `isSystemInDarkTheme()`; expose user override if needed |
| Color pairing | Always pair `primary`/`onPrimary`, `surface`/`onSurface`, `*Container`/`on*Container` |
| Typography | Use default M3 type scale; override only specific slots for branding |
| Shapes | Use default M3 shape scale; override per-slot (`small`, `medium`, `large`) |
| Scaffold | Use `Scaffold` for screens with app bars, FAB, snackbar, or bottom bar |
| Navigation | `NavigationSuiteScaffold` auto-switches bar/rail by window size |
| Snackbar | `SnackbarHostState` in Route; show via `Effect` from ViewModel |
| Bottom sheet | `ModalBottomSheet` with `SheetState`; control via `show()`/`hide()` |
| Dialog | `AlertDialog` for simple confirm/dismiss; custom `Dialog` for complex content |
| Adaptive layout | Derive window size class once at app level; pass down as state |
## Theming Baseline
### Theme Setup
```kotlin
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
```
### Key Rules
- Define `LightColorScheme` and `DarkColorScheme` using `lightColorScheme()` / `darkColorScheme()`.
- Generate brand colors via [Material Theme Builder](https://m3.material.io/theme-builder) for guaranteed tonal palettes.
- Dynamic color is Android-only; CMP projects fall back to brand schemes on non-Android targets.
## Color Roles and Dark/Light
### Role Pairing Rules
| Container | Content on it |
|---|---|
| `primary` | `onPrimary` |
| `primaryContainer` | `onPrimaryContainer` |
| `secondary` | `onSecondary` |
| `secondaryContainer` | `onSecondaryContainer` |
| `tertiary` | `onTertiary` |
| `tertiaryContainer` | `onTertiaryContainer` |
| `surface` | `onSurface` |
| `surfaceVariant` | `onSurfaceVariant` |
| `error` | `onError` |
| `errorContainer` | `onErrorContainer` |
### Accessibility Guardrails
- Always use the correct `on*` color for text/icons on a container.
- Do not mix unrelated pairs (e.g., `tertiaryContainer` background with `primaryContainer` text).
- M3 tonal palettes guarantee 3:1+ contrast when paired correctly.
### Do / Don't
| Do | Don't |
|---|---|
| `containerColor = primary`, `contentColor = onPrimary` | `containerColor = primary`, `contentColor = tertiaryContainer` |
| Access colors via `MaterialTheme.colorScheme.*` | Hardcode hex colors in components |
| Test both light and dark themes | Assume light-only usage |
## Typography and Shapes
### Typography
M3 defines 15 text styles across 5 categories:
| Category | Sizes |
|---|---|
| Display | `displayLarge`, `displayMedium`, `displaySmall` |
| Headline | `headlineLarge`, `headlineMedium`, `headlineSmall` |
| Title | `titleLarge`, `titleMedium`, `titleSmall` |
| Body | `bodyLarge`, `bodyMedium`, `bodySmall` |
| Label | `labelLarge`, `labelMedium`, `labelSmall` |
**Default**: Use M3 defaults. Override individual slots for brand fonts:
```kotlin
val AppTypography = Typography(
titleLarge = TextStyle(fontFamily = BrandFont, fontWeight = FontWeight.SemiBold, fontSize = 22.sp)
)
```
### Shapes
M3 shape scale: `extraSmall`, `small`, `medium`, `large`, `extraLarge`.
**Default**: Use M3 defaults. Override only when brand requires specific corner radii:
```kotlin
val AppShapes = Shapes(
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp)
)
```
## Component Decision Matrix
### Scaffold
| Slot | When to use |
|---|---|
| `topBar` | Screen has a top app bar |
| `bottomBar` | Screen has bottom navigation or bottom app bar |
| `floatingActionButton` | Primary action needs FAB |
| `snackbarHost` | Screen can show snackbars |
| `content` | Main screen content; receives `PaddingValues` to apply |
**Rule**: Always apply `innerPadding` from `Scaffold` to content root.
### Top App Bar
| Variant | Use case | Scroll / default |
|---|---|---|
| `TopAppBar` (small) | Simple screens, minimal actions | Default: `pinnedScrollBehavior` unless you need collapse |
| `CenterAlignedTopAppBar` | Single primary action, centered title | Same bar family as small |
| `MediumTopAppBar` | Moderate navigation, collapsible on scroll | `exitUntilCollapsedScrollBehavior` (also `enterAlwaysScrollBehavior` where needed) |
| `LargeTopAppBar` | Hero screens, prominent title, collapsible | Same scroll behavior family as medium |
### Navigation
| Window size | Component |
|---|---|
| Compact (phones portrait) | `NavigationBar` (bottom) |
| Medium/Expanded (tablets, landscape) | `NavigationRail` (side) |
| Auto-switch | `NavigationSuiteScaffold` |
**Default**: Use `NavigationSuiteScaffold` for apps with 3-5 top-level destinations. It adapts automatically.
```kotlin
NavigationSuiteScaffold(
navigationSuiteItems = {
destinations.forEach { dest ->
item(
selected = currentDest == dest,
onClick = { currentDest = dest },
icon = { Icon(dest.icon, contentDescription = null) },
label = { Text(dest.label) }
)
}
}
) { DestinationContent(currentDest) }
```
### Bottom Sheet
| Type | Use case |
|---|---|
| `ModalBottomSheet` | Overlays content, dismissible |
| `BottomSheetScaffold` | Persistent sheet integrated with screen |
**State control**: Use `rememberModalBottomSheetState()` + `SheetState.show()`/`hide()`.
**MVI pattern**: ViewModel emits `Effect.ShowSheet`; Route composable calls `sheetState.show()` in `LaunchedEffect`.
### Snackbar
**Setup**: `SnackbarHostState` remembered in Route; passed to `Scaffold.snackbarHost`.
**Pattern**:
```kotlin
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
is Effect.ShowSnackbar -> {
val result = snackbarHostState.showSnackbar(effect.message, effect.actionLabel)
if (result == SnackbarResult.ActionPerformed) viewModel.onEvent(Event.SnackbarAction)
}
}
}
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { /* ... */ }
```
### Dialog
| Type | Use case |
|---|---|
| `AlertDialog` | Simple title + text + confirm/dismiss buttons |
| `Dialog` + `Card` | Complex content, forms, custom layouts |
**MVI pattern**: Dialog visibility controlled by `state.showDialog: Boolean`. Confirm/dismiss dispatch events.
## Adaptive Layout Defaults
### Window Size Classes
| Class | Width breakpoint | Typical devices |
|---|---|---|
| Compact | < 600dp | Phones portrait |
| Medium | 600dp 840dp | Tablets portrait, large unfolded |
| Expanded | ≥ 840dp | Tablets landscape, desktop |
**Rule**: Compute `WindowSizeClass` once at app/activity level via `currentWindowAdaptiveInfo()`. Pass derived layout decisions down as state.
### Canonical Layouts
| Layout | Use case | Compose component |
|---|---|---|
| List-detail | Master list + detail pane | `ListDetailPaneScaffold`, `NavigableListDetailPaneScaffold` |
| Supporting pane | Main content + supplementary info | `SupportingPaneScaffold`, `NavigableSupportingPaneScaffold` |
| Feed | Grid of browsable content | `LazyVerticalGrid` with `GridCells.Adaptive` |
**Default**: For list-detail apps, use `NavigableListDetailPaneScaffold` which handles pane visibility and back navigation.
**Adaptive navigation:** read `windowSizeClass` (or related adaptive info) once at the root and pass derived flags (e.g. whether to show a top app bar) into your main screen composable.
## M2 to M3 Migration Notes
| M2 | M3 |
|---|---|
| `Colors` | `ColorScheme` |
| `lightColors()` / `darkColors()` | `lightColorScheme()` / `darkColorScheme()` |
| `BottomNavigation` | `NavigationBar` |
| `BottomNavigationItem` | `NavigationBarItem` |
| `ModalBottomSheetLayout` | `ModalBottomSheet` |
| `ModalDrawer` | `ModalNavigationDrawer` |
| `Scaffold` with `scaffoldState` | `Scaffold` with `snackbarHost` slot |
| `BackdropScaffold` | `BottomSheetScaffold` or custom |
| `TopAppBar` elevation | `TopAppBar` with `scrollBehavior` |
**Key change**: M3 `Scaffold` no longer has `drawerState`. Use `ModalNavigationDrawer` wrapping `Scaffold` instead.