Skip to main content

husi-actions-dropdown-menu

Husi actions overflow menu conventions for Compose Material3 DropdownMenuPopup. Use this whenever editing or creating a DropdownMenuPopup inside CapsuleTopBar or CapsuleSearchTopBar actions, adding a more_vert actions menu, touching MenuDefaults.Label section headers, DropdownMenuGroup shapes, checked/selected menu rows, or improving the visual hierarchy of topbar action menus in composeApp/.

Source facts

Repository
xchacha20-poly1305/husi
Last source activity
September 25, 2026 at 14:09
Detected SKILL.md language
English
Stars
1,850
Forks
111

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
husi-actions-dropdown-menu
description
Husi actions overflow menu conventions for Compose Material3 DropdownMenuPopup. Use this whenever editing or creating a DropdownMenuPopup inside CapsuleTopBar or CapsuleSearchTopBar actions, adding a more_vert actions menu, touching MenuDefaults.Label section headers, DropdownMenuGroup shapes, checked/selected menu rows, or improving the visual hierarchy of topbar action menus in composeApp/.
# Husi Actions Dropdown Menus Use this skill when a topbar `actions = { ... }` block contains an overflow menu, usually a `more_vert` `SimpleIconButton` wrapped by `CapsuleActionButton`, and the menu is implemented with Material3 Expressive `DropdownMenuPopup`. If the task also changes topbar structure, first follow the `husi-topbar` skill. This skill only covers the overflow menu content and its visual hierarchy. ## Current house pattern Actions overflow menus should look like this: ```kotlin CapsuleActionButton { Box { SimpleIconButton( imageVector = vectorResource(Res.drawable.more_vert), contentDescription = stringResource(Res.string.more), onClick = { isOverflowMenuExpanded = true }, ) DropdownMenuPopup( expanded = isOverflowMenuExpanded, onDismissRequest = { isOverflowMenuExpanded = false }, ) { DropdownMenuGroup( shapes = MenuDefaults.groupShape(0, 1), ) { DropdownMenuSectionHeader(stringResource(Res.string.custom_config)) DropdownMenuItem( text = { Text(stringResource(Res.string.outbound)) }, onClick = { isOverflowMenuExpanded = false // action }, shape = MenuDefaults.itemShape(0, 2).shape, ) DropdownMenuItem( text = { Text(stringResource(Res.string.full)) }, onClick = { isOverflowMenuExpanded = false // action }, shape = MenuDefaults.itemShape(1, 2).shape, ) } } } } ``` Required pieces: - Wrap the overflow icon in `CapsuleActionButton`, and place the popup in the same `Box`. - Use `DropdownMenuPopup`, not the older plain `DropdownMenu`, for new topbar actions menus unless the surrounding screen already intentionally uses plain `DropdownMenu`. - Use `DropdownMenuGroup` for each visual group and separate groups with `Spacer(Modifier.height(MenuDefaults.GroupSpacing))`. - Use `MenuDefaults.groupShape(groupIndex, groupCount)` for grouped popup sections. ## Section headers Section headers in actions menus are labels, not actions. Do not create fake title rows like: ```kotlin DropdownMenuItem( text = { MenuDefaults.Label { Text(...) } }, onClick = {}, ) ``` That makes the title look and behave like a normal clickable menu row. It also adds misleading touch/ripple semantics. Use the shared helper instead: ```kotlin DropdownMenuSectionHeader(stringResource(Res.string.sort_mode)) ``` The helper lives at: `composeApp/src/commonMain/kotlin/fr/husi/compose/DropdownMenuSectionHeader.kt` It applies the current house style: - `MenuDefaults.Label` - full-width centered text - `MaterialTheme.typography.labelMedium` - `FontWeight.SemiBold` - `MaterialTheme.colorScheme.onSurfaceVariant` - `HorizontalDivider` with `MenuDefaults.HorizontalDividerPadding` ## Item shapes after headers Headers do not count as menu items for item shape indexing. Count only the actionable rows in that group. For plain click rows, Material3's API takes a single `Shape`, so use `.shape`: ```kotlin DropdownMenuItem( text = { Text(stringResource(Res.string.full)) }, onClick = { /* ... */ }, shape = MenuDefaults.itemShape(index, itemCount).shape, ) ``` For `selected` or `checked` rows, Material3's API takes `MenuItemShapes`, so use `shapes`: ```kotlin DropdownMenuItem( selected = sortMode == uiState.sortMode, onClick = { /* ... */ }, text = { Text(stringResource(text)) }, shapes = MenuDefaults.itemShape(index, itemCount), ) DropdownMenuItem( checked = sortMode == uiState.sortMode, onCheckedChange = { checked -> if (!checked) return@DropdownMenuItem // action }, text = { Text(stringResource(text)) }, shapes = MenuDefaults.itemShape(index, itemCount), ) ``` This distinction matters: using `shapes = ...` on a plain `onClick` item will not compile, while using `shape = MenuDefaults.itemShape(...).shape` preserves the grouped rounded-corner behavior. ## Checked filters For rows that toggle a boolean and show a checkbox, prefer making the row itself the action: ```kotlin DropdownMenuItem( text = { Text(stringResource(Res.string.connection_status_active)) }, onClick = { viewModel.setQueryActivate(!uiState.showActivate) }, leadingIcon = { Checkbox( checked = uiState.showActivate, onCheckedChange = null, ) }, shape = MenuDefaults.itemShape(0, 2).shape, ) ``` Use `onCheckedChange = null` on the checkbox when the containing row handles the click. This keeps the interaction target simple and avoids nested competing actions. ## Dismiss behavior Close the popup when the action navigates, opens another screen/dialog, or commits a one-shot selection: ```kotlin onClick = { isOverflowMenuExpanded = false onOpenConfigEditor(...) } ``` For non-destructive filter toggles that can be toggled repeatedly, leaving the menu open can be reasonable. Match the surrounding menu's behavior. ## Imports and local components Prefer Husi wrappers inside menu content: - `fr.husi.compose.material3.Text` - `fr.husi.compose.material3.Icon` - `fr.husi.compose.material3.Checkbox` - `fr.husi.compose.DropdownMenuSectionHeader` Use Compose resource helpers: - `stringResource(Res.string...)` - `vectorResource(Res.drawable...)` Avoid hardcoded colors or text sizes in menu rows. Use `MaterialTheme` roles in shared helpers only when the visual treatment is a deliberate app convention. ## Search and verification Before editing: ```bash rg -n "DropdownMenuPopup|MenuDefaults\\.Label|more_vert|actions\\s*=" composeApp/src/commonMain/kotlin/fr/husi -g '*.kt' ``` After editing: ```bash rg -n "MenuDefaults\\.Label" composeApp/src/commonMain/kotlin/fr/husi -g '*.kt' make test_gradle git diff --check -- composeApp/src/commonMain/kotlin/fr/husi ``` If the change touches Android-only UI or resources, also run: ```bash make apk_debug ``` Known reference implementations: - `composeApp/src/commonMain/kotlin/fr/husi/ui/RouteSettingsScreen.kt` - `composeApp/src/commonMain/kotlin/fr/husi/ui/dashboard/Dashboard.kt` - `composeApp/src/commonMain/kotlin/fr/husi/ui/profile/ProfileEditorScreen.kt` - `composeApp/src/commonMain/kotlin/fr/husi/compose/DropdownMenuSectionHeader.kt`
View on GitHub