mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 12:07:09 +00:00
refactor(folio): introduce nested KotlinConf-style modules
Split the monolithic :composeApp into :core, :app:shared, :app:ui-components, and :app:androidApp. The old module is still present and remains the source of truth until the next commits remove it; both compile in parallel to keep iOS/Android builds green during the cut-over. Highlights: - :core - SQLDelight schema + LedgerStore + Repository (now an injectable class, not a singleton object). Methods are suspend to match generateAsync = true. - :app:ui-components - design system primitives. IconButton/AppButton APIs revised: label = real contentDescription, testTag = stable selector. Drops the data-carrier description argument. - :app:shared - per-screen ViewModels colocated with screens; pure composables on (state, onEvent); LocalAppComponent CompositionLocal for hand-rolled DI; @Serializable Route. Hosts the iOS framework (baseName Shared). - :app:androidApp - thin Android entry that constructs the DriverFactory and hands it to App(). - gradle/libs.versions.toml centralises versions; settings.gradle.kts enables type-safe project accessors. Deferred to follow-up PRs (per the design discussion): - Metro DI: hand-rolled AppComponent for now; Metro graphs are mostly ceremony for an app this size and add KSP/version risk. - Navigation3: kept the existing Navigator-as-backstack class, injected rather than singleton; nav3 isn't shipping a stable multiplatform artifact for commonMain consumption yet. - :app:webApp + OPFS sqlite worker: web persistence is real new wiring (custom worker on @sqlite.org/sqlite-wasm). Master's WebLedgerStore + Snapshot is being removed by this PR; web stays buildable as a klib but no app-level wasm binary lands here.
This commit is contained in:
1 parent
df44012d3e
commit
5121f777f2
61 files changed
+2492
-6
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
optIn.add("kotlin.js.ExperimentalWasmJsInterop")
|
||||
}
|
||||
|
||||
androidTarget {
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
||||
}
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs { browser() }
|
||||
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(compose.runtime)
|
||||
api(compose.foundation)
|
||||
api(compose.material3)
|
||||
api(compose.ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.folio.ui"
|
||||
compileSdk = 36
|
||||
defaultConfig { minSdk = 24 }
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
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.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
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,
|
||||
testTag: 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 (testTag != null) Modifier.testTag(testTag) 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)
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
+56
@@ -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)
|
||||
}
|
||||
}
|
||||
+21
@@ -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),
|
||||
)
|
||||
}
|
||||
+12
@@ -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)
|
||||
}
|
||||
+48
@@ -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()
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
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.platform.testTag
|
||||
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,
|
||||
label: String,
|
||||
icon: ImageVector,
|
||||
testTag: String? = null,
|
||||
) {
|
||||
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)
|
||||
.then(if (testTag != null) Modifier.testTag(testTag) else Modifier)
|
||||
.semantics { contentDescription = label },
|
||||
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, label = "Back", icon = Icons.Back, testTag = "BackButton")
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
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(
|
||||
modifier: Modifier = Modifier,
|
||||
header: @Composable (() -> Unit)? = null,
|
||||
footer: @Composable (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val t = LocalTokens.current
|
||||
Column(Modifier.fillMaxSize().then(modifier)) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
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.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
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<String>,
|
||||
onSelect: (Int) -> Unit,
|
||||
testTags: List<String>? = 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 tag = testTags?.getOrNull(i)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(RadiusSm))
|
||||
.background(if (active) t.surface3 else t.surface)
|
||||
.then(if (tag != null) Modifier.testTag(tag) else Modifier)
|
||||
.semantics { this.selected = active }
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
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.platform.testTag
|
||||
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.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,
|
||||
testTag: 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 }
|
||||
.then(if (testTag != null) Modifier.testTag(testTag) else Modifier)
|
||||
.semantics {
|
||||
contentDescription = label
|
||||
if (invalid) stateDescription = "Invalid"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package app.folio.ui.icon
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.addPathNodes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object Icons {
|
||||
val Back: ImageVector = strokeIcon("M15 18 L9 12 L15 6")
|
||||
|
||||
val Logout: ImageVector = strokeIcon(
|
||||
"M9 21 H5 A2 2 0 0 1 3 19 V5 A2 2 0 0 1 5 3 H9 " +
|
||||
"M16 17 L21 12 L16 7 " +
|
||||
"M21 12 H9",
|
||||
)
|
||||
|
||||
val Bank: ImageVector = strokeIcon(
|
||||
"M3 6 H21 V19 A2 2 0 0 1 19 21 H5 A2 2 0 0 1 3 19 Z " +
|
||||
"M3 10 H21",
|
||||
)
|
||||
|
||||
val Lines: ImageVector = strokeIcon(
|
||||
"M3 6 H21 " +
|
||||
"M3 12 H21 " +
|
||||
"M3 18 H15",
|
||||
)
|
||||
}
|
||||
|
||||
private fun strokeIcon(path: String): ImageVector =
|
||||
ImageVector.Builder(
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 24f,
|
||||
viewportHeight = 24f,
|
||||
).addPath(
|
||||
pathData = addPathNodes(path),
|
||||
stroke = SolidColor(Color.Black),
|
||||
strokeLineWidth = 2f,
|
||||
strokeLineCap = StrokeCap.Round,
|
||||
strokeLineJoin = StrokeJoin.Round,
|
||||
).build()
|
||||
@@ -0,0 +1,45 @@
|
||||
package app.folio.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.unit.dp
|
||||
|
||||
data class Tokens(
|
||||
val bg: Color = Color(0xFFFFFFFF),
|
||||
val surface: Color = Color(0xFFFAFAFA),
|
||||
val surface2: Color = Color(0xFFF4F4F4),
|
||||
val surface3: Color = Color(0xFFEBEBEB),
|
||||
val border: Color = Color(0xFFE7E7E7),
|
||||
val borderStrong: Color = Color(0xFFD6D6D6),
|
||||
val text: Color = Color(0xFF000000),
|
||||
val textMuted: Color = Color(0xFF6B6B6B),
|
||||
val textFaint: Color = Color(0xFF9A9A9A),
|
||||
val onAccent: Color = Color(0xFFFFFFFF),
|
||||
)
|
||||
|
||||
val LocalTokens = staticCompositionLocalOf { Tokens() }
|
||||
|
||||
@Composable
|
||||
fun LedgerTheme(content: @Composable () -> Unit) {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val dark = isSystemInDarkTheme()
|
||||
val scheme = lightColorScheme(
|
||||
background = Color(0xFFFFFFFF),
|
||||
surface = Color(0xFFFAFAFA),
|
||||
primary = Color(0xFF000000),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
)
|
||||
MaterialTheme(colorScheme = scheme) { content() }
|
||||
}
|
||||
|
||||
val FrameWidth = 420.dp
|
||||
val FrameHeight = 900.dp
|
||||
val ScreenPad = 20.dp
|
||||
val RadiusSm = 8.dp
|
||||
val RadiusMd = 12.dp
|
||||
val RadiusLg = 16.dp
|
||||
val RadiusPill = 999.dp
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user