Create a Miuix Component
Add a new component to the miuix library and complete every companion change beyond the component file itself — example registration, docs, sidebar, and so on: 8 touch points in total (see the checklist at the end).
The authoritative sources for conventions are AGENTS.md at the repo root (API Conventions, Key Patterns, Critical Constraints) and the reference source files listed below. This skill only describes the workflow and where the changes go; wherever it conflicts with AGENTS.md or existing source, they win — source code does not go stale, details copied into a skill do.
Step 1: Gather requirements
Ask the user for whatever is not yet provided:
- Component name — PascalCase (e.g.
Chip, Badge, Tooltip)
- Category —
- basic (
basic/ in miuix-ui): standalone UI components, e.g. Button, Slider, Switch, TabRow
- preference (
preference/ in miuix-preference): settings-item components built on BasicComponent with title/summary/action slots, named XxxPreference (SwitchPreference, CheckboxPreference, …)
- Overlays (
overlay/, window/) and menus/popups (menu/, popup/ in miuix-preference) are rarely added; confirm the design with the user before starting
- Brief description — what it does and where it is used
- Key parameters — state, callbacks, content slots, configuration
- Whether a Colors class is needed — most components need one; minimal components can take plain
Color parameters (see how Divider does it)
Step 2: Read reference files by component type
Before generating code, read the API Conventions section of AGENTS.md, pick the closest type from the table below, and read its reference source in full. Match coding style, indentation, and idioms exactly:
| Component type |
Reference (under miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/ unless noted) |
Key points |
| Clickable filled |
basic/Button.kt |
Box + Modifier.squircleSurface(color, cornerRadius) + clickable(interactionSource, LocalIndication.current, …); do not invent a Surface wrapper |
| Container |
basic/Card.kt |
two overloads: non-interactive @NonRestartableComposable one + interactive one (combinedClickable / pressable / HoldDownObserver) |
| Minimal drawn |
basic/Divider.kt |
Canvas drawing; plain Color parameters, no Colors class; theme colors in Defaults via @Composable get() |
| Animated / custom-drawn |
basic/ProgressIndicator.kt, basic/Switch.kt |
looping animation, drag gestures, haptics, semantics such as progressBarRangeInfo |
| Settings item |
miuix-preference/.../preference/SwitchPreference.kt |
delegates to BasicComponent, control placed in endActions, whole-row click callback + correct Role |
| BasicComponent API |
basic/Component.kt |
see AGENTS.md Critical Constraints: its custom Layout must not be replaced with Row + weight(1f) |
| Grouped Colors (selected-state aware) |
miuix-preference/.../preference/RadioButtonPreference.kt |
@Immutable data class holding several BasicComponentColors + @Stable internal fun xxx(selected) accessors |
Step 3: Write the component source file
Locations:
- basic:
miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/{Name}.kt
- preference:
miuix-preference/src/commonMain/kotlin/top/yukonga/miuix/kmp/preference/{Name}Preference.kt
Structure and conventions follow AGENTS.md (license header, parameter order, Defaults object, @Immutable Colors class, when @NonRestartableComposable applies, where rememberUpdatedState applies, @Immutable/@Stable and collection-stability rules). Below are only the pitfalls AGENTS.md does not spell out:
- Backgrounds and shapes: filled backgrounds (including those of clickable components) use
Modifier.squircleSurface(color = …, cornerRadius = …); borders use Modifier.squircleBorder(…); squircle-radius clipping uses Modifier.squircleClip(…) (all from miuix-squircle; they fall back to rounded rectangles when LocalSquircleEnabled is off, and their outlines align at equal radii). Where an actual Shape value is required (e.g. dropShadow, the shape parameter of Surface), use RoundedCornerShape(cornerRadius) / CircleShape
- Theming: colors always come from
MiuixTheme.colorScheme.* (full list in theme/Colors.kt) and text styles from MiuixTheme.textStyles.* (theme/TextStyles.kt); never hardcode
- Resolving state to color: prefer a
@Stable internal fun on the Colors class (e.g. color(enabled: Boolean); most of the library does this); a simple two-state case may also resolve inline like Button.kt does — follow the chosen reference file
- Haptics: toggle-like components fire haptics inside the click callback (
HapticFeedbackType.ToggleOn/ToggleOff, following the rememberUpdatedState(hapticFeedback) pattern in Switch.kt/Checkbox.kt/RadioButton.kt); never fire on state change — programmatic state writes would trigger it
- Semantics: set the correct
Role (Button/Switch/Checkbox/RadioButton…); the non-interactive form (onClick = null) still needs a semantics block declaring role and state
- RTL: for directional icons, capture
val layoutDirection = LocalLayoutDirection.current first, then graphicsLayer { scaleX = if (layoutDirection == LayoutDirection.Rtl) -1f else 1f } (GraphicsLayerScope does not expose layoutDirection; see miuix-preference/.../preference/ArrowPreference.kt)
- Platforms: implement in
commonMain first; use expect/actual only for genuine platform differences (see Platform Source Sets in AGENTS.md)
- Naming and wording: no Material components or dependencies; code, comments, and KDoc must not reference external design systems or vendor names
- Single-file layout: the main composable, private helper composables, the Defaults object, and the Colors class live in one file; extract complex
startAction/endActions content into private helper composables
Step 4: Example app demo
Create example/shared/src/commonMain/kotlin/component/{Name}Section.kt:
fun LazyListScope.xxxSection() {
item(key = "xxx") {
SmallTitle(text = "Xxx")
Card(
modifier = Modifier
.padding(horizontal = 12.dp)
.padding(bottom = 12.dp),
) {
// demo content
}
}
}
The demo should cover the default look, key parameter variants, and the disabled state. Use several Cards (each with the padding above) for independent groups.
Register in example/shared/src/commonMain/kotlin/MainPage.kt: add import component.xxxSection to the alphabetized import block, and insert the xxxSection() call in display order inside the if (notExpanded) { … } block of the LazyColumn.
Step 5: Docs
Interactive demo: create docs/demo/src/commonMain/kotlin/{Name}Demo.kt, structured like RadioButtonDemo.kt:
@Composable
fun XxxDemo() {
Box(
modifier = Modifier
.fillMaxSize()
.background(demoBackground()),
contentAlignment = Alignment.Center,
) {
Column(
Modifier
.padding(16.dp)
.widthIn(max = 600.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// demo content
}
}
}
Register the demo: add one line AvailableComponent("Xxx", "xxx") { XxxDemo() } to the availableComponents list in docs/demo/src/commonMain/kotlin/Demo.kt, keeping the existing order. The id is camelCase; the docs page iframe references it via ?id=, and the demo home screen picks it up automatically.
Doc pages: docs/components/{slug}.md and docs/zh_CN/components/{slug}.md (slug all-lowercase, e.g. progressindicator). Structure follows docs/components/radiobutton.md: intro + iframe (src="../compose/index.html?id={demoId}"; the zh version has one extra ../ in the path), Import, Basic Usage, Properties tables, Defaults object, Colors class, Advanced Usage. Keep the EN and zh content in one-to-one correspondence; size the iframe container height to the demo's actual content.
Component overview: add one row each to the overview tables in docs/components/index.md and docs/zh_CN/components/index.md (every component page has a row there — do not skip this).
Sidebar: add one entry to the matching group in docs/.vitepress/locales/en_US.ts and zh_CN.ts (basic components go under Basic Components/基础组件; preference and similar under Extended Components/扩展组件): { text: "Xxx", link: "/components/{slug}" }, with the /zh_CN link prefix in the zh file.
Step 6: Verify
./gradlew compileKotlinDesktop (fast compile of library + example); also run ./gradlew :docs:demo:compileKotlinJs if the docs demo changed
./gradlew spotlessCheck; run spotlessApply only if it reports violations
- Run the example app on at least Android and Desktop to confirm visuals and interaction (commands in AGENTS.md Key Commands)
Commits follow the AGENTS.md Git Commit Style (the component and its example/docs companion changes go in one library: commit).
Checklist (self-check)
A new component lands only when all 8 are done:
- Component source file (miuix-ui or miuix-preference)
example/shared/.../component/{Name}Section.kt
example/shared/.../MainPage.kt registration (import + call)
docs/demo/.../{Name}Demo.kt
availableComponents entry in docs/demo/.../Demo.kt
docs/components/{slug}.md + docs/zh_CN/components/{slug}.md
- One row each in
docs/components/index.md + docs/zh_CN/components/index.md
- Sidebar entries in
docs/.vitepress/locales/en_US.ts + zh_CN.ts
1---2name: create-component-23description: Create a new Compose Multiplatform UI component for the miuix library, including every required wiring point (library source, example app section + registration, docs interactive demo + registration, EN/zh_CN docs pages, component overview index, VitePress sidebar). Use whenever the user wants to add a new component, create a new composable, scaffold a component, or add a UI element to miuix — even if they only name the component ("add a Chip"). Triggers on "create component", "new component", "add a component", "scaffold component", "new composable", "新建组件", "添加组件", "新组件", "加个组件".4---56# Create a Miuix Component78Add a new component to the miuix library and complete every companion change beyond the component file itself — example registration, docs, sidebar, and so on: 8 touch points in total (see the checklist at the end).910The authoritative sources for conventions are `AGENTS.md` at the repo root (API Conventions, Key Patterns, Critical Constraints) and the reference source files listed below. This skill only describes the workflow and where the changes go; wherever it conflicts with AGENTS.md or existing source, they win — source code does not go stale, details copied into a skill do.1112## Step 1: Gather requirements1314Ask the user for whatever is not yet provided:15161. **Component name** — PascalCase (e.g. `Chip`, `Badge`, `Tooltip`)172. **Category** —18 - **basic** (`basic/` in `miuix-ui`): standalone UI components, e.g. Button, Slider, Switch, TabRow19 - **preference** (`preference/` in `miuix-preference`): settings-item components built on `BasicComponent` with title/summary/action slots, named `XxxPreference` (SwitchPreference, CheckboxPreference, …)20 - Overlays (`overlay/`, `window/`) and menus/popups (`menu/`, `popup/` in `miuix-preference`) are rarely added; confirm the design with the user before starting213. **Brief description** — what it does and where it is used224. **Key parameters** — state, callbacks, content slots, configuration235. **Whether a Colors class is needed** — most components need one; minimal components can take plain `Color` parameters (see how Divider does it)2425## Step 2: Read reference files by component type2627Before generating code, read the API Conventions section of `AGENTS.md`, pick the closest type from the table below, and read its reference source in full. Match coding style, indentation, and idioms exactly:2829| Component type | Reference (under `miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/` unless noted) | Key points |30| :--- | :--- | :--- |31| Clickable filled | `basic/Button.kt` | `Box` + `Modifier.squircleSurface(color, cornerRadius)` + `clickable(interactionSource, LocalIndication.current, …)`; do not invent a Surface wrapper |32| Container | `basic/Card.kt` | two overloads: non-interactive `@NonRestartableComposable` one + interactive one (`combinedClickable` / `pressable` / `HoldDownObserver`) |33| Minimal drawn | `basic/Divider.kt` | `Canvas` drawing; plain `Color` parameters, no Colors class; theme colors in Defaults via `@Composable get()` |34| Animated / custom-drawn | `basic/ProgressIndicator.kt`, `basic/Switch.kt` | looping animation, drag gestures, haptics, semantics such as `progressBarRangeInfo` |35| Settings item | `miuix-preference/.../preference/SwitchPreference.kt` | delegates to `BasicComponent`, control placed in `endActions`, whole-row click callback + correct `Role` |36| BasicComponent API | `basic/Component.kt` | see AGENTS.md Critical Constraints: its custom `Layout` must not be replaced with `Row + weight(1f)` |37| Grouped Colors (selected-state aware) | `miuix-preference/.../preference/RadioButtonPreference.kt` | `@Immutable` data class holding several `BasicComponentColors` + `@Stable internal fun xxx(selected)` accessors |3839## Step 3: Write the component source file4041Locations:4243- basic: `miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/{Name}.kt`44- preference: `miuix-preference/src/commonMain/kotlin/top/yukonga/miuix/kmp/preference/{Name}Preference.kt`4546Structure and conventions follow AGENTS.md (license header, parameter order, Defaults object, `@Immutable` Colors class, when `@NonRestartableComposable` applies, where `rememberUpdatedState` applies, `@Immutable`/`@Stable` and collection-stability rules). Below are only the pitfalls AGENTS.md does not spell out:4748- **Backgrounds and shapes**: filled backgrounds (including those of clickable components) use `Modifier.squircleSurface(color = …, cornerRadius = …)`; borders use `Modifier.squircleBorder(…)`; squircle-radius clipping uses `Modifier.squircleClip(…)` (all from `miuix-squircle`; they fall back to rounded rectangles when `LocalSquircleEnabled` is off, and their outlines align at equal radii). Where an actual `Shape` value is required (e.g. `dropShadow`, the `shape` parameter of `Surface`), use `RoundedCornerShape(cornerRadius)` / `CircleShape`49- **Theming**: colors always come from `MiuixTheme.colorScheme.*` (full list in `theme/Colors.kt`) and text styles from `MiuixTheme.textStyles.*` (`theme/TextStyles.kt`); never hardcode50- **Resolving state to color**: prefer a `@Stable internal fun` on the Colors class (e.g. `color(enabled: Boolean)`; most of the library does this); a simple two-state case may also resolve inline like `Button.kt` does — follow the chosen reference file51- **Haptics**: toggle-like components fire haptics inside the click callback (`HapticFeedbackType.ToggleOn`/`ToggleOff`, following the `rememberUpdatedState(hapticFeedback)` pattern in `Switch.kt`/`Checkbox.kt`/`RadioButton.kt`); never fire on state change — programmatic state writes would trigger it52- **Semantics**: set the correct `Role` (Button/Switch/Checkbox/RadioButton…); the non-interactive form (`onClick = null`) still needs a `semantics` block declaring role and state53- **RTL**: for directional icons, capture `val layoutDirection = LocalLayoutDirection.current` first, then `graphicsLayer { scaleX = if (layoutDirection == LayoutDirection.Rtl) -1f else 1f }` (`GraphicsLayerScope` does not expose layoutDirection; see `miuix-preference/.../preference/ArrowPreference.kt`)54- **Platforms**: implement in `commonMain` first; use expect/actual only for genuine platform differences (see Platform Source Sets in AGENTS.md)55- **Naming and wording**: no Material components or dependencies; code, comments, and KDoc must not reference external design systems or vendor names56- **Single-file layout**: the main composable, private helper composables, the Defaults object, and the Colors class live in one file; extract complex `startAction`/`endActions` content into private helper composables5758## Step 4: Example app demo59601. Create `example/shared/src/commonMain/kotlin/component/{Name}Section.kt`:6162 ```kotlin63 fun LazyListScope.xxxSection() {64 item(key = "xxx") {65 SmallTitle(text = "Xxx")66 Card(67 modifier = Modifier68 .padding(horizontal = 12.dp)69 .padding(bottom = 12.dp),70 ) {71 // demo content72 }73 }74 }75 ```7677 The demo should cover the default look, key parameter variants, and the disabled state. Use several Cards (each with the padding above) for independent groups.78792. Register in `example/shared/src/commonMain/kotlin/MainPage.kt`: add `import component.xxxSection` to the alphabetized import block, and insert the `xxxSection()` call in display order inside the `if (notExpanded) { … }` block of the `LazyColumn`.8081## Step 5: Docs82831. **Interactive demo**: create `docs/demo/src/commonMain/kotlin/{Name}Demo.kt`, structured like `RadioButtonDemo.kt`:8485 ```kotlin86 @Composable87 fun XxxDemo() {88 Box(89 modifier = Modifier90 .fillMaxSize()91 .background(demoBackground()),92 contentAlignment = Alignment.Center,93 ) {94 Column(95 Modifier96 .padding(16.dp)97 .widthIn(max = 600.dp)98 .fillMaxWidth(),99 verticalArrangement = Arrangement.spacedBy(16.dp),100 horizontalAlignment = Alignment.CenterHorizontally,101 ) {102 // demo content103 }104 }105 }106 ```1071082. **Register the demo**: add one line `AvailableComponent("Xxx", "xxx") { XxxDemo() }` to the `availableComponents` list in `docs/demo/src/commonMain/kotlin/Demo.kt`, keeping the existing order. The id is camelCase; the docs page iframe references it via `?id=`, and the demo home screen picks it up automatically.1093. **Doc pages**: `docs/components/{slug}.md` and `docs/zh_CN/components/{slug}.md` (slug all-lowercase, e.g. `progressindicator`). Structure follows `docs/components/radiobutton.md`: intro + iframe (`src="../compose/index.html?id={demoId}"`; the zh version has one extra `../` in the path), Import, Basic Usage, Properties tables, Defaults object, Colors class, Advanced Usage. Keep the EN and zh content in one-to-one correspondence; size the iframe container height to the demo's actual content.1104. **Component overview**: add one row each to the overview tables in `docs/components/index.md` and `docs/zh_CN/components/index.md` (every component page has a row there — do not skip this).1115. **Sidebar**: add one entry to the matching group in `docs/.vitepress/locales/en_US.ts` and `zh_CN.ts` (basic components go under Basic Components/基础组件; preference and similar under Extended Components/扩展组件): `{ text: "Xxx", link: "/components/{slug}" }`, with the `/zh_CN` link prefix in the zh file.112113## Step 6: Verify114115- `./gradlew compileKotlinDesktop` (fast compile of library + example); also run `./gradlew :docs:demo:compileKotlinJs` if the docs demo changed116- `./gradlew spotlessCheck`; run `spotlessApply` only if it reports violations117- Run the example app on at least Android and Desktop to confirm visuals and interaction (commands in AGENTS.md Key Commands)118119Commits follow the AGENTS.md Git Commit Style (the component and its example/docs companion changes go in one `library:` commit).120121## Checklist (self-check)122123A new component lands only when all 8 are done:1241251. Component source file (miuix-ui or miuix-preference)1262. `example/shared/.../component/{Name}Section.kt`1273. `example/shared/.../MainPage.kt` registration (import + call)1284. `docs/demo/.../{Name}Demo.kt`1295. `availableComponents` entry in `docs/demo/.../Demo.kt`1306. `docs/components/{slug}.md` + `docs/zh_CN/components/{slug}.md`1317. One row each in `docs/components/index.md` + `docs/zh_CN/components/index.md`1328. Sidebar entries in `docs/.vitepress/locales/en_US.ts` + `zh_CN.ts`