106 lines
6.5 KiB
Markdown
106 lines
6.5 KiB
Markdown
# 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<ParameterDefinition>` 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<String, String>`
|
|
- `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<XyzUiState>` — 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.
|