diff --git a/CLAUDE.md b/CLAUDE.md index 0f5db664e..676845a24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ This is a multi-module Gradle project: ### Testing -See **[docs/RUNNING_TESTS.md](docs/RUNNING_TESTS.md)** for the full guide including prerequisites, gotchas, and how to write tests. +See **[docs/TESTING_GUIDE.md](docs/TESTING_GUIDE.md)** for the full guide including prerequisites, gotchas, and how to write tests. ```bash # Fastest — no external tools needed @@ -120,7 +120,7 @@ Pages implement the `Page` interface and are annotated with `@Routable`: ```kotlin @Routable("your/path") object YourPage : Page { - override fun ViewWriter.render(): Unit = run { + override fun ElementWriter.CanAddTheme.render(): Unit = run { // UI code } } @@ -130,13 +130,13 @@ For pages with parameters: ```kotlin @Routable("items/{id}") class ItemDetailPage(val id: String) : Page { - override fun ViewWriter.render(): Unit = run { + override fun ElementWriter.CanAddTheme.render(): Unit = run { // Access id parameter } } ``` -Navigate using `pageNavigator.navigate(SomePage)` or use `link` components. +Navigate using `context.pageNavigator.navigate(SomePage)` or use `link` components. ### ViewWriter and Component Creation diff --git a/README.md b/README.md index 7a469b0cc..dfddb0a50 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ If you want to try another theme, start [here](https://kiteui.cs.lightningkite.c @Routable("sample/login") object SampleLogInPage : Page { - override fun ViewWriter.render(): Unit = run { + override fun ElementWriter.CanAddTheme.render(): Unit = run { val email = Signal("") val password = Signal("") unpadded.frame { @@ -143,9 +143,9 @@ object SampleLogInPage : Page { } } - private suspend fun ViewWriter.fakeLogin(email: Signal) { + private suspend fun ElementWriter.fakeLogin(email: Signal) { fetch("fake-login/${email()}") - pageNavigator.navigate(ControlsPage) + context.pageNavigator.navigate(ControlsPage) } } ``` diff --git a/example-app/build.gradle.kts b/example-app/build.gradle.kts index 93d5d74ca..0f8bbc43c 100644 --- a/example-app/build.gradle.kts +++ b/example-app/build.gradle.kts @@ -49,7 +49,6 @@ kotlin { } } if (onMac) { - iosX64() iosArm64() iosSimulatorArm64() } diff --git a/example-app/src/androidMain/AndroidManifest.xml b/example-app/src/androidMain/AndroidManifest.xml index d428117bc..2f0a0c1d1 100644 --- a/example-app/src/androidMain/AndroidManifest.xml +++ b/example-app/src/androidMain/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/App.kt b/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/App.kt index 67aa8eb1a..acb499a3b 100644 --- a/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/App.kt +++ b/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/App.kt @@ -24,7 +24,7 @@ class ToastException(override val message: String) : Exception() fun ViewWriter.app(navigator: PageNavigator, dialog: PageNavigator) { debugMode = true - configureTelemetry(navigator) +// configureTelemetry(navigator) context.exceptionHandlers.installDebugHandlers() diff --git a/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/docs/LayoutPage.kt b/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/docs/LayoutPage.kt index 9599d130e..4854f4a32 100644 --- a/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/docs/LayoutPage.kt +++ b/example-app/src/commonMain/kotlin/com/lightningkite/mppexampleapp/docs/LayoutPage.kt @@ -307,26 +307,6 @@ object LayoutPage : DocPage { } } } - emphasized.text("However, per-element gap useful in some situations. Here's how to do it:") - example(""" - col { - card.text("Start") - card.text("Normal gap above me") - spacingOverrideBeforeNext(2.px) - card.text("2.px above me") - spacingOverrideBeforeNext(5.rem) - card.text("5.rem above me") - } - """.trimIndent()) { - col { - card.text("Start") - card.text("Normal gap above me") - spacingOverrideBeforeNext(2.px) - card.text("2.px above me") - spacingOverrideBeforeNext(5.rem) - card.text("5.rem above me") - } - } } } } diff --git a/example-app/src/jvmSsrMain/kotlin/com/lightningkite/mppexampleapp/SsrPrerender.kt b/example-app/src/jvmSsrMain/kotlin/com/lightningkite/mppexampleapp/SsrPrerender.kt index 5b9fdd388..c44d1ee15 100644 --- a/example-app/src/jvmSsrMain/kotlin/com/lightningkite/mppexampleapp/SsrPrerender.kt +++ b/example-app/src/jvmSsrMain/kotlin/com/lightningkite/mppexampleapp/SsrPrerender.kt @@ -190,11 +190,8 @@ fun main(args: Array) { println("Starting prerender to: $outputDir") val results = SsrPrerender.prerenderAll(File(outputDir)) - // Exit with error code if any failures val failures = results.count { it.value.isFailure } - if (failures > 0) { - System.exit(1) - } + System.exit(if (failures > 0) 1 else 0) } args.getOrNull(0) == "server" -> { SsrServer.start(port = port, hydrate = hydrate, wait = true) diff --git a/gradle-plugin/src/main/kotlin/KiteUiPlugin.kt b/gradle-plugin/src/main/kotlin/KiteUiPlugin.kt index 78de79b9d..d5dcdbaff 100644 --- a/gradle-plugin/src/main/kotlin/KiteUiPlugin.kt +++ b/gradle-plugin/src/main/kotlin/KiteUiPlugin.kt @@ -56,9 +56,9 @@ class KiteUiPlugin : Plugin { resourcesCommon(resourceFolder, out, ext) } } - tasks.matching { it.name == "compileCommonMainKotlinMetadata" }.configureEach { dependsOn(this) } + tasks.matching { it.name == "compileCommonMainKotlinMetadata" }.configureEach { dependsOn(task) } tasks.matching { it.name.startsWith("ksp") && it.name.contains("metadata") } - .configureEach { println("CONFIGURE kspKotlinJs"); dependsOn(this) } + .configureEach { println("CONFIGURE kspKotlinJs"); dependsOn(task) } } tasks.register("kiteuiResourcesJsNonVitePart", Copy::class.java).apply { diff --git a/library-camera/build.gradle.kts b/library-camera/build.gradle.kts index 712817939..74d4f3595 100644 --- a/library-camera/build.gradle.kts +++ b/library-camera/build.gradle.kts @@ -31,7 +31,6 @@ kotlin { } } if (onMac) { - iosX64() iosArm64() iosSimulatorArm64() } diff --git a/library-lottie/build.gradle.kts b/library-lottie/build.gradle.kts index d45860dc0..b1aec41db 100644 --- a/library-lottie/build.gradle.kts +++ b/library-lottie/build.gradle.kts @@ -30,7 +30,6 @@ kotlin { } } if (onMac) { - iosX64() iosArm64() iosSimulatorArm64() } diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 187741065..8937f1262 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -41,7 +41,6 @@ kotlin { } } if (iosTarget) { - iosX64() iosArm64() iosSimulatorArm64() } diff --git a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/NativeElement.android.kt b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/NativeElement.android.kt index 613f175c3..e70338023 100644 --- a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/NativeElement.android.kt +++ b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/NativeElement.android.kt @@ -27,7 +27,6 @@ import com.lightningkite.kiteui.Log import com.lightningkite.kiteui.OverrideOnly import com.lightningkite.kiteui.afterTimeout import com.lightningkite.kiteui.debugPrint -import com.lightningkite.kiteui.models.AccessibleSemantic import com.lightningkite.kiteui.models.Align import com.lightningkite.kiteui.models.CornerRadii import com.lightningkite.kiteui.models.DragData @@ -119,15 +118,6 @@ actual abstract class NativeElement actual constructor(context: ElementContext) native.contentDescription = value } - override var accessibleSemantic: AccessibleSemantic? - get() = super.accessibleSemantic - set(value) { - super.accessibleSemantic = value - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - native.isAccessibilityHeading = value is AccessibleSemantic.Heading - } - } - override var accessibleLiveRegion: LiveRegionMode get() = super.accessibleLiveRegion set(value) { diff --git a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/DateTimeHelpers.kt b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/DateTimeHelpers.kt index 2cd7cda5a..e13aba139 100644 --- a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/DateTimeHelpers.kt +++ b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/DateTimeHelpers.kt @@ -13,15 +13,12 @@ fun View.showDatePicker( max: LocalDate? = null, onResult: (LocalDate) -> Unit ) { - DatePickerDialog(context).apply { - updateDate(start.year, start.monthNumber - 1, start.dayOfMonth) + DatePickerDialog(context, { _, year, month, dayOfMonth -> + val selected = LocalDate(year, month + 1, dayOfMonth) + onResult(selected) + }, start.year, start.monthNumber - 1, start.dayOfMonth).apply { min?.let { datePicker.minDate = it.atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() } max?.let { datePicker.maxDate = it.atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() } - - setOnDateSetListener { _, year, month, dayOfMonth -> - val selected = LocalDate(year, month + 1, dayOfMonth) - onResult(selected) - } show() } } diff --git a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.android.kt b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.android.kt index 4ad958089..ed0af8605 100644 --- a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.android.kt +++ b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.android.kt @@ -1,13 +1,13 @@ package com.lightningkite.kiteui.views.direct -import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageView import com.lightningkite.kiteui.models.* import com.lightningkite.kiteui.views.Path.PathDrawable import android.content.Context import com.lightningkite.kiteui.views.* @Suppress("ACTUAL_WITHOUT_EXPECT") -actual class NIconView(context: Context) : ImageView(context) { +actual class NIconView(context: Context) : AppCompatImageView(context) { init { scaleType = ScaleType.CENTER_INSIDE } diff --git a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/TextViewWithGradient.kt b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/TextViewWithGradient.kt index b2ee89ba0..4f0e6cb42 100644 --- a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/TextViewWithGradient.kt +++ b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/TextViewWithGradient.kt @@ -2,12 +2,12 @@ package com.lightningkite.kiteui.views.direct import android.content.Context import android.graphics.Shader -import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView import com.lightningkite.kiteui.models.Color import com.lightningkite.kiteui.models.LinearGradient import com.lightningkite.kiteui.models.Paint -class TextViewWithGradient(context: Context): android.widget.TextView(context) { +class TextViewWithGradient(context: Context): AppCompatTextView(context) { var kuiPaintForeground: Paint = Color.black set(f) { diff --git a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.android.kt b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.android.kt index 46ee79034..ad0f2083f 100644 --- a/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.android.kt +++ b/library/src/androidMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.android.kt @@ -587,3 +587,27 @@ internal object TypedValueAnimator { } } } + +@ViewModifierDsl3 +actual fun ElementWriter.CanAddTheme.asHeading(level: Int): ElementWriter.CanAddTheme = + beforeSetup { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) native.isAccessibilityHeading = true + } + +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asMain: ElementWriter.CanAddTheme get() = this +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asNavigation: ElementWriter.CanAddTheme get() = this +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asBanner: ElementWriter.CanAddTheme get() = this +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asContentInfo: ElementWriter.CanAddTheme get() = this +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asComplementary: ElementWriter.CanAddTheme get() = this +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asSearch: ElementWriter.CanAddTheme get() = this + +@ViewModifierDsl3 +actual val ElementWriter.CanAddTheme.asPresentation: ElementWriter.CanAddTheme get() = + beforeSetup { native.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS } + +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asList: ElementWriter.CanAddTheme get() = this + +@ViewModifierDsl3 actual val ElementWriter.CanAddAlignment.asListItem: ElementWriter.CanAddAlignment get() = this + +@InternalKiteUi +internal actual fun ContainerElement.setupAsListContainer() {} // TalkBack infers list structure from content diff --git a/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/NativeElement.commonHtml.kt b/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/NativeElement.commonHtml.kt index 25b70a4b4..4576bb477 100644 --- a/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/NativeElement.commonHtml.kt +++ b/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/NativeElement.commonHtml.kt @@ -9,7 +9,6 @@ import com.lightningkite.kiteui.checkLeakAfterDelay import com.lightningkite.kiteui.dom.Event import com.lightningkite.kiteui.models.* import com.lightningkite.kiteui.models.DropTargetDelegate -import com.lightningkite.kiteui.views.NativeElementCommonCode.ThemePipeline private var labelForIdCounter = 0 @@ -58,22 +57,6 @@ actual abstract class NativeElement actual constructor(context: ElementContext) native.setAttribute("aria-label", value) } - override var accessibleSemantic: AccessibleSemantic? - get() = super.accessibleSemantic - set(value) { - super.accessibleSemantic = value - when (value) { - is AccessibleSemantic.Heading -> native.tag = "h${value.level.coerceIn(1, 6)}" - is AccessibleSemantic.Main -> native.tag = "main" - is AccessibleSemantic.Navigation -> native.tag = "nav" - is AccessibleSemantic.Banner -> native.tag = "header" - is AccessibleSemantic.ContentInfo -> native.tag = "footer" - is AccessibleSemantic.Complementary -> native.tag = "aside" - is AccessibleSemantic.Search -> native.tag = "search" - null -> {} // Don't reset tag — we don't know the original - } - } - override var accessibleLiveRegion: LiveRegionMode get() = super.accessibleLiveRegion set(value) { @@ -88,18 +71,22 @@ actual abstract class NativeElement actual constructor(context: ElementContext) override var labelFor: Element? get() = super.labelFor set(value) { + val previous = super.labelFor super.labelFor = value if (value != null) { val targetNative = value.underlyingNativeElement.native if (targetNative.id == null) { targetNative.id = "kiteui-a11y-${labelForIdCounter++}" } - if (native.tag == "span") { + if (native.tag == "span" || native.tag == "p") { native.tag = "label" + native.setAttribute("for", targetNative.id!!) + } else { + targetNative.setAttribute("aria-labelledby", targetNative.id!!) } - native.setAttribute("for", targetNative.id!!) } else { - native.setAttribute("for", null) + if(native.tag == "label") native.setAttribute("for", null) + else previous?.underlyingNativeElement?.native?.setAttribute("aria-labelledby", null) } } @@ -217,6 +204,8 @@ actual abstract class NativeElement actual constructor(context: ElementContext) } } +// TODO: transform this to point to the _STYLE PARTICIPATING_ element, not necessarily the direct element +// That's what we're using it for in every case it's used... val Element.native: FutureElement get() = underlyingNativeElement.native expect class FutureElementStyle diff --git a/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.commonHtml.kt b/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.commonHtml.kt index 8c62e7277..61714ce18 100644 --- a/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.commonHtml.kt +++ b/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/IconView.commonHtml.kt @@ -20,6 +20,7 @@ actual class IconView actual constructor(context: ElementContext) : NativeElemen native.appendChild(FutureElement().apply { tag = "svg" xmlns = "http://www.w3.org/2000/svg" + setAttribute("aria-hidden", "true") style.width = value.width.value.toString() style.height = value.height.value.toString() setStyleProperty("fill", "currentColor") @@ -50,13 +51,20 @@ actual class IconView actual constructor(context: ElementContext) : NativeElemen actual var description: String? = null set(value) { field = value - native.children.firstOrNull()?.let { - it.children.find { it.tag == "title" } - ?.let { it.content = value } - ?: it.appendChild(FutureElement().apply { - tag = "title" - content = value - }) + if (value == "") { + accessibleLabel = null + native.setAttribute("aria-hidden", "true") + } else { + accessibleLabel = value + native.setAttribute("aria-hidden", null) } +// native.children.firstOrNull()?.let { +// it.children.find { it.tag == "title" } +// ?.let { it.content = value } +// ?: it.appendChild(FutureElement().apply { +// tag = "title" +// content = value +// }) +// } } } diff --git a/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.commonHtml.kt b/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.commonHtml.kt index 2649f8c26..263226389 100644 --- a/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.commonHtml.kt +++ b/library/src/commonHtmlMain/kotlin/com/lightningkite/kiteui/views/direct/modifiers.commonHtml.kt @@ -5,6 +5,7 @@ package com.lightningkite.kiteui.views.direct import com.lightningkite.kiteui.ExperimentalKiteUi import com.lightningkite.kiteui.InternalKiteUi +import com.lightningkite.kiteui.OverrideOnly import com.lightningkite.kiteui.models.* import com.lightningkite.kiteui.reactive.Action import com.lightningkite.kiteui.views.* @@ -240,4 +241,59 @@ internal expect fun ContainerElement.nativeAnimateHide(transition: ScreenTransit internal expect fun ContainerElement.nativeAnimateWeight(fromWeight: Float, toWeight: Float) @PublishedApi -internal expect fun Element.nativeSetupPullToRefresh(refreshAction: Action) \ No newline at end of file +internal expect fun Element.nativeSetupPullToRefresh(refreshAction: Action) + +private class ApplyTag( + val tag: String, + val wraps: ElementWriter, +) : ViewWriter, ElementWriter by wraps { + + var wrapperElement: NativeContainerElement? = null + + @OverrideOnly + override fun willAddChild(element: Element) { + if (element.native.tag == "span" || element.native.tag == "div") { + wrapperElement = null + element.native.tag = tag + wraps.willAddChild(element) + } else { + val we: NativeContainerElement = PassthroughContainer(wraps.context) + we.native.tag = tag + wraps.willAddChild(we) + we.willAddChild(element) + wrapperElement = we + } + } + + @OverrideOnly + override fun addChild(element: Element) { + wrapperElement?.let { + it.addChild(element) + it.onStartup() + wraps.addChild(it) + } ?: wraps.addChild(element) + } +} + +internal class PassthroughContainer(context: ElementContext): NativeContainerElement(context) { + init { + native.tag = "div" + native.style.display = "block" + } +} + +@ViewModifierDsl3 actual fun ElementWriter.CanAddTheme.asHeading(level: Int): ElementWriter.CanAddTheme = ApplyTag("h${level.coerceIn(1, 6)}", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asMain: ElementWriter.CanAddTheme get() = ApplyTag("main", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asNavigation: ElementWriter.CanAddTheme get() = ApplyTag("nav", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asBanner: ElementWriter.CanAddTheme get() = ApplyTag("header", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asContentInfo: ElementWriter.CanAddTheme get() = ApplyTag("footer", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asComplementary: ElementWriter.CanAddTheme get() = ApplyTag("aside", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asSearch: ElementWriter.CanAddTheme get() = ApplyTag("search", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asPresentation: ElementWriter.CanAddTheme get() = + beforeSetup { native.setAttribute("aria-hidden", "true") } +@ViewModifierDsl3 actual val ElementWriter.CanAddTheme.asList: ElementWriter.CanAddTheme get() = ApplyTag("ul", this) +@ViewModifierDsl3 actual val ElementWriter.CanAddAlignment.asListItem: ElementWriter.CanAddAlignment get() = ApplyTag("li", this) + +internal actual fun ContainerElement.setupAsListContainer() { + if (native.tag == "div" || native.tag == "span") native.tag = "ul" +} diff --git a/library/src/commonMain/kotlin/com/lightningkite/kiteui/models/AccessibleSemantic.kt b/library/src/commonMain/kotlin/com/lightningkite/kiteui/models/AccessibleSemantic.kt deleted file mode 100644 index fa871c4ef..000000000 --- a/library/src/commonMain/kotlin/com/lightningkite/kiteui/models/AccessibleSemantic.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.lightningkite.kiteui.models - -/** - * Semantic role for an element, used by assistive technologies and for HTML semantic elements. - * - * On web, this sets the actual HTML tag (e.g., `

`, `