Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e258914
refactor(core-ui): move the tab bar to commonMain
bmc08gt Oct 26, 2026
b7ddaca
feat(desktop): add the window shell from the desktop design
bmc08gt Oct 26, 2026
2c9a17e
feat(desktop): width classes for the three-pane shell
bmc08gt Oct 30, 2026
bd2c0ee
feat(desktop): pane state for sidebar, main and context
bmc08gt Oct 30, 2026
160436c
feat(desktop): three-pane shell with sidebar footer and context pane
bmc08gt Oct 30, 2026
2cd7913
feat(desktop): restore window size and position
bmc08gt Oct 30, 2026
a9dd835
feat(desktop): shortcuts and macOS menu bar
bmc08gt Oct 30, 2026
5c5f9b4
revert(core-ui): keep the tab bar Android-only
bmc08gt Oct 30, 2026
d3ab53f
fix(desktop): window state and pane closing from review
bmc08gt Oct 30, 2026
7d0fcdc
feat(desktop): Edit menu on macOS
bmc08gt Oct 30, 2026
ad1f8ff
feat(desktop): sidebar rail, hide toggle and hover peek
bmc08gt Oct 30, 2026
f4f14be
feat(desktop): center the traffic lights in a 52dp title row
bmc08gt Oct 30, 2026
7e689fa
feat(desktop): flat sidebar, and panes slide instead of jumping
bmc08gt Oct 30, 2026
be014b2
fix(desktop): keep the sidebar toggle on the floating list
bmc08gt Oct 30, 2026
57874cf
docs: step desktop motion frames before calling a layout change done
bmc08gt Oct 30, 2026
6dd951c
fix(ui): shrink a rolling number until its digits fit
bmc08gt Oct 31, 2026
8cbcc60
fix(desktop): give the floating sidebar list a visible shadow
bmc08gt Oct 31, 2026
e5f835f
fix(desktop): widen the context pane so currency cards match a phone
bmc08gt Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ The feature plugin automatically includes `:libs:logging`, `:ui:core`, `:ui:comp

A code change is done when the touched modules' unit tests pass and `./gradlew :apps:flipcash:app:compileDebugKotlin` succeeds.

For a layout or motion change in `apps/desktop`, also render `./gradlew :apps:desktop:snapshot -Pmotion=…` and step the frames before calling it done; stills can't show a transition.

## Namespaces

- App: `com.flipcash.app.android` (no `applicationIdSuffix` — debug and release share the ID, so installing one replaces the other)
Expand Down
24 changes: 21 additions & 3 deletions apps/desktop/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.metro)
alias(libs.plugins.kotlin.serialization)
}

kotlin {
Expand All @@ -22,9 +23,15 @@ metro {
dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.jetbrains.compose.material3)
implementation(libs.jetbrains.compose.material.icons.extended)
implementation(libs.kotlinx.serialization.json)
implementation(project(":services:opencode"))
implementation(project(":apps:flipcash:features:balance"))
implementation(project(":apps:flipcash:core"))
implementation(project(":apps:flipcash:core-ui"))
implementation(project(":apps:flipcash:card-expand"))
implementation(project(":apps:flipcash:shared:common-ui"))
implementation(project(":ui:components"))
implementation(project(":apps:flipcash:shared:transaction-history"))
implementation(project(":apps:flipcash:shared:tokens"))
implementation(project(":apps:flipcash:shared:funding"))
Expand All @@ -49,10 +56,13 @@ dependencies {
implementation(libs.coil3)
implementation(libs.coil3.network)
implementation(libs.event.bus)
implementation(libs.jbr.api)

testImplementation(project(":apps:flipcash:startup-contract"))
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(compose.desktop.uiTestJUnit4)
testImplementation(compose.desktop.currentOs)
}

tasks.withType<Test>().configureEach {
Expand All @@ -68,6 +78,11 @@ val devMnemonic = tryReadProperty(rootProject.rootDir, "flipcash.test.mnemonic",
compose.desktop {
application {
mainClass = "com.flipcash.desktop.MainKt"
// Names the macOS app menu and Dock item when run from Gradle; a packaged app takes packageName.
// -Xdock is a macOS-only JVM flag, so other hosts must not get it.
if (System.getProperty("os.name").lowercase().startsWith("mac")) {
jvmArgs += listOf("-Xdock:name=Flipcash", "-Dapple.awt.application.name=Flipcash")
}
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Flipcash"
Expand All @@ -78,12 +93,15 @@ compose.desktop {

tasks.register<JavaExec>("snapshot") {
group = "application"
description = "Renders the wallet (or, with -Pscreen=link, the link screen) off-screen to a PNG (-Pout=<file>)."
description = "Renders a screen off-screen to a PNG: -Pout=<file> -Pscreen=chat|money|you|link -Pwidth=<dp> -Pheight=<dp>, or frames into a directory with -Pmotion=peek|info."
classpath = sourceSets["main"].runtimeClasspath
mainClass.set("com.flipcash.desktop.SnapshotMainKt")
args(
providers.gradleProperty("out").getOrElse(layout.buildDirectory.file("wallet.png").get().asFile.path),
providers.gradleProperty("screen").getOrElse("wallet"),
providers.gradleProperty("out").getOrElse(layout.buildDirectory.file("snapshot.png").get().asFile.path),
providers.gradleProperty("screen").getOrElse("chat"),
providers.gradleProperty("width").getOrElse("1040"),
providers.gradleProperty("height").getOrElse("760"),
providers.gradleProperty("motion").getOrElse(""),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ import androidx.compose.ui.Modifier
import com.flipcash.app.devicelink.DesktopLinkHost
import com.flipcash.app.devicelink.ui.DesktopLinkScreen
import com.flipcash.app.theme.FlipcashTheme
import com.flipcash.desktop.shell.DesktopNavState
import com.getcode.theme.CodeTheme
import java.net.Inet4Address
import java.net.NetworkInterface

/** The wallet when signed in; otherwise the link screen, which stores the phone's seed and signs in. */
/** The main window when signed in; otherwise the link screen, which stores the phone's seed and signs in. */
@Composable
fun DesktopApp(graph: DesktopGraph, signedIn: Boolean) {
fun DesktopApp(graph: DesktopGraph, signedIn: Boolean, nav: DesktopNavState = remember { DesktopNavState() }) {
var linked by remember { mutableStateOf(signedIn) }
if (linked) {
DesktopWalletContent(graph)
DesktopMainContent(graph, nav = nav)
} else {
DesktopLinkContent(graph, onSignedIn = { linked = true })
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.flipcash.desktop

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.flipcash.app.analytics.LocalAnalytics
import com.flipcash.app.core.tokens.TokenPurpose
import com.flipcash.app.theme.FlipcashTheme
import com.flipcash.app.tokens.ui.SelectTokenViewModel
import com.flipcash.desktop.shell.ChatListPlaceholder
import com.flipcash.desktop.shell.ContextContent
import com.flipcash.desktop.shell.ContextPane
import com.flipcash.desktop.shell.ConversationPlaceholder
import com.flipcash.desktop.shell.DesktopNavState
import com.flipcash.desktop.shell.DesktopShell
import com.flipcash.desktop.shell.RailFooter
import com.flipcash.desktop.shell.SidebarFooter
import com.flipcash.desktop.shell.WalletPane
import com.flipcash.shared.common.ui.ContactAvatar
import com.getcode.opencode.compose.LocalExchange
import com.getcode.theme.CodeTheme
import com.getcode.util.resources.LocalResources
import com.getcode.utils.network.LocalNetworkObserver

/**
* The signed-in window, with the locals and window background that Android's app root provides
* (`App.kt` paints [CodeTheme.colors.background] behind every screen). The graph doesn't scope view
* models, so each read of [DesktopGraph.walletViewModel] builds a new one; they're held here.
* [initialContext] opens the context pane on first composition, for snapshots.
*/
@Composable
fun DesktopMainContent(
graph: DesktopGraph,
initialContext: ContextContent? = null,
onSettings: () -> Unit = {},
nav: DesktopNavState = remember { DesktopNavState().apply { context = initialContext } },
) {
val walletViewModel = remember { graph.walletViewModel }
val tokenViewModel = remember { graph.selectTokenViewModel }
LaunchedEffect(tokenViewModel) {
tokenViewModel.dispatchEvent(SelectTokenViewModel.Event.OnPurposeChanged(TokenPurpose.Balance))
}
val user by graph.userManager.state.collectAsState()
val displayName = user.userProfile?.displayName.orEmpty()
val tokenState by tokenViewModel.stateFlow.collectAsState()
// The same total the wallet header shows.
val balance = tokenState.totalBalance?.nativeAmount?.formatted().orEmpty()
FlipcashTheme {
CompositionLocalProvider(
LocalExchange provides graph.exchange,
LocalResources provides graph.resources,
LocalNetworkObserver provides graph.networkObserver,
LocalAnalytics provides graph.analytics,
) {
Box(Modifier.fillMaxSize().background(CodeTheme.colors.background)) {
DesktopShell(
nav = nav,
sidebar = { m -> ChatListPlaceholder(onOpenChat = nav::openChat, modifier = m) },
main = { m -> ConversationPlaceholder(m) },
context = { content, m ->
ContextPane(
content = content,
nav = nav,
wallet = { wm -> WalletPane(nav, walletViewModel, tokenViewModel, wm) },
modifier = m,
)
},
railFooter = { m ->
RailFooter(
balance = balance,
onYou = nav::openYou,
onWallet = nav::openWallet,
onSettings = onSettings,
modifier = m,
avatar = { am -> ContactAvatar(photoUri = null, displayName = displayName, modifier = am) },
)
},
footer = { m ->
SidebarFooter(
displayName = displayName,
balance = balance,
onYou = nav::openYou,
onWallet = nav::openWallet,
onSettings = onSettings,
modifier = m,
// Initials on the avatar gradient. The photo needs the blob URL resolver the
// Android app provides, which desktop doesn't have yet.
avatar = { am -> ContactAvatar(photoUri = null, displayName = displayName, modifier = am) },
)
},
)
}
}
}
}

This file was deleted.

Loading
Loading