mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
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:
246
.codex/skills/compose-skill/references/material-design.md
Normal file
246
.codex/skills/compose-skill/references/material-design.md
Normal 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.
|
||||
Reference in New Issue
Block a user