Compose Multiplatform UI component library. Targets Android, iOS, Desktop (JVM), macOS, Web (Wasm/JS).
- For significant features or refactors, sketch an Plan first; keep it updated as you work.
- Run the component-specific checks below before handing work off; do not skip failing steps.
- Use Context7 to pull library/API docs when you touch unfamiliar Compose/Android/JVM/Js/WasmJs/Swift APIs or deps.
- Always use Chinese to communicate with users and output Plan content, but the generated code (including comments and KDoc) must remain in English.
| Action | Command |
|---|---|
| Build (full) | ./gradlew assemble |
| Build (quick check) | ./gradlew compileKotlinDesktop |
| Test | ./gradlew check |
| Check formatting | ./gradlew spotlessCheck |
| Fix formatting | ./gradlew spotlessApply |
| Run Android demo | ./gradlew :example:android:installDebug |
| Run Desktop demo | ./gradlew :example:desktop:hotRunDesktop --auto |
| Run WasmJs demo | ./gradlew :example:web:wasmJsBrowserRun |
| Run Js demo | ./gradlew :example:web:jsBrowserDevelopmentRun |
| Run macOS demo | ./gradlew :example:macos:runDebugExecutableMacosArm64 |
| Run iOS demo | Open example/ios/iosApp.xcodeproj in Xcode and run |
Before committing, run ./gradlew spotlessCheck; only run ./gradlew spotlessApply if the check reports violations. CI will reject formatting violations.
| Directory | Purpose |
|---|---|
miuix-core/ |
Utilities + MiuixIcons base (depended on by ui and icons) |
miuix-ui/ |
Main UI library |
miuix-preference/ |
Preference / menu / popup components |
miuix-shader/ |
Runtime shader / render effect abstraction (used by blur and squircle) |
miuix-blur/ |
Blur effects (Android minSdk=33; depends on miuix-shader) |
miuix-squircle/ |
Squircle corner shapes (depends on miuix-shader) |
miuix-icons/ |
Extended icon resources |
miuix-navigation3-ui/ |
Navigation 3 UI (depends on miuix-squircle for transition clip) |
example/ |
Demo app |
baselineprofile/ |
Android baseline profile generation |
docs/ |
VitePress documentation site |
build-plugins/ |
Custom Gradle plugins |
gradle/ |
Version catalog + wrapper |
miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/:
| Subdir | Contents |
|---|---|
basic/ |
Fundamental components (Button, Switch, TextField, Surface, …) |
overlay/ |
Scaffold-hosted overlays (OverlayDialog, OverlayBottomSheet, …) |
window/ |
Platform window components (WindowDialog, WindowBottomSheet, …) |
layout/ |
Shared layout primitives |
theme/ |
MiuixTheme, Colors, TextStyles, ThemeController, DynamicColors |
color/ |
Color utilities, Material Color |
anim/ |
Animation utilities |
utils/ |
General utilities |
icon/ |
Built-in basic icons (ArrowRight, Check, Search, …) |
interfaces/ |
Shared interfaces |
miuix-preference/src/commonMain/kotlin/top/yukonga/miuix/kmp/:
| Subdir | Contents |
|---|---|
preference/ |
Preference components (SwitchPreference, CheckboxPreference, …) |
menu/ |
Dropdown menus (Overlay/Window variants, cascading) |
popup/ |
Dropdown popup primitives shared by menus |
commonMain
├── androidMain
└── skikoMain (Skiko rendering — all non-Android targets)
├── darwinMain (iOS + macOS)
│ ├── iosMain
│ └── macosMain
├── desktopMain (JVM)
└── webMain
├── wasmJsMain
└── jsMain
99% of UI logic lives in commonMain. Only use platform source sets for genuinely platform-specific code.
-
Formatter: Spotless + ktlint with Compose rules (
io.nlopez.compose.rules:ktlint). Exact versions live inbuild-plugins/src/main/kotlin/module.spotless.gradle.kts -
License header (required on all
.ktand.ktsfiles):// Copyright $YEAR, compose-miuix-ui contributors // SPDX-License-Identifier: Apache-2.0Spotless auto-fills
$YEARwith the current year. Do not manually change years in existing file headers. -
Spotless exclusions: Icon files (
**/icon/**/*.kt) and a subset of navigation3 sources (**/navigation3/ListUtils.kt,**/navigation3/scene/*.kt,**/navigation3/ui/*.kt) are excluded from formatting. -
Line endings: platform-native
-
Composable function names may use PascalCase (ktlint rule disabled for
@Composable)
Follow this parameter ordering:
@Composable
fun ComponentName(
// 1. Required parameters (functional callbacks first)
onClick: () -> Unit,
// 2. Modifier
modifier: Modifier = Modifier,
// 3. Boolean flags
enabled: Boolean = true,
// 4. Visual parameters with defaults from ComponentDefaults
cornerRadius: Dp = ComponentDefaults.CornerRadius,
minWidth: Dp = ComponentDefaults.MinWidth,
minHeight: Dp = ComponentDefaults.MinHeight,
colors: ComponentColors = ComponentDefaults.componentColors(),
insideMargin: PaddingValues = ComponentDefaults.InsideMargin,
// 5. Content lambda (always last)
content: @Composable () -> Unit,
)Add @NonRestartableComposable only when the component is a thin wrapper that fully delegates to another composable and reads no state itself — see the Key Patterns below. Do not include it on the default template.
Each component provides a ComponentDefaults object:
object ButtonDefaults {
val MinWidth = 58.dp // Constant dimensions as val
val MinHeight = 40.dp
val CornerRadius = 16.dp
val InsideMargin = PaddingValues(horizontal = 16.dp, vertical = 13.dp)
@Composable
fun buttonColors( // Color factories must be @Composable
color: Color = MiuixTheme.colorScheme.secondaryVariant,
disabledColor: Color = MiuixTheme.colorScheme.disabledSecondaryVariant,
contentColor: Color = MiuixTheme.colorScheme.onSecondaryVariant,
disabledContentColor: Color = MiuixTheme.colorScheme.disabledOnSecondaryVariant,
): ButtonColors = remember(color, disabledColor, contentColor, disabledContentColor) {
ButtonColors(
color = color,
disabledColor = disabledColor,
contentColor = contentColor,
disabledContentColor = disabledContentColor,
)
}
}@Immutable
data class ButtonColors(
val color: Color,
val disabledColor: Color,
val contentColor: Color,
val disabledContentColor: Color,
)rememberUpdatedStatefor values whose latest reading must be visible to a long-lived closure without re-running an effect or rebuilding a Modifier: use insideLaunchedEffect/DisposableEffectso the effect body sees the newest callback without being keyed on it, and insideremember { }-cached lambdas (e.g., theonClickyou pass toModifier.clickableafter wrapping it inremember) so the cached body still calls the up-to-date callback. Note: this prevents stale captures, it does not stabilize the outer lambda's identity — aModifier.clickable { current() }literal is still a fresh object on each composition. Do NOT use it when forwarding a callback directly to a child composable (e.g.,Button(onClick = onClick)) — Compose's skip mechanism handles lambda stability thererememberwith keys for derived values:val alpha = remember(enabled) { if (enabled) 1f else 0.38f }@NonRestartableComposableon thin wrapper composables that fully delegate to other composables and read no state themselves; avoid on composables with multiple internal state reads (they benefit from smart recomposition)@Immutableon color/style data classes- Shapes: Use
RoundedCornerShape(cornerRadius)for rounded corners andCircleShapefor capsules fromandroidx.compose.foundation.shape - Theme colors: Always use
MiuixTheme.colorScheme.*, never hardcode colors - Text styles: Always use
MiuixTheme.textStyles.*(e.g.,MiuixTheme.textStyles.button)
BasicComponent (basic/Component.kt) uses a custom Layout with intrinsic measurement and a weighted distribution algorithm (2:5:3). Do NOT replace it with Row + weight(1f) — this was tried and caused catastrophic layout breakage (text rendered as single vertical characters) when start/end content overflows.
LaunchedEffectkeys: only include values actually read in the effect bodyminIntrinsicWidth/maxIntrinsicWidthtriggers full subtree traversal — defer to overflow branch when possible- Use
@Immutableon truly immutable data classes (allval, never mutated, and no lambda/callback fields — lambda equality is reference-based and breaks@Immutable's "equals stays equal forever" contract); use@Stablefor data classes that hold lambdas/callbacks, or for classes whose mutable properties notify Compose viaMutableState;@Stableis also the standard pattern for internal helper functions within@Immutabledata classes (e.g.,@Stable internal fun color(enabled: Boolean): Color) - Standard collections (
List,Set,Map) are unstable to Compose. Preferkotlinx.collections.immutable(ImmutableList/PersistentList) — the Compose compiler recognizes them as stable. Wrapping a rawListin an@Immutabledata class only works if the caller keeps the wrapper's identity stable (e.g., builds it inside aremember); otherwise the wrapper is "new" each composition and the@Immutableannotation is a false promise to Compose
- Create the
@Composablefunction inmiuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/(orpreference/inmiuix-preferencefor preference components) - Follow API conventions above (parameter ordering, Defaults object, Colors data class)
- Add a demo section in
example/shared/src/commonMain/kotlin/component/ - Register the demo in the example app
- Verify on at least Android and Desktop
When changing a component's API, defaults, or behavior, check and update all related artifacts:
- Documentation (
docs/components/anddocs/zh_CN/components/): update properties tables, Defaults object sections, and code examples in both English and Chinese docs - Docs demo code (
docs/demo/): update the interactive demo if it uses changed APIs - Example app (
example/shared/src/commonMain/kotlin/component/): update demo code to reflect the changes
- Reproduce in the
exampleapp - Fix in
miuix-ui/ormiuix-preference/ - If platform-specific, verify the fix across affected platforms
Format: <scope>: <summary>
- Scopes:
library,docs,example,build,fix,fix(deps),chore(deps) - Keep subject line ≤ 72 characters, sentence case, no trailing period
- Reference PRs/issues as
(#1234)at end - Check recent
git log --onelineto stay consistent with current conventions - Run
./gradlew spotlessCheckbefore every commit; only run./gradlew spotlessApplyif violations are reported