diff --git a/compose/api/compose.klib.api b/compose/api/compose.klib.api index 7c11078..df66b19 100644 --- a/compose/api/compose.klib.api +++ b/compose/api/compose.klib.api @@ -687,6 +687,10 @@ final object app.tinyui.components.generated/BuiltinSchemas { // app.tinyui.comp final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Box.|(){}[0] final val Button // app.tinyui.components.generated/BuiltinSchemas.Button|{}Button[0] final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Button.|(){}[0] + final val Checkbox // app.tinyui.components.generated/BuiltinSchemas.Checkbox|{}Checkbox[0] + final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Checkbox.|(){}[0] + final val CodeField // app.tinyui.components.generated/BuiltinSchemas.CodeField|{}CodeField[0] + final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.CodeField.|(){}[0] final val Column // app.tinyui.components.generated/BuiltinSchemas.Column|{}Column[0] final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Column.|(){}[0] final val Divider // app.tinyui.components.generated/BuiltinSchemas.Divider|{}Divider[0] @@ -697,10 +701,14 @@ final object app.tinyui.components.generated/BuiltinSchemas { // app.tinyui.comp final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.LazyColumn.|(){}[0] final val Loading // app.tinyui.components.generated/BuiltinSchemas.Loading|{}Loading[0] final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Loading.|(){}[0] + final val Option // app.tinyui.components.generated/BuiltinSchemas.Option|{}Option[0] + final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Option.|(){}[0] final val RadioButton // app.tinyui.components.generated/BuiltinSchemas.RadioButton|{}RadioButton[0] final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.RadioButton.|(){}[0] final val Row // app.tinyui.components.generated/BuiltinSchemas.Row|{}Row[0] final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Row.|(){}[0] + final val Select // app.tinyui.components.generated/BuiltinSchemas.Select|{}Select[0] + final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Select.|(){}[0] final val Spacer // app.tinyui.components.generated/BuiltinSchemas.Spacer|{}Spacer[0] final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.Spacer.|(){}[0] final val Text // app.tinyui.components.generated/BuiltinSchemas.Text|{}Text[0] diff --git a/compose/src/androidMain/kotlin/app/tinyui/Platform.android.kt b/compose/src/androidMain/kotlin/app/tinyui/Platform.android.kt index 4cb9935..0625a02 100644 --- a/compose/src/androidMain/kotlin/app/tinyui/Platform.android.kt +++ b/compose/src/androidMain/kotlin/app/tinyui/Platform.android.kt @@ -7,6 +7,7 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import java.util.Locale import java.util.TimeZone +import androidx.compose.ui.text.input.PlatformImeOptions import okio.FileSystem internal actual fun platformInfo(): Map = mapOf( @@ -25,6 +26,9 @@ internal actual fun zoneOffsetMinutes(zone: String, epochMs: Long): Int? = internal actual fun plainTextClipEntry(text: String): ClipEntry = ClipEntry(ClipData.newPlainText(null, text)) +// Android autofill takes the content type from semantics (the component sets it), so nothing goes through the IME options +internal actual fun oneTimeCodeImeOptions(oneTimeCode: Boolean): PlatformImeOptions? = null + internal actual val platformFileSystem: FileSystem = FileSystem.SYSTEM internal actual fun defaultHttpClient(): HttpClient = HttpClient(OkHttp) diff --git a/compose/src/commonMain/kotlin/app/tinyui/HostInterfaces.kt b/compose/src/commonMain/kotlin/app/tinyui/HostInterfaces.kt index 6c5a921..de02316 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/HostInterfaces.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/HostInterfaces.kt @@ -1,6 +1,7 @@ package app.tinyui import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.text.input.PlatformImeOptions import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.update @@ -145,3 +146,9 @@ internal expect fun systemTimeZone(): String internal expect fun zoneOffsetMinutes(zone: String, epochMs: Long): Int? internal expect fun plainTextClipEntry(text: String): ClipEntry + +/** + * iOS reads the text content type only from here, never from semantics, and otherwise derives it from the keyboard: + * NumberPassword would make a code field a password field. Null where the platform has nothing to say (Android). + */ +internal expect fun oneTimeCodeImeOptions(oneTimeCode: Boolean): PlatformImeOptions? diff --git a/compose/src/commonMain/kotlin/app/tinyui/PageHost.kt b/compose/src/commonMain/kotlin/app/tinyui/PageHost.kt index bb8dd0d..b33a8fa 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/PageHost.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/PageHost.kt @@ -125,6 +125,9 @@ class PageHost internal constructor( private set private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + // the engine lock is fair, but entries launched on a multi-threaded pool can reach it out of order: one lane keeps + // them in call order up to the lock (onChange before onComplete, onFocusChange before onCommit) + private val entryLane = Dispatchers.Default.limitedParallelism(1) private val timers = HashMap() private val subscriptions = HashMap() private val storeVersions = HashMap() @@ -205,7 +208,7 @@ class PageHost internal constructor( private fun entry(name: String, vararg args: JsValue, before: () -> Unit = {}, skipWhen: () -> Boolean = { false }) { if (failure != null) return - scope.launch { + scope.launch(entryLane) { try { withTimeout(entryTimeoutMs) { runtime.withEngine { diff --git a/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt b/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt index 81f97bd..7571d38 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt @@ -1,25 +1,39 @@ package app.tinyui.components +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.DividerDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LoadingIndicatorDefaults import androidx.compose.material3.LocalContentColor @@ -43,15 +57,28 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -62,6 +89,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import app.tinyui.components.generated.BuiltinSchemas +import app.tinyui.oneTimeCodeImeOptions import app.tinyui.node.UINode import app.tinyui.schema.Commands import app.tinyui.schema.IconValue @@ -92,8 +120,12 @@ fun ComponentRegistry.registerBuiltins(): ComponentRegistry = apply { } register(BuiltinSchemas.Text) { scope -> val maxLines = scope.get("maxLines")?.toInt()?.takeIf { it > 0 } ?: Int.MAX_VALUE + val raw = scope.get("text") ?: "" + val primary = MaterialTheme.colorScheme.primary + // without a handler the brackets are just text: a page that never asked for links never gets them + val text = if (scope.has("onLinkClick")) remember(raw, primary) { linkedText(raw, primary) { id -> scope.dispatch("onLinkClick", payload("id", id)) } } else AnnotatedString(raw) Text( - text = scope["text"] ?: "", + text = text, style = scope.get("style")?.let { Theme.textStyle(MaterialTheme.typography, it) } ?: LocalTextStyle.current, color = scope.color("color") ?: Color.Unspecified, fontSize = scope["fontSize"] ?: TextUnit.Unspecified, @@ -123,7 +155,19 @@ fun ComponentRegistry.registerBuiltins(): ComponentRegistry = apply { modifier = scope.modifier(clickable = false), ) } + register(BuiltinSchemas.Checkbox) { scope -> + Checkbox( + checked = scope.get("checked") == true, + onCheckedChange = if (scope.has("onClick")) ({ scope.dispatch("onClick") }) else null, + enabled = scope.get("enabled") ?: true, + modifier = scope.modifier(clickable = false), + ) + } register(BuiltinSchemas.TextField) { scope -> TextFieldComponent(scope) } + register(BuiltinSchemas.CodeField) { scope -> CodeFieldComponent(scope) } + register(BuiltinSchemas.Select) { scope -> SelectComponent(scope) } + // data for its Select, never drawn + register(BuiltinSchemas.Option) { } register(BuiltinSchemas.LazyColumn) { scope -> LazyColumnComponent(scope) } register(BuiltinSchemas.Spacer) { scope -> Spacer(scope.modifier()) } register(BuiltinSchemas.Icon) { scope -> @@ -171,16 +215,27 @@ private fun boxAlignment(name: String?): Alignment = when (name) { else -> Alignment.TopStart } -/** Text and cursor stay here; JS gets `onChange` / `onCommit` and sends `setText` / `focus` / `blur` (docs/adr-004 §3.3). */ +/** + * Text and cursor stay here; JS gets `onChange` / `onCommit` / `onFocusChange` and sends `setText` / `focus` / `blur` + * (docs/adr-004 §3.3). Error, supporting text, prefix and trailing icon are looks the page drives through props. + */ @Composable private fun TextFieldComponent(scope: NodeScope) { - var text by remember { mutableStateOf(scope.get("initialText") ?: "") } + val maxLength = scope.get("maxLength")?.toInt()?.takeIf { it > 0 } + fun clip(t: String) = clipTo(t, maxLength) + var text by remember { mutableStateOf(clip(scope.get("initialText") ?: "")) } + var focused by remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val commit = { if (scope.has("onCommit")) scope.dispatch("onCommit", payload("text", text)) } + // a lowered limit applies to what is already there, reported like any other change + LaunchedEffect(maxLength) { + val t = clip(text) + if (t != text) { text = t; if (scope.has("onChange")) scope.dispatch("onChange", payload("text", t)) } + } scope.Commands { command -> when (command.name) { - "setText" -> { text = command.args["text"] as? String ?: ""; if (scope.has("onChange")) scope.dispatch("onChange", payload("text", text)) } + "setText" -> { text = clip(command.args["text"] as? String ?: ""); if (scope.has("onChange")) scope.dispatch("onChange", payload("text", text)) } "focus" -> focusRequester.requestFocus() "blur" -> focusManager.clearFocus() } @@ -189,18 +244,167 @@ private fun TextFieldComponent(scope: NodeScope) { "number" -> KeyboardType.Number; "email" -> KeyboardType.Email; "phone" -> KeyboardType.Phone; "password" -> KeyboardType.Password else -> KeyboardType.Text } + val secure = scope.get("secure") ?: (keyboard == KeyboardType.Password) + val next = scope.get("imeAction") == "next" + val prefix = scope.get("prefixText")?.takeIf { it.isNotEmpty() } + val trailing = scope.get("trailingIcon") OutlinedTextField( value = text, - onValueChange = { text = it; if (scope.has("onChange")) scope.dispatch("onChange", payload("text", it)) }, + onValueChange = { val t = clip(it); if (t != text) { text = t; if (scope.has("onChange")) scope.dispatch("onChange", payload("text", t)) } }, placeholder = scope.get("placeholder")?.takeIf { it.isNotEmpty() }?.let { { Text(it) } }, + // the leading slot rather than M3 `prefix`: that one hides while the field is empty and unfocused, and cannot take a tap + leadingIcon = prefix?.let { p -> + { + val tap = if (scope.has("onPrefixClick")) Modifier.clickable { scope.dispatch("onPrefixClick") } else Modifier + Text(p, modifier = tap.padding(start = 16.dp, end = 8.dp)) + } + }, + trailingIcon = trailing?.let { icon -> + { + if (scope.has("onTrailingClick")) IconButton(onClick = { scope.dispatch("onTrailingClick") }) { Icon(icon.vector, contentDescription = null) } + else Icon(icon.vector, contentDescription = null) + } + }, + isError = scope.get("error") == true, + supportingText = scope.get("supportingText")?.takeIf { it.isNotEmpty() }?.let { { Text(it) } }, singleLine = scope.get("singleLine") ?: true, - keyboardOptions = KeyboardOptions(keyboardType = keyboard, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { commit() }), - visualTransformation = if (keyboard == KeyboardType.Password) PasswordVisualTransformation() else VisualTransformation.None, - modifier = scope.modifier(clickable = false).focusRequester(focusRequester).onFocusChanged { if (!it.isFocused) commit() }, + keyboardOptions = KeyboardOptions(keyboardType = keyboard, imeAction = if (next) ImeAction.Next else ImeAction.Done), + // moving focus away commits through onFocusChanged; commit here only when there was nowhere to go + keyboardActions = KeyboardActions(onDone = { commit() }, onNext = { if (!focusManager.moveFocus(FocusDirection.Next)) commit() }), + visualTransformation = if (secure) PasswordVisualTransformation() else VisualTransformation.None, + modifier = scope.modifier(clickable = false).focusRequester(focusRequester).onFocusChanged { + // the first callback reports the initial unfocused state: not a change, not a focus loss + if (it.isFocused == focused) return@onFocusChanged + focused = it.isFocused + if (scope.has("onFocusChange")) scope.dispatch("onFocusChange", buildJsonObject { put("focused", focused) }.toString()) + if (!focused) commit() + }, + ) +} + +/** + * Digit cells drawn over one invisible input, so focus and the IME never hop between fields across the bridge + * (docs/adr-004 §3.3); `onComplete` fires each time the last cell gets filled. + */ +@Composable +private fun CodeFieldComponent(scope: NodeScope) { + val length = (scope.get("length")?.toInt() ?: 6).coerceIn(1, MAX_CODE_LENGTH) + var value by remember { mutableStateOf(TextFieldValue("")) } + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + var focused by remember { mutableStateOf(false) } + // [force]: commands report onChange even when nothing changed, like TextField.setText + fun update(raw: String, force: Boolean = false) { + val t = raw.filter { it in '0'..'9' }.take(length) + val changed = t != value.text + value = TextFieldValue(t, selection = TextRange(t.length)) + if (!changed && !force) return + if (scope.has("onChange")) scope.dispatch("onChange", payload("text", t)) + if (changed && t.length == length && scope.has("onComplete")) scope.dispatch("onComplete", payload("text", t)) + } + // fewer cells drop the digits that no longer fit at once, rather than keeping them hidden until the next edit + LaunchedEffect(length) { if (value.text.length > length) update(value.text) } + scope.Commands { command -> + when (command.name) { + "setText" -> update(command.args["text"] as? String ?: "", force = true) + "clear" -> update("", force = true) + "focus" -> focusRequester.requestFocus() + } + } + val secure = scope.get("secure") == true + val error = scope.get("error") == true + val oneTimeCode = scope.get("autofill") == "oneTimeCode" + val colors = MaterialTheme.colorScheme + BasicTextField( + value = value, + onValueChange = { update(it.text) }, + enabled = scope.get("enabled") ?: true, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done, platformImeOptions = oneTimeCodeImeOptions(oneTimeCode)), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + // invisible, but its semantics are what a screen reader reads: masked like the cells + visualTransformation = if (secure) PasswordVisualTransformation() else VisualTransformation.None, + modifier = scope.modifier(clickable = false).focusRequester(focusRequester).onFocusChanged { focused = it.isFocused } + .then(if (oneTimeCode) Modifier.semantics { contentType = ContentType.SmsOtpCode } else Modifier), + decorationBox = { inner -> + Box { + // the field below is the one accessible node; the cells would read the code a second time + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.clearAndSetSemantics {}) { + for (i in 0 until length) { + val digit = value.text.getOrNull(i) + val current = focused && i == minOf(value.text.length, length - 1) + val outline = when { error -> colors.error; current -> colors.primary; else -> colors.outline } + Box( + contentAlignment = Alignment.Center, + // the minimum keeps cells usable where weight has no space to share (a scrolling Row) + modifier = Modifier.weight(1f).widthIn(min = 40.dp).height(56.dp).border(if (current || error) 2.dp else 1.dp, outline, RoundedCornerShape(8.dp)), + ) { + if (digit != null) Text(if (secure) "\u2022" else digit.toString(), style = MaterialTheme.typography.headlineSmall) + } + } + } + // the real input stays composed (iOS keeps its text session on it) but never shows + Box(Modifier.matchParentSize().alpha(0f)) { inner() } + } + }, ) } +/** Choices are the `Option` children, read as data; the selection is the page's prop. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SelectComponent(scope: NodeScope) { + val options = scope.node.children.filter { it.type == "Option" }.map { (it.props["value"] as? String ?: "") to (it.props["text"] as? String ?: "") } + val selected = scope.get("value") + val enabled = scope.get("enabled") ?: true + var expanded by remember { mutableStateOf(false) } + // disabled while open: the menu goes, so nothing can be picked from it any more + if (!enabled && expanded) expanded = false + ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { if (enabled) expanded = it }, modifier = scope.modifier(clickable = false)) { + OutlinedTextField( + value = options.firstOrNull { it.first == selected }?.second ?: "", + onValueChange = {}, + readOnly = true, + enabled = enabled, + singleLine = true, + label = scope.get("label")?.takeIf { it.isNotEmpty() }?.let { { Text(it) } }, + placeholder = scope.get("placeholder")?.takeIf { it.isNotEmpty() }?.let { { Text(it) } }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled).fillMaxWidth(), + ) + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + for ((value, text) in options) { + DropdownMenuItem(text = { Text(text) }, enabled = enabled, onClick = { expanded = false; if (scope.has("onSelect")) scope.dispatch("onSelect", payload("value", value)) }) + } + } + } +} + +private const val MAX_CODE_LENGTH = 12 + +/** At most [max] UTF-16 units, never ending inside a surrogate pair (an emoji keeps both halves or neither). */ +internal fun clipTo(text: String, max: Int?): String { + if (max == null || text.length <= max) return text + val end = if (text[max - 1].isHighSurrogate()) max - 1 else max + return text.substring(0, end) +} + +private val LINK = Regex("""\[([^\]]+)]\(([^)\s]+)\)""") + +/** The `[label](id)` spans of [text] as links in [color]; everything else, stray brackets included, stays literal. */ +internal fun linkedText(text: String, color: Color, onClick: (String) -> Unit): AnnotatedString = buildAnnotatedString { + var at = 0 + for (m in LINK.findAll(text)) { + append(text.substring(at, m.range.first)) + val id = m.groupValues[2] + pushLink(LinkAnnotation.Clickable(tag = id, styles = TextLinkStyles(SpanStyle(color = color))) { onClick(id) }) + append(m.groupValues[1]) + pop() + at = m.range.last + 1 + } + append(text.substring(at)) +} + /** Rows are the node's children; virtualisation only in composition (docs/adr-003 §3.6). */ @Composable private fun LazyColumnComponent(scope: NodeScope) { diff --git a/compose/src/commonMain/kotlin/app/tinyui/components/generated/BuiltinSchemas.kt b/compose/src/commonMain/kotlin/app/tinyui/components/generated/BuiltinSchemas.kt index f590a46..5c6fc82 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/components/generated/BuiltinSchemas.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/components/generated/BuiltinSchemas.kt @@ -67,6 +67,7 @@ object BuiltinSchemas { ), events = mapOf( "onClick" to emptyMap(), + "onLinkClick" to mapOf("id" to FieldSpec.Str), ), commands = mapOf( ), @@ -112,10 +113,20 @@ object BuiltinSchemas { "placeholder" to PropSpec.Str(default = "", required = false, initial = false), "singleLine" to PropSpec.Bool(default = true, required = false, initial = false), "keyboard" to PropSpec.Enum(values = setOf("text", "number", "email", "phone", "password"), default = "text", required = false, initial = false), + "secure" to PropSpec.Bool(default = null, required = false, initial = false), + "error" to PropSpec.Bool(default = false, required = false, initial = false), + "supportingText" to PropSpec.Str(default = null, required = false, initial = false), + "prefixText" to PropSpec.Str(default = null, required = false, initial = false), + "trailingIcon" to PropSpec.IconSpec(required = false, initial = false), + "imeAction" to PropSpec.Enum(values = setOf("done", "next"), default = "done", required = false, initial = false), + "maxLength" to PropSpec.Num(default = 0.0, required = false, initial = false), ), events = mapOf( "onChange" to mapOf("text" to FieldSpec.Str), "onCommit" to mapOf("text" to FieldSpec.Str), + "onFocusChange" to mapOf("focused" to FieldSpec.Bool), + "onTrailingClick" to emptyMap(), + "onPrefixClick" to emptyMap(), ), commands = mapOf( "setText" to mapOf("text" to FieldSpec.Str), @@ -213,7 +224,75 @@ object BuiltinSchemas { layout = true, ) - val all: List = listOf(Column, Row, Box, Text, Button, RadioButton, TextField, LazyColumn, Spacer, Icon, Loading, BottomSheet, Divider) + val Checkbox = ComponentSchema( + type = "Checkbox", + props = mapOf( + "checked" to PropSpec.Bool(default = null, required = true, initial = false), + "enabled" to PropSpec.Bool(default = true, required = false, initial = false), + ), + events = mapOf( + "onClick" to emptyMap(), + ), + commands = mapOf( + ), + children = false, + layout = true, + ) + + val CodeField = ComponentSchema( + type = "CodeField", + props = mapOf( + "length" to PropSpec.Num(default = 6.0, required = false, initial = false), + "secure" to PropSpec.Bool(default = false, required = false, initial = false), + "enabled" to PropSpec.Bool(default = true, required = false, initial = false), + "error" to PropSpec.Bool(default = false, required = false, initial = false), + "autofill" to PropSpec.Enum(values = setOf("none", "oneTimeCode"), default = "none", required = false, initial = false), + ), + events = mapOf( + "onChange" to mapOf("text" to FieldSpec.Str), + "onComplete" to mapOf("text" to FieldSpec.Str), + ), + commands = mapOf( + "setText" to mapOf("text" to FieldSpec.Str), + "focus" to emptyMap(), + "clear" to emptyMap(), + ), + children = false, + layout = true, + ) + + val Select = ComponentSchema( + type = "Select", + props = mapOf( + "value" to PropSpec.Str(default = null, required = false, initial = false), + "placeholder" to PropSpec.Str(default = "", required = false, initial = false), + "label" to PropSpec.Str(default = null, required = false, initial = false), + "enabled" to PropSpec.Bool(default = true, required = false, initial = false), + ), + events = mapOf( + "onSelect" to mapOf("value" to FieldSpec.Str), + ), + commands = mapOf( + ), + children = true, + layout = true, + ) + + val Option = ComponentSchema( + type = "Option", + props = mapOf( + "value" to PropSpec.Str(default = null, required = true, initial = false), + "text" to PropSpec.Str(default = null, required = true, initial = false), + ), + events = mapOf( + ), + commands = mapOf( + ), + children = false, + layout = false, + ) + + val all: List = listOf(Column, Row, Box, Text, Button, RadioButton, TextField, LazyColumn, Spacer, Icon, Loading, BottomSheet, Divider, Checkbox, CodeField, Select, Option) /** Theme token names the schema DSL knows; Theme.kt must resolve each (checked by test). */ val colorTokens: Set = setOf("primary", "onPrimary", "primaryContainer", "onPrimaryContainer", "inversePrimary", "secondary", "onSecondary", "secondaryContainer", "onSecondaryContainer", "tertiary", "onTertiary", "tertiaryContainer", "onTertiaryContainer", "background", "onBackground", "surface", "onSurface", "surfaceVariant", "onSurfaceVariant", "surfaceTint", "inverseSurface", "inverseOnSurface", "error", "onError", "errorContainer", "onErrorContainer", "outline", "outlineVariant", "scrim", "surfaceBright", "surfaceDim", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "surfaceContainerLow", "surfaceContainerLowest", "primaryFixed", "primaryFixedDim", "onPrimaryFixed", "onPrimaryFixedVariant", "secondaryFixed", "secondaryFixedDim", "onSecondaryFixed", "onSecondaryFixedVariant", "tertiaryFixed", "tertiaryFixedDim", "onTertiaryFixed", "onTertiaryFixedVariant") diff --git a/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt b/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt index fad0613..b1a00ea 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/generated/RuntimeBytecode.kt @@ -4,7 +4,7 @@ package app.tinyui.generated /** Base64 of the runtime modules' bytecode, split to fit JVM string constants. */ internal object RuntimeBytecode { val core: Array = arrayOf( - "UUpLQjQAAAABAAAAMDRiZTI0NjAwMTU5OWY1OTk1ZmEyZjJkOGM5MWEwZjE5OGQzZjM0YwWdAhZ0aW55dWktY29yZRZCb3R0b21TaGVldAZCb3gMQnV0dG9uDENvbHVtbg5EaXZpZGVyBkZvchBGcmFnbWVudBJIb3N0RXJyb3IISWNvbhRMYXp5Q29sdW1uDkxvYWRpbmcWUmFkaW9CdXR0b24GUm93CFNob3cMU3BhY2VyCFRleHQSVGV4dEZpZWxkDlZFUlNJT04MZWZmZWN0AmgQaW50ZXJuYWwQbWFuaWZlc3QIbWVtbxRvYnNlcnZhYmxlEm9uQ2xlYW51cBZwYWdlVmlzaWJsZQZyZWYQcmVzb3VyY2UMc2lnbmFsCnRodW5rDnVudHJhY2sMdW53cmFwEl9fZGVmUHJvcBBfX2V4cG9ydBpjdXJyZW50RWZmZWN0GGN1cnJlbnRPd25lcg5wZW5kaW5nBnJhbiRNQVhfUlVOU19QRVJfRkxVU0gWY3JlYXRlT3duZXIYcnVuV2l0aE93bmVyGGRpc3Bvc2VPd25lchhpbnNpZGVSZW5kZXIQdHJhY2tpbmcUcnVuUGVuZGluZyRjcmVhdGVFZmZlY3RSZWNvcmQScnVuRWZmZWN0GmRpc3Bvc2VFZmZlY3QWdW5zdWJzY3JpYmUWcnVuQ2xlYW51cHMKdHJhY2sMbm90aWZ5DkNPTlRST0wKVGh1bmsOcGF0Y2hlcxBoYW5kbGVycwxuZXh0SWQQcmVzZXRJZHMIU2xvdBpjYWxsQ29tcG9uZW50GmNyZWF0ZUVsZW1lbnQQYmluZFByb3AMc2NhbGFyDmZsYXR0ZW4acmVuZGVySW5Pd25lchBwZW5kaW5nMhBuZXh0Q2JJZApxdWVyeQhjYWxsCHNlbmQMcmVwb3J0HHJlc29sdmVQZW5kaW5nGnJlamVjdFBlbmRpbmcaY2FuY2VsUGVuZGluZxhhbGxvY2F0ZUNiSWQecmVnaXN0ZXJQZW5kaW5nFHNldFRpbWVvdXQYY2xlYXJUaW1lb3V0HGluc3RhbGxHbG9iYWxzCHJvb3QOY3VycmVudA52aXNpYmxlFHNldFZpc2libGUMdG9waWNzDG9uRW1pdAxsaXN0ZW4OZ3VhcmRlZA5lbnRyaWVzDmluc3RhbGwGUkFXCEtFWVMOcHJveGllcxZzdWJzY3JpYmVycxR1bndyYXBEZWVwGnVud3JhcFNoYWxsb3cWaXNXcmFwcGFibGUId3JhcAhzdWJzDm9ic2VydmUOY2hhbmdlZA5pc0luZGV4EmhhbmRsZXJzMiBpbnRlcm5hbF9leHBvcnRzGF9faG9zdF9hcHBseRhfX2hvc3RfcXVlcnkWX19ob3N0X2NhbGwWX19ob3N0X3NlbmQaX19ob3N0X3JlcG9ydA5SZWZsZWN0DDAuMTMuMBx0aW55dWkuY29udHJvbAptb3VudAxpbnNlcnQIbW92ZQxyZW1vdmUMb2Zmc2V0DGFzc2lnbhRjb21wb25lbnRzGGNhcGFiaWxpdGllcw51bm1vdW50EGRpc3BhdGNoCGVtaXQKZmx1c2gUdGlueXVpLnJhdxZ0aW55dWkua2V5cwZhbGwOZWZmZWN0cxBjbGVhbnVwcxBkaXNwb3NlZApvd25lcgRmbhJwcmV2T3duZXIUcHJldkVmZmVjdAJlAmMMc3BsaWNlDnJldmVyc2UIaW5pdApzdWJzMgJ2DmVmZmVjdDICbQpkaXJ0eQhwdXNobG9uQ2xlYW51cCgpIGNhbGxlZCBvdXRzaWRlIGEgc3luY2hyb25vdXMgcmVuZGVyIHBlcmlvZAhwcmV2AmkMcXVldWVkCHJ1bnN8dXBkYXRlIGxvb3A6IGFuIGVmZmVjdCByZS1yYW4gbW9yZSB0aGFuIDEwMCB0aW1lcyBpbiBvbmUgZmx1c2hmZWZmZWN0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCGRlcHMMcGFyZW50EGVudHJpZXMyEHBvc2l0aW9uCm5vZGVzCG5vZGUEYXQEdG8OaW5kZXhPZixtb3ZlOiBub2RlIG5vdCBpbiBzbG90MHJlbW92ZTogbm9kZSBub3QgaW4gc2xvdAJyAm4EaWQGY21kCGFyZ3NecmVmLmNtZCgpIGJlZm9yZSB0aGUgcmVmIHdhcyBhdHRhY2hlZCB0byBhIG5vZGUCeBJGcmFnbWVudDIKcHJvcHMQY2hpbGRyZW4IdHlwZVxoKCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kAnAGa2V5DHJlc3VsdAh3aGF0fGEgUHJvbWlzZSAoYXN5bmMgY29tcG9uZW50cyBhcmUgbm90IHN1cHBvcnRlZDsgdXNlIHJlc291cmNlKCkpDmlzQXJyYXlUc2V2ZXJhbCBub2RlcyAod3JhcCB0aGVtIGluIG9uZSBjb250YWluZXIpTmEgRm9yIC8gU2hvdyAod3JhcCBpdCBpbiBvbmUgY29udGFpbmVyKRRjb21wb25lbnQgFjxhbm9ueW1vdXM+RiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlLCBnb3QgAmsKY2hpbGQIdGVzdAI8AiAoPiBtdXN0IGJlIGEgZnVuY3Rpb24COghsYXN0CmZpcnN0AnRWPiBpcyBhIGZ1bmN0aW9uOyBkaWQgeW91IGZvcmdldCB0byBjYWxsIGl0P6oBPiBtdXN0IGJlIGEgc3RyaW5nLCBudW1iZXIsIGJvb2xlYW4gb3IgbnVsbDsgcHJvcHMgZG8gbm90IGNyb3NzIHRoZSBicmlkZ2UgYXMgb2JqZWN0cwZvdXQMcmVuZGVyOiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlCGNvZGUOZGV0YWlscw5oZWFkZXJzCGJvZHkScGF0Y2hKc29uCnBhcnNlEnN0cmluZ2lmeQhjYklkCHNpdGUIa2luZAxkZXRhaWwUcmVzdWx0SnNvbhRFX0JBRF9KU09OMmhvc3QgcmVzdWx0IGlzIG5vdCBKU09OOiASZXJyb3JKc29uCmVycm9yIGhvc3QgY2FsbCBmYWlsZWQEbXMcdGltZXIuc2NoZWR1bGUYdGltZXIuY2FuY2VsAmcKdG9waWNmb25FbWl0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCmVudHJ5BmVycgRFMQhwYWdlEnByb3BzSnNvbhBob3N0SnNvbihtb3VudCgpIGNhbGxlZCB0d2ljZU5wYWdlIG11c3QgcmV0dXJuIGV4YWN0bHkgb25lIG5vZGUsIGdvdCASYSBQcm9taXNlCmNsZWFyDG5vZGVJZApldmVudBZwYXlsb2FkSnNvbg5oYW5kbGVyDnBheWxvYWQIanNvbhBfX3Rpbnl1aVpvYnNlcnZhYmxlKCkgdGFrZXMgYSBwbGFpbiBvYmplY3Qgb3IgYW4gYXJyYXkIc2VlbgJ1CnByb3RvEGlz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ExYQDQ8RDgcELwoADEIGAQABAAECAQMBEwH+BQEAQOgFABIAkggBGgCWCAIaAGQAAOgCKWQBAL4A7Q5kAgAJ7SnmAw+ZBhMDERwDCAMbDg0NFhQADEIGAQAAAAABAAEAAgD+BQABANso5gMFnAYOAgwADEIGAQABAAECAAMAEgHyBQEAAOgFABIAmAgDGgCWCAIaAGQAAOgCKWQBAM/tDmQCAAntKeYDEZ4GBwMGHAMIAxESBwENDxYUAAwCBgEAAAAAAQABAAIA8AQwQgHbKOYDBawGCAIMAAwCBgEAAAAAAQABAAIAkgVDQgHbKOYDBa0GCgIMAAwCBgEAAAAAAQABAAIAkAVCQgHbKOYDBa4GCgIMAAwCBgEAAAAAAQABAAIA7gQvQgHbKOYDBa8GCQIMAAwCBgEAAAAAAQABAAIA8gQxQgHbKOYDBbAGCAIMAA==", + "UUpLQjQAAAABAAAAMDRiZTI0NjAwMTU5OWY1OTk1ZmEyZjJkOGM5MWEwZjE5OGQzZjM0YwWhAhZ0aW55dWktY29yZRZCb3R0b21TaGVldAZCb3gMQnV0dG9uEENoZWNrYm94EkNvZGVGaWVsZAxDb2x1bW4ORGl2aWRlcgZGb3IQRnJhZ21lbnQSSG9zdEVycm9yCEljb24UTGF6eUNvbHVtbg5Mb2FkaW5nDE9wdGlvbhZSYWRpb0J1dHRvbgZSb3cMU2VsZWN0CFNob3cMU3BhY2VyCFRleHQSVGV4dEZpZWxkDlZFUlNJT04MZWZmZWN0AmgQaW50ZXJuYWwQbWFuaWZlc3QIbWVtbxRvYnNlcnZhYmxlEm9uQ2xlYW51cBZwYWdlVmlzaWJsZQZyZWYQcmVzb3VyY2UMc2lnbmFsCnRodW5rDnVudHJhY2sMdW53cmFwEl9fZGVmUHJvcBBfX2V4cG9ydBpjdXJyZW50RWZmZWN0GGN1cnJlbnRPd25lcg5wZW5kaW5nBnJhbiRNQVhfUlVOU19QRVJfRkxVU0gWY3JlYXRlT3duZXIYcnVuV2l0aE93bmVyGGRpc3Bvc2VPd25lchhpbnNpZGVSZW5kZXIQdHJhY2tpbmcUcnVuUGVuZGluZyRjcmVhdGVFZmZlY3RSZWNvcmQScnVuRWZmZWN0GmRpc3Bvc2VFZmZlY3QWdW5zdWJzY3JpYmUWcnVuQ2xlYW51cHMKdHJhY2sMbm90aWZ5DkNPTlRST0wKVGh1bmsOcGF0Y2hlcxBoYW5kbGVycwxuZXh0SWQQcmVzZXRJZHMIU2xvdBpjYWxsQ29tcG9uZW50GmNyZWF0ZUVsZW1lbnQQYmluZFByb3AMc2NhbGFyDmZsYXR0ZW4acmVuZGVySW5Pd25lchBwZW5kaW5nMhBuZXh0Q2JJZApxdWVyeQhjYWxsCHNlbmQMcmVwb3J0HHJlc29sdmVQZW5kaW5nGnJlamVjdFBlbmRpbmcaY2FuY2VsUGVuZGluZxhhbGxvY2F0ZUNiSWQecmVnaXN0ZXJQZW5kaW5nFHNldFRpbWVvdXQYY2xlYXJUaW1lb3V0HGluc3RhbGxHbG9iYWxzCHJvb3QOY3VycmVudA52aXNpYmxlFHNldFZpc2libGUMdG9waWNzDG9uRW1pdAxsaXN0ZW4OZ3VhcmRlZA5lbnRyaWVzDmluc3RhbGwGUkFXCEtFWVMOcHJveGllcxZzdWJzY3JpYmVycxR1bndyYXBEZWVwGnVud3JhcFNoYWxsb3cWaXNXcmFwcGFibGUId3JhcAhzdWJzDm9ic2VydmUOY2hhbmdlZA5pc0luZGV4EmhhbmRsZXJzMiBpbnRlcm5hbF9leHBvcnRzGF9faG9zdF9hcHBseRhfX2hvc3RfcXVlcnkWX19ob3N0X2NhbGwWX19ob3N0X3NlbmQaX19ob3N0X3JlcG9ydA5SZWZsZWN0DDAuMTMuMBx0aW55dWkuY29udHJvbAptb3VudAxpbnNlcnQIbW92ZQxyZW1vdmUMb2Zmc2V0DGFzc2lnbhRjb21wb25lbnRzGGNhcGFiaWxpdGllcw51bm1vdW50EGRpc3BhdGNoCGVtaXQKZmx1c2gUdGlueXVpLnJhdxZ0aW55dWkua2V5cwZhbGwOZWZmZWN0cxBjbGVhbnVwcxBkaXNwb3NlZApvd25lcgRmbhJwcmV2T3duZXIUcHJldkVmZmVjdAJlAmMMc3BsaWNlDnJldmVyc2UIaW5pdApzdWJzMgJ2DmVmZmVjdDICbQpkaXJ0eQhwdXNobG9uQ2xlYW51cCgpIGNhbGxlZCBvdXRzaWRlIGEgc3luY2hyb25vdXMgcmVuZGVyIHBlcmlvZAhwcmV2AmkMcXVldWVkCHJ1bnN8dXBkYXRlIGxvb3A6IGFuIGVmZmVjdCByZS1yYW4gbW9yZSB0aGFuIDEwMCB0aW1lcyBpbiBvbmUgZmx1c2hmZWZmZWN0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCGRlcHMMcGFyZW50EGVudHJpZXMyEHBvc2l0aW9uCm5vZGVzCG5vZGUEYXQEdG8OaW5kZXhPZixtb3ZlOiBub2RlIG5vdCBpbiBzbG90MHJlbW92ZTogbm9kZSBub3QgaW4gc2xvdAJyAm4EaWQGY21kCGFyZ3NecmVmLmNtZCgpIGJlZm9yZSB0aGUgcmVmIHdhcyBhdHRhY2hlZCB0byBhIG5vZGUCeBJGcmFnbWVudDIKcHJvcHMQY2hpbGRyZW4IdHlwZVxoKCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kAnAGa2V5DHJlc3VsdAh3aGF0fGEgUHJvbWlzZSAoYXN5bmMgY29tcG9uZW50cyBhcmUgbm90IHN1cHBvcnRlZDsgdXNlIHJlc291cmNlKCkpDmlzQXJyYXlUc2V2ZXJhbCBub2RlcyAod3JhcCB0aGVtIGluIG9uZSBjb250YWluZXIpTmEgRm9yIC8gU2hvdyAod3JhcCBpdCBpbiBvbmUgY29udGFpbmVyKRRjb21wb25lbnQgFjxhbm9ueW1vdXM+RiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlLCBnb3QgAmsKY2hpbGQIdGVzdAI8AiAoPiBtdXN0IGJlIGEgZnVuY3Rpb24COghsYXN0CmZpcnN0AnRWPiBpcyBhIGZ1bmN0aW9uOyBkaWQgeW91IGZvcmdldCB0byBjYWxsIGl0P6oBPiBtdXN0IGJlIGEgc3RyaW5nLCBudW1iZXIsIGJvb2xlYW4gb3IgbnVsbDsgcHJvcHMgZG8gbm90IGNyb3NzIHRoZSBicmlkZ2UgYXMgb2JqZWN0cwZvdXQMcmVuZGVyOiBtdXN0IHJldHVybiBleGFjdGx5IG9uZSBub2RlCGNvZGUOZGV0YWlscw5oZWFkZXJzCGJvZHkScGF0Y2hKc29uCnBhcnNlEnN0cmluZ2lmeQhjYklkCHNpdGUIa2luZAxkZXRhaWwUcmVzdWx0SnNvbhRFX0JBRF9KU09OMmhvc3QgcmVzdWx0IGlzIG5vdCBKU09OOiASZXJyb3JKc29uCmVycm9yIGhvc3QgY2FsbCBmYWlsZWQEbXMcdGltZXIuc2NoZWR1bGUYdGltZXIuY2FuY2VsAmcKdG9waWNmb25FbWl0KCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCmVudHJ5BmVycgRFMQhwYWdlEnByb3BzSnNvbhBob3N0SnNvbihtb3VudCgpIGNhbGxlZCB0d2ljZU5wYWdlIG11c3QgcmV0dXJuIGV4YWN0bHkgb25lIG5vZGUsIGdvdCASYSBQcm9taXNlCmNsZWFyDG5vZGVJZApldmVudBZwYXlsb2FkSnNvbg5oYW5kbGVyDnBheWxvYWQIanNvbhBfX3Rpbnl1aVpvYnNlcnZhYmxlKCkgdGFrZXMgYSBwbGFpbiBv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", ) val native: Array = arrayOf( diff --git a/compose/src/commonMain/kotlin/app/tinyui/schema/NodeScope.kt b/compose/src/commonMain/kotlin/app/tinyui/schema/NodeScope.kt index bb3ac74..91bb85d 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/schema/NodeScope.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/schema/NodeScope.kt @@ -3,6 +3,8 @@ package app.tinyui.schema import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -46,11 +48,13 @@ interface NodeScope { @Composable fun NodeScope.Commands(handler: suspend (Command) -> Unit) { val node = node + // the effect outlives recompositions: without this it would keep the first handler and the props it closed over + val current by rememberUpdatedState(handler) LaunchedEffect(node) { snapshotFlow { node.commands.toList() }.collect { pending -> if (pending.isEmpty()) return@collect node.commands.removeAll(pending) - for (c in pending) handler(c) + for (c in pending) current(c) } } } diff --git a/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt b/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt index beaa174..722b4ad 100644 --- a/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt +++ b/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt @@ -28,7 +28,7 @@ class PageHostTest { private val zoneFlow = MutableStateFlow("America/New_York") private val core = module("tinyui-core", """ - let patches = [], count = 0, handler = null, page = null, emits = 0, settled = []; const settle = (line) => { settled.push(line); patches.push(["p",1,"text", settled.slice().sort().join(";")]); }; + let patches = [], seq = [], count = 0, handler = null, page = null, emits = 0, settled = []; const settle = (line) => { settled.push(line); patches.push(["p",1,"text", settled.slice().sort().join(";")]); }; globalThis.__tinyui = { mount(p, props, host) { page = p; const name = JSON.parse(props).name ?? "world"; @@ -61,6 +61,7 @@ class PageHostTest { // 2026-01-01 and 2026-07-01 UTC: either side of daylight saving if (event === "onZone") patches.push(["p",1,"text", [__host_query("i18n.timeZone", "{}"), __host_query("i18n.zoneOffset", JSON.stringify({ ms: 1767225600000 })), __host_query("i18n.zoneOffset", JSON.stringify({ ms: 1782864000000 }))].join("|")]); if (event === "onI18nSubscribe") { __host_send("i18n.subscribe", "{}"); patches.push(["p",1,"text","subscribed"]); } + if (event === "onSeq") { seq.push(JSON.parse(payloadJson).i); patches.push(["p",1,"text","seq " + seq.length + " " + (seq.every((v, i) => v === i) ? "ordered" : "shuffled")]); } }, emit(topic, json) { emits++; patches.push(["p",1,"text", topic + " " + json + " #" + emits]); }, flush() { if (count > 2) throw new Error("render exploded"); if (patches.length) { __host_apply(JSON.stringify(patches)); patches = []; } }, @@ -113,7 +114,7 @@ class PageHostTest { val host = host("""{"name":"tinyui"}""") host.start() host.await { host.tree.root.children.isNotEmpty() } - assertEquals("hello tinyui / 13", host.tree.node(1)!!.props["text"]) + assertEquals("hello tinyui / 17", host.tree.node(1)!!.props["text"]) assertNull(host.failure) host.close() } @@ -131,6 +132,18 @@ class PageHostTest { host.close() } + @Test + fun eventsReachThePageInTheOrderTheyWereDispatched() = runTest { + val host = host() + host.start() + host.await { host.tree.root.children.isNotEmpty() } + // onChange then onComplete from one keystroke: the page must never see them swapped + withContext(Dispatchers.Default) { repeat(500) { host.dispatch(2, "onSeq", """{"i":$it}""") } } + host.await { (host.tree.node(1)!!.props["text"] as String).startsWith("seq 500") } + assertEquals("seq 500 ordered", host.tree.node(1)!!.props["text"]) + host.close() + } + @Test fun timersComeBackThroughResolveAndE1ReportsKeepTheirFields() = runTest { val host = host() diff --git a/compose/src/commonTest/kotlin/app/tinyui/components/ClipToTest.kt b/compose/src/commonTest/kotlin/app/tinyui/components/ClipToTest.kt new file mode 100644 index 0000000..fa8065e --- /dev/null +++ b/compose/src/commonTest/kotlin/app/tinyui/components/ClipToTest.kt @@ -0,0 +1,22 @@ +package app.tinyui.components + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ClipToTest { + @Test + fun noLimitOrShortEnoughIsUntouched() { + assertEquals("abc", clipTo("abc", null)) + assertEquals("abc", clipTo("abc", 3)) + } + + @Test + fun cutsAtTheLimit() = assertEquals("ab", clipTo("abc", 2)) + + @Test + fun neverSplitsASurrogatePair() { + // "a😀b": the emoji is two UTF-16 units at 1..2 + assertEquals("a", clipTo("a😀b", 2)) + assertEquals("a😀", clipTo("a😀b", 3)) + } +} diff --git a/compose/src/commonTest/kotlin/app/tinyui/components/LinkedTextTest.kt b/compose/src/commonTest/kotlin/app/tinyui/components/LinkedTextTest.kt new file mode 100644 index 0000000..73e7f65 --- /dev/null +++ b/compose/src/commonTest/kotlin/app/tinyui/components/LinkedTextTest.kt @@ -0,0 +1,33 @@ +package app.tinyui.components + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.LinkAnnotation +import kotlin.test.Test +import kotlin.test.assertEquals + +class LinkedTextTest { + private fun links(text: String) = linkedText(text, Color.Blue) {}.let { s -> + s.getLinkAnnotations(0, s.length).map { s.text.substring(it.start, it.end) to (it.item as LinkAnnotation.Clickable).tag } + } + + @Test + fun spansBecomeLinksAndBracketsGo() { + val s = linkedText("我同意 [隐私政策](privacy) 及 [服务协议](terms)", Color.Blue) {} + assertEquals("我同意 隐私政策 及 服务协议", s.text) + assertEquals(listOf("隐私政策" to "privacy", "服务协议" to "terms"), links("我同意 [隐私政策](privacy) 及 [服务协议](terms)")) + } + + @Test + fun strayBracketsStayLiteral() { + assertEquals("a [b] (c) [d]( e)", linkedText("a [b] (c) [d]( e)", Color.Blue) {}.text) + assertEquals(emptyList(), links("a [b] (c) [d]( e)")) + } + + @Test + fun tapReportsTheId() { + var tapped: String? = null + val s = linkedText("[重新输入](edit)", Color.Blue) { tapped = it } + (s.getLinkAnnotations(0, s.length).single().item as LinkAnnotation.Clickable).linkInteractionListener!!.onClick(s.getLinkAnnotations(0, s.length).single().item) + assertEquals("edit", tapped) + } +} diff --git a/compose/src/iosMain/kotlin/app/tinyui/Platform.ios.kt b/compose/src/iosMain/kotlin/app/tinyui/Platform.ios.kt index 2d9cc47..02e5d80 100644 --- a/compose/src/iosMain/kotlin/app/tinyui/Platform.ios.kt +++ b/compose/src/iosMain/kotlin/app/tinyui/Platform.ios.kt @@ -2,6 +2,7 @@ package app.tinyui import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.text.input.PlatformImeOptions import io.ktor.client.HttpClient import io.ktor.client.engine.darwin.Darwin import okio.FileSystem @@ -16,6 +17,12 @@ import platform.Foundation.localeIdentifier import platform.Foundation.preferredLanguages import platform.Foundation.timeZoneWithName import platform.UIKit.UIDevice +import platform.UIKit.UITextContentTypeOneTimeCode + +// explicit either way: unset, NumberPassword would be announced as a password and offered keychain entries +@OptIn(ExperimentalComposeUiApi::class) +internal actual fun oneTimeCodeImeOptions(oneTimeCode: Boolean): PlatformImeOptions? = + PlatformImeOptions { textContentType(if (oneTimeCode) UITextContentTypeOneTimeCode else null) } internal actual fun platformInfo(): Map = mapOf( "os" to "ios", diff --git a/docs/components.md b/docs/components.md index bca1674..f623a2c 100644 --- a/docs/components.md +++ b/docs/components.md @@ -1,6 +1,6 @@ # 内置组件:schema、布局 prop、首批清单 -- 状态:已定(2026-09-16);M2 的实现依据;2026-09-25 按 [ADR-007](./adr-007-host-boundary.md) 加 `Icon`、`Loading` 与公共 prop `role` / `selected` +- 状态:已定(2026-09-16);M2 的实现依据;2026-09-25 按 [ADR-007](./adr-007-host-boundary.md) 加 `Icon`、`Loading` 与公共 prop `role` / `selected`;2026-10-28 为注册流程加 `Checkbox`、`CodeField`、`Select` / `Option`、`TextField` 外观与焦点、`Text` 内联链接 - 来源:ADR-003 §3.3 / §3.4(注册表、schema、公共布局 prop)、ADR-004(事件三分、命令、文本框);roadmap C 组 - 相关:[runtime-api.md](./runtime-api.md) §3(`h()` 怎么处理 prop)、[patch-protocol.md](./patch-protocol.md) §4(值类型) @@ -69,10 +69,14 @@ prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB |---|---|---|---|---| | `Column` / `Row` | `gap`、`align`(交叉轴 start / center / end)、`justify`(主轴 start / center / end / spaceBetween)、`scroll`(沿主轴滚动;`padding` 在滚动内容之外,不随内容滚) | `onClick` | | 是 | | `Box` | `align`(九宫格) | `onClick` | | 是 | -| `Text` | `text`(必填)、`style`(M3 文字样式名,§6)、`color`、`fontSize`、`fontWeight`(normal / medium / bold;设了就覆盖 `style` 的对应字段,不设则跟 `style`)、`maxLines`(0 = 不限,超出省略号)、`align` | `onClick` | | | +| `Text` | `text`(必填)、`style`(M3 文字样式名,§6)、`color`、`fontSize`、`fontWeight`(normal / medium / bold;设了就覆盖 `style` 的对应字段,不设则跟 `style`)、`maxLines`(0 = 不限,超出省略号)、`align` | `onClick`、`onLinkClick{id}`(给了它,`text` 里的 `[文案](id)` 渲染成链接) | | | | `Button` | `text`、`enabled`、`variant`(filled / outlined / text) | `onClick` | | 可选:有 children 就当标签渲染、忽略 `text`(按钮内放加载指示或图标;2026-09-17 加) | | `RadioButton` | `selected`(必填)、`enabled` | `onClick` | | | -| `TextField` | `initialText`(initial)、`placeholder`、`singleLine`、`keyboard` | `onChange{text}`、`onCommit{text}`(IME Done 或失焦) | `setText{text}`、`focus`、`blur` | | +| `Checkbox` | `checked`(必填)、`enabled` | `onClick` | | | +| `TextField` | `initialText`(initial)、`placeholder`、`singleLine`、`keyboard`、`secure`(缺省跟 `keyboard="password"`)、`error`、`supportingText`、`prefixText`、`trailingIcon`(`icon` 类型)、`imeAction`(done / next)、`maxLength`(0 = 不限) | `onChange{text}`、`onCommit{text}`(IME 动作或失焦)、`onFocusChange{focused}`、`onTrailingClick`、`onPrefixClick` | `setText{text}`、`focus`、`blur` | | +| `CodeField` | `length`(1–12,默认 6)、`secure`、`enabled`、`error`、`autofill`(none / oneTimeCode) | `onChange{text}`、`onComplete{text}`(填满最后一格) | `setText{text}`、`focus`、`clear` | | +| `Select` | `value`、`placeholder`、`label`、`enabled` | `onSelect{value}` | | 是:`Option` | +| `Option` | `value`(必填)、`text`(必填);不接受布局 prop | | | | | `LazyColumn` | `gap` | `onReachEnd`、`onScrollEnd{index}` | `scrollTo{index}` | 是,通常是一个 `` | | `Spacer` | (只有布局 prop) | | | | | `Icon` | `icon`(必填,`icon` 类型)、`size`(dp,默认 24)、`tint`(color,缺省跟随内容色)、`label`(读屏念的名字;紧挨文字的装饰图标不设) | `onClick` | | | @@ -82,6 +86,23 @@ prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB `RadioButton` 的选中态是普通 prop(2026-09-17 加):它只报点击,选中哪个由 JS 决定——单选组的真值本来就在页面状态里,不属于 ADR-004 的"高频交互状态"。`TextField` 的文本与光标永远在 Kotlin 侧(ADR-004 §3.3):`setText` 会同时触发 `onChange`。`LazyColumn` 的行就是它的 children,虚拟化只在组合层(ADR-003 §3.6)。 +`Checkbox` 与 `RadioButton` 同一模式(2026-10-28 加):只报点击,勾没勾是页面状态。勾选框带一句文案时,把两者包进 ``,整行一个读屏节点,`Checkbox` 本身不给 `onClick`。 + +`TextField` 的外观与通知(2026-10-28 加,注册流程触发):`error` 把描边、`supportingText` 染成错误色,`supportingText` 是字段下方一行提示或错误文案;`secure` 不设时跟 `keyboard="password"`,显隐切换就是页面翻转它,配 `trailingIcon` + `onTrailingClick`(清除按钮同理:`trailingIcon` + `setText("")`,不另加组件)。`prefixText` 是输入前的固定文字(区号),画在 leading 槽而不是 M3 `prefix`——后者在空且未聚焦时隐藏、也不能点;`onPrefixClick` 让它可点(打开区号选择)。`imeAction="next"` 时键盘动作先 `onCommit` 再把焦点移到下一个可聚焦项。`maxLength` 超长截断而不是拒绝。`onFocusChange` 只在焦点真的变化时发,首次组合的"未聚焦"不算,所以 `onCommit` 也不会在首帧被误发(此前会)。文本仍归 Kotlin(ADR-004),这些都只是外观与通知。 + +`CodeField` 是一排数字格画在一个看不见的输入框之上(2026-10-28 加)。叫 Code 而不是 Pin / Otp:短信验证码与交易密码是同一个组件,`secure` 与 `autofill` 区分两者,名字不偏向任何一种。Compose 与 M3 都没有对应组件,它是框架自绘的(`BasicTextField` + 自画格子),格子尺寸由框架定、颜色跟主题。焦点与 IME 归 Kotlin,只接受数字、到 `length` 为止;不能用多个 `TextField` 拼,焦点逐格跳要跨桥往返(ADR-004 的反例)。`autofill="oneTimeCode"` 让系统把刚收到的短信验证码作为建议(Android 自动填充 `SmsOtpCode`、iOS 键盘上方「来自信息」);缺省 `none` 时明确告诉系统什么都别建议——iOS 不设内容类型会按 `NumberPassword` 键盘把它当密码框、弹钥匙串,交易密码不该这样。iOS 只从 `PlatformImeOptions` 读内容类型、不看 semantics,所以两端各走各的路,平台差异在框架里消化。`onComplete` 每次填满最后一格时发,`clear` / `setText` 与 `TextField.setText` 一样会触发 `onChange`——所以"输错 → 清空并标红"时,清错误态要放在有输入的 `onChange` 里,否则 `clear` 带来的空 `onChange` 会立刻把红色抹掉: + +```tsx +const pin = ref(); + { if (e.text) setWrong(false) }} + onComplete={(e) => verify(e.text).catch(() => { setWrong(true); pin.cmd("clear", {}) })} /> +``` + +`Select` 是 M3 `ExposedDropdownMenuBox`(2026-10-28 加)。选项不能做成数组 prop(prop 只过标量,patch-protocol.md §4),所以是 `Option` children:`Select` 像 `Row` 读 child 的 `weight` 一样读 `Option` 的 `value` / `text`,`Option` 自己什么都不画。选中值叫 `value` 而不是 `selected`,因为后者是公共布局 prop(可选中容器)。选中态归页面:`onSelect` 报选了哪个,页面把它写回 `value`。 + +`Text` 内联链接(2026-10-28 加):给了 `onLinkClick`,`text` 里的 `[文案](id)` 渲染成 primary 色链接,点击报 `{id}`;没给时按字面显示。协议句"我同意 [隐私政策](privacy) 及 [服务协议](terms)"要行内折行,`Row` 拼几个 `Text` 做不到;markdown 链接子集是最小够用的标记,i18n 文案里直接写。只认 `[` `]` `(` `)` 紧挨、id 不含空白的形式,其余方括号原样显示;不支持嵌套与转义。 + `Icon` 不内置任何图标,图标数据随包(2026-09-25,ADR-007 §3.7)。一个图标是一个字符串:填充图标 `"|"`,描边图标 `"||<线宽>"`(圆头圆角)。`viewBox` 是四个数(`0 -960 960 960`),`d` 是 SVG path 数据,多个形状合并成一个 `d`,按 `tint` 着色;用字符串是因为 prop 不过桥对象(patch-protocol.md §4)。schema 里是 prop 类型 `icon()`,Kotlin 写入时解析成 `ImageVector` 并按字符串缓存,格式不对按 E5 跳过。字符串由 `tinyui build` 的 `.svg` 加载器从 SVG 文件生成(build-chain.md §9),页面直接 import SVG:Material Symbols 用 npm 包 `@material-symbols/svg-400`,品牌图标放一个 `.svg` 进工程;类型声明见 `tinyui-core/assets`。多色图标与位图归 `Image`。 ```tsx diff --git a/packages/core/src/generated/components.ts b/packages/core/src/generated/components.ts index 3f77cc6..9e3ed65 100644 --- a/packages/core/src/generated/components.ts +++ b/packages/core/src/generated/components.ts @@ -67,7 +67,13 @@ export interface BoxProps extends LayoutProps { export interface TextClickPayload { } +export interface TextLinkClickPayload { + /** the `(id)` of the tapped link */ + id: string; +} + export interface TextProps extends LayoutProps { + /** with onLinkClick set, `[label](id)` spans render as links */ text: string; /** Material 3 text style; fontSize / fontWeight override its fields */ style?: "displayLarge" | "displayMedium" | "displaySmall" | "headlineLarge" | "headlineMedium" | "headlineSmall" | "titleLarge" | "titleMedium" | "titleSmall" | "bodyLarge" | "bodyMedium" | "bodySmall" | "labelLarge" | "labelMedium" | "labelSmall"; @@ -78,6 +84,7 @@ export interface TextProps extends LayoutProps { maxLines?: number; align?: "start" | "center" | "end"; onClick?: (e: TextClickPayload) => void; + onLinkClick?: (e: TextLinkClickPayload) => void; } export interface ButtonClickPayload { @@ -111,6 +118,16 @@ export interface TextFieldCommitPayload { text: string; } +export interface TextFieldFocusChangePayload { + focused: boolean; +} + +export interface TextFieldTrailingClickPayload { +} + +export interface TextFieldPrefixClickPayload { +} + export interface TextFieldSetTextArgs { text: string; } @@ -133,8 +150,25 @@ export interface TextFieldProps extends LayoutProps { placeholder?: string; singleLine?: boolean; keyboard?: "text" | "number" | "email" | "phone" | "password"; + /** masks the text; unset follows keyboard="password", so a visibility toggle just flips it */ + secure?: boolean; + /** error colours on the outline, label and supportingText */ + error?: boolean; + /** one line under the field: a hint, or the error message while error is true */ + supportingText?: string; + /** fixed text before the input, such as a calling code */ + prefixText?: string; + /** icon at the end, such as show / hide or clear; tappable when onTrailingClick is set */ + trailingIcon?: string; + /** next moves focus to the following field */ + imeAction?: "done" | "next"; + /** 0 = unlimited; longer input is cut, not rejected */ + maxLength?: number; onChange?: (e: TextFieldChangePayload) => void; onCommit?: (e: TextFieldCommitPayload) => void; + onFocusChange?: (e: TextFieldFocusChangePayload) => void; + onTrailingClick?: (e: TextFieldTrailingClickPayload) => void; + onPrefixClick?: (e: TextFieldPrefixClickPayload) => void; ref?: Ref; } @@ -204,6 +238,78 @@ export interface DividerProps extends LayoutProps { color?: ColorValue; } +export interface CheckboxClickPayload { +} + +/** Same model as RadioButton: `onClick` reports the tap, `checked` is a plain prop the page owns. */ +export interface CheckboxProps extends LayoutProps { + checked: boolean; + enabled?: boolean; + onClick?: (e: CheckboxClickPayload) => void; +} + +export interface CodeFieldChangePayload { + text: string; +} + +export interface CodeFieldCompletePayload { + /** when the last cell is filled */ + text: string; +} + +export interface CodeFieldSetTextArgs { + text: string; +} + +export interface CodeFieldFocusArgs { +} + +export interface CodeFieldClearArgs { +} + +export type CodeFieldCommands = { + setText: CodeFieldSetTextArgs; + focus: CodeFieldFocusArgs; + clear: CodeFieldClearArgs; +}; + +/** A row of digit cells over one input; text, focus and the IME stay on the Kotlin side like TextField (docs/adr-004 §3.3). */ +export interface CodeFieldProps extends LayoutProps { + /** number of cells, 1 to 12; input stops there */ + length?: number; + /** shows a dot instead of each digit */ + secure?: boolean; + enabled?: boolean; + /** error colour on the cell outlines */ + error?: boolean; + /** oneTimeCode lets the system offer a code it just received by SMS; none keeps it from offering anything, saved passwords included */ + autofill?: "none" | "oneTimeCode"; + onChange?: (e: CodeFieldChangePayload) => void; + onComplete?: (e: CodeFieldCompletePayload) => void; + ref?: Ref; +} + +export interface SelectSelectPayload { + value: string; +} + +/** M3 exposed dropdown; the choices are its `Option` children. The selection is the page's: `onSelect` reports the pick, `value` shows it. */ +export interface SelectProps extends LayoutProps { + /** value of the chosen Option; unset or unmatched shows the placeholder (not `selected`, the common selectable-container prop) */ + value?: string; + placeholder?: string; + label?: string; + enabled?: boolean; + onSelect?: (e: SelectSelectPayload) => void; + children?: Node | Node[]; +} + +/** One choice of its parent Select, which reads these props; renders nothing anywhere else. */ +export interface OptionProps { + value: string; + text: string; +} + export interface IntrinsicElements { Column: ColumnProps; Row: RowProps; @@ -218,6 +324,10 @@ export interface IntrinsicElements { Loading: LoadingProps; BottomSheet: BottomSheetProps; Divider: DividerProps; + Checkbox: CheckboxProps; + CodeField: CodeFieldProps; + Select: SelectProps; + Option: OptionProps; } export const Column = "Column"; @@ -233,3 +343,7 @@ export const Icon = "Icon"; export const Loading = "Loading"; export const BottomSheet = "BottomSheet"; export const Divider = "Divider"; +export const Checkbox = "Checkbox"; +export const CodeField = "CodeField"; +export const Select = "Select"; +export const Option = "Option"; diff --git a/sample/js-extra/src/pages/form.tsx b/sample/js-extra/src/pages/form.tsx new file mode 100644 index 0000000..291c17b --- /dev/null +++ b/sample/js-extra/src/pages/form.tsx @@ -0,0 +1,49 @@ +import { Checkbox, Column, Option, CodeField, ref, Row, Select, signal, Text, TextField, type CodeFieldCommands } from "tinyui-core"; +import { ui } from "tinyui-native"; +import visibility from "@material-symbols/svg-400/outlined/visibility.svg"; +import visibilityOff from "@material-symbols/svg-400/outlined/visibility_off.svg"; + +/** The form pieces a sign-up flow needs: checkbox, field adornments, PIN cells, dropdown, inline links. */ +export default function Form() { + const [email, setEmail] = signal(""); + const [shown, setShown] = signal(false); + const [focused, setFocused] = signal(false); + const [agreed, setAgreed] = signal(false); + const [voice, setVoice] = signal(""); + const [pinError, setPinError] = signal(false); + const pin = ref(); + const emailError = () => email() !== "" && !email().includes("@"); + + return ( + + setEmail(e.text)} + /> + setShown(!shown())} + supportingText={focused() ? "8+ characters, letters and digits" : ""} + onFocusChange={(e) => setFocused(e.focused)} + /> + void ui.toast("pick a calling code")} /> + setAgreed(!agreed())}> + + void ui.toast(`open ${e.id}`)} /> + + + + void ui.toast(`code ${e.text}`)} /> + + { if (e.text) setPinError(false) }} + onComplete={(e) => { if (e.text.startsWith("1234")) { setPinError(true); pin.cmd("clear", {}); } else void ui.toast("PIN set") }} + /> + + ); +} diff --git a/sample/shared/src/commonMain/kotlin/app/tinyui/sample/App.kt b/sample/shared/src/commonMain/kotlin/app/tinyui/sample/App.kt index 77c7eb2..ffd10dd 100644 --- a/sample/shared/src/commonMain/kotlin/app/tinyui/sample/App.kt +++ b/sample/shared/src/commonMain/kotlin/app/tinyui/sample/App.kt @@ -41,7 +41,7 @@ import kotlinx.coroutines.sync.withLock /** The two packages the App embeds, each under its own resource directory (docs/updates.md §3). */ private val packages = listOf("sample", "sample-extra") -private val pages = listOf("sample/todos", "sample-extra/hello", "sample-extra/features") +private val pages = listOf("sample/todos", "sample-extra/hello", "sample-extra/features", "sample-extra/form") /** * One [Updates] and one [UpdatesScheduler] per process, whatever happens to the composition: a recreated Activity diff --git a/schema/components/checkbox.ts b/schema/components/checkbox.ts new file mode 100644 index 0000000..7c2c5e6 --- /dev/null +++ b/schema/components/checkbox.ts @@ -0,0 +1,10 @@ +import { boolean, defineComponent } from "tinyui-cli/schema"; + +export default defineComponent("Checkbox", { + doc: "Same model as RadioButton: `onClick` reports the tap, `checked` is a plain prop the page owns.", + props: { + checked: boolean({ required: true }), + enabled: boolean({ default: true }), + }, + events: { onClick: {} }, +}); diff --git a/schema/components/code-field.ts b/schema/components/code-field.ts new file mode 100644 index 0000000..f3fe6c6 --- /dev/null +++ b/schema/components/code-field.ts @@ -0,0 +1,21 @@ +import { boolean, defineComponent, enumOf, field, number } from "tinyui-cli/schema"; + +export default defineComponent("CodeField", { + doc: "A row of digit cells over one input; text, focus and the IME stay on the Kotlin side like TextField (docs/adr-004 §3.3).", + props: { + length: number({ default: 6, doc: "number of cells, 1 to 12; input stops there" }), + secure: boolean({ default: false, doc: "shows a dot instead of each digit" }), + enabled: boolean({ default: true }), + error: boolean({ default: false, doc: "error colour on the cell outlines" }), + autofill: enumOf(["none", "oneTimeCode"], { default: "none", doc: "oneTimeCode lets the system offer a code it just received by SMS; none keeps it from offering anything, saved passwords included" }), + }, + events: { + onChange: { text: field.string() }, + onComplete: { text: field.string("when the last cell is filled") }, + }, + commands: { + setText: { text: field.string() }, + focus: {}, + clear: {}, + }, +}); diff --git a/schema/components/option.ts b/schema/components/option.ts new file mode 100644 index 0000000..5893d15 --- /dev/null +++ b/schema/components/option.ts @@ -0,0 +1,10 @@ +import { defineComponent, string } from "tinyui-cli/schema"; + +export default defineComponent("Option", { + doc: "One choice of its parent Select, which reads these props; renders nothing anywhere else.", + props: { + value: string({ required: true }), + text: string({ required: true }), + }, + layout: false, +}); diff --git a/schema/components/select.ts b/schema/components/select.ts new file mode 100644 index 0000000..0d52542 --- /dev/null +++ b/schema/components/select.ts @@ -0,0 +1,13 @@ +import { boolean, defineComponent, field, string } from "tinyui-cli/schema"; + +export default defineComponent("Select", { + doc: "M3 exposed dropdown; the choices are its `Option` children. The selection is the page's: `onSelect` reports the pick, `value` shows it.", + props: { + value: string({ doc: "value of the chosen Option; unset or unmatched shows the placeholder (not `selected`, the common selectable-container prop)" }), + placeholder: string({ default: "" }), + label: string(), + enabled: boolean({ default: true }), + }, + events: { onSelect: { value: field.string() } }, + children: true, +}); diff --git a/schema/components/text-field.ts b/schema/components/text-field.ts index 367286c..0c06cd1 100644 --- a/schema/components/text-field.ts +++ b/schema/components/text-field.ts @@ -1,4 +1,4 @@ -import { boolean, defineComponent, enumOf, field, string } from "tinyui-cli/schema"; +import { boolean, defineComponent, enumOf, field, icon, number, string } from "tinyui-cli/schema"; export default defineComponent("TextField", { doc: "Text and cursor live on the Kotlin side; JS gets notified and issues commands (docs/adr-004 §3.3).", @@ -7,10 +7,20 @@ export default defineComponent("TextField", { placeholder: string({ default: "" }), singleLine: boolean({ default: true }), keyboard: enumOf(["text", "number", "email", "phone", "password"], { default: "text" }), + secure: boolean({ doc: "masks the text; unset follows keyboard=\"password\", so a visibility toggle just flips it" }), + error: boolean({ default: false, doc: "error colours on the outline, label and supportingText" }), + supportingText: string({ doc: "one line under the field: a hint, or the error message while error is true" }), + prefixText: string({ doc: "fixed text before the input, such as a calling code" }), + trailingIcon: icon({ doc: "icon at the end, such as show / hide or clear; tappable when onTrailingClick is set" }), + imeAction: enumOf(["done", "next"], { default: "done", doc: "next moves focus to the following field" }), + maxLength: number({ default: 0, doc: "0 = unlimited; longer input is cut, not rejected" }), }, events: { onChange: { text: field.string() }, onCommit: { text: field.string("on IME action or focus loss") }, + onFocusChange: { focused: field.boolean() }, + onTrailingClick: {}, + onPrefixClick: {}, }, commands: { setText: { text: field.string() }, diff --git a/schema/components/text.ts b/schema/components/text.ts index 3fa0194..d84e1f9 100644 --- a/schema/components/text.ts +++ b/schema/components/text.ts @@ -1,8 +1,8 @@ -import { color, defineComponent, enumOf, number, sp, string, TEXT_STYLES } from "tinyui-cli/schema"; +import { color, defineComponent, enumOf, field, number, sp, string, TEXT_STYLES } from "tinyui-cli/schema"; export default defineComponent("Text", { props: { - text: string({ required: true }), + text: string({ required: true, doc: "with onLinkClick set, `[label](id)` spans render as links" }), style: enumOf(TEXT_STYLES, { doc: "Material 3 text style; fontSize / fontWeight override its fields" }), color: color(), fontSize: sp(), @@ -10,5 +10,8 @@ export default defineComponent("Text", { maxLines: number({ doc: "0 = unlimited", default: 0 }), align: enumOf(["start", "center", "end"], { default: "start" }), }, - events: { onClick: {} }, + events: { + onClick: {}, + onLinkClick: { id: field.string("the `(id)` of the tapped link") }, + }, }); diff --git a/schema/index.ts b/schema/index.ts index 88233ec..35065ea 100644 --- a/schema/index.ts +++ b/schema/index.ts @@ -6,6 +6,7 @@ import Box from "./components/box.ts"; import Text from "./components/text.ts"; import Button from "./components/button.ts"; import RadioButton from "./components/radio-button.ts"; +import Checkbox from "./components/checkbox.ts"; import TextField from "./components/text-field.ts"; import LazyColumn from "./components/lazy-column.ts"; import Spacer from "./components/spacer.ts"; @@ -13,5 +14,8 @@ import Icon from "./components/icon.ts"; import Loading from "./components/loading.ts"; import BottomSheet from "./components/bottom-sheet.ts"; import Divider from "./components/divider.ts"; +import CodeField from "./components/code-field.ts"; +import Select from "./components/select.ts"; +import Option from "./components/option.ts"; -export default [Column, Row, Box, Text, Button, RadioButton, TextField, LazyColumn, Spacer, Icon, Loading, BottomSheet, Divider]; +export default [Column, Row, Box, Text, Button, RadioButton, TextField, LazyColumn, Spacer, Icon, Loading, BottomSheet, Divider, Checkbox, CodeField, Select, Option];