# Feature Specification: M3 Expressive Design System Adoption

**Feature Branch**: `20260513-160000-m3-expressive-adoption`  
**Created**: 2026-05-13  
**Status**: Implementation Complete  
**Input**: User description: "Adopt Material 3 Expressive design system across the Meshtastic Android app UI, leveraging new typography, animations, component styles, and interaction patterns to create a more dynamic and accessible user experience."  
**Cross-Platform Spec**: N/A — platform-specific only. M3 Expressive APIs are Android/Compose-specific; desktop and iOS targets will receive equivalent improvements via CMP material3 as those APIs stabilize.

## Summary

Adopt the Material 3 Expressive design system throughout the Meshtastic Android app by replacing custom UI implementations with native M3 library components from `compose-multiplatform-material3:1.11.0-alpha07`. The guiding principle is **zero self-rolled components** — only native M3 APIs are used. Custom wrappers that existed in `core/ui` have been migrated to thin delegating layers over native M3 components, or deleted entirely when they were dead code.

## Goals

1. **Unified expressive typography**: Replace hardcoded `TextStyle` references with M3 Expressive typescale including emphasized variants (`titleMediumEmphasized`, `headlineSmallEmphasized`, etc.) auto-generated by `MaterialExpressiveTheme`
2. **Native M3 component adoption**: Migrate all custom component wrappers to delegate to native M3 interactive ListItem, ModalBottomSheet, OutlinedIconToggleButton, DropdownMenuGroup, CircularWavyProgressIndicator, etc.
3. **Dead code elimination**: Remove unused custom components (ClickableTextField, TimeTickWithLifecycle, BottomSheetDialog) that were superseded or never called
4. **Proper M3 disabled/color delegation**: Let M3 handle disabled content colors natively through its token system instead of manual alpha overrides
5. **Process-death state preservation**: Maintain CurrentTabSaver for active tab persistence across process death

## Non-Goals

- Self-rolling components not available in CMP material3 (e.g., SwipeToRevealBox, animated focus rings, reduce-motion locals) — **deferred until native APIs ship**
- Migrating all ~176 `HorizontalDivider` usages (separate follow-up PR)
- Migrating remaining legacy `CircularProgressIndicator` sites (IndoorAirQuality, LoadingOverlay, TAKConfigItemList) — low-priority follow-up
- Changing the app's color palette or brand identity
- Introducing new features or screens — this is a design-system upgrade of existing UI
- Supporting M3 Expressive on desktop/iOS targets immediately

## What Was Implemented

### Native M3 Components Adopted

| Custom/Old | Native M3 Replacement | Location |
|-----------|----------------------|----------|
| Custom `ListItem` wrapper (clickable Column) | M3 interactive `ListItem(onClick=...)` | `core/ui/component/ListItem.kt` |
| Custom `RegularPreference` (Column + clickable) | M3 interactive `ListItem` with slots | `core/ui/component/RegularPreference.kt` |
| Custom `SwitchPreference` (toggleable ListItem) | M3 toggleable `ListItem(checked=..., onCheckedChange=...)` | `core/ui/component/SwitchPreference.kt` |
| `IconToggleButton` | `OutlinedIconToggleButton` | `feature/node/component/DeviceActions.kt` |
| `BottomSheetDialog` (custom) | `ModalBottomSheet` | `core/ui/emoji/EmojiPickerDialog.kt`, `feature/messaging/component/Reaction.kt` |
| `CircularProgressIndicator` (in SwitchPref) | `CircularWavyProgressIndicator` | `core/ui/component/SwitchPreference.kt` |
| Manual disabled color logic | M3 native disabled token delegation (`Color.Unspecified`) | All ListItem wrappers |
| Hardcoded `TextStyle(...)` | `MaterialTheme.typography.*Emphasized` | All feature modules |

### Dead Code Removed

| Component | Reason |
|-----------|--------|
| `ClickableTextField` | Zero callers — superseded by ExposedDropdownMenuBox |
| `TimeTickWithLifecycle` (expect + 3 actuals) | Zero callers — lifecycle tick no longer needed |
| `BottomSheetDialog` | Zero callers after ModalBottomSheet migration |

### Key Design Decisions

1. **RectangleShape for resting ListItem**: M3 interactive ListItem defaults to `CornerExtraSmall` (4dp rounded) which clashed with `HorizontalDivider` between items. Override `ListItemDefaults.shapes(shape = RectangleShape)` preserves press/hover/focus shape animations while keeping divider compatibility.

2. **Color.Unspecified over LocalContentColor.current**: Passing `Color.Unspecified` to Text/Icon in M3 slots lets M3 apply its own content colors including disabled states. Explicit colors override M3's provision.

3. **icon() helper**: Resolves `Color.Unspecified` → `LocalContentColor.current` at render time so M3 disabled content color flows through while explicit colors (like error red) still work.

4. **CurrentTabSaver**: `rememberSaveable` with ordinal-based saver persists the active navigation tab across process death. Was accidentally dropped during Route graph refactoring and restored.

5. **ModalBottomSheet with skipPartiallyExpanded**: Both EmojiPickerDialog and ReactionDialog use `skipPartiallyExpanded = true` to show full content immediately rather than partial peek.

## Architecture

### Key Components

| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `AppTheme` | `core/ui/theme/Theme.kt` | Root theme using `MaterialExpressiveTheme` with full expressive typography |
| `AppTypography` | `core/ui/theme/Type.kt` | Custom typescale with 16sp body minimum; emphasized variants auto-generated |
| `BasicListItem` | `core/ui/component/ListItem.kt` | Thin wrapper over M3 interactive ListItem with RectangleShape and Color.Unspecified defaults |
| `SwitchPreference` | `core/ui/component/SwitchPreference.kt` | Thin wrapper over M3 toggleable ListItem |
| `RegularPreference` | `core/ui/component/RegularPreference.kt` | Thin wrapper over M3 interactive ListItem |
| `MeshtasticDialog` | `core/ui/component/AlertDialogs.kt` | Uses expressive shape (28dp) and emphasized typography |
| `MainAppBar` | `core/ui/component/MainAppBar.kt` | Uses `titleLargeEmphasized` typography |
| `MultiBackstack` | `core/navigation/MultiBackstack.kt` | Tab state persisted via CurrentTabSaver |

## Requirements *(final)*

### Functional Requirements

- **FR-001**: ✅ The app theme uses `MaterialExpressiveTheme` with expressive `MotionScheme` on all screens
- **FR-002**: ✅ The typography system uses emphasized variants auto-generated by MaterialExpressiveTheme with 16sp body minimum
- **FR-005**: ✅ SwitchPreference and FirmwareUpdateScreen use `CircularWavyProgressIndicator` for indeterminate states
- **FR-008**: ✅ Primary action buttons use expressive button styling
- **FR-011**: ✅ The top app bar uses expressive styling with emphasized typography
- **FR-012**: ✅ Expressive APIs wrapped with `@OptIn(ExperimentalMaterial3ExpressiveApi::class)` at file level

### Deferred Requirements (awaiting native CMP APIs)

- **FR-003**: Navigation spring-physics indicators — already provided by MotionScheme.expressive(), no additional work needed
- **FR-004**: FAB tooltip labels — available via PlainTooltip (already in MenuFAB)
- **FR-006**: Expressive slider styling — deferred (no native API beyond current)
- **FR-007**: Swipe-to-action gestures — **REVERTED** (no native SwipeToRevealBox in M3; self-rolled version violated zero-custom-component constraint)
- **FR-009**: Animated focus rings — **REVERTED** (no native M3 focus indication API; custom implementation violated constraint)
- **FR-010**: Reduce-motion local — **REVERTED** (M3's MotionScheme already respects system animator duration scale natively)

### Non-Functional Requirements

- **NFR-004**: ✅ All expressive API usage confined to `commonMain` source set
- **NFR-005**: ✅ Screenshot tests updated for new component appearances
- **NFR-006**: ✅ Direct permanent replacement with no feature flag
- **NFR-007**: ✅ No manual reimplementations of APIs not yet available in CMP material3

## Source-Set Impact

| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | Modified: theme, ListItem, SwitchPreference, RegularPreference, AlertDialogs, MainAppBar, EmojiPickerDialog, ChannelItem; Deleted: ClickableTextField, TimeTickWithLifecycle, BottomSheetDialog | All UI logic lives in commonMain |
| `androidMain` | Deleted: TimeTickWithLifecycle actual | Dead code removal |
| `jvmMain` | Deleted: TimeTickWithLifecycle actual | Dead code removal |
| `iosMain` | Modified: NoopStubs.kt (removed TimeTickWithLifecycle actual) | Dead code removal |

## Success Criteria *(final)*

- **SC-001**: ✅ All screens render using `MaterialExpressiveTheme`
- **SC-004**: ✅ Typography hierarchy renders with emphasized variants and distinct weight/size levels
- **SC-008**: ✅ Zero accessibility regressions — existing TalkBack flows unaffected

## Clarifications

### Session 2026-05-18

- Q: Should we use self-rolled M3 components (SwipeToRevealBox, focus indication, reduce motion) or only native library APIs? → A: **Only native library APIs.** All self-rolled implementations were reverted. Zero custom components policy.
- Q: Should dividers between ListItems be removed now that M3 uses rounded shapes? → A: Keep dividers + use RectangleShape for resting state. Divider removal is a separate focused PR (~176 usages across 40 files).
- Q: Should we use `OutlinedIconToggleButton` or regular `IconToggleButton` for favorite? → A: `OutlinedIconToggleButton` for clearer affordance.
- Q: Should RegularPreference manually compute disabled colors? → A: No — delegate to M3's native disabled token system via `Color.Unspecified`.

## Assumptions

- CMP material3 library version 1.11.0-alpha07 provides sufficient expressive API surface
- `ExperimentalMaterial3ExpressiveApi` annotation will remain available in near-term CMP updates
- No custom font bundling; system fonts with graceful degradation on pre-Android 12
- Existing screenshot test infrastructure validates visual changes
