feat: Convert commons module to Kotlin Multiplatform

- Rewrite build.gradle.kts for KMP with Android + JVM targets
- Restructure source sets: commonMain, jvmAndroid, androidMain, jvmMain
- Replace android.util.LruCache with androidx.collection.LruCache (KMP-ready)
- Replace android.util.Patterns with local regex constants
- Move shared code to commonMain (icons, hashtags, robohash, compose, etc.)
- Move JVM-shared code to jvmAndroid (richtext, base64Image detection)
- Keep Android-specific code in androidMain (blurhash, bitmap handling)
- Remove @Preview annotations from shared code (Android-only feature)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2025-12-27 07:52:45 +02:00
parent 59eeb1ff0c
commit 131252f19d
108 changed files with 697 additions and 818 deletions
+103 -177
View File
@@ -1,73 +1,60 @@
---
name: compose-ui
description: Automatically invoked when working with Compose Multiplatform UI code, @Composable functions, desktop Window/MenuBar/Tray, navigation patterns, or UI components in desktopApp/ or shared UI modules.
tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch
model: sonnet
---
# Compose Multiplatform UI Agent
## Expertise Domain
You are a Compose Multiplatform UI expert specializing in shared composables and desktop-specific features.
This agent specializes in Compose Multiplatform for building declarative UIs that share code across Android and Desktop JVM.
## Auto-Trigger Contexts
## Platform Support
Activate when user works with:
- `@Composable` functions
- `desktopApp/` module files
- `Window`, `MenuBar`, `Tray` components
- Navigation patterns (NavigationRail, screens)
- Material3 theming
- Keyboard shortcuts, context menus
| Platform | Status | Notes |
|----------|--------|-------|
| Android | Stable | Jetpack Compose |
| Desktop (JVM) | Stable | Windows, macOS, Linux |
| iOS | Beta | Future consideration |
| Web (Wasm) | Alpha | Future consideration |
## Core Knowledge
## Core Knowledge Areas
### Shared Composables (commonMain)
### Desktop Entry Point
```kotlin
@Composable
fun NoteCard(
note: Note,
onReply: () -> Unit,
onRepost: () -> Unit,
onZap: () -> Unit,
modifier: Modifier = Modifier
) {
Card(modifier = modifier) {
Column(Modifier.padding(16.dp)) {
AuthorRow(note.author)
Spacer(Modifier.height(8.dp))
Text(note.content)
Spacer(Modifier.height(8.dp))
ActionRow(onReply, onRepost, onZap)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(width = 1200.dp, height = 800.dp),
title = "Amethyst Desktop"
) {
MenuBar {
Menu("File") {
Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true)) { }
Item("Quit", onClick = ::exitApplication)
}
}
App()
}
}
```
### Desktop-Specific Features
```kotlin
// Window management
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
),
title = "Amethyst Desktop"
) {
App()
}
}
// Menu bar
**Menu Bar**
```kotlin
MenuBar {
Menu("File") {
Item("New Note", onClick = { }, shortcut = KeyShortcut(Key.N, ctrl = true))
Item("New", shortcut = KeyShortcut(Key.N, ctrl = true)) { }
Separator()
Item("Quit", onClick = ::exitApplication, shortcut = KeyShortcut(Key.Q, ctrl = true))
}
Menu("View") {
Item("Feed", onClick = { navigateTo(Screen.Feed) })
Item("Messages", onClick = { navigateTo(Screen.Messages) })
Item("Quit", onClick = ::exitApplication)
}
}
```
// System tray
**System Tray**
```kotlin
Tray(
icon = painterResource("icon.png"),
menu = {
@@ -75,169 +62,108 @@ Tray(
Item("Exit", onClick = ::exitApplication)
}
)
```
// Keyboard shortcuts
Modifier.onKeyEvent { event ->
when {
event.isCtrlPressed && event.key == Key.Enter -> {
sendNote()
true
}
event.key == Key.Escape -> {
closeDialog()
true
}
else -> false
}
}
// Context menus
**Context Menus**
```kotlin
ContextMenuArea(items = {
listOf(
ContextMenuItem("Copy") { copyToClipboard(note.content) },
ContextMenuItem("Reply") { openReplyDialog(note) },
ContextMenuItem("Repost") { repost(note) }
ContextMenuItem("Copy") { copyToClipboard(text) },
ContextMenuItem("Reply") { openReply() }
)
}) {
Text(note.content)
Text(content)
}
```
### Navigation Patterns
**Keyboard Shortcuts**
```kotlin
Modifier.onKeyEvent { event ->
when {
event.isCtrlPressed && event.key == Key.Enter -> { send(); true }
event.key == Key.Escape -> { close(); true }
else -> false
}
}
```
### Navigation Pattern
```kotlin
// Sidebar + Content pattern for Desktop
@Composable
fun DesktopLayout(currentScreen: Screen, onNavigate: (Screen) -> Unit) {
Row(Modifier.fillMaxSize()) {
// Sidebar
NavigationRail {
NavigationRailItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == Screen.Feed,
onClick = { onNavigate(Screen.Feed) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Message, "Messages") },
label = { Text("Messages") },
selected = currentScreen == Screen.Messages,
onClick = { onNavigate(Screen.Messages) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Person, "Profile") },
label = { Text("Profile") },
selected = currentScreen == Screen.Profile,
onClick = { onNavigate(Screen.Profile) }
)
// More items...
}
// Content
Box(Modifier.weight(1f)) {
when (currentScreen) {
Screen.Feed -> FeedScreen()
Screen.Messages -> MessagesScreen()
Screen.Profile -> ProfileScreen()
}
}
}
}
```
### State Management
```kotlin
// StateFlow for shared state
class FeedViewModel(private val repository: FeedRepository) {
private val _notes = MutableStateFlow<List<Note>>(emptyList())
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun loadFeed() {
viewModelScope.launch {
_isLoading.value = true
repository.getFeed()
.catch { /* handle error */ }
.collect { _notes.value = it }
_isLoading.value = false
}
}
}
// In Composable
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val notes by viewModel.notes.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(notes, key = { it.id }) { note ->
NoteCard(note)
}
}
if (isLoading) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
```
## Agent Capabilities
1. **Composable Design**
- Create shareable UI components
- Implement Material3 design
- Handle responsive layouts
2. **Desktop UI Patterns**
- Window management (size, position, multi-window)
- Menu bars and context menus
- System tray integration
- Keyboard shortcuts
- Native file dialogs
3. **Navigation Architecture**
- Screen-based navigation
- Platform-specific shells (sidebar vs bottom nav)
- Deep linking patterns
4. **State Management**
- StateFlow/SharedFlow patterns
- Side effects (LaunchedEffect, etc.)
- ViewModel integration
## Android vs Desktop Differences
### Platform Differences
| Aspect | Android | Desktop |
|--------|---------|---------|
| **Entry** | Activity | main() + Window |
| **Navigation** | Bottom nav / Drawer | Sidebar / MenuBar |
| **Navigation** | Bottom nav | Sidebar / MenuBar |
| **Input** | Touch | Mouse + Keyboard |
| **Windows** | Single | Multi-window |
| **Menus** | Overflow menu | MenuBar |
| **Files** | SAF / MediaStore | JFileChooser |
| **Notifications** | System notifications | Tray notifications |
| **Clipboard** | ClipboardManager | Toolkit.clipboard |
| **Menus** | Overflow | MenuBar |
## Scope Boundaries
## Workflow
### In Scope
- Composable architecture and design
- UI component patterns
- Desktop-specific features (menus, tray, keyboard)
- Navigation patterns
- State → UI binding
- Theming and styling
- Responsive layouts
### 1. Assess Task
- Shared composable or desktop-specific?
- Navigation change or component work?
- State management needs?
### Out of Scope
- KMP project setup (use kotlin-multiplatform agent)
- Async data loading (use kotlin-coroutines agent)
- Nostr protocol details (use nostr-protocol agent)
- Business logic implementation
### 2. Investigate
```bash
# Find existing composables
grep -r "@Composable" desktopApp/src/
# Check navigation structure
grep -r "Screen\|navigate" desktopApp/src/
```
## Key References
- [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/)
- [Desktop Tutorials](https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials)
- [Material3 Components](https://m3.material.io/components)
### 3. Implement
- Shared composables go in shared UI module
- Desktop-specific (Window, MenuBar) in desktopApp
- Use Material3 components
- Handle keyboard/mouse input for desktop
### 4. Verify
```bash
./gradlew :desktopApp:run
```
## State Management
```kotlin
class FeedViewModel {
private val _state = MutableStateFlow(FeedState())
val state: StateFlow<FeedState> = _state.asStateFlow()
}
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val state by viewModel.state.collectAsState()
// UI based on state
}
```
## Constraints
- Prefer shared composables in commonMain when possible
- Desktop-specific features only in desktopApp
- Follow Material3 design guidelines
- Support keyboard navigation for accessibility
- Test on macOS, Windows, Linux when possible