diff --git a/.gitignore b/.gitignore index 0ee1c3a..7e8aa50 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ packages/*/dist/ sample/iosApp/*.xcodeproj/xcuserdata/ sample/iosApp/*.xcodeproj/project.xcworkspace/xcuserdata/ DerivedData/ + +# editor swap files +*.swp diff --git a/compose/api/compose.klib.api b/compose/api/compose.klib.api index 0c24bc3..1040f48 100644 --- a/compose/api/compose.klib.api +++ b/compose/api/compose.klib.api @@ -50,7 +50,12 @@ abstract interface wang.harlon.tinyui.schema/NodeScope { // wang.harlon.tinyui.s abstract fun RenderChild(wang.harlon.tinyui.node/UINode, androidx.compose.runtime/Composer?, kotlin/Int) // wang.harlon.tinyui.schema/NodeScope.RenderChild|RenderChild(wang.harlon.tinyui.node.UINode;androidx.compose.runtime.Composer?;kotlin.Int){}[0] abstract fun dispatch(kotlin/String, kotlin/String = ...) // wang.harlon.tinyui.schema/NodeScope.dispatch|dispatch(kotlin.String;kotlin.String){}[0] abstract fun has(kotlin/String): kotlin/Boolean // wang.harlon.tinyui.schema/NodeScope.has|has(kotlin.String){}[0] - abstract fun modifier(kotlin/Boolean = ...): androidx.compose.ui/Modifier // wang.harlon.tinyui.schema/NodeScope.modifier|modifier(kotlin.Boolean){}[0] + abstract fun modifier(kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui/Modifier // wang.harlon.tinyui.schema/NodeScope.modifier|modifier(kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int){}[0] + open fun color(kotlin/String, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/Color? // wang.harlon.tinyui.schema/NodeScope.color|color(kotlin.String;androidx.compose.runtime.Composer?;kotlin.Int){}[0] + + final class ComposeDefaultImpls { // wang.harlon.tinyui.schema/NodeScope.ComposeDefaultImpls|null[0] + final fun modifier$default(kotlin/Boolean, wang.harlon.tinyui.schema/NodeScope, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.ui/Modifier // wang.harlon.tinyui.schema/NodeScope.ComposeDefaultImpls.modifier$default|modifier$default#static(kotlin.Boolean;wang.harlon.tinyui.schema.NodeScope;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] + } } abstract interface wang.harlon.tinyui/HttpClient { // wang.harlon.tinyui/HttpClient|null[0] @@ -82,6 +87,36 @@ abstract interface wang.harlon.tinyui/Store { // wang.harlon.tinyui/Store|null[0 abstract fun set(kotlin/String, kotlin/String) // wang.harlon.tinyui/Store.set|set(kotlin.String;kotlin.String){}[0] } +sealed interface wang.harlon.tinyui.schema/ColorValue { // wang.harlon.tinyui.schema/ColorValue|null[0] + open fun resolve(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/Color // wang.harlon.tinyui.schema/ColorValue.resolve|resolve(androidx.compose.runtime.Composer?;kotlin.Int){}[0] + + final class Literal : wang.harlon.tinyui.schema/ColorValue { // wang.harlon.tinyui.schema/ColorValue.Literal|null[0] + constructor (androidx.compose.ui.graphics/Color) // wang.harlon.tinyui.schema/ColorValue.Literal.|(androidx.compose.ui.graphics.Color){}[0] + + final val color // wang.harlon.tinyui.schema/ColorValue.Literal.color|{}color[0] + final fun (): androidx.compose.ui.graphics/Color // wang.harlon.tinyui.schema/ColorValue.Literal.color.|(){}[0] + + final fun component1(): androidx.compose.ui.graphics/Color // wang.harlon.tinyui.schema/ColorValue.Literal.component1|component1(){}[0] + final fun copy(androidx.compose.ui.graphics/Color = ...): wang.harlon.tinyui.schema/ColorValue.Literal // wang.harlon.tinyui.schema/ColorValue.Literal.copy|copy(androidx.compose.ui.graphics.Color){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // wang.harlon.tinyui.schema/ColorValue.Literal.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // wang.harlon.tinyui.schema/ColorValue.Literal.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // wang.harlon.tinyui.schema/ColorValue.Literal.toString|toString(){}[0] + } + + final class Token : wang.harlon.tinyui.schema/ColorValue { // wang.harlon.tinyui.schema/ColorValue.Token|null[0] + constructor (kotlin/String) // wang.harlon.tinyui.schema/ColorValue.Token.|(kotlin.String){}[0] + + final val name // wang.harlon.tinyui.schema/ColorValue.Token.name|{}name[0] + final fun (): kotlin/String // wang.harlon.tinyui.schema/ColorValue.Token.name.|(){}[0] + + final fun component1(): kotlin/String // wang.harlon.tinyui.schema/ColorValue.Token.component1|component1(){}[0] + final fun copy(kotlin/String = ...): wang.harlon.tinyui.schema/ColorValue.Token // wang.harlon.tinyui.schema/ColorValue.Token.copy|copy(kotlin.String){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // wang.harlon.tinyui.schema/ColorValue.Token.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // wang.harlon.tinyui.schema/ColorValue.Token.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // wang.harlon.tinyui.schema/ColorValue.Token.toString|toString(){}[0] + } +} + sealed interface wang.harlon.tinyui.schema/SizeValue { // wang.harlon.tinyui.schema/SizeValue|null[0] final class Fixed : wang.harlon.tinyui.schema/SizeValue { // wang.harlon.tinyui.schema/SizeValue.Fixed|null[0] constructor (androidx.compose.ui.unit/Dp) // wang.harlon.tinyui.schema/SizeValue.Fixed.|(androidx.compose.ui.unit.Dp){}[0] @@ -420,7 +455,7 @@ sealed class wang.harlon.tinyui.schema/PropSpec { // wang.harlon.tinyui.schema/P constructor (kotlin/String?, kotlin/Boolean = ..., kotlin/Boolean = ...) // wang.harlon.tinyui.schema/PropSpec.ColorSpec.|(kotlin.String?;kotlin.Boolean;kotlin.Boolean){}[0] final val default // wang.harlon.tinyui.schema/PropSpec.ColorSpec.default|{}default[0] - final fun (): androidx.compose.ui.graphics/Color? // wang.harlon.tinyui.schema/PropSpec.ColorSpec.default.|(){}[0] + final fun (): wang.harlon.tinyui.schema/ColorValue? // wang.harlon.tinyui.schema/PropSpec.ColorSpec.default.|(){}[0] final fun convert(kotlinx.serialization.json/JsonPrimitive): kotlin/Any? // wang.harlon.tinyui.schema/PropSpec.ColorSpec.convert|convert(kotlinx.serialization.json.JsonPrimitive){}[0] } @@ -501,6 +536,10 @@ final object wang.harlon.tinyui.components.generated/BuiltinSchemas { // wang.ha final fun (): wang.harlon.tinyui.schema/ComponentSchema // wang.harlon.tinyui.components.generated/BuiltinSchemas.TextField.|(){}[0] final val all // wang.harlon.tinyui.components.generated/BuiltinSchemas.all|{}all[0] final fun (): kotlin.collections/List // wang.harlon.tinyui.components.generated/BuiltinSchemas.all.|(){}[0] + final val colorTokens // wang.harlon.tinyui.components.generated/BuiltinSchemas.colorTokens|{}colorTokens[0] + final fun (): kotlin.collections/Set // wang.harlon.tinyui.components.generated/BuiltinSchemas.colorTokens.|(){}[0] + final val textStyles // wang.harlon.tinyui.components.generated/BuiltinSchemas.textStyles|{}textStyles[0] + final fun (): kotlin.collections/Set // wang.harlon.tinyui.components.generated/BuiltinSchemas.textStyles.|(){}[0] } final object wang.harlon.tinyui.schema/LayoutProps { // wang.harlon.tinyui.schema/LayoutProps|null[0] @@ -508,6 +547,12 @@ final object wang.harlon.tinyui.schema/LayoutProps { // wang.harlon.tinyui.schem final fun (): kotlin.collections/Map // wang.harlon.tinyui.schema/LayoutProps.specs.|(){}[0] } +final object wang.harlon.tinyui.schema/Theme { // wang.harlon.tinyui.schema/Theme|null[0] + final fun color(androidx.compose.material3/ColorScheme, kotlin/String): androidx.compose.ui.graphics/Color? // wang.harlon.tinyui.schema/Theme.color|color(androidx.compose.material3.ColorScheme;kotlin.String){}[0] + final fun isColorToken(kotlin/String): kotlin/Boolean // wang.harlon.tinyui.schema/Theme.isColorToken|isColorToken(kotlin.String){}[0] + final fun textStyle(androidx.compose.material3/Typography, kotlin/String): androidx.compose.ui.text/TextStyle? // wang.harlon.tinyui.schema/Theme.textStyle|textStyle(androidx.compose.material3.Typography;kotlin.String){}[0] +} + final object wang.harlon.tinyui/TinyUI { // wang.harlon.tinyui/TinyUI|null[0] final val engineVersion // wang.harlon.tinyui/TinyUI.engineVersion|{}engineVersion[0] final fun (): kotlin/String // wang.harlon.tinyui/TinyUI.engineVersion.|(){}[0] @@ -525,6 +570,8 @@ final val wang.harlon.tinyui.node/wang_harlon_tinyui_node_Command$stableprop // final val wang.harlon.tinyui.node/wang_harlon_tinyui_node_NodeTree$stableprop // wang.harlon.tinyui.node/wang_harlon_tinyui_node_NodeTree$stableprop|#static{}wang_harlon_tinyui_node_NodeTree$stableprop[0] final val wang.harlon.tinyui.node/wang_harlon_tinyui_node_PatchProblem$stableprop // wang.harlon.tinyui.node/wang_harlon_tinyui_node_PatchProblem$stableprop|#static{}wang_harlon_tinyui_node_PatchProblem$stableprop[0] final val wang.harlon.tinyui.node/wang_harlon_tinyui_node_UINode$stableprop // wang.harlon.tinyui.node/wang_harlon_tinyui_node_UINode$stableprop|#static{}wang_harlon_tinyui_node_UINode$stableprop[0] +final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Literal$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Literal$stableprop|#static{}wang_harlon_tinyui_schema_ColorValue_Literal$stableprop[0] +final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Token$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Token$stableprop|#static{}wang_harlon_tinyui_schema_ColorValue_Token$stableprop[0] final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentRegistry$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentRegistry$stableprop|#static{}wang_harlon_tinyui_schema_ComponentRegistry$stableprop[0] final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentSchema$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentSchema$stableprop|#static{}wang_harlon_tinyui_schema_ComponentSchema$stableprop[0] final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_LayoutProps$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_LayoutProps$stableprop|#static{}wang_harlon_tinyui_schema_LayoutProps$stableprop[0] @@ -540,6 +587,7 @@ final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_PropSpec_Str$stabl final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fill$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fill$stableprop|#static{}wang_harlon_tinyui_schema_SizeValue_Fill$stableprop[0] final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fixed$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fixed$stableprop|#static{}wang_harlon_tinyui_schema_SizeValue_Fixed$stableprop[0] final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Wrap$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Wrap$stableprop|#static{}wang_harlon_tinyui_schema_SizeValue_Wrap$stableprop[0] +final val wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_Theme$stableprop // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_Theme$stableprop|#static{}wang_harlon_tinyui_schema_Theme$stableprop[0] final val wang.harlon.tinyui/wang_harlon_tinyui_BuildManifest$stableprop // wang.harlon.tinyui/wang_harlon_tinyui_BuildManifest$stableprop|#static{}wang_harlon_tinyui_BuildManifest$stableprop[0] final val wang.harlon.tinyui/wang_harlon_tinyui_EventBus$stableprop // wang.harlon.tinyui/wang_harlon_tinyui_EventBus$stableprop|#static{}wang_harlon_tinyui_EventBus$stableprop[0] final val wang.harlon.tinyui/wang_harlon_tinyui_HostException$stableprop // wang.harlon.tinyui/wang_harlon_tinyui_HostException$stableprop|#static{}wang_harlon_tinyui_HostException$stableprop[0] @@ -567,6 +615,8 @@ final fun wang.harlon.tinyui.node/wang_harlon_tinyui_node_Command$stableprop_get final fun wang.harlon.tinyui.node/wang_harlon_tinyui_node_NodeTree$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.node/wang_harlon_tinyui_node_NodeTree$stableprop_getter|wang_harlon_tinyui_node_NodeTree$stableprop_getter(){}[0] final fun wang.harlon.tinyui.node/wang_harlon_tinyui_node_PatchProblem$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.node/wang_harlon_tinyui_node_PatchProblem$stableprop_getter|wang_harlon_tinyui_node_PatchProblem$stableprop_getter(){}[0] final fun wang.harlon.tinyui.node/wang_harlon_tinyui_node_UINode$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.node/wang_harlon_tinyui_node_UINode$stableprop_getter|wang_harlon_tinyui_node_UINode$stableprop_getter(){}[0] +final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Literal$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Literal$stableprop_getter|wang_harlon_tinyui_schema_ColorValue_Literal$stableprop_getter(){}[0] +final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Token$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ColorValue_Token$stableprop_getter|wang_harlon_tinyui_schema_ColorValue_Token$stableprop_getter(){}[0] final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentRegistry$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentRegistry$stableprop_getter|wang_harlon_tinyui_schema_ComponentRegistry$stableprop_getter(){}[0] final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentSchema$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_ComponentSchema$stableprop_getter|wang_harlon_tinyui_schema_ComponentSchema$stableprop_getter(){}[0] final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_LayoutProps$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_LayoutProps$stableprop_getter|wang_harlon_tinyui_schema_LayoutProps$stableprop_getter(){}[0] @@ -582,6 +632,7 @@ final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_PropSpec_Str$stabl final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fill$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fill$stableprop_getter|wang_harlon_tinyui_schema_SizeValue_Fill$stableprop_getter(){}[0] final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fixed$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Fixed$stableprop_getter|wang_harlon_tinyui_schema_SizeValue_Fixed$stableprop_getter(){}[0] final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Wrap$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_SizeValue_Wrap$stableprop_getter|wang_harlon_tinyui_schema_SizeValue_Wrap$stableprop_getter(){}[0] +final fun wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_Theme$stableprop_getter(): kotlin/Int // wang.harlon.tinyui.schema/wang_harlon_tinyui_schema_Theme$stableprop_getter|wang_harlon_tinyui_schema_Theme$stableprop_getter(){}[0] final fun wang.harlon.tinyui/PageFailureScreen(wang.harlon.tinyui/PageFailure, androidx.compose.runtime/Composer?, kotlin/Int) // wang.harlon.tinyui/PageFailureScreen|PageFailureScreen(wang.harlon.tinyui.PageFailure;androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun wang.harlon.tinyui/TinyUIPage(wang.harlon.tinyui/RuntimeBundle, wang.harlon.tinyui/PageModule, wang.harlon.tinyui.schema/ComponentRegistry, wang.harlon.tinyui/PageSink, wang.harlon.tinyui/HostServices?, kotlin/String?, androidx.compose.ui/Modifier?, wang.harlon.tinyui/SourceMaps?, kotlin/Function3?, kotlin/Function1?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // wang.harlon.tinyui/TinyUIPage|TinyUIPage(wang.harlon.tinyui.RuntimeBundle;wang.harlon.tinyui.PageModule;wang.harlon.tinyui.schema.ComponentRegistry;wang.harlon.tinyui.PageSink;wang.harlon.tinyui.HostServices?;kotlin.String?;androidx.compose.ui.Modifier?;wang.harlon.tinyui.SourceMaps?;kotlin.Function3?;kotlin.Function1?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun wang.harlon.tinyui/wang_harlon_tinyui_BuildManifest$stableprop_getter(): kotlin/Int // wang.harlon.tinyui/wang_harlon_tinyui_BuildManifest$stableprop_getter|wang_harlon_tinyui_BuildManifest$stableprop_getter(){}[0] diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt index 3c0ef2d..bc6391d 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/PageHost.kt @@ -17,7 +17,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -318,12 +317,13 @@ class PageHost( override fun dispatch(event: String, payload: String) = this@PageHost.dispatch(node.id, event, payload) /** docs/components.md §2: size → clip → background → gesture → padding. */ + @Composable override fun modifier(clickable: Boolean): Modifier { var m: Modifier = Modifier when (val w = get("width")) { is SizeValue.Fixed -> m = m.width(w.dp); SizeValue.Fill -> m = m.fillMaxWidth(); SizeValue.Wrap -> m = m.wrapContentWidth(); null -> {} } when (val h = get("height")) { is SizeValue.Fixed -> m = m.height(h.dp); SizeValue.Fill -> m = m.fillMaxHeight(); SizeValue.Wrap -> m = m.wrapContentHeight(); null -> {} } get("cornerRadius")?.let { m = m.clip(RoundedCornerShape(it)) } - get("background")?.let { m = m.background(it) } + color("background")?.let { m = m.background(it) } if (clickable && has("onClick")) m = m.clickable { dispatch("onClick") } get("padding")?.let { m = m.padding(it) } return m diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/Builtins.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/Builtins.kt index 426b990..40515e9 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/Builtins.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/Builtins.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -46,6 +48,7 @@ import wang.harlon.tinyui.components.generated.BuiltinSchemas import wang.harlon.tinyui.schema.Commands import wang.harlon.tinyui.schema.ComponentRegistry import wang.harlon.tinyui.schema.NodeScope +import wang.harlon.tinyui.schema.Theme /** First batch of built-ins (docs/components.md §3); schemas come from schema/ via `tinyui schema`. */ fun ComponentRegistry.registerBuiltins(): ComponentRegistry = apply { @@ -70,9 +73,10 @@ fun ComponentRegistry.registerBuiltins(): ComponentRegistry = apply { val maxLines = scope.get("maxLines")?.toInt()?.takeIf { it > 0 } ?: Int.MAX_VALUE Text( text = scope["text"] ?: "", - color = scope["color"] ?: Color.Unspecified, + style = scope.get("style")?.let { Theme.textStyle(MaterialTheme.typography, it) } ?: LocalTextStyle.current, + color = scope.color("color") ?: Color.Unspecified, fontSize = scope["fontSize"] ?: TextUnit.Unspecified, - fontWeight = when (scope.get("fontWeight")) { "medium" -> FontWeight.Medium; "bold" -> FontWeight.Bold; else -> FontWeight.Normal }, + fontWeight = when (scope.get("fontWeight")) { "normal" -> FontWeight.Normal; "medium" -> FontWeight.Medium; "bold" -> FontWeight.Bold; else -> null }, maxLines = maxLines, overflow = if (maxLines == Int.MAX_VALUE) TextOverflow.Clip else TextOverflow.Ellipsis, textAlign = when (scope.get("align")) { "center" -> TextAlign.Center; "end" -> TextAlign.End; else -> TextAlign.Start }, diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/generated/BuiltinSchemas.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/generated/BuiltinSchemas.kt index 5b629f5..b57f326 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/generated/BuiltinSchemas.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/components/generated/BuiltinSchemas.kt @@ -56,9 +56,10 @@ object BuiltinSchemas { type = "Text", props = mapOf( "text" to PropSpec.Str(default = null, required = true, initial = false), + "style" to PropSpec.Enum(values = setOf("displayLarge", "displayMedium", "displaySmall", "headlineLarge", "headlineMedium", "headlineSmall", "titleLarge", "titleMedium", "titleSmall", "bodyLarge", "bodyMedium", "bodySmall", "labelLarge", "labelMedium", "labelSmall"), default = null, required = false, initial = false), "color" to PropSpec.ColorSpec(default = null, required = false, initial = false), "fontSize" to PropSpec.Sp(default = null, required = false, initial = false), - "fontWeight" to PropSpec.Enum(values = setOf("normal", "medium", "bold"), default = "normal", required = false, initial = false), + "fontWeight" to PropSpec.Enum(values = setOf("normal", "medium", "bold"), default = null, required = false, initial = false), "maxLines" to PropSpec.Num(default = 0.0, required = false, initial = false), "align" to PropSpec.Enum(values = setOf("start", "center", "end"), default = "start", required = false, initial = false), ), @@ -137,4 +138,8 @@ object BuiltinSchemas { ) val all: List = listOf(Column, Row, Box, Text, Button, TextField, LazyColumn, Spacer) + + /** 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") + val textStyles: Set = setOf("displayLarge", "displayMedium", "displaySmall", "headlineLarge", "headlineMedium", "headlineSmall", "titleLarge", "titleMedium", "titleSmall", "bodyLarge", "bodyMedium", "bodySmall", "labelLarge", "labelMedium", "labelSmall") } diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/ComponentSchema.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/ComponentSchema.kt index 6a45f1b..cd24402 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/ComponentSchema.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/ComponentSchema.kt @@ -51,10 +51,10 @@ sealed class PropSpec(val kind: String, val required: Boolean, val initial: Bool override fun convert(value: JsonPrimitive): Any? = value.takeUnless { it.isString }?.doubleOrNull?.sp } - /** `#RRGGBB` or `#AARRGGBB`. */ + /** `#RRGGBB` / `#AARRGGBB`, or a theme token name (Theme.kt). */ class ColorSpec(default: String?, required: Boolean = false, initial: Boolean = false) : PropSpec("color", required, initial) { - override val default: Color? = default?.let(::parseColor) - override fun convert(value: JsonPrimitive): Any? = value.takeIf { it.isString }?.content?.let(::parseColor) + override val default: ColorValue? = default?.let(::parseColorValue) + override fun convert(value: JsonPrimitive): Any? = value.takeIf { it.isString }?.content?.let(::parseColorValue) } /** Stored as the enum name; the composable maps it. */ @@ -87,6 +87,10 @@ enum class FieldSpec { } } +internal fun parseColorValue(text: String): ColorValue? = + if (text.startsWith("#")) parseColor(text)?.let { ColorValue.Literal(it) } + else text.takeIf(Theme::isColorToken)?.let { ColorValue.Token(it) } + internal fun parseColor(text: String): Color? { if (!text.startsWith("#")) return null val hex = text.substring(1) diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/NodeScope.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/NodeScope.kt index e57cbcd..1c0dda1 100644 --- a/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/NodeScope.kt +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/NodeScope.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import wang.harlon.tinyui.node.Command import wang.harlon.tinyui.node.UINode @@ -20,7 +21,12 @@ interface NodeScope { /** K2: fire and forget, on any thread. [payload] is the flat JSON object the event schema declares. */ fun dispatch(event: String, payload: String = "{}") + /** A `color` prop with theme tokens resolved against the current MaterialTheme. */ + @Composable + fun color(key: String): Color? = get(key)?.resolve() + /** Layout modifier from the common props; with [clickable] the click gesture is attached when `onClick` is registered. */ + @Composable fun modifier(clickable: Boolean = true): Modifier @Composable diff --git a/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/Theme.kt b/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/Theme.kt new file mode 100644 index 0000000..cbf02c8 --- /dev/null +++ b/compose/src/commonMain/kotlin/wang/harlon/tinyui/schema/Theme.kt @@ -0,0 +1,99 @@ +package wang.harlon.tinyui.schema + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle + +/** A `color` prop: a literal, or a theme token the host resolves at composition (docs/components.md §6). */ +sealed interface ColorValue { + data class Literal(val color: Color) : ColorValue + data class Token(val name: String) : ColorValue + + @Composable + fun resolve(): Color = when (this) { + is Literal -> color + is Token -> Theme.color(MaterialTheme.colorScheme, name) ?: Color.Unspecified + } +} + +/** Token names → MaterialTheme values. The name lists in the schema DSL (cli `schema/tokens.ts`) must match these. */ +object Theme { + fun isColorToken(name: String): Boolean = color(PROBE, name) != null + + fun color(scheme: ColorScheme, token: String): Color? = when (token) { + "primary" -> scheme.primary + "onPrimary" -> scheme.onPrimary + "primaryContainer" -> scheme.primaryContainer + "onPrimaryContainer" -> scheme.onPrimaryContainer + "inversePrimary" -> scheme.inversePrimary + "secondary" -> scheme.secondary + "onSecondary" -> scheme.onSecondary + "secondaryContainer" -> scheme.secondaryContainer + "onSecondaryContainer" -> scheme.onSecondaryContainer + "tertiary" -> scheme.tertiary + "onTertiary" -> scheme.onTertiary + "tertiaryContainer" -> scheme.tertiaryContainer + "onTertiaryContainer" -> scheme.onTertiaryContainer + "background" -> scheme.background + "onBackground" -> scheme.onBackground + "surface" -> scheme.surface + "onSurface" -> scheme.onSurface + "surfaceVariant" -> scheme.surfaceVariant + "onSurfaceVariant" -> scheme.onSurfaceVariant + "surfaceTint" -> scheme.surfaceTint + "inverseSurface" -> scheme.inverseSurface + "inverseOnSurface" -> scheme.inverseOnSurface + "error" -> scheme.error + "onError" -> scheme.onError + "errorContainer" -> scheme.errorContainer + "onErrorContainer" -> scheme.onErrorContainer + "outline" -> scheme.outline + "outlineVariant" -> scheme.outlineVariant + "scrim" -> scheme.scrim + "surfaceBright" -> scheme.surfaceBright + "surfaceDim" -> scheme.surfaceDim + "surfaceContainer" -> scheme.surfaceContainer + "surfaceContainerHigh" -> scheme.surfaceContainerHigh + "surfaceContainerHighest" -> scheme.surfaceContainerHighest + "surfaceContainerLow" -> scheme.surfaceContainerLow + "surfaceContainerLowest" -> scheme.surfaceContainerLowest + "primaryFixed" -> scheme.primaryFixed + "primaryFixedDim" -> scheme.primaryFixedDim + "onPrimaryFixed" -> scheme.onPrimaryFixed + "onPrimaryFixedVariant" -> scheme.onPrimaryFixedVariant + "secondaryFixed" -> scheme.secondaryFixed + "secondaryFixedDim" -> scheme.secondaryFixedDim + "onSecondaryFixed" -> scheme.onSecondaryFixed + "onSecondaryFixedVariant" -> scheme.onSecondaryFixedVariant + "tertiaryFixed" -> scheme.tertiaryFixed + "tertiaryFixedDim" -> scheme.tertiaryFixedDim + "onTertiaryFixed" -> scheme.onTertiaryFixed + "onTertiaryFixedVariant" -> scheme.onTertiaryFixedVariant + else -> null + } + + fun textStyle(typography: Typography, name: String): TextStyle? = when (name) { + "displayLarge" -> typography.displayLarge + "displayMedium" -> typography.displayMedium + "displaySmall" -> typography.displaySmall + "headlineLarge" -> typography.headlineLarge + "headlineMedium" -> typography.headlineMedium + "headlineSmall" -> typography.headlineSmall + "titleLarge" -> typography.titleLarge + "titleMedium" -> typography.titleMedium + "titleSmall" -> typography.titleSmall + "bodyLarge" -> typography.bodyLarge + "bodyMedium" -> typography.bodyMedium + "bodySmall" -> typography.bodySmall + "labelLarge" -> typography.labelLarge + "labelMedium" -> typography.labelMedium + "labelSmall" -> typography.labelSmall + else -> null + } + + private val PROBE = lightColorScheme() +} diff --git a/compose/src/commonTest/kotlin/wang/harlon/tinyui/node/NodeTreeTest.kt b/compose/src/commonTest/kotlin/wang/harlon/tinyui/node/NodeTreeTest.kt index fd0fc06..9606a8f 100644 --- a/compose/src/commonTest/kotlin/wang/harlon/tinyui/node/NodeTreeTest.kt +++ b/compose/src/commonTest/kotlin/wang/harlon/tinyui/node/NodeTreeTest.kt @@ -1,6 +1,7 @@ package wang.harlon.tinyui.node import androidx.compose.ui.graphics.Color +import wang.harlon.tinyui.schema.ColorValue import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import wang.harlon.tinyui.components.registerBuiltins @@ -30,7 +31,7 @@ class NodeTreeTest { fun convertsPropsOnWriteAndRestoresDefaultsOnNull() { tree.apply("""[["c",1,"Text"],["p",1,"text","t"],["p",1,"color","#FF0000"],["p",1,"fontSize",18],["i",0,1,0]]""") val node = tree.node(1)!! - assertEquals(Color(0xFFFF0000), node.props["color"]) + assertEquals(ColorValue.Literal(Color(0xFFFF0000)), node.props["color"]) assertEquals(18.sp, node.props["fontSize"]) tree.apply("""[["p",1,"color",null]]""") assertNull(node.props["color"], "null restores the schema default, which Text.color does not have") diff --git a/compose/src/commonTest/kotlin/wang/harlon/tinyui/schema/ThemeTest.kt b/compose/src/commonTest/kotlin/wang/harlon/tinyui/schema/ThemeTest.kt new file mode 100644 index 0000000..4f4b0f4 --- /dev/null +++ b/compose/src/commonTest/kotlin/wang/harlon/tinyui/schema/ThemeTest.kt @@ -0,0 +1,32 @@ +package wang.harlon.tinyui.schema + +import androidx.compose.material3.Typography +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import kotlinx.serialization.json.JsonPrimitive +import wang.harlon.tinyui.components.generated.BuiltinSchemas +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ThemeTest { + @Test + fun everyTokenTheSchemaDslKnowsResolves() { + val scheme = lightColorScheme() + for (token in BuiltinSchemas.colorTokens) assertNotNull(Theme.color(scheme, token), "color token $token") + val typography = Typography() + for (name in BuiltinSchemas.textStyles) assertNotNull(Theme.textStyle(typography, name), "text style $name") + } + + @Test + fun colorPropsTakeLiteralsAndTokensOnly() { + val spec = PropSpec.ColorSpec(default = "primary") + assertEquals(ColorValue.Token("primary"), spec.default) + assertEquals(ColorValue.Literal(Color(0xFF112233)), spec.convert(JsonPrimitive("#112233"))) + assertEquals(ColorValue.Token("onSurfaceVariant"), spec.convert(JsonPrimitive("onSurfaceVariant"))) + assertNull(spec.convert(JsonPrimitive("Primary")), "token names are case-sensitive") + assertNull(spec.convert(JsonPrimitive("#12")), "malformed literal") + assertNull(spec.convert(JsonPrimitive(1))) + } +} diff --git a/docs/components.md b/docs/components.md index 5e97258..0416607 100644 --- a/docs/components.md +++ b/docs/components.md @@ -30,7 +30,7 @@ defineComponent("TextField", { }); ``` -prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB` / `#AARRGGBB`)/ `enumOf([...])` / `size`(数字 dp,或 `"fill"` / `"wrap"`)。选项:`required`、`default`、`initial`(只认创建时的值,之后的写入按 E5 跳过并上报)、`doc`。事件 payload 与命令参数的字段只有 `field.string / number / boolean`,扁平。 +prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB` / `#AARRGGBB`,或 §6 的主题 token 名)/ `enumOf([...])` / `size`(数字 dp,或 `"fill"` / `"wrap"`)。选项:`required`、`default`、`initial`(只认创建时的值,之后的写入按 E5 跳过并上报)、`doc`。事件 payload 与命令参数的字段只有 `field.string / number / boolean`,扁平。 ## 2. 公共布局 prop 与 Modifier 顺序 @@ -56,7 +56,7 @@ prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB |---|---|---|---|---| | `Column` / `Row` | `gap`、`align`(交叉轴 start / center / end)、`justify`(主轴 start / center / end / spaceBetween) | `onClick` | | 是 | | `Box` | `align`(九宫格) | `onClick` | | 是 | -| `Text` | `text`(必填)、`color`、`fontSize`、`fontWeight`(normal / medium / bold)、`maxLines`(0 = 不限,超出省略号)、`align` | `onClick` | | | +| `Text` | `text`(必填)、`style`(M3 文字样式名,§6)、`color`、`fontSize`、`fontWeight`(normal / medium / bold;设了就覆盖 `style` 的对应字段,不设则跟 `style`)、`maxLines`(0 = 不限,超出省略号)、`align` | `onClick` | | | | `Button` | `text`(必填)、`enabled`、`variant`(filled / outlined / text) | `onClick` | | | | `TextField` | `initialText`(initial)、`placeholder`、`singleLine`、`keyboard` | `onChange{text}`、`onCommit{text}`(IME Done 或失焦) | `setText{text}`、`focus`、`blur` | | | `LazyColumn` | `gap` | `onReachEnd`、`onScrollEnd{index}` | `scrollTo{index}` | 是,通常是一个 `` | @@ -73,3 +73,12 @@ prop 类型:`string` / `number` / `boolean` / `dp` / `sp` / `color`(`#RRGGBB ## 5. `Placeholder` 未注册的类型渲染 `Placeholder`,同时 E5 上报。debug 构建(Android `FLAG_DEBUGGABLE`、iOS debug binary)画红框加类型名与 id;release 渲染零尺寸的空 `Box`。 + +## 6. 主题 token + +定于 2026-09-17。**主题色与文字样式只能以 token 名过桥,Kotlin 在组合期从 `MaterialTheme` 解析;JS 永远拿不到主题的具体值。** 深浅切换、宿主换主题色时页面不发任何 patch 就跟着变——主题是宿主的运行时状态,不是页面的数据(与 ADR-004"高频状态留 Kotlin"同一原理)。需要派生色(如 primary 变浅)时由宿主注册成新 token,不在页面里算。 + +- `color` 类型的 prop 同时接受字面量(`#RRGGBB` / `#AARRGGBB`)与 token 名;生成器出联合类型(`` `#${string}` | ColorToken ``),拼错的 token 编译期报,Kotlin 侧按首字符 `#` 分流。字面量留给品牌色、图表色这类本来就不属于主题的颜色 +- token 词表就是 Material 3 的字段名:颜色为 `ColorScheme` 的全部字段,文字样式为 `Typography` 的十五个名字(cli `schema/tokens.ts` 是词表真值,`compose/.../schema/Theme.kt` 逐一解析,测试保证两边一致) +- 暂不做:宿主自定义 token(触发条件:某宿主出现 M3 之外的语义色要跨页复用,届时走清单下发 + 宿主类型包生成,与宿主组件同一条路);间距 / 圆角 / 阴影的 token + diff --git a/packages/cli/src/schema/dsl.ts b/packages/cli/src/schema/dsl.ts index 1d1bd72..cf6cb9a 100644 --- a/packages/cli/src/schema/dsl.ts +++ b/packages/cli/src/schema/dsl.ts @@ -48,6 +48,7 @@ export const number = (o: PropOptions & { default?: number } = {}) => prop("numb export const boolean = (o: PropOptions & { default?: boolean } = {}) => prop("boolean", o); export const dp = (o: PropOptions & { default?: number } = {}) => prop("dp", o); export const sp = (o: PropOptions & { default?: number } = {}) => prop("sp", o); +/** `#RRGGBB` / `#AARRGGBB`, or a theme token name (tokens.ts). */ export const color = (o: PropOptions & { default?: string } = {}) => prop("color", o); /** A number in dp, or "fill" / "wrap". */ export const size = (o: PropOptions & { default?: number | "fill" | "wrap" } = {}) => prop("size", o); diff --git a/packages/cli/src/schema/generate.ts b/packages/cli/src/schema/generate.ts index 55cad35..8801409 100644 --- a/packages/cli/src/schema/generate.ts +++ b/packages/cli/src/schema/generate.ts @@ -1,11 +1,20 @@ import type { ComponentDef, FieldDef, PropDef } from "./dsl.ts"; import { LAYOUT_PROPS } from "./dsl.ts"; +import { COLOR_TOKENS, TEXT_STYLES } from "./tokens.ts"; const HEADER = "// Generated by `tinyui schema` from schema/; do not edit."; /** [nodeImport] is where `Node` and `Ref` come from: `@tiny-ui/core` for host packages, `../node.ts` inside core itself. */ export function generateTs(components: ComponentDef[], nodeImport = "@tiny-ui/core"): string { - const out: string[] = [HEADER, `import type { Node, Ref } from ${JSON.stringify(nodeImport)};`, ""]; + const out: string[] = [HEADER]; + if (nodeImport === "@tiny-ui/core") { + out.push(`import type { ColorValue, Node, Ref } from "@tiny-ui/core";`, ""); + } else { + out.push(`import type { Node, Ref } from ${JSON.stringify(nodeImport)};`, ""); + out.push(`export type ColorToken = ${COLOR_TOKENS.map((t) => JSON.stringify(t)).join(" | ")};`); + out.push("/** A literal `#RRGGBB` / `#AARRGGBB`, or a theme token resolved by the host at composition. */"); + out.push("export type ColorValue = `#${string}` | ColorToken;", ""); + } out.push("export interface LayoutProps {"); for (const [k, p] of Object.entries(LAYOUT_PROPS)) out.push(` ${k}?: ${tsPropType(p)};`); out.push("}", ""); @@ -55,6 +64,11 @@ export function generateKt(components: ComponentDef[], pkg: string, objectName: out.push(` )`, ""); } out.push(` val all: List = listOf(${components.map((c) => ktIdent(c.name)).join(", ")})`); + if (objectName === "BuiltinSchemas") { + out.push("", " /** Theme token names the schema DSL knows; Theme.kt must resolve each (checked by test). */"); + out.push(` val colorTokens: Set = setOf(${COLOR_TOKENS.map((t) => JSON.stringify(t)).join(", ")})`); + out.push(` val textStyles: Set = setOf(${TEXT_STYLES.map((t) => JSON.stringify(t)).join(", ")})`); + } out.push("}"); return out.join("\n") + "\n"; } @@ -71,7 +85,8 @@ function tsInterface(name: string, fields: Record): string[] { function tsPropType(p: PropDef): string { switch (p.kind) { - case "string": case "color": return "string"; + case "string": return "string"; + case "color": return "ColorValue"; case "number": case "dp": case "sp": return "number"; case "boolean": return "boolean"; case "size": return 'number | "fill" | "wrap"'; diff --git a/packages/cli/src/schema/index.ts b/packages/cli/src/schema/index.ts index 1b675fc..368924c 100644 --- a/packages/cli/src/schema/index.ts +++ b/packages/cli/src/schema/index.ts @@ -1,2 +1,3 @@ // What schema authors import; the generator and loader stay inside the CLI. export * from "./dsl.ts"; +export * from "./tokens.ts"; diff --git a/packages/cli/src/schema/tokens.ts b/packages/cli/src/schema/tokens.ts new file mode 100644 index 0000000..231c0aa --- /dev/null +++ b/packages/cli/src/schema/tokens.ts @@ -0,0 +1,26 @@ +// Theme tokens a page may name instead of a literal value; Kotlin resolves them from MaterialTheme at composition +// (docs/components.md §6). Names are Material 3's own; the Kotlin resolver in compose/.../schema/Theme.kt must cover every entry. + +export const COLOR_TOKENS = [ + "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", +] as const; + +export const TEXT_STYLES = [ + "displayLarge", "displayMedium", "displaySmall", + "headlineLarge", "headlineMedium", "headlineSmall", + "titleLarge", "titleMedium", "titleSmall", + "bodyLarge", "bodyMedium", "bodySmall", + "labelLarge", "labelMedium", "labelSmall", +] as const; diff --git a/packages/cli/test/schema.test.ts b/packages/cli/test/schema.test.ts new file mode 100644 index 0000000..0f80791 --- /dev/null +++ b/packages/cli/test/schema.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { color, defineComponent, enumOf, string, TEXT_STYLES } from "../src/schema/index.ts"; +import { generateKt, generateTs } from "../src/schema/generate.ts"; + +const card = defineComponent("pp.Card", { + props: { title: string({ required: true }), tint: color({ default: "primary" }), style: enumOf(TEXT_STYLES) }, + events: {}, +}); + +describe("schema generator", () => { + it("types color props as ColorValue; host packages import it from core, core defines it", () => { + const host = generateTs([card]); + assert.match(host, /import type \{ ColorValue, Node, Ref \} from "@tiny-ui\/core"/); + assert.match(host, /tint\?: ColorValue;/); + assert.doesNotMatch(host, /export type ColorValue/); + const core = generateTs([card], "../node.ts"); + assert.match(core, /export type ColorValue = `#\$\{string\}` \| ColorToken;/); + assert.match(core, /export type ColorToken = "primary" \|/); + }); + + it("emits the token name sets only for the built-in object", () => { + const builtin = generateKt([card], "x", "BuiltinSchemas"); + assert.match(builtin, /val colorTokens: Set = setOf\("primary"/); + assert.match(builtin, /val textStyles: Set = setOf\("displayLarge"/); + assert.match(builtin, /"tint" to PropSpec\.ColorSpec\(default = "primary"/); + assert.doesNotMatch(generateKt([card], "x", "HostSchemas"), /colorTokens/); + }); +}); diff --git a/packages/core/src/generated/components.ts b/packages/core/src/generated/components.ts index 3f0b2be..a9a8930 100644 --- a/packages/core/src/generated/components.ts +++ b/packages/core/src/generated/components.ts @@ -1,11 +1,15 @@ // Generated by `tinyui schema` from schema/; do not edit. import type { Node, Ref } from "../node.ts"; +export type ColorToken = "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"; +/** A literal `#RRGGBB` / `#AARRGGBB`, or a theme token resolved by the host at composition. */ +export type ColorValue = `#${string}` | ColorToken; + export interface LayoutProps { width?: number | "fill" | "wrap"; height?: number | "fill" | "wrap"; cornerRadius?: number; - background?: string; + background?: ColorValue; padding?: number; } @@ -54,7 +58,9 @@ export interface TextClickPayload { export interface TextProps extends LayoutProps { text: string; - color?: 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"; + color?: ColorValue; fontSize?: number; fontWeight?: "normal" | "medium" | "bold"; /** 0 = unlimited */ diff --git a/sample/js/src/pages/todos.tsx b/sample/js/src/pages/todos.tsx index 72b151e..90ca236 100644 --- a/sample/js/src/pages/todos.tsx +++ b/sample/js/src/pages/todos.tsx @@ -41,7 +41,7 @@ export default function Todos() { return ( - t.done).length} done`} fontSize={20} fontWeight="bold" /> + t.done).length} done`} style="titleLarge" /> }> {() => ( @@ -63,8 +63,8 @@ export default function Todos() { function TodoRow(props: { todo: Todo; onRemove: () => void }) { return ( - { props.todo.done = !props.todo.done; }}> - + { props.todo.done = !props.todo.done; }}> +