diff --git a/README.md b/README.md index ae4864b..c55e14e 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,5 @@ Autonomous property-based testing for mobile apps. Specs in TypeScript. Core in - [Getting started](https://priyanshujain.github.io/uatu/manual/getting-started.html) - [Writing specs](https://priyanshujain.github.io/uatu/manual/writing-specs.html) -- [Example](https://github.com/priyanshujain/uatu/tree/master/examples/sample-app) +- [Example](https://github.com/priyanshujain/uatu/tree/master/examples/folio) - [Architecture](https://priyanshujain.github.io/uatu/development/architecture.html) diff --git a/cmd/internal-tools/bundle-check/main.go b/cmd/internal-tools/bundle-check/main.go index 17c0c81..a30111b 100644 --- a/cmd/internal-tools/bundle-check/main.go +++ b/cmd/internal-tools/bundle-check/main.go @@ -9,17 +9,30 @@ import ( ) func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: bundle-check ") + os.Exit(1) + } + + entryFile, err := filepath.Abs(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "resolve spec path: %v\n", err) + os.Exit(1) + } + repoRoot, err := os.Getwd() if err != nil { fmt.Fprintf(os.Stderr, "getwd: %v\n", err) os.Exit(1) } specApiPath := filepath.Join(repoRoot, "pkg/spec-api/src/index.ts") + defaultPropertiesPath := filepath.Join(repoRoot, "pkg/spec-api/src/defaults/properties.ts") result, err := bundler.Bundle(bundler.Options{ - EntryFile: "examples/sample-app/spec.ts", + EntryFile: entryFile, Aliases: map[string]string{ - "@uatu/spec": specApiPath, + "@uatu/spec": specApiPath, + "@uatu/spec/defaults/properties": defaultPropertiesPath, }, }) if err != nil { diff --git a/cmd/uatu-sample-harness/main.go b/cmd/uatu-sample-harness/main.go deleted file mode 100644 index 786c933..0000000 --- a/cmd/uatu-sample-harness/main.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "net" - "os" - "os/exec" - "os/signal" - "syscall" - "time" - - "github.com/priyanshujain/uatu/internal/agent" -) - -func main() { - port := flag.Int("port", 0, "TCP port to listen on (0 = ephemeral)") - socketName := flag.String("socket-name", "uatu-agent", "abstract socket name on the device") - cycles := flag.Int("cycles", 3, "number of snapshot cycles to run") - serial := flag.String("serial", "", "adb device serial (empty = default)") - cycleInterval := flag.Duration("interval", 500*time.Millisecond, "delay between snapshot cycles") - flag.Parse() - - listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port)) - if err != nil { - die("listen: %v", err) - } - defer listener.Close() - listenerAddr := listener.Addr().(*net.TCPAddr) - fmt.Printf("listening on 127.0.0.1:%d\n", listenerAddr.Port) - - if err := adbReverse(*serial, *socketName, listenerAddr.Port); err != nil { - die("adb reverse: %v", err) - } - defer func() { - if err := adbReverseRemove(*serial, *socketName); err != nil { - fmt.Fprintf(os.Stderr, "adb reverse cleanup: %v\n", err) - } - }() - fmt.Printf("forwarded localabstract:%s -> tcp:%d on device\n", *socketName, listenerAddr.Port) - - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer cancel() - - server := agent.NewServer(listener) - fmt.Println("waiting for SDK to connect (launch the sample app on the device)...") - conn, err := server.Accept(ctx) - if err != nil { - die("accept: %v", err) - } - defer conn.Close() - hello := conn.Hello() - fmt.Printf("HELLO from platform=%s app=%s sdk=%s\n", hello.Platform, hello.AppPackage, hello.Version) - - for index := 1; index <= *cycles; index++ { - snapshotCtx, snapshotCancel := context.WithTimeout(ctx, 5*time.Second) - state, err := conn.Snapshot(snapshotCtx) - snapshotCancel() - if err != nil { - die("snapshot #%d: %v", index, err) - } - marshalled, _ := json.Marshal(state.Snapshots) - fmt.Printf("STATE %d: %s\n", index, marshalled) - if err := conn.Release(ctx); err != nil { - die("release #%d: %v", index, err) - } - if index < *cycles { - time.Sleep(*cycleInterval) - } - } - fmt.Println("round-trip complete") -} - -func adbReverse(serial, socketName string, port int) error { - commandArguments := []string{"reverse", fmt.Sprintf("localabstract:%s", socketName), fmt.Sprintf("tcp:%d", port)} - if serial != "" { - commandArguments = append([]string{"-s", serial}, commandArguments...) - } - command := exec.Command("adb", commandArguments...) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - return command.Run() -} - -func adbReverseRemove(serial, socketName string) error { - commandArguments := []string{"reverse", "--remove", fmt.Sprintf("localabstract:%s", socketName)} - if serial != "" { - commandArguments = append([]string{"-s", serial}, commandArguments...) - } - return exec.Command("adb", commandArguments...).Run() -} - -func die(format string, args ...any) { - fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...) - os.Exit(1) -} diff --git a/docs/manual/getting-started.md b/docs/manual/getting-started.md index 898f937..9c995b1 100644 --- a/docs/manual/getting-started.md +++ b/docs/manual/getting-started.md @@ -50,14 +50,14 @@ dependencies { ## Your first run -The repo ships a working sample at `examples/sample-app`. From that directory: +The repo ships a working sample at `examples/folio`. From that directory: ```sh npm install (cd android && ./gradlew installDebug) uatu test \ --spec spec.ts \ - --bundle-id dev.uatu.sample \ + --bundle-id app.folio \ --platform android \ --duration 2m ``` diff --git a/examples/sample-app/.gitignore b/examples/folio/.gitignore similarity index 82% rename from examples/sample-app/.gitignore rename to examples/folio/.gitignore index 9d44317..52e8ba2 100644 --- a/examples/sample-app/.gitignore +++ b/examples/folio/.gitignore @@ -6,6 +6,7 @@ iosApp/build/ iosApp/iosApp.xcodeproj/ iosApp/iosApp.xcodeproj/** .DS_Store -runs/ +uatu/node_modules/ +uatu/runs/ .playwright-mcp/ web-*.png diff --git a/examples/sample-app/README.md b/examples/folio/README.md similarity index 51% rename from examples/sample-app/README.md rename to examples/folio/README.md index 789445f..26f391b 100644 --- a/examples/sample-app/README.md +++ b/examples/folio/README.md @@ -1,9 +1,9 @@ -# uatu sample app +# Folio -A minimal Kotlin Multiplatform ledger app that mirrors the React reference: -login with demo credentials, create accounts, add credits and debits. Same -features, monospace look, and demo creds, shared across Android and iOS via -Compose Multiplatform. +A minimal Kotlin Multiplatform personal-ledger app: login with demo +credentials, create accounts, add credits and debits. Shared UI across +Android, iOS, and Web via Compose Multiplatform. Doubles as the example +uatu runs its property-based specs against. ## Stack @@ -12,10 +12,6 @@ Compose Multiplatform. - kotlinx.coroutines for state flows - uatu `sdk-android` for harness integration on Android -Everything in `composeApp/src/commonMain/kotlin/dev/uatu/sample/` is shared -between platforms. Platform-specific I/O (file storage, clock, UUID) lives -in `androidMain/` and `iosMain/` as `actual`s of the `Platform` expect object. - ## Prerequisites - `just` @@ -46,7 +42,7 @@ launches. ## Demo credentials ``` -email: demo@ledger.app +email: demo@folio.app password: ledger123 ``` @@ -60,7 +56,7 @@ If no device is connected, uatu boots the single AVD it finds. With multiple AVDs, pick one: ```sh -just AVD=Pixel_7 test +AVD=Pixel_7 just test ``` Persistent settings can live in `.env` alongside the justfile: @@ -70,28 +66,13 @@ AVD=Pixel_7 DURATION=5m ``` -Traces land in `./runs//`. - -## Layout - -``` -composeApp/ - src/commonMain/kotlin/dev/uatu/sample/ shared domain, state, UI - src/androidMain/ Android Application + Activity - src/iosMain/ iOS UIViewController entry -iosApp/ - project.yml xcodegen spec - iosApp/iOSApp.swift SwiftUI host - iosApp/Info.plist -justfile -spec.ts uatu test spec -``` +Traces land in `./uatu/runs//`. ## How it connects to uatu -- `composeApp/src/androidMain/.../SampleApplication.kt` calls `Uatu.start(this)` +- `composeApp/src/androidMain/.../FolioApplication.kt` calls `Uatu.start(this)` and registers snapshot extractors (`logged_in`, `account_count`, `total_balance`, `route`) -- `spec.ts` imports `@uatu/spec`, reads those snapshots, asserts properties, - and weights the actions the fuzzer picks from +- `uatu/spec.ts` imports `@uatu/spec`, reads those snapshots, asserts + properties, and weights the actions the fuzzer picks from - `just test` invokes `uatu test` against the installed APK diff --git a/examples/sample-app/build.gradle.kts b/examples/folio/build.gradle.kts similarity index 100% rename from examples/sample-app/build.gradle.kts rename to examples/folio/build.gradle.kts diff --git a/examples/sample-app/composeApp/build.gradle.kts b/examples/folio/composeApp/build.gradle.kts similarity index 94% rename from examples/sample-app/composeApp/build.gradle.kts rename to examples/folio/composeApp/build.gradle.kts index 678b0fa..fd96ece 100644 --- a/examples/sample-app/composeApp/build.gradle.kts +++ b/examples/folio/composeApp/build.gradle.kts @@ -35,7 +35,7 @@ kotlin { target.binaries.framework { baseName = "ComposeApp" isStatic = true - binaryOption("bundleId", "dev.uatu.sample") + binaryOption("bundleId", "app.folio") } } @@ -93,11 +93,11 @@ kotlin { } android { - namespace = "dev.uatu.sample" + namespace = "app.folio" compileSdk = 36 defaultConfig { - applicationId = "dev.uatu.sample" + applicationId = "app.folio" minSdk = 24 targetSdk = 36 versionCode = 1 @@ -124,7 +124,7 @@ android { sqldelight { databases { create("LedgerDatabase") { - packageName.set("dev.uatu.sample.db") + packageName.set("app.folio.db") } } } diff --git a/examples/sample-app/composeApp/src/androidMain/AndroidManifest.xml b/examples/folio/composeApp/src/androidMain/AndroidManifest.xml similarity index 89% rename from examples/sample-app/composeApp/src/androidMain/AndroidManifest.xml rename to examples/folio/composeApp/src/androidMain/AndroidManifest.xml index bbcfc8e..748409f 100644 --- a/examples/sample-app/composeApp/src/androidMain/AndroidManifest.xml +++ b/examples/folio/composeApp/src/androidMain/AndroidManifest.xml @@ -1,8 +1,8 @@ LoginPage(onLoggedIn = { Navigator.replace(Route.Home) }) - Route.Home -> HomePage( + Route.Login -> LoginScreen(onLoggedIn = { Navigator.replace(Route.Home) }) + Route.Home -> HomeScreen( user = session?.user ?: "", onLogout = { Repository.clearSession() Navigator.replace(Route.Login) }, ) - Route.AddAccount -> AddAccountPage() - is Route.Ledger -> LedgerPage(accountId = r.accountId) - is Route.AddTransaction -> AddTransactionPage(accountId = r.accountId) + Route.AddAccount -> AddAccountScreen() + is Route.Ledger -> LedgerScreen(accountId = r.accountId) + is Route.AddTransaction -> AddTransactionScreen(accountId = r.accountId) } } } diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/FocusTracker.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/FocusTracker.kt similarity index 96% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/FocusTracker.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/FocusTracker.kt index fe95708..b0e464a 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/FocusTracker.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/FocusTracker.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Account.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Account.kt new file mode 100644 index 0000000..5601094 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Account.kt @@ -0,0 +1,7 @@ +package app.folio.data + +data class Account( + val id: String, + val name: String, + val createdAt: Long, +) diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/LedgerStore.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/LedgerStore.kt similarity index 95% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/LedgerStore.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/LedgerStore.kt index 75fe0c0..29f4a0e 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/LedgerStore.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/LedgerStore.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.data import kotlinx.coroutines.flow.StateFlow diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Repository.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Repository.kt similarity index 96% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Repository.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Repository.kt index c79b8fc..38293c3 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Repository.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Repository.kt @@ -1,5 +1,6 @@ -package dev.uatu.sample +package app.folio.data +import app.folio.platform.Platform import kotlinx.coroutines.flow.StateFlow object Repository { diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Session.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Session.kt new file mode 100644 index 0000000..0e0929f --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Session.kt @@ -0,0 +1,6 @@ +package app.folio.data + +data class Session( + val user: String, + val loggedInAt: Long, +) diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Models.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Transaction.kt similarity index 52% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Models.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Transaction.kt index 2661f55..c9f1a54 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Models.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/data/Transaction.kt @@ -1,10 +1,4 @@ -package dev.uatu.sample - -data class Account( - val id: String, - val name: String, - val createdAt: Long, -) +package app.folio.data enum class TxnType { credit, debit } @@ -16,8 +10,3 @@ data class Transaction( val note: String, val createdAt: Long, ) - -data class Session( - val user: String, - val loggedInAt: Long, -) diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/AddAccount.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/account/AddAccountScreen.kt similarity index 67% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/AddAccount.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/account/AddAccountScreen.kt index 3b48ed3..5c6286e 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/AddAccount.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/account/AddAccountScreen.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.feature.account import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -14,37 +14,47 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import dev.uatu.sample.Navigator -import dev.uatu.sample.Repository -import dev.uatu.sample.Route -import dev.uatu.sample.UiState +import app.folio.data.Repository +import app.folio.navigation.Navigator +import app.folio.navigation.Route +import app.folio.ui.BackHandler +import app.folio.ui.component.AppButton +import app.folio.ui.component.BackButton +import app.folio.ui.component.ButtonStyle +import app.folio.ui.component.ErrorText +import app.folio.ui.component.FieldLabel +import app.folio.ui.component.Header +import app.folio.ui.component.Screen +import app.folio.ui.component.TextInput +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.Type @Composable -fun AddAccountPage() { +fun AddAccountScreen() { val t = LocalTokens.current var name by remember { mutableStateOf("") } - val err by UiState.addAccountError.collectAsState() + val err by AddAccountUiState.addAccountError.collectAsState() BackHandler { Navigator.back(Route.Home) } DisposableEffect(Unit) { - onDispose { UiState.addAccountError.value = "" } + onDispose { AddAccountUiState.addAccountError.value = "" } } fun submit() { val trimmed = name.trim() if (trimmed.isEmpty()) { - UiState.addAccountError.value = "Account name is required"; return + AddAccountUiState.addAccountError.value = "Account name is required"; return } if (trimmed.length > 40) { - UiState.addAccountError.value = "Name is too long (max 40 characters)"; return + AddAccountUiState.addAccountError.value = "Name is too long (max 40 characters)"; return } try { Repository.createAccount(trimmed) - UiState.addAccountError.value = "" + AddAccountUiState.addAccountError.value = "" Navigator.replace(Route.Home) } catch (e: IllegalArgumentException) { - UiState.addAccountError.value = e.message ?: "Could not create account" + AddAccountUiState.addAccountError.value = e.message ?: "Could not create account" } } @@ -70,7 +80,7 @@ fun AddAccountPage() { FieldLabel("Account name") TextInput( value = name, - onChange = { name = it; UiState.addAccountError.value = "" }, + onChange = { name = it; AddAccountUiState.addAccountError.value = "" }, placeholder = "e.g. Checking", invalid = err.isNotEmpty(), label = "Account name", diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/account/AddAccountUiState.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/account/AddAccountUiState.kt new file mode 100644 index 0000000..3c78d4e --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/account/AddAccountUiState.kt @@ -0,0 +1,7 @@ +package app.folio.feature.account + +import kotlinx.coroutines.flow.MutableStateFlow + +object AddAccountUiState { + val addAccountError = MutableStateFlow("") +} diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Login.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/auth/LoginScreen.kt similarity index 71% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Login.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/auth/LoginScreen.kt index f0b4a67..b5066bc 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Login.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/auth/LoginScreen.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.feature.auth import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -17,31 +17,43 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import dev.uatu.sample.DEMO_EMAIL -import dev.uatu.sample.DEMO_PASSWORD -import dev.uatu.sample.Repository -import dev.uatu.sample.UiState -import dev.uatu.sample.checkCredentials +import app.folio.data.Repository +import app.folio.ui.component.AppButton +import app.folio.ui.component.ButtonStyle +import app.folio.ui.component.Card +import app.folio.ui.component.ErrorText +import app.folio.ui.component.FieldLabel +import app.folio.ui.component.Screen +import app.folio.ui.component.TextInput +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.Type + +const val DEMO_EMAIL = "demo@folio.app" +const val DEMO_PASSWORD = "ledger123" + +private fun checkCredentials(email: String, password: String): Boolean { + return email.trim().lowercase() == DEMO_EMAIL && password == DEMO_PASSWORD +} @Composable -fun LoginPage(onLoggedIn: (String) -> Unit) { +fun LoginScreen(onLoggedIn: (String) -> Unit) { val t = LocalTokens.current var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } - val err by UiState.loginError.collectAsState() + val err by LoginUiState.loginError.collectAsState() DisposableEffect(Unit) { - onDispose { UiState.loginError.value = "" } + onDispose { LoginUiState.loginError.value = "" } } fun submit() { if (email.isBlank() || password.isEmpty()) { - UiState.loginError.value = "Enter email and password"; return + LoginUiState.loginError.value = "Enter email and password"; return } if (!checkCredentials(email, password)) { - UiState.loginError.value = "Invalid email or password"; return + LoginUiState.loginError.value = "Invalid email or password"; return } - UiState.loginError.value = "" + LoginUiState.loginError.value = "" val user = email.trim().lowercase() Repository.setSession(user) onLoggedIn(user) @@ -57,7 +69,7 @@ fun LoginPage(onLoggedIn: (String) -> Unit) { FieldLabel("Email") TextInput( value = email, - onChange = { email = it; UiState.loginError.value = "" }, + onChange = { email = it; LoginUiState.loginError.value = "" }, placeholder = DEMO_EMAIL, invalid = err.isNotEmpty(), keyboardType = KeyboardType.Email, @@ -69,7 +81,7 @@ fun LoginPage(onLoggedIn: (String) -> Unit) { FieldLabel("Password") TextInput( value = password, - onChange = { password = it; UiState.loginError.value = "" }, + onChange = { password = it; LoginUiState.loginError.value = "" }, placeholder = "••••••••", password = true, invalid = err.isNotEmpty(), diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/auth/LoginUiState.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/auth/LoginUiState.kt new file mode 100644 index 0000000..e2c40e3 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/auth/LoginUiState.kt @@ -0,0 +1,7 @@ +package app.folio.feature.auth + +import kotlinx.coroutines.flow.MutableStateFlow + +object LoginUiState { + val loginError = MutableStateFlow("") +} diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Home.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/home/HomeScreen.kt similarity index 86% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Home.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/home/HomeScreen.kt index 88107c6..cbe68bc 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Home.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/home/HomeScreen.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.feature.home import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -23,16 +23,26 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.runtime.collectAsState -import dev.uatu.sample.Navigator -import dev.uatu.sample.Repository -import dev.uatu.sample.Route -import dev.uatu.sample.balanceOf -import dev.uatu.sample.formatCents -import dev.uatu.sample.initialsOf -import dev.uatu.sample.signedAmount +import app.folio.data.Repository +import app.folio.navigation.Navigator +import app.folio.navigation.Route +import app.folio.platform.balanceOf +import app.folio.platform.formatCents +import app.folio.platform.initialsOf +import app.folio.platform.signedAmount +import app.folio.ui.component.AppButton +import app.folio.ui.component.ButtonStyle +import app.folio.ui.component.EmptyState +import app.folio.ui.component.Header +import app.folio.ui.component.IconButton +import app.folio.ui.component.Screen +import app.folio.ui.icon.Icons +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.RadiusLg +import app.folio.ui.theme.Type @Composable -fun HomePage(user: String, onLogout: () -> Unit) { +fun HomeScreen(user: String, onLogout: () -> Unit) { val t = LocalTokens.current val accounts by Repository.accounts.collectAsState() val txns by Repository.transactions.collectAsState() diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/AddTransaction.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionScreen.kt similarity index 73% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/AddTransaction.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionScreen.kt index ec89ae3..cef4b05 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/AddTransaction.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionScreen.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.feature.ledger import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,17 +15,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.uatu.sample.Navigator -import dev.uatu.sample.Repository -import dev.uatu.sample.Route -import dev.uatu.sample.TxnType -import dev.uatu.sample.UiState -import dev.uatu.sample.parseCents +import app.folio.data.Repository +import app.folio.data.TxnType +import app.folio.navigation.Navigator +import app.folio.navigation.Route +import app.folio.platform.parseCents +import app.folio.ui.BackHandler +import app.folio.ui.component.AppButton +import app.folio.ui.component.BackButton +import app.folio.ui.component.ButtonStyle +import app.folio.ui.component.EmptyState +import app.folio.ui.component.ErrorText +import app.folio.ui.component.FieldLabel +import app.folio.ui.component.Header +import app.folio.ui.component.Screen +import app.folio.ui.component.Segmented +import app.folio.ui.component.TextInput +import app.folio.ui.theme.Type private val AMOUNT_REGEX = Regex("""^\d*(\.\d{0,2})?$""") @Composable -fun AddTransactionPage(accountId: String) { +fun AddTransactionScreen(accountId: String) { val accounts by Repository.accounts.collectAsState() val account = accounts.firstOrNull { it.id == accountId } @@ -50,31 +61,31 @@ fun AddTransactionPage(accountId: String) { var type by remember { mutableStateOf(TxnType.credit) } var amount by remember { mutableStateOf("") } var note by remember { mutableStateOf("") } - val err by UiState.txnError.collectAsState() + val err by AddTransactionUiState.txnError.collectAsState() DisposableEffect(type) { - UiState.txnFormType.value = if (type == TxnType.credit) "credit" else "debit" - onDispose { UiState.txnFormType.value = null } + AddTransactionUiState.txnFormType.value = if (type == TxnType.credit) "credit" else "debit" + onDispose { AddTransactionUiState.txnFormType.value = null } } DisposableEffect(Unit) { - onDispose { UiState.txnError.value = "" } + onDispose { AddTransactionUiState.txnError.value = "" } } fun submit() { val cents = parseCents(amount) if (cents == null) { - UiState.txnError.value = "Enter a valid amount (e.g. 12.34)"; return + AddTransactionUiState.txnError.value = "Enter a valid amount (e.g. 12.34)"; return } if (cents <= 0) { - UiState.txnError.value = "Amount must be greater than zero"; return + AddTransactionUiState.txnError.value = "Amount must be greater than zero"; return } try { Repository.createTransaction(accountId, type, cents, note) - UiState.txnError.value = "" + AddTransactionUiState.txnError.value = "" Navigator.back(Route.Home) } catch (e: IllegalArgumentException) { - UiState.txnError.value = e.message ?: "Could not save transaction" + AddTransactionUiState.txnError.value = e.message ?: "Could not save transaction" } } @@ -104,7 +115,7 @@ fun AddTransactionPage(accountId: String) { selected = if (type == TxnType.credit) 0 else 1, labels = listOf("Credit", "Debit"), onSelect = { - UiState.txnError.value = "" + AddTransactionUiState.txnError.value = "" type = if (it == 0) TxnType.credit else TxnType.debit }, descriptions = listOf("txn_credit", "txn_debit"), @@ -114,7 +125,7 @@ fun AddTransactionPage(accountId: String) { TextInput( value = amount, onChange = { - UiState.txnError.value = "" + AddTransactionUiState.txnError.value = "" if (AMOUNT_REGEX.matches(it) || it.isEmpty()) { amount = it } @@ -133,7 +144,7 @@ fun AddTransactionPage(accountId: String) { TextInput( value = note, onChange = { - UiState.txnError.value = "" + AddTransactionUiState.txnError.value = "" note = it.take(80) }, placeholder = "What's this for?", diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/UiState.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionUiState.kt similarity index 52% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/UiState.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionUiState.kt index 39a77a9..1c6289f 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/UiState.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionUiState.kt @@ -1,10 +1,8 @@ -package dev.uatu.sample +package app.folio.feature.ledger import kotlinx.coroutines.flow.MutableStateFlow -object UiState { - val loginError = MutableStateFlow("") - val addAccountError = MutableStateFlow("") +object AddTransactionUiState { val txnError = MutableStateFlow("") val txnFormType = MutableStateFlow(null) } diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Ledger.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/LedgerScreen.kt similarity index 86% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Ledger.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/LedgerScreen.kt index dd6f4f0..4f34905 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Ledger.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/feature/ledger/LedgerScreen.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.feature.ledger import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -22,17 +22,29 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import dev.uatu.sample.Navigator -import dev.uatu.sample.Repository -import dev.uatu.sample.Route -import dev.uatu.sample.TxnType -import dev.uatu.sample.balanceOf -import dev.uatu.sample.formatCents -import dev.uatu.sample.formatDate -import dev.uatu.sample.signedAmount +import app.folio.data.Repository +import app.folio.navigation.Navigator +import app.folio.navigation.Route +import app.folio.data.TxnType +import app.folio.platform.balanceOf +import app.folio.platform.formatCents +import app.folio.platform.formatDate +import app.folio.platform.signedAmount +import app.folio.ui.BackHandler +import app.folio.ui.component.AppButton +import app.folio.ui.component.BackButton +import app.folio.ui.component.ButtonStyle +import app.folio.ui.component.Card +import app.folio.ui.component.EmptyState +import app.folio.ui.component.Header +import app.folio.ui.component.Screen +import app.folio.ui.icon.Icons +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.RadiusLg +import app.folio.ui.theme.Type @Composable -fun LedgerPage(accountId: String) { +fun LedgerScreen(accountId: String) { val t = LocalTokens.current val accounts by Repository.accounts.collectAsState() val allTxns by Repository.transactions.collectAsState() diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Navigation.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/navigation/Navigator.kt similarity index 74% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Navigation.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/navigation/Navigator.kt index 12db5d3..841dc00 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Navigation.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/navigation/Navigator.kt @@ -1,17 +1,9 @@ -package dev.uatu.sample +package app.folio.navigation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -sealed interface Route { - data object Login : Route - data object Home : Route - data object AddAccount : Route - data class Ledger(val accountId: String) : Route - data class AddTransaction(val accountId: String) : Route -} - object Navigator { private val stack = ArrayDeque().apply { addLast(Route.Home) } private val _current = MutableStateFlow(Route.Home) diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/navigation/Route.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/navigation/Route.kt new file mode 100644 index 0000000..cb8a6e3 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/navigation/Route.kt @@ -0,0 +1,9 @@ +package app.folio.navigation + +sealed interface Route { + data object Login : Route + data object Home : Route + data object AddAccount : Route + data class Ledger(val accountId: String) : Route + data class AddTransaction(val accountId: String) : Route +} diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Format.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/platform/Format.kt similarity index 95% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Format.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/platform/Format.kt index 9e691b2..efbb4d1 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Format.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/platform/Format.kt @@ -1,5 +1,7 @@ -package dev.uatu.sample +package app.folio.platform +import app.folio.data.Transaction +import app.folio.data.TxnType import kotlin.math.absoluteValue private const val CURRENCY = "$" diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Platform.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/platform/Platform.kt similarity index 73% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Platform.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/platform/Platform.kt index 608e13a..dc4e3ce 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/Platform.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/platform/Platform.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.platform expect object Platform { fun now(): Long diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/BackHandler.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/BackHandler.kt similarity index 78% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/BackHandler.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/BackHandler.kt index 830118e..47044cf 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/BackHandler.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/BackHandler.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.ui import androidx.compose.runtime.Composable diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/AppButton.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/AppButton.kt new file mode 100644 index 0000000..d6012e4 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/AppButton.kt @@ -0,0 +1,57 @@ +package app.folio.ui.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.RadiusMd +import app.folio.ui.theme.Type + +enum class ButtonStyle { Primary, Secondary, Ghost } + +@Composable +fun AppButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: ButtonStyle = ButtonStyle.Secondary, + enabled: Boolean = true, + description: String? = null, +) { + val t = LocalTokens.current + val (bg, fg, border) = when (style) { + ButtonStyle.Primary -> Triple(t.text, t.onAccent, t.text) + ButtonStyle.Secondary -> Triple(t.surface2, t.text, t.borderStrong) + ButtonStyle.Ghost -> Triple(t.bg, t.textMuted, t.bg) + } + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(RadiusMd)) + .background(if (enabled) bg else t.surface3) + .border(BorderStroke(1.dp, if (enabled) border else t.border), RoundedCornerShape(RadiusMd)) + .then( + if (description != null) Modifier.semantics { contentDescription = description } + else Modifier + ) + .clickable(enabled = enabled, role = Role.Button, onClick = onClick) + .padding(vertical = 14.dp, horizontal = 16.dp), + contentAlignment = Alignment.Center, + ) { + Text(text, style = Type.button, color = if (enabled) fg else t.textFaint) + } +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Card.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Card.kt new file mode 100644 index 0000000..236633b --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Card.kt @@ -0,0 +1,33 @@ +package app.folio.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.RadiusLg + +@Composable +fun Card( + modifier: Modifier = Modifier, + dashed: Boolean = false, + content: @Composable () -> Unit, +) { + val t = LocalTokens.current + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(RadiusLg)) + .background(if (dashed) t.surface2 else t.surface) + .border(1.dp, t.border, RoundedCornerShape(RadiusLg)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { content() } +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/EmptyState.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/EmptyState.kt new file mode 100644 index 0000000..69d52c8 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/EmptyState.kt @@ -0,0 +1,56 @@ +package app.folio.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.Type + +@Composable +fun EmptyState(title: String, subtitle: String, icon: ImageVector? = null) { + val t = LocalTokens.current + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 48.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (icon != null) { + Box( + modifier = Modifier + .size(56.dp) + .background(t.surface2, CircleShape) + .border(1.dp, t.border, CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = rememberVectorPainter(icon), + contentDescription = null, + tint = t.textMuted, + modifier = Modifier.size(22.dp), + ) + } + Spacer(Modifier.height(2.dp)) + } + Text(title, style = Type.bodyStrong, color = t.text, textAlign = TextAlign.Center) + Text(subtitle, style = Type.caption, color = t.textMuted, textAlign = TextAlign.Center) + } +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/ErrorText.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/ErrorText.kt new file mode 100644 index 0000000..c9322be --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/ErrorText.kt @@ -0,0 +1,21 @@ +package app.folio.ui.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.Type + +@Composable +fun ErrorText(err: String?) { + val t = LocalTokens.current + Text( + err ?: "", + style = Type.caption, + color = t.text, + modifier = Modifier.fillMaxWidth().height(18.dp), + ) +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/FieldLabel.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/FieldLabel.kt new file mode 100644 index 0000000..ce6ea55 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/FieldLabel.kt @@ -0,0 +1,12 @@ +package app.folio.ui.component + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.Type + +@Composable +fun FieldLabel(text: String) { + val t = LocalTokens.current + Text(text.uppercase(), style = Type.label, color = t.textMuted) +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Header.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Header.kt new file mode 100644 index 0000000..58af281 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Header.kt @@ -0,0 +1,48 @@ +package app.folio.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.ScreenPad +import app.folio.ui.theme.Type + +@Composable +fun Header( + title: String, + subtitle: String? = null, + left: @Composable (() -> Unit)? = null, + right: @Composable (() -> Unit)? = null, +) { + val t = LocalTokens.current + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = ScreenPad, end = ScreenPad, top = 14.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (left != null) left() + Column(Modifier.weight(1f)) { + Text( + title, + style = Type.title, + color = t.text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (subtitle != null) { + Text(subtitle, style = Type.caption, color = t.textMuted) + } + } + if (right != null) right() + } +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/IconButton.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/IconButton.kt new file mode 100644 index 0000000..81624e7 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/IconButton.kt @@ -0,0 +1,51 @@ +package app.folio.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.folio.ui.icon.Icons +import app.folio.ui.theme.LocalTokens + +@Composable +fun IconButton( + onClick: () -> Unit, + description: String, + icon: ImageVector, +) { + val t = LocalTokens.current + Box( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(t.surface2) + .border(1.dp, t.border, RoundedCornerShape(10.dp)) + .clickable(onClick = onClick) + .semantics { contentDescription = description }, + contentAlignment = Alignment.Center, + ) { + Icon( + painter = rememberVectorPainter(icon), + contentDescription = null, + tint = t.text, + modifier = Modifier.size(16.dp), + ) + } +} + +@Composable +fun BackButton(onClick: () -> Unit) { + IconButton(onClick = onClick, description = "Back", icon = Icons.Back) +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Screen.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Screen.kt new file mode 100644 index 0000000..222e56b --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Screen.kt @@ -0,0 +1,53 @@ +package app.folio.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.ScreenPad + +@Composable +fun Screen( + header: @Composable (() -> Unit)? = null, + footer: @Composable (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + val t = LocalTokens.current + Column(Modifier.fillMaxSize()) { + if (header != null) header() + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = ScreenPad, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + content() + Spacer(Modifier.height(16.dp)) + } + if (footer != null) { + Box(Modifier.fillMaxWidth().height(1.dp).background(t.border)) + Column( + modifier = Modifier + .fillMaxWidth() + .background(t.bg) + .padding(horizontal = ScreenPad, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + footer() + } + } + } +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Segmented.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Segmented.kt new file mode 100644 index 0000000..89a1ea3 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/Segmented.kt @@ -0,0 +1,64 @@ +package app.folio.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.RadiusMd +import app.folio.ui.theme.RadiusSm +import app.folio.ui.theme.Type + +@Composable +fun Segmented( + selected: Int, + labels: List, + onSelect: (Int) -> Unit, + descriptions: List? = null, +) { + val t = LocalTokens.current + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(RadiusMd)) + .background(t.surface) + .border(1.dp, t.border, RoundedCornerShape(RadiusMd)) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + labels.forEachIndexed { i, label -> + val active = i == selected + val desc = descriptions?.getOrNull(i) + Box( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(RadiusSm)) + .background(if (active) t.surface3 else t.surface) + .semantics { + this.selected = active + if (desc != null) contentDescription = desc + } + .clickable(role = Role.Tab) { onSelect(i) } + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center, + ) { + Text(label, style = Type.button, color = if (active) t.text else t.textMuted) + } + } + } +} diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/TextInput.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/TextInput.kt new file mode 100644 index 0000000..a2ecdd3 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/component/TextInput.kt @@ -0,0 +1,95 @@ +package app.folio.ui.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.folio.FocusTracker +import app.folio.ui.theme.LocalTokens +import app.folio.ui.theme.RadiusMd +import app.folio.ui.theme.Type + +@Composable +fun TextInput( + value: String, + onChange: (String) -> Unit, + placeholder: String = "", + invalid: Boolean = false, + password: Boolean = false, + keyboardType: KeyboardType = KeyboardType.Text, + textAlign: TextAlign = TextAlign.Start, + textStyle: TextStyle = Type.body, + label: String? = null, + description: String? = null, + modifier: Modifier = Modifier, +) { + val t = LocalTokens.current + var focused by remember { mutableStateOf(false) } + val borderColor = if (focused || invalid) t.text else t.border + val bg = if (focused) t.surface2 else t.surface + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(RadiusMd)) + .background(bg) + .border(BorderStroke(1.dp, borderColor), RoundedCornerShape(RadiusMd)) + .padding(horizontal = 14.dp, vertical = 14.dp), + contentAlignment = if (textAlign == TextAlign.Center) Alignment.Center else Alignment.CenterStart, + ) { + if (value.isEmpty() && placeholder.isNotEmpty()) { + Text( + placeholder, + style = textStyle.copy(color = t.textFaint, textAlign = textAlign), + modifier = Modifier.fillMaxWidth(), + ) + } + BasicTextField( + value = value, + onValueChange = onChange, + textStyle = textStyle.copy(color = t.text, textAlign = textAlign), + singleLine = true, + visualTransformation = if (password) PasswordVisualTransformation() else VisualTransformation.None, + keyboardOptions = KeyboardOptions(keyboardType = keyboardType), + cursorBrush = SolidColor(t.text), + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { + focused = it.isFocused + if (description != null) { + if (it.isFocused) FocusTracker.enter(description) + else FocusTracker.leave(description) + } + } + .semantics { + val desc = description ?: label + if (desc != null) contentDescription = desc + if (invalid) stateDescription = "Invalid" + }, + ) + } +} diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Icons.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/icon/Icons.kt similarity index 97% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Icons.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/icon/Icons.kt index de35f36..2f1b183 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Icons.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/icon/Icons.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.ui.icon import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor diff --git a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Theme.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/theme/Theme.kt similarity index 52% rename from examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Theme.kt rename to examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/theme/Theme.kt index 3a12b22..0cc1ef7 100644 --- a/examples/sample-app/composeApp/src/commonMain/kotlin/dev/uatu/sample/ui/Theme.kt +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -6,11 +6,7 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp data class Tokens( val bg: Color = Color(0xFFFFFFFF), @@ -27,20 +23,6 @@ data class Tokens( val LocalTokens = staticCompositionLocalOf { Tokens() } -val Mono: FontFamily = FontFamily.Monospace - -object Type { - val body = TextStyle(fontFamily = Mono, fontSize = 15.sp, fontWeight = FontWeight.Normal) - val bodyStrong = TextStyle(fontFamily = Mono, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) - val title = TextStyle(fontFamily = Mono, fontSize = 20.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.2).sp) - val caption = TextStyle(fontFamily = Mono, fontSize = 12.sp, fontWeight = FontWeight.Normal) - val label = TextStyle(fontFamily = Mono, fontSize = 12.sp, fontWeight = FontWeight.Normal, letterSpacing = 0.7.sp) - val balance = TextStyle(fontFamily = Mono, fontSize = 28.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.3).sp) - val amountInput = TextStyle(fontFamily = Mono, fontSize = 36.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.3).sp) - val button = TextStyle(fontFamily = Mono, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) - val brand = TextStyle(fontFamily = Mono, fontSize = 22.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.2).sp) -} - @Composable fun LedgerTheme(content: @Composable () -> Unit) { @Suppress("UNUSED_VARIABLE") diff --git a/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/theme/Type.kt b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/theme/Type.kt new file mode 100644 index 0000000..44ccbd8 --- /dev/null +++ b/examples/folio/composeApp/src/commonMain/kotlin/app/folio/ui/theme/Type.kt @@ -0,0 +1,20 @@ +package app.folio.ui.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Mono: FontFamily = FontFamily.Monospace + +object Type { + val body = TextStyle(fontFamily = Mono, fontSize = 15.sp, fontWeight = FontWeight.Normal) + val bodyStrong = TextStyle(fontFamily = Mono, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) + val title = TextStyle(fontFamily = Mono, fontSize = 20.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.2).sp) + val caption = TextStyle(fontFamily = Mono, fontSize = 12.sp, fontWeight = FontWeight.Normal) + val label = TextStyle(fontFamily = Mono, fontSize = 12.sp, fontWeight = FontWeight.Normal, letterSpacing = 0.7.sp) + val balance = TextStyle(fontFamily = Mono, fontSize = 28.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.3).sp) + val amountInput = TextStyle(fontFamily = Mono, fontSize = 36.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.3).sp) + val button = TextStyle(fontFamily = Mono, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) + val brand = TextStyle(fontFamily = Mono, fontSize = 22.sp, fontWeight = FontWeight.Bold, letterSpacing = (-0.2).sp) +} diff --git a/examples/sample-app/composeApp/src/commonMain/sqldelight/dev/uatu/sample/db/Ledger.sq b/examples/folio/composeApp/src/commonMain/sqldelight/app/folio/db/Ledger.sq similarity index 100% rename from examples/sample-app/composeApp/src/commonMain/sqldelight/dev/uatu/sample/db/Ledger.sq rename to examples/folio/composeApp/src/commonMain/sqldelight/app/folio/db/Ledger.sq diff --git a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/MainViewController.kt b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/MainViewController.kt similarity index 93% rename from examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/MainViewController.kt rename to examples/folio/composeApp/src/iosMain/kotlin/app/folio/MainViewController.kt index dd2e8df..bb8a853 100644 --- a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/MainViewController.kt +++ b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/MainViewController.kt @@ -1,7 +1,8 @@ -package dev.uatu.sample +package app.folio import androidx.compose.ui.window.ComposeUIViewController -import dev.uatu.sample.ui.IosBackGesture +import app.folio.data.Repository +import app.folio.ui.IosBackGesture import kotlin.native.ObjCName import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.ExperimentalForeignApi diff --git a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/DriverFactory.ios.kt b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/data/DriverFactory.ios.kt similarity index 78% rename from examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/DriverFactory.ios.kt rename to examples/folio/composeApp/src/iosMain/kotlin/app/folio/data/DriverFactory.ios.kt index 1d366c1..598ccfb 100644 --- a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/DriverFactory.ios.kt +++ b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/data/DriverFactory.ios.kt @@ -1,8 +1,8 @@ -package dev.uatu.sample +package app.folio.data import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver -import dev.uatu.sample.db.LedgerDatabase +import app.folio.db.LedgerDatabase actual class DriverFactory { actual fun create(): SqlDriver = diff --git a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/LedgerStore.ios.kt b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/data/LedgerStore.ios.kt similarity index 76% rename from examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/LedgerStore.ios.kt rename to examples/folio/composeApp/src/iosMain/kotlin/app/folio/data/LedgerStore.ios.kt index 514f59a..594de08 100644 --- a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/LedgerStore.ios.kt +++ b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/data/LedgerStore.ios.kt @@ -1,3 +1,3 @@ -package dev.uatu.sample +package app.folio.data actual fun createLedgerStore(): LedgerStore = SqlLedgerStore(DriverFactory()) diff --git a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/Format.ios.kt b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/platform/Format.ios.kt similarity index 94% rename from examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/Format.ios.kt rename to examples/folio/composeApp/src/iosMain/kotlin/app/folio/platform/Format.ios.kt index 4877dc2..c78fc7b 100644 --- a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/Format.ios.kt +++ b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/platform/Format.ios.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.platform import platform.Foundation.NSDate import platform.Foundation.NSDateFormatter diff --git a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/Platform.ios.kt b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/platform/Platform.ios.kt similarity index 91% rename from examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/Platform.ios.kt rename to examples/folio/composeApp/src/iosMain/kotlin/app/folio/platform/Platform.ios.kt index 166f210..6276e14 100644 --- a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/Platform.ios.kt +++ b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/platform/Platform.ios.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.platform import platform.Foundation.NSDate import platform.Foundation.NSUUID diff --git a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/ui/BackHandler.ios.kt b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/ui/BackHandler.ios.kt similarity index 94% rename from examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/ui/BackHandler.ios.kt rename to examples/folio/composeApp/src/iosMain/kotlin/app/folio/ui/BackHandler.ios.kt index 4c02ba8..bb3dcab 100644 --- a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/ui/BackHandler.ios.kt +++ b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/ui/BackHandler.ios.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect diff --git a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/ui/IosBackGesture.kt b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/ui/IosBackGesture.kt similarity index 94% rename from examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/ui/IosBackGesture.kt rename to examples/folio/composeApp/src/iosMain/kotlin/app/folio/ui/IosBackGesture.kt index a04bd46..de1bcc5 100644 --- a/examples/sample-app/composeApp/src/iosMain/kotlin/dev/uatu/sample/ui/IosBackGesture.kt +++ b/examples/folio/composeApp/src/iosMain/kotlin/app/folio/ui/IosBackGesture.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.ui object IosBackGesture { private var nextId = 0 diff --git a/examples/sample-app/composeApp/src/sqlMain/kotlin/dev/uatu/sample/DriverFactory.kt b/examples/folio/composeApp/src/sqlMain/kotlin/app/folio/data/DriverFactory.kt similarity index 80% rename from examples/sample-app/composeApp/src/sqlMain/kotlin/dev/uatu/sample/DriverFactory.kt rename to examples/folio/composeApp/src/sqlMain/kotlin/app/folio/data/DriverFactory.kt index 5c4854c..50b6df4 100644 --- a/examples/sample-app/composeApp/src/sqlMain/kotlin/dev/uatu/sample/DriverFactory.kt +++ b/examples/folio/composeApp/src/sqlMain/kotlin/app/folio/data/DriverFactory.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.data import app.cash.sqldelight.db.SqlDriver diff --git a/examples/sample-app/composeApp/src/sqlMain/kotlin/dev/uatu/sample/SqlLedgerStore.kt b/examples/folio/composeApp/src/sqlMain/kotlin/app/folio/data/SqlLedgerStore.kt similarity index 94% rename from examples/sample-app/composeApp/src/sqlMain/kotlin/dev/uatu/sample/SqlLedgerStore.kt rename to examples/folio/composeApp/src/sqlMain/kotlin/app/folio/data/SqlLedgerStore.kt index 6110535..7357c76 100644 --- a/examples/sample-app/composeApp/src/sqlMain/kotlin/dev/uatu/sample/SqlLedgerStore.kt +++ b/examples/folio/composeApp/src/sqlMain/kotlin/app/folio/data/SqlLedgerStore.kt @@ -1,9 +1,9 @@ -package dev.uatu.sample +package app.folio.data import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOneOrNull -import dev.uatu.sample.db.LedgerDatabase +import app.folio.db.LedgerDatabase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -58,7 +58,7 @@ class SqlLedgerStore(factory: DriverFactory) : LedgerStore { } } -private fun dev.uatu.sample.db.Txns.toDomain(): Transaction = +private fun app.folio.db.Txns.toDomain(): Transaction = Transaction( id = id, accountId = accountId, diff --git a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Snapshot.kt b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/data/Snapshot.kt similarity index 99% rename from examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Snapshot.kt rename to examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/data/Snapshot.kt index 2c1a33b..3bbd0da 100644 --- a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Snapshot.kt +++ b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/data/Snapshot.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.data internal data class Snapshot( val accounts: List, diff --git a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/WebLedgerStore.kt b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/data/WebLedgerStore.kt similarity index 98% rename from examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/WebLedgerStore.kt rename to examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/data/WebLedgerStore.kt index b42c746..f31a5fc 100644 --- a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/WebLedgerStore.kt +++ b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/data/WebLedgerStore.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.data import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/main.kt b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/main.kt similarity index 87% rename from examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/main.kt rename to examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/main.kt index cbd65fd..baaeb83 100644 --- a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/main.kt +++ b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/main.kt @@ -1,8 +1,9 @@ -package dev.uatu.sample +package app.folio import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport -import dev.uatu.sample.ui.WebBackGesture +import app.folio.data.Repository +import app.folio.ui.WebBackGesture import kotlinx.browser.document import kotlinx.browser.window import org.w3c.dom.events.Event diff --git a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Format.wasmJs.kt b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/platform/Format.wasmJs.kt similarity index 97% rename from examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Format.wasmJs.kt rename to examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/platform/Format.wasmJs.kt index 84c2e1f..bc99afc 100644 --- a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Format.wasmJs.kt +++ b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/platform/Format.wasmJs.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.platform private val MONTHS = arrayOf( "Jan", "Feb", "Mar", "Apr", "May", "Jun", diff --git a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Platform.wasmJs.kt b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/platform/Platform.wasmJs.kt similarity index 96% rename from examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Platform.wasmJs.kt rename to examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/platform/Platform.wasmJs.kt index a569ac2..467b5d8 100644 --- a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/Platform.wasmJs.kt +++ b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/platform/Platform.wasmJs.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample +package app.folio.platform import kotlin.random.Random diff --git a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/ui/BackHandler.wasmJs.kt b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/ui/BackHandler.wasmJs.kt similarity index 97% rename from examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/ui/BackHandler.wasmJs.kt rename to examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/ui/BackHandler.wasmJs.kt index 243fa31..747c6b0 100644 --- a/examples/sample-app/composeApp/src/wasmJsMain/kotlin/dev/uatu/sample/ui/BackHandler.wasmJs.kt +++ b/examples/folio/composeApp/src/wasmJsMain/kotlin/app/folio/ui/BackHandler.wasmJs.kt @@ -1,4 +1,4 @@ -package dev.uatu.sample.ui +package app.folio.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect diff --git a/examples/sample-app/composeApp/src/wasmJsMain/resources/index.html b/examples/folio/composeApp/src/wasmJsMain/resources/index.html similarity index 94% rename from examples/sample-app/composeApp/src/wasmJsMain/resources/index.html rename to examples/folio/composeApp/src/wasmJsMain/resources/index.html index 7a40391..dfc1982 100644 --- a/examples/sample-app/composeApp/src/wasmJsMain/resources/index.html +++ b/examples/folio/composeApp/src/wasmJsMain/resources/index.html @@ -3,7 +3,7 @@ - Uatu Sample + Folio