# core:ui Material 3 theme and shared Compose components used across all feature modules. Purely presentational -- no business logic, no ViewModels, no repositories. ## What This Module Provides ### Theme (`theme/`) - `Theme.kt`: `LibreChatTheme` wrapping Material 3 `MaterialTheme`. Maps LibreChat web colors to M3 color roles. - `Color.kt`: Color palette derived from the web app's CSS variables. - `Type.kt`: Typography scale. - `Shape.kt`: Corner radius definitions. ### Shared Components (`components/`) - `LibreChatTopBar` - App bar with optional back navigation and actions. - `LoadingIndicator` - Centered circular progress. - `ErrorBanner` - Dismissible error message bar. - `EmptyState` - Illustration + message for empty lists. - `AvatarImage` - User/agent/model avatar with Coil image loading and fallback. - `ConfirmationDialog` - Reusable confirm/cancel dialog. - `ModelIcon` - Endpoint-specific model icons. - `EndpointBadge` - Colored badge showing the AI provider. - `SearchBar` - Reusable search input field. - `BottomSheetScaffold` - Wrapper for modal bottom sheets. - `PullToRefresh` - Pull-to-refresh wrapper. - `BannerDisplay` - Dismissible server banner cards (info/warning/error types). ### Markdown (`markdown/`) - `MarkdownRenderer` - Compose-native markdown rendering (commonmark). - `CodeBlock` - Syntax-highlighted code blocks (Compose-native, NOT WebView per block). - `LatexRenderer` - Math rendering via WebView or compose-math. ### Message Components (`message/`) - `MessageBubble` - Shared message rendering used by `:feature:chat`. - `ToolCallCard` - Expandable tool call display. - `FileAttachmentChip` - File reference chip. - `FeedbackButtons` - Thumbs up/down. - `StreamingIndicator` - Pulsing indicator during SSE streaming. ## Rules - **No business logic.** No ViewModels, no repository calls, no use cases. - Components accept domain models from `:core:model` as parameters and render them. - All components must be stateless or hoist state to the caller. - Dependencies: `:core:model`, `:core:common`, Coil for image loading, Compose libraries. - Convention plugins: `librechat.mobile.library` + `librechat.mobile.compose`. - No DI in this module (no Koin modules, no injected classes). - Use `@Preview` annotations on all components for Android Studio preview support. - During SSE streaming, buffer markdown re-renders to ~100ms intervals to avoid frame drops. ### Dynamic Parameters (`components/Dynamic*.kt`) - `DynamicParameterPanel` dispatches `List` to typed controls (slider/dropdown/checkbox/input/textarea) - `ParameterDefinition` contains: key, type, label, description, default, min, max, step, options - `DynamicSlider` snaps to step increments; calculates stepCount from range - All controls are stateless — caller manages values via `Map` - `ModelParameterSheet` falls back to existing fixed params when no schema available - **Gotcha**: Slider step count calculated as `((max - min) / step).toInt()` — ensure step divides range evenly to avoid drift ### Accessibility (`theme/AccessibilityUtils.kt`) - `Modifier.minTouchTarget()` — enforces 48dp minimum size - `Modifier.semanticHeading()` — marks composable as heading for TalkBack - Helper functions: `endpointContentDescription()`, `modelIconContentDescription()` - Applied to `LoadingIndicator` and `EmptyState` components ## Compose Performance Rules These rules apply to ALL composables across feature modules, not just core:ui. ### UI State Architecture 1. **Single UI state data class per screen.** Each ViewModel exposes ONE `StateFlow` — never 5+ individual StateFlows that each trigger recomposition independently. 2. **UI state contains only display-ready data.** The ViewModel maps domain models (e.g., `Conversation` with 28 fields) to minimal display data classes (e.g., `DrawerConversationDisplayData` with 7 fields). Composables should never receive full domain models. 3. **Mark UI state classes `@Immutable`.** This lets the Compose compiler skip recomposition when the reference hasn't changed. All fields must be `val` with stable types. 4. **Collect state at the narrowest scope.** If only `DrawerContent` uses drawer state, collect inside `DrawerContent` — not in the parent `PhoneLayout` which also owns the NavDisplay. State changes should only recompose the composable that reads them. 5. **Use `SharingStarted.Eagerly`** for UI state that should be ready before the composable subscribes (avoids empty→populated two-phase render jank on first frame). ### Stability 6. **All types passed to composables must be stable.** Primitives, `String`, `@Immutable` data classes, and enums are stable. Standard `List`/`Set`/`Map` are NOT stable by default — they are declared stable in `compose-stability.conf` at the project root. 7. **Never pass lambdas that capture unstable references.** Prefer method references (`viewModel::doThing`) or `remember`'d lambdas over inline lambdas that capture changing state. ### LazyColumn / LazyList 8. **Always provide `key`** on `items()` calls. Keys must be unique, stable identifiers (e.g., `conversationId`). 9. **Always provide `contentType`** when a LazyColumn has mixed item types (headers, content rows, loading indicators). This enables Compose to reuse compositions across items of the same type. 10. **One element per `item {}` block.** Multiple composables in one block prevents independent recycling. 11. **No nested same-direction scrollables.** Never put `LazyColumn` inside `verticalScroll` — combine into one `LazyColumn` using `item {}` blocks. ### Avoiding Unnecessary Work 12. **Move data transformations to the ViewModel.** Sorting, filtering, grouping, and model mapping happen in the ViewModel — not in `remember` blocks in composables. 13. **Cache expensive objects as top-level constants.** `RoundedCornerShape`, `Regex`, `DateTimeFormatter` — allocating these per-item per-frame is wasteful. 14. **Use `background(color, shape)` instead of `clip(shape).background(color)`.** The `clip` modifier creates a persistent clip layer per composable; `background` with a shape parameter draws the clipped background without the layer overhead. 15. **Avoid `copy(alpha = ...)` on colors.** Alpha-blended colors force GPU compositing. Use opaque colors from the theme where possible. 16. **Defer state reads to later phases.** Use lambda-based modifiers (`Modifier.offset { }`, `Modifier.drawBehind { }`) instead of value-based ones (`Modifier.offset(x, y)`, `Modifier.background(animatedColor)`) for scroll/animation state.