From 5354c78a48995ef0a0d1ff368b85e810e466e19b Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 28 Oct 2026 15:34:53 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=B3=A8=E5=86=8C=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E7=BB=84=E4=BB=B6=E2=80=94=E2=80=94Checkbox=E3=80=81P?= =?UTF-8?q?inField=E3=80=81Select=20/=20Option=E3=80=81TextField=20?= =?UTF-8?q?=E5=A4=96=E8=A7=82=E4=B8=8E=E7=84=A6=E7=82=B9=E3=80=81Text=20?= =?UTF-8?q?=E5=86=85=E8=81=94=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PingPong 注册试点(TinyUI 页做注册)逼出的五项内置组件: - Checkbox:与 RadioButton 同模式,只报点击,checked 归页面 - TextField:secure(缺省跟 keyboard="password")、error、supportingText、prefixText(leading 槽,可点 onPrefixClick)、trailingIcon + onTrailingClick、imeAction done / next、maxLength;新事件 onFocusChange。顺带修首帧未聚焦被当作失焦、误发 onCommit - PinField:一排数字格画在一个隐藏输入框上,焦点与 IME 归 Kotlin;onChange / onComplete,命令 setText / focus / clear - Select + Option:M3 ExposedDropdownMenuBox,选项走 Option children;选中值叫 value(selected 是公共布局 prop) - Text:给了 onLinkClick 时 `[文案](id)` 渲染成链接 sample-extra 加 form 页演示,Android 模拟器与 iOS 模拟器逐项验过;components.md §3 同步。 Co-Authored-By: Claude Opus 5.5 --- compose/api/compose.klib.api | 8 + .../kotlin/app/tinyui/components/Builtins.kt | 190 +++++++++++++++++- .../components/generated/BuiltinSchemas.kt | 80 +++++++- .../app/tinyui/generated/RuntimeBytecode.kt | 2 +- .../kotlin/app/tinyui/PageHostTest.kt | 2 +- .../app/tinyui/components/LinkedTextTest.kt | 33 +++ docs/components.md | 27 ++- packages/core/src/generated/components.ts | 112 +++++++++++ sample/js-extra/src/pages/form.tsx | 47 +++++ .../kotlin/app/tinyui/sample/App.kt | 2 +- schema/components/checkbox.ts | 10 + schema/components/option.ts | 10 + schema/components/pin-field.ts | 20 ++ schema/components/select.ts | 13 ++ schema/components/text-field.ts | 12 +- schema/components/text.ts | 9 +- schema/index.ts | 6 +- 17 files changed, 562 insertions(+), 21 deletions(-) create mode 100644 compose/src/commonTest/kotlin/app/tinyui/components/LinkedTextTest.kt create mode 100644 sample/js-extra/src/pages/form.tsx create mode 100644 schema/components/checkbox.ts create mode 100644 schema/components/option.ts create mode 100644 schema/components/pin-field.ts create mode 100644 schema/components/select.ts diff --git a/compose/api/compose.klib.api b/compose/api/compose.klib.api index 7c11078..c34eacd 100644 --- a/compose/api/compose.klib.api +++ b/compose/api/compose.klib.api @@ -687,6 +687,8 @@ 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 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 +699,16 @@ 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 PinField // app.tinyui.components.generated/BuiltinSchemas.PinField|{}PinField[0] + final fun (): app.tinyui.schema/ComponentSchema // app.tinyui.components.generated/BuiltinSchemas.PinField.|(){}[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/commonMain/kotlin/app/tinyui/components/Builtins.kt b/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt index 81f97bd..90be961 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt @@ -1,25 +1,38 @@ 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.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 +56,24 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +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.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 @@ -92,8 +114,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 +149,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.PinField) { scope -> PinFieldComponent(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 +209,22 @@ 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) = if (maxLength != null && t.length > maxLength) t.take(maxLength) else t + 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)) } 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 +233,146 @@ 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), + keyboardActions = KeyboardActions(onDone = { commit() }, onNext = { commit(); focusManager.moveFocus(FocusDirection.Next) }), + 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 PinFieldComponent(scope: NodeScope) { + val length = (scope.get("length")?.toInt() ?: 6).coerceIn(1, 12) + var value by remember { mutableStateOf(TextFieldValue("")) } + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + var focused by remember { mutableStateOf(false) } + fun update(raw: String) { + val t = raw.filter { it in '0'..'9' }.take(length) + val changed = t != value.text + value = TextFieldValue(t, selection = TextRange(t.length)) + if (!changed) return + if (scope.has("onChange")) scope.dispatch("onChange", payload("text", t)) + if (t.length == length && scope.has("onComplete")) scope.dispatch("onComplete", payload("text", t)) + } + scope.Commands { command -> + when (command.name) { + "setText" -> update(command.args["text"] as? String ?: "") + "clear" -> update("") + "focus" -> focusRequester.requestFocus() + } + } + val secure = scope.get("secure") == true + val error = scope.get("error") == true + 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), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + modifier = scope.modifier(clickable = false).focusRequester(focusRequester).onFocusChanged { focused = it.isFocused }, + decorationBox = { inner -> + Box { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + 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, + modifier = Modifier.weight(1f).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) } + 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) }, onClick = { expanded = false; if (scope.has("onSelect")) scope.dispatch("onSelect", payload("value", value)) }) + } + } + } +} + +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..09e3754 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,74 @@ 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 PinField = ComponentSchema( + type = "PinField", + 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), + ), + 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, PinField, 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..ae23d98 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==", + "UUpLQjQAAAABAAAAMDRiZTI0NjAwMTU5OWY1OTk1ZmEyZjJkOGM5MWEwZjE5OGQzZjM0YwWhAhZ0aW55dWktY29yZRZCb3R0b21TaGVldAZCb3gMQnV0dG9uEENoZWNrYm94DENvbHVtbg5EaXZpZGVyBkZvchBGcmFnbWVudBJIb3N0RXJyb3IISWNvbhRMYXp5Q29sdW1uDkxvYWRpbmcMT3B0aW9uEFBpbkZpZWxkFlJhZGlvQnV0dG9uBlJvdwxTZWxlY3QIU2hvdwxTcGFjZXIIVGV4dBJUZXh0RmllbGQOVkVSU0lPTgxlZmZlY3QCaBBpbnRlcm5hbBBtYW5pZmVzdAhtZW1vFG9ic2VydmFibGUSb25DbGVhbnVwFnBhZ2VWaXNpYmxlBnJlZhByZXNvdXJjZQxzaWduYWwKdGh1bmsOdW50cmFjawx1bndyYXASX19kZWZQcm9wEF9fZXhwb3J0GmN1cnJlbnRFZmZlY3QYY3VycmVudE93bmVyDnBlbmRpbmcGcmFuJE1BWF9SVU5TX1BFUl9GTFVTSBZjcmVhdGVPd25lchhydW5XaXRoT3duZXIYZGlzcG9zZU93bmVyGGluc2lkZVJlbmRlchB0cmFja2luZxRydW5QZW5kaW5nJGNyZWF0ZUVmZmVjdFJlY29yZBJydW5FZmZlY3QaZGlzcG9zZUVmZmVjdBZ1bnN1YnNjcmliZRZydW5DbGVhbnVwcwp0cmFjawxub3RpZnkOQ09OVFJPTApUaHVuaw5wYXRjaGVzEGhhbmRsZXJzDG5leHRJZBByZXNldElkcwhTbG90GmNhbGxDb21wb25lbnQaY3JlYXRlRWxlbWVudBBiaW5kUHJvcAxzY2FsYXIOZmxhdHRlbhpyZW5kZXJJbk93bmVyEHBlbmRpbmcyEG5leHRDYklkCnF1ZXJ5CGNhbGwIc2VuZAxyZXBvcnQccmVzb2x2ZVBlbmRpbmcacmVqZWN0UGVuZGluZxpjYW5jZWxQZW5kaW5nGGFsbG9jYXRlQ2JJZB5yZWdpc3RlclBlbmRpbmcUc2V0VGltZW91dBhjbGVhclRpbWVvdXQcaW5zdGFsbEdsb2JhbHMIcm9vdA5jdXJyZW50DnZpc2libGUUc2V0VmlzaWJsZQx0b3BpY3MMb25FbWl0DGxpc3Rlbg5ndWFyZGVkDmVudHJpZXMOaW5zdGFsbAZSQVcIS0VZUw5wcm94aWVzFnN1YnNjcmliZXJzFHVud3JhcERlZXAadW53cmFwU2hhbGxvdxZpc1dyYXBwYWJsZQh3cmFwCHN1YnMOb2JzZXJ2ZQ5jaGFuZ2VkDmlzSW5kZXgSaGFuZGxlcnMyIGludGVybmFsX2V4cG9ydHMYX19ob3N0X2FwcGx5GF9faG9zdF9xdWVyeRZfX2hvc3RfY2FsbBZfX2hvc3Rfc2VuZBpfX2hvc3RfcmVwb3J0DlJlZmxlY3QMMC4xMy4wHHRpbnl1aS5jb250cm9sCm1vdW50DGluc2VydAhtb3ZlDHJlbW92ZQxvZmZzZXQMYXNzaWduFGNvbXBvbmVudHMYY2FwYWJpbGl0aWVzDnVubW91bnQQZGlzcGF0Y2gIZW1pdApmbHVzaBR0aW55dWkucmF3FnRpbnl1aS5rZXlzBmFsbA5lZmZlY3RzEGNsZWFudXBzEGRpc3Bvc2VkCm93bmVyBGZuEnByZXZPd25lchRwcmV2RWZmZWN0AmUCYwxzcGxpY2UOcmV2ZXJzZQhpbml0CnN1YnMyAnYOZWZmZWN0MgJtCmRpcnR5CHB1c2hsb25DbGVhbnVwKCkgY2FsbGVkIG91dHNpZGUgYSBzeW5jaHJvbm91cyByZW5kZXIgcGVyaW9kCHByZXYCaQxxdWV1ZWQIcnVuc3x1cGRhdGUgbG9vcDogYW4gZWZmZWN0IHJlLXJhbiBtb3JlIHRoYW4gMTAwIHRpbWVzIGluIG9uZSBmbHVzaGZlZmZlY3QoKSBjYWxsZWQgb3V0c2lkZSBhIHN5bmNocm9ub3VzIHJlbmRlciBwZXJpb2QIZGVwcwxwYXJlbnQQZW50cmllczIQcG9zaXRpb24Kbm9kZXMIbm9kZQRhdAR0bw5pbmRleE9mLG1vdmU6IG5vZGUgbm90IGluIHNsb3QwcmVtb3ZlOiBub2RlIG5vdCBpbiBzbG90AnICbgRpZAZjbWQIYXJnc15yZWYuY21kKCkgYmVmb3JlIHRoZSByZWYgd2FzIGF0dGFjaGVkIHRvIGEgbm9kZQJ4EkZyYWdtZW50Mgpwcm9wcxBjaGlsZHJlbgh0eXBlXGgoKSBjYWxsZWQgb3V0c2lkZSBhIHN5bmNocm9ub3VzIHJlbmRlciBwZXJpb2QCcAZrZXkMcmVzdWx0CHdoYXR8YSBQcm9taXNlIChhc3luYyBjb21wb25lbnRzIGFyZSBub3Qgc3VwcG9ydGVkOyB1c2UgcmVzb3VyY2UoKSkOaXNBcnJheVRzZXZlcmFsIG5vZGVzICh3cmFwIHRoZW0gaW4gb25lIGNvbnRhaW5lcilOYSBGb3IgLyBTaG93ICh3cmFwIGl0IGluIG9uZSBjb250YWluZXIpFGNvbXBvbmVudCAWPGFub255bW91cz5GIG11c3QgcmV0dXJuIGV4YWN0bHkgb25lIG5vZGUsIGdvdCACawpjaGlsZAh0ZXN0AjwCICg+IG11c3QgYmUgYSBmdW5jdGlvbgI6CGxhc3QKZmlyc3QCdFY+IGlzIGEgZnVuY3Rpb247IGRpZCB5b3UgZm9yZ2V0IHRvIGNhbGwgaXQ/qgE+IG11c3QgYmUgYSBzdHJpbmcsIG51bWJlciwgYm9vbGVhbiBvciBudWxsOyBwcm9wcyBkbyBub3QgY3Jvc3MgdGhlIGJyaWRnZSBhcyBvYmplY3RzBm91dAxyZW5kZXI6IG11c3QgcmV0dXJuIGV4YWN0bHkgb25lIG5vZGUIY29kZQ5kZXRhaWxzDmhlYWRlcnMIYm9keRJwYXRjaEpzb24KcGFyc2USc3RyaW5naWZ5CGNiSWQIc2l0ZQhraW5kDGRldGFpbBRyZXN1bHRKc29uFEVfQkFEX0pTT04yaG9zdCByZXN1bHQgaXMgbm90IEpTT046IBJlcnJvckpzb24KZXJyb3IgaG9zdCBjYWxsIGZhaWxlZARtcxx0aW1lci5zY2hlZHVsZRh0aW1lci5jYW5jZWwCZwp0b3BpY2ZvbkVtaXQoKSBjYWxsZWQgb3V0c2lkZSBhIHN5bmNocm9ub3VzIHJlbmRlciBwZXJpb2QKZW50cnkGZXJyBEUxCHBhZ2UScHJvcHNKc29uEGhvc3RKc29uKG1vdW50KCkgY2FsbGVkIHR3aWNlTnBhZ2UgbXVzdCByZXR1cm4gZXhhY3RseSBvbmUgbm9kZSwgZ290IBJhIFByb21pc2UKY2xlYXIMbm9kZUlkCmV2ZW50FnBheWxvYWRKc29uDmhhbmRsZXIOcGF5bG9hZAhqc29uEF9fdGlueXVpWm9ic2VydmFibGUoKSB0YWtlcyBhIHBsYWluIG9i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", ) val native: Array = arrayOf( diff --git a/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt b/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt index beaa174..09bd036 100644 --- a/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt +++ b/compose/src/commonTest/kotlin/app/tinyui/PageHostTest.kt @@ -113,7 +113,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() } 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/docs/components.md b/docs/components.md index bca1674..d6ea0f4 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`、`PinField`、`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` | | +| `PinField` | `length`(默认 6)、`secure`、`enabled`、`error` | `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),这些都只是外观与通知。 + +`PinField` 是一排数字格画在一个看不见的输入框之上(2026-10-28 加):焦点与 IME 归 Kotlin,只接受数字、到 `length` 为止;不能用多个 `TextField` 拼,焦点逐格跳要跨桥往返(ADR-004 的反例)。`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..3e7cb97 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,76 @@ 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 PinFieldChangePayload { + text: string; +} + +export interface PinFieldCompletePayload { + /** when the last cell is filled */ + text: string; +} + +export interface PinFieldSetTextArgs { + text: string; +} + +export interface PinFieldFocusArgs { +} + +export interface PinFieldClearArgs { +} + +export type PinFieldCommands = { + setText: PinFieldSetTextArgs; + focus: PinFieldFocusArgs; + clear: PinFieldClearArgs; +}; + +/** 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 PinFieldProps extends LayoutProps { + /** number of cells; input stops there */ + length?: number; + /** shows a dot instead of each digit */ + secure?: boolean; + enabled?: boolean; + /** error colour on the cell outlines */ + error?: boolean; + onChange?: (e: PinFieldChangePayload) => void; + onComplete?: (e: PinFieldCompletePayload) => 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 +322,10 @@ export interface IntrinsicElements { Loading: LoadingProps; BottomSheet: BottomSheetProps; Divider: DividerProps; + Checkbox: CheckboxProps; + PinField: PinFieldProps; + Select: SelectProps; + Option: OptionProps; } export const Column = "Column"; @@ -233,3 +341,7 @@ export const Icon = "Icon"; export const Loading = "Loading"; export const BottomSheet = "BottomSheet"; export const Divider = "Divider"; +export const Checkbox = "Checkbox"; +export const PinField = "PinField"; +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..f5ccd76 --- /dev/null +++ b/sample/js-extra/src/pages/form.tsx @@ -0,0 +1,47 @@ +import { Checkbox, Column, Option, PinField, ref, Row, Select, signal, Text, TextField, type PinFieldCommands } 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}`)} /> + + + + { 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/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/pin-field.ts b/schema/components/pin-field.ts new file mode 100644 index 0000000..12c0fde --- /dev/null +++ b/schema/components/pin-field.ts @@ -0,0 +1,20 @@ +import { boolean, defineComponent, field, number } from "tinyui-cli/schema"; + +export default defineComponent("PinField", { + 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; 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" }), + }, + 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/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..694fbe6 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 PinField from "./components/pin-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, PinField, Select, Option]; From 871d0ccaaae3e0c18c22d834fce97e1cd55c4e92 Mon Sep 17 00:00:00 2001 From: HarlonWang <81813780@qq.com> Date: Wed, 28 Oct 2026 15:47:20 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(PinField):=20autofill=E2=80=94?= =?UTF-8?q?=E2=80=94oneTimeCode=20=E8=AE=A9=E7=B3=BB=E7=BB=9F=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=E7=9F=AD=E4=BF=A1=E9=AA=8C=E8=AF=81=E7=A0=81=EF=BC=8C?= =?UTF-8?q?none=20=E6=98=8E=E7=A1=AE=E4=B8=8D=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Android:semantics contentType = SmsOtpCode - iOS:Compose 只从 PlatformImeOptions 读 textContentType,不设时按 NumberPassword 推成 Password(交易密码会弹钥匙串); 所以两种取值都显式设置:oneTimeCode → UITextContentTypeOneTimeCode,none → null - sample form 页加一个短信验证码示例;components.md 同步 Co-Authored-By: Claude Opus 5.5 --- .../androidMain/kotlin/app/tinyui/Platform.android.kt | 4 ++++ .../src/commonMain/kotlin/app/tinyui/HostInterfaces.kt | 7 +++++++ .../kotlin/app/tinyui/components/Builtins.kt | 10 ++++++++-- .../app/tinyui/components/generated/BuiltinSchemas.kt | 1 + compose/src/iosMain/kotlin/app/tinyui/Platform.ios.kt | 7 +++++++ docs/components.md | 4 ++-- packages/core/src/generated/components.ts | 2 ++ sample/js-extra/src/pages/form.tsx | 2 ++ schema/components/pin-field.ts | 3 ++- 9 files changed, 35 insertions(+), 5 deletions(-) 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/components/Builtins.kt b/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt index 90be961..1823ccf 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/components/Builtins.kt @@ -56,6 +56,7 @@ 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 @@ -63,6 +64,8 @@ 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.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 @@ -84,6 +87,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 @@ -298,15 +302,17 @@ private fun PinFieldComponent(scope: NodeScope) { } 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), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done, platformImeOptions = oneTimeCodeImeOptions(oneTimeCode)), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - modifier = scope.modifier(clickable = false).focusRequester(focusRequester).onFocusChanged { focused = it.isFocused }, + modifier = scope.modifier(clickable = false).focusRequester(focusRequester).onFocusChanged { focused = it.isFocused } + .then(if (oneTimeCode) Modifier.semantics { contentType = ContentType.SmsOtpCode } else Modifier), decorationBox = { inner -> Box { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 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 09e3754..aac92d8 100644 --- a/compose/src/commonMain/kotlin/app/tinyui/components/generated/BuiltinSchemas.kt +++ b/compose/src/commonMain/kotlin/app/tinyui/components/generated/BuiltinSchemas.kt @@ -246,6 +246,7 @@ object BuiltinSchemas { "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), 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 d6ea0f4..1ca8005 100644 --- a/docs/components.md +++ b/docs/components.md @@ -74,7 +74,7 @@ prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB | `RadioButton` | `selected`(必填)、`enabled` | `onClick` | | | | `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` | | -| `PinField` | `length`(默认 6)、`secure`、`enabled`、`error` | `onChange{text}`、`onComplete{text}`(填满最后一格) | `setText{text}`、`focus`、`clear` | | +| `PinField` | `length`(默认 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}` | 是,通常是一个 `` | @@ -90,7 +90,7 @@ prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB `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),这些都只是外观与通知。 -`PinField` 是一排数字格画在一个看不见的输入框之上(2026-10-28 加):焦点与 IME 归 Kotlin,只接受数字、到 `length` 为止;不能用多个 `TextField` 拼,焦点逐格跳要跨桥往返(ADR-004 的反例)。`onComplete` 每次填满最后一格时发,`clear` / `setText` 与 `TextField.setText` 一样会触发 `onChange`——所以"输错 → 清空并标红"时,清错误态要放在有输入的 `onChange` 里,否则 `clear` 带来的空 `onChange` 会立刻把红色抹掉: +`PinField` 是一排数字格画在一个看不见的输入框之上(2026-10-28 加):焦点与 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(); diff --git a/packages/core/src/generated/components.ts b/packages/core/src/generated/components.ts index 3e7cb97..a5ebce3 100644 --- a/packages/core/src/generated/components.ts +++ b/packages/core/src/generated/components.ts @@ -282,6 +282,8 @@ export interface PinFieldProps extends LayoutProps { 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: PinFieldChangePayload) => void; onComplete?: (e: PinFieldCompletePayload) => void; ref?: Ref; diff --git a/sample/js-extra/src/pages/form.tsx b/sample/js-extra/src/pages/form.tsx index f5ccd76..664474a 100644 --- a/sample/js-extra/src/pages/form.tsx +++ b/sample/js-extra/src/pages/form.tsx @@ -36,6 +36,8 @@ export default function Form() {