Skip to content

Other small fixes - #182

Closed
UnknownJoe796 wants to merge 65 commits into
version-8from
api-cleanup
Closed

Other small fixes#182
UnknownJoe796 wants to merge 65 commits into
version-8from
api-cleanup

Conversation

@UnknownJoe796

Copy link
Copy Markdown
Contributor

No description provided.

UnknownJoe796 and others added 30 commits July 6, 2026 23:38
Multi-agent review of version-8 covering core view architecture, reactivity,
theming/models, navigation/l2, and platforms/build/testing. Prioritized P0-P3
roadmap with verified file:line references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The file previously existed only in the gitignored buildSrc mirror (regenerated
by copying from gradle-plugin/src), so a git clean would have destroyed it. Move
it to gradle-plugin/src/main/kotlin/ (the mirror's source of truth) and wire
registerAiDriverTasks(project) into KiteUiPlugin.apply, which its own doc comment
already documented as the intended call site. Registration is lazy and no-ops for
consumers without an :ai-driver-server project.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DependentAction is a DependencyChangeListener (a CoroutineContext.Element), but
startAction launched the action coroutine with context = extraContext, omitting
`this`. The reactive await()/state() functions locate the listener via
currentCoroutineContext()[DependencyChangeListener.Key], so with the listener
absent from the context, no dependencies were ever registered and
onDependencyChange/onDependencyNotReady never fired. Since DependentAction is the
default returned by Action(...), every button that clears its error on dependency
change was silently broken.

Add `this` to the launched coroutine context. Add a regression test verifying a
dependency change clears the error state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, dead-container log

- working/loading extension props cached a per-element FalseWhenSuccessful wrapper into
  the subtree-shared ElementContext.addons under a constant key, so every sibling element
  read the first element's process status. The wrapper is trivial; construct it per access.
- Clear labelFor/describedBy on shutdown; they hold strong references to arbitrary elements
  and could keep a whole removed subtree (and its native views) alive.
- Replace the bare println in checkIsShutdown with a proper Log.warn including the view path.
  Kept the no-op-return behavior (a teardown race can legitimately reach it) but made it
  visible instead of a silent stderr print.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empty nav stubs

- ColorPicker.bindColor compared .green twice and never compared .alpha, so alpha-only
  changes within tolerance were dropped. Compare alpha as the fourth channel.
- Graph drew two println(minX/maxX) debug lines on every single canvas draw. Removed.
- Removed navLayout and navSideBar from AppNavV2: empty no-op public functions with zero
  in-repo usages (navBottomBar, which has a real body, is kept). Source-compat note: any
  external caller was already getting a no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
onNewIntent decoded the entire key=value pair before splitting on '=', so a value
containing an encoded '=' (%3D) would corrupt the key/value split. Split on the raw
delimiter first, then decode each half — matching the JS urlLike() behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PageNavigatorBehavior.Separate (the default) kept a HashMap<UrlLikePath, List<Page>>
that grew unbounded for the session's lifetime, strongly retaining every Page of every
stack ever visited. Replace with a bounded (50-entry) insertion-ordered map with
oldest-first eviction and recency refresh on store; beyond the cap an old URL simply
reparses into a fresh single-page stack.

Note: the non-default Link mode still has a separate dead-code issue (its history-sync
reactiveScope sets suppressNav=true immediately before an if(!suppressNav) guard, making
the body unreachable) and never cleans its localStorage main-stack-* entries. Collapsing
the three history strategies to one is a design decision left for maintainer review; not
changed here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MIGRATION.md: status changed from 'IN PROGRESS/BROKEN/target version-7' to landed on
  version-8; removed the nonexistent CanAddDynamicTheme, added the real
  CanAddListElementModifier stage; replaced all '-' operator modifier examples with the
  actual dot-chaining syntax; withUnrestrictedModifiers -> withUnsafeModifiers.
- CLAUDE.md: dot-chaining in Modifiers/Theming examples; jvmRun -> ssrServerRun.
- README.md: Kotlin badge 2.2.0 -> 2.3.20.
- docs/TESTING_GUIDE.md: RContext -> ElementContext.
- docs/rview-basics.md: prominent notice that RView/RContext/ViewWriter are historical;
  points at the current Element/NativeElement/ElementWriter model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…names

Content-neutral renames (Kotlin filename != class name, so no code changes):
- RContext.{ios,android,commonHtml}.kt -> ElementContext.<platform>.kt (they contain the
  ElementContext actuals)
- RView.commonHtml.jvm.kt -> FutureElement.commonHtml.jvmSsr.kt (contains FutureElement +
  FutureElementStyle/Attributes)
- Deleted RViewTest.kt, which was 100% commented-out dead code referencing the removed
  RContext.test() API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generateRoutes.kt string-scans Kotlin sources; several defects fixed:
- @QueryParameter scan was unbounded to EOF, so the first @Routable in a file collected a
  later class's query params. Bound the scan to the class body via a new brace-matching
  afterBraces() helper in parsingHelpers.kt.
- Property-name extraction used indexOf("va"), matching the "va" inside `private` etc.
  Replaced with a word-boundary regex \bva[lr]\b.
- Silent break on malformed input (missing path string, unclosed literal, no class/object)
  dropped the rest of a file's routes; now throws with file path + offset (fail-fast).
- Route precedence was filesystem-walk-order dependent (parse takes firstOrNull). Parsers
  are now sorted specificity-first (fewer variables win), alphabetical tiebreak, and
  structurally-duplicate route templates throw with both class names.

Adds GenerateRoutesKtTest (5 golden tests) covering per-class query attribution, keyword
matching, ordering, duplicate detection, and malformed-input errors. Reviewed and verified
:gradle-plugin:test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The theming core previously had zero tests. Lock in the pure model-level invariants so
future refactors (deduplicating the Theme setter, converting lambda derivations to
Semantics) can't silently break them:
- ThemeAndBack.plus OR-combines drawBackground/padding flags (ThemeRules.md contract)
- SemanticOverrides precedence: instance override > type override > default
- Theme.copy revert propagation (non-cascading sets earliest revert; cascading propagates)
- Theme equals/hashCode are id-based (invisible contract), copy chains ids, customize does not
- theme[semantic] memoization returns the identical cached instance

All 24 pass against current behavior. Invariants needing an element-level harness
(cascading no-child-refresh optimization, full ThemePipeline ordering) are noted as
out of scope in the file header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All commented-out of settings.gradle and unbuilt; the swing modules still referenced the
removed RView class and could not compile. Recoverable from git history if ever needed.

- library-swing (80 files), library-lottie-swing, library-camera-swing, example-app-swing
  (only referenced each other; removed the commented includes from settings.gradle.kts)
- example-app/src/wasmJsMain (6 files) — orphaned; example-app declares no wasmJs target
- test-utilities/src/jvmDesktopMain — unwired (jvmDesktop target commented out per KMP
  single-JVM-target limitation)
- library/src/commonJvmMain Example.kt — stray 2-line file in a wrong package; the
  commonJvmMain source set itself stays (it carries jvmSsr deps)

Verified :library and :example-app jvmSsr compile clean after removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduces elementTree { } — a live SSR-backed element tree for tests to build a subtree
via the normal DSL and inspect it (themeAndBack/drawBackground/theme.id, shown, children,
findByName, live-instance count, shutdown). Fills the gap where theming/view invariants
could only be tested at the pure-model level.

Adds 6 tests locking in element-level theme cascade: card draws background, plain child
inside a card does not, important child switches theme and does, nested cards each draw,
redundant refreshTheming is a no-op, and shutdown returns the live-instance count to
baseline (no leak). Prerequisite for safely landing the themeAndBack setter dedup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ement duality)

Element extended both KiteUiCoroutineScopeHelpers (-> CoroutineScope) and StatusListener
(-> CoroutineContext.Element), so every element was simultaneously a CoroutineScope AND a
CoroutineContext.Element - and NativeElement placed the element into its own coroutineContext
as that StatusListener. That dual identity is the root of the element.job ambiguity in the
Element.kt TODO (child jobs mis-parented).

Fix: Element is now only a CoroutineScope, not a StatusListener. NativeElement supplies a
SEPARATE StatusListener object into its coroutineContext (delegating to the element's own,
now-plain, watch{Background,Foreground}Process methods). Verified nothing consumes
StatusListener via the Element type - all consumers read it from the coroutine context.

This surfaced a latent bug: TelemetryContext.viewPath() called element.outermostElement.viewPath()
which had been silently binding to CoroutineContext.viewPath() (because Element was a
CoroutineContext) and always returning an empty string. It now correctly resolves to
Element.viewPath(); added the import.

Verified: :library:jvmSsrTest green; android/js/ios compile clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Back (Android system back; browser back to follow) should dismiss the top open dialog
before popping the page. Adds an app-global stack of dismiss lambdas on ElementContext
(shared via the root context, since lazyContextAddon defaults store there):
- pushDismissableDialog(dismiss)/dismissTopDialog() in ViewContextExtensions.kt
- dialog() registers a dismissable dialog and funnels back/tap-outside/programmatic close
  through one idempotent dismiss that also unregisters from the stack
- Android handleOnBackPressed dismisses the top dialog before mainNavigator.goBack()

iOS needs no change (modal UIViewController + no hardware back). The browser popstate guard
lands with the Navigator.js history rewrite. Source-compatible: dialog() signature unchanged;
alert()/confirmDanger() get back-to-dismiss for free; dismissable=false dialogs stay put.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the sprawl (forEach, forEachUpdating, forEachById, forEachByIdWithoutAnimation,
forEachAnimated, childrenLazyLoading, Recycler2.children) with one renderList family:
- keyed overload (ID diffing, animate flag, live Reactive<T>) and unkeyed overload
  (positional slot reuse with placeholders + a poolCap that bounds the hidden-view pool -
  the old forEachUpdating grew it unboundedly)
- Recycler2.renderList / renderListMultipleTypes harmonize the virtualized entry point
  (virtualization stays separate by necessity)
All old functions become @deprecated(ReplaceWith(...)) delegating to renderList, so behavior
is identical (they were all @InternalKiteUi). lazyColumn/lazyRow now call renderList internally.
Trivial in-repo callers migrated. Adds RenderListTest (8 tests, incl. poolCap eviction).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recycler2PullToRefreshTest and Recycler2TestPage both claimed @Routable("recycler2-test"),
so one page was silently unreachable (the pre-hardening codegen picked one by filesystem
order). The new fail-fast duplicate detection caught it. Give the pull-to-refresh page its
own route so both are reachable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Behavioral + structural coverage for the interactive elements, using the driver-backed
uiTest {} harness and the elementTree {} structural harness. Each file covers the standard
template: renders, binds content/value, enabled/disabled reflects action presence, action
fires (click/toggle/setValue), theme applied where expected, and no leak after shutdown.

Elements: Button, TextInput, Checkbox, Switch, ToggleButton, Select, Slider, TextArea,
NumberInput (67 tests). Adds assertEnabled/assertDisabled to UiTestScope and findByDebugName
to the element harness. Two test bugs found while verifying and fixed here (the 500ms Action
frequency cap swallows a rapid second click; a slider must set max before min to avoid a
transient inverted range) - both worth remembering when writing element tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sses dialogs

Collapses the three PageNavigatorBehavior strategies to just Separate (each navigate() = one
browser history entry, closest to native back semantics). Link and Deprecated are kept as
@deprecated no-op aliases and PageNavigatorUseExperimentalBehavior is an inert deprecated
setter, so nothing external breaks. Removes the dead Link-mode code (the suppressNav guard
that made its history sync unreachable) and cleans up the localStorage keys those modes left.

- reset() (stack shrinks to 1) now clears browser forward/back via history.go(-(length-1)) +
  replaceState, so back can't return into the old stack. Detected as size==1 && prev>1.
- Browser back dismisses the top open dismissable dialog first (popstate intercept +
  re-pushState so back keeps working), matching the Android back behavior.
- Keeps the bounded lastStackForPath cache.

Compiles clean (:library:compileKotlinJs). NB: browser back/forward/reset behavior cannot be
verified headlessly and needs a manual browser pass (see checklist in the PR/notes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turn on explicitApiWarning() for the library and add the explicit `public`
modifier at every declaration the compiler flagged for missing visibility
across all source sets (commonMain, commonHtmlMain, jsMain, androidMain,
iosMain, nativeMain, jvmSsrMain).

This is a pure mechanical pass: `public` is the explicit-API default fill and
every flagged declaration was already effectively public, so there is ZERO ABI
or behavior change. Verified by recompiling every target (JS, JVM/SSR,
iosArm64, Android debug) — 0 errors, 0 remaining visibility warnings.

Insertions were generated deterministically by tools/usage-scan/fill_public.py
from the compiler warning log (column-based prefix insertion, no line shifts).

Not yet addressed (follow-up commits): 2,239 "Return type must be specified"
sites, which require inferred return types (judgment), and the internal/
visibility-narrowing pass driven by the downstream forced-public floor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Add explicit return types to all 413 commonMain public declarations flagged by
explicit API mode, across 60 files. Generated by Opus workers file-by-file and
gated with :library:compileCommonMainKotlinMetadata after each batch —
commonMain now reports 0 return-type and 0 visibility warnings, 0 errors.

Declared types are the compiler-inferred types, with public supertypes chosen
over internal implementation types where that matches the API intent (noted in
per-declaration review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
A Fable audit of the commonMain return-type commit flagged 9 declarations whose
compiler-valid types were poor public API. Fixes:

- Navigator.navigateUrlLikePath/resetUrlLikePath: Unit? -> Unit (the nullability
  was an accidental artifact of `?.let {}`; converted to block bodies).
- Six properties leaking concrete ArrayList/HashMap where sibling declarations in
  the same commit used the MutableList/MutableMap interfaces; declared the
  interface type, kept the concrete initializer (no behavior/perf change).
- Canvas.fallbackView: () -> TextView -> () -> Element, so overrides of this open
  accessibility hook aren't forced to return a TextView specifically.

Verified: compileCommonMainKotlinMetadata — 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
UnknownJoe796 and others added 27 commits July 21, 2026 10:18
Internalize 32 SSR declarations provably unused by external repos and in-repo
consumers: hydration internals (HydrationCursor, SsrResource state members),
SsrContext render/serialize members, SsrResult, SsrPreloadable helper, and
SsrDocument/SsrRouter internal render methods + config props.

Kept public (consumer API + signature/inline-forced): the page-metadata DSL
(pageMeta/PageMetaBuilder/OpenGraphBuilder/TwitterCardBuilder + PageMeta model),
SsrRouter (+ renderPageWithPreload/renderOrFallbackWithPreload), SsrDocument,
ssrResource + its inline-referenced registry members, and SsrContext class.

Verified: library strict (common+jvmSsr+js) + library tests + example-app
(jvmSsr+js) compile, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
The dom package has zero consumer usage (pure internal implementation).
Internalize the HTML micro-parser/sanitizer (MPNode, okTags/okAttrs allowlists,
parseMPNodes) and the JS ResizeObserver external interop declarations.

dom2 expect/actual DOM event types deferred (need coordinated expect+actual
handling). Verified: library (js+jvmSsr) + tests + example-app, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Internalize concrete built-in-widget glue in androidMain/views that is provably
unused by external repos and in-repo consumers: per-widget helper-class members
(TwoWayNestedScrollView, NCircularProgress, NSeparator/NSpace, NIconView,
FlexboxLayout, NProgrammaticLayout, PathDrawable, TextViewWithGradient, etc.),
theming/layout plumbing, and internal convenience helpers.

Kept public (custom-native-view extension API — downstream authors build custom
views on it, verified against library-camera): NativeElement/.native accessors,
NativeContainerElement, InteractiveElement bases, ElementContext.activity,
reusable layout infrastructure (SimplifiedLinearLayout, DesiredSizeView, lparams),
all expect/actual component classes, and any class exposed as a public
`override val native` (only its unused members were internalized) or referenced
by a public inline function.

Verified: library (debug+release) + example-app + library-camera + library-lottie
+ test-utilities compile, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Internalize concrete built-in-widget glue in iosMain/views that is provably
unused by external repos and in-repo consumers: per-widget UIView-subclass
members and helper classes (FlexLayout/LinearLayout/ScrollLayout members,
FrameLayoutButton members, GlassFrameLayout, TextFieldInput, UILabelWithGradient,
blur/drop plumbing, path/vector parsing internals, pusedoframe/sizeThatFits2,
etc.).

Kept public (custom-native-view extension API, verified against library-camera
AND library-lottie which build custom iOS views): NativeElement/.native surface,
NativeContainerElement, InteractiveElement bases, FrameLayout/WrapperView,
ElementContext presentation API, @ObjCAction selector-target methods, classes
exposed via public `override val native` (members internalized instead),
declarations referenced by public inline functions (ScrollView.scroller), and
DrawingContext2DImpl (used by library-lottie), UILabelWithLayerBackground (used
by example-app's custom Code view).

Verified: library (iosArm64 + simulator) + example-app + library-camera +
library-lottie + test-utilities compile, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
…g protocol docs

Extend the compiler plugin's decls mode to emit `path:line` source locations per
declaration and to record only PUBLIC declarations (via IrDeclarationWithVisibility),
so the used-set join yields precise, actionable per-file internalization candidates.

Add the worker protocol docs used to drive the migration: RETURN-TYPE-WORKER.md,
INTERNAL-NARROWING-WORKER.md, and NATIVE-VIEW-KEEP-PUBLIC.md (the custom-native-view
extension surface that must stay public).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Internalize concrete built-in-widget glue in commonHtmlMain/jsMain views that is
provably unused by external repos and in-repo consumers: hidden DOM element
holders (<input>/<textarea>/floating), widget state backings, CSS-sheet
internals (DynamicCss), popover/measurement helpers, hydration/attribute-diff
plumbing (NativeElement.commonHtml.js), and non-actual styling extras.

Kept public (custom-web-view extension API): the Html/.element/.onElement
direct-DOM DSL, FutureElement(Style/Attributes), NativeElement.native, KiteUiCss
(public actual constructor param), helpers reused across widgets, ResizeObserver
infra, all expect/actual/override members, and everything referenced by public
inline functions. commonHtml changes verified on BOTH js and jvmSsr.

Verified: library (js+jvmSsr) + tests + example-app (js+jvmSsr) + library-camera
+ library-lottie + test-utilities compile, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Internalize SSR-internal view rendering plumbing (DrawingContext2DStub,
FutureElement.render/childrenBack HTML serializer internals, DynamicCss
accumulator state + counters) provably unused outside the library module.
FutureElement web-view surface and all expect/actual members kept public.

Verified: library (jvmSsr) + tests + example-app + library-lottie +
library-camera + test-utilities compile, 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
The dialog PageNavigator was a long-deprecated near-duplicate of the main
navigator (differing only in a semantic + ignoreInteraction) that was still
constructed on every app boot and SSR request. Its popover / navClosable
replacement already exists and is what the JS back button integrates with.

- Delete navigatorViewDialog and every dialogPageNavigator accessor/companion
  (deprecated.kt, per-platform deprecated.*.kt files removed).
- Drop the dialog: PageNavigator? parameter from appBase/appNav and the second
  SSR navigator stack in SsrRouter.
- Port example-app demos off dialogPageNavigator to the real popover/dialog {}
  replacement (or a full page where appropriate).
- Sweep the downstream-dead hasPopover (ERROR-deprecated, zero live callers,
  built on the dialog nav) and neutralize DismissBackground's default click
  (which called dialogPageNavigator.clear()) to a no-op matching the opt-in
  onClick contract; every real call site already overrides onClick.

Verified: :example-app:jvmSsrTest, :library:jvmSsrTest,
:example-app:testDebugUnitTest, JS + iOS compile all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
UrlLikePath.render() and both hand-duplicated parsers (Routes.kt and the JS
Location.urlLike()) were asymmetric: query keys were decoded on parse but used
raw (B10), query keys were never encoded on render, and path segments were
joined without encoding and split without decoding. A segment containing '/',
a space, '%', or non-ASCII, or a query key containing '='/'&', would corrupt.

- Decode query keys symmetrically with values (B10).
- Encode path segments and query keys on render; decode segments on parse, in
  both Routes.kt and Navigator.js.kt (the third copy in KiteUiActivity.kt was
  already correct — Android pre-decodes Uri.path — left untouched).
- Add UrlLikePathTest: 8 round-trip cases (render(parse(x)) == x) covering
  '/', space, '%', unicode segments and special-char query keys/values.

Verified: :library:jvmSsrTest full suite (503) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
T2 — root-cause and fix the "do not call super.themeAndBack, it breaks
everything" workaround. It was the Kotlin two-backing-fields override trap:
NativeContainerElement re-overrode themeAndBack as a var, minting a second
backing field, so super.themeAndBack wrote the base's hidden field while every
read went through the container's frozen field. Fix: base setter now invokes a
protected open themeAndBackChanged(previous, current) hook and the property is a
final override var (trap permanently closed); the container implements only the
hook with the cascade delta. Behavior is byte-for-byte equivalent; the vague
comment is replaced with an explanation of the trap. Note: themeAndBack is now
final in NativeElementCommonCode — custom views intercept via the hook.

V4 — small self-contained cleanups:
- Collapse the 4x-duplicated InteractiveElement action-watch body into one
  private rewatchAction helper.
- Replace bare `this as NativeElement` casts (3 sites) with a shared
  asNativeElementOrFail() that fails fast naming the invariant (no extra cost
  over the bare checkcast).
- Attribute Processes.recalculateState exceptions to outermostElement instead
  of the inner element (removes a TODO).
- Document the iOS willRemoveSubview "cursed GC crash" guard and mark it for a
  tracked issue. spacingOverrideBeforeNext left intact (wanted, pending redesign).

Verified: :library:jvmSsrTest (incl. new ElementThemeCascadeTest cascade tests),
:example-app:jvmSsrTest, Robolectric (library + example), JS/iOS/JVM compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
…ces (V1/B2)

V2 — SimplifiedLinearLayout is a fork of AOSP LinearLayout carrying machinery
KiteUI never reaches. Removed the provably-unreachable parts (1454 -> 1082
lines): largest-child measurement, the non-overridable virtual-child hooks and
their dead call sites, null-child branches, and literal if(false)/!false
conditionals. RTL and baseline-alignment were verified LOAD-BEARING (example-app
sets supportsRtl; align() maps to relative Gravity.START/END; per-child align in
a row consults childBaseline) and deliberately KEPT — hence ~26%, not >50%.
Added SimplifiedLinearLayoutTest (Robolectric, 8 cases) locking weight/gap/
gravity/orientation behavior; validated they pass against the pre-prune file too.

V1/B2 — weight/align coordination routed through concrete-class casts inside a
broad catch(Throwable) that silently mis-sized a misplaced modifier. Introduced
capability interfaces LinearChildHost (isHorizontal) + WeightedLayoutParams
(weight), implemented by SimplifiedLinearLayout, mirroring the existing
MaxSizeLayoutParams pattern. weight/dynamicWeight/align now check the interface
(as?) and cleanly no-op under a non-host parent; the broad catch is gone. Does
NOT throw and does NOT add compile-time container enforcement (reserved for a
separate type-system design). iOS needed no change (it sets child extension
props; the container self-introspects — no foreign cast).

Verified: :library:testDebugUnitTest (incl. both new tests), :example-app:
testDebugUnitTest, :library/:example-app jvmSsrTest, JS + iOS compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Bump the reactive dependency to 6.0.0-prerelease-52-local (published to
mavenLocal) so KiteUI builds against the reactive fixes: DependencyTracker
off-by-one + repeated-read dedup (B1/R4), async cache-collision + stale-work
cancellation (B6), reactiveState CancellationException rethrow (B7), false-KDoc
removal (B8), and the thread-confinement fail-fast guard (R1).

NOTE: this pins a -local (unpublished) reactive build. Before shipping, reactive
must be committed + published and this pin bumped to the released version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
- B5: Color's primary constructor defaulted alpha=0f, so Color(red=1f) was
  invisible (inconsistent with fromHex's alpha=1). Default alpha to 1f. Only one
  site relied on the old default (Color.transparent) — made it explicit
  Color(alpha=0f). As a side effect this also fixes fromRgbString's rgb() branch,
  which had silently produced transparent colors.
- B3: remove a stray "!important" fragment (dead, followed a semicolon) from the
  .circle-progress-background stroke rule in KiteUiCss.
- B4: delete the invalid unitless ".scroll-horizontal * { max-width: 100 }" rule.

Verified: :example-app:jvmSsrTest, :library:compileKotlinJs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Theme identity is id-only by deliberate design (a major perf advantage); the
convention that keeps it safe is that themes are derived through semantics, which
guarantees unique ids. Make that checkable without changing the identity model:

- Add Theme.Debugger (opt-in, mirroring Element.Debugger): when checkIdCollisions
  is on, every Theme construction registers its id and throws if a previously-seen
  id now maps to a structurally different theme. Off by default = one boolean read.
- Add internal Theme.structurallyEquals over the 17 rendering-affecting fields
  (excludes id/provenance and the closure-valued semanticOverrides). equals/
  hashCode remain id-only.

Derivation review: copy()/withBack/alter and the themeDerivations helpers already
chain parent ids correctly; root factories take a manual id by nature. customize
(newId) intentionally does not chain (pinned by a passing test + example-app use)
— left as-is with an expanded KDoc rather than deprecated. See report for the
open question of whether customize should chain.

Verified: :library:jvmSsrTest (3 new collision tests), :example-app:jvmSsrTest,
Robolectric, JS + iOS compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
…h counter (N1)

N2 — SsrContext's windowWidth/windowHeight (defaulted 1920x1080) were dead
constructor params with zero readers anywhere (verified across library +
example-app; rowCollapsingToColumn is already CSS-only, virtualization uses
null-safe DOM measurement). Replaced them with getters that throw
UnsupportedOperationException, so any future attempt to make DOM-structural
layout depend on viewport during SSR fails fast (structural responsiveness must
be CSS-only). Pure guardrail, no behavior change.

N1 — hydration contract visibility (recover-on-mismatch stays the intended
behavior; not adding a strict-throw mode):
- Add SsrGoldenSnapshotTest: renders FourOhFour, HomePage, and RecyclerViewPage
  to SSR HTML and asserts against committed golden fixtures (self-bootstrapping:
  writes+fails once if a golden is missing). Locks the server serializer.
- Make HydrationContext's hydrated/created/mismatched counters publicly readable
  (private set), and fix a bug where clear() zeroed them before a dev could
  inspect them — reset now happens in resetStats() at the next page load. Two
  new HydrationTest cases cover the counter and the clear/resetStats split.

Verified: :library:jvmSsrTest, :example-app:jvmSsrTest (incl. golden),
:library:compileKotlinJs, :example-app:testDebugUnitTest. NOTE: :library:jsTest
(HydrationTest) needs a real browser and was not runnable in this sandbox —
to be exercised in the browser verification pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
…totype)

Consumes the new reactive QuiescenceTracker API (reactive 5.1.3-
reactivebugfixes-53) to give SSR a deterministic settle point instead of the
delay(1) "to ensure reactive bindings have propagated" hack.

- Pin reactive to 5.1.3-reactivebugfixes-53 (adds QuiescenceTracker).
- SsrContext installs a per-request QuiescenceTracker on its loading/render
  scopes; awaitAllResources() now awaits quiescence.awaitQuiescence() instead of
  delay(1). Determinism comes from SsrResource.startLoading registering its load
  job with the tracker so the state write + full listener cascade run inside the
  tracked job body; the work count only drops after, closing the race delay(1)
  papered over.
- Add ElementContext.ssrQuiescence addon (mirrors ssrDispatcher) so freshly-built
  element coroutine contexts carry the tracker; NativeElement threads it in.
- New SsrQuiescenceTest: an ssrResource whose binding chains a further async on
  load settles correctly (verified to FAIL under the old delay(1)); a never-ready
  LateInitSignal binding still settles (work-based, not readiness-based, semantics).

Left as-is: JS hydration queueMicrotask (hydration must run promptly, not block on
all in-flight work — commented in root.kt). Preload-mechanism consolidation
deferred (SsrPreloadable is public API).

PROTOTYPE — flagged for maintainer review: no timeout on the SSR settle (a
never-completing async in a page binding would hang the request; consider
withTimeout at SsrRouter); reactive version string derives from the branch name;
remember{}-internal async isn't tracked. See session notes.

Verified: reactive jvmTest 124/0; kiteui :library:jvmSsrTest 508/0,
:example-app:jvmSsrTest 16/0 (golden passes), :example-app:testDebugUnitTest 28/0,
compileKotlinJs + compileKotlinIosSimulatorArm64. JS hydration runtime unverified
(needs the browser pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Install ReactiveThreadCheck.currentThread on the UI platforms so off-main
reactive-graph mutation fails fast (clear exception) instead of silently
corrupting the unsynchronized listener/dependency lists:
- Android: KiteUiActivity.onCreate installs { Thread.currentThread() }, once per
  process, gated on Build.debug (reads FLAG_DEBUGGABLE).
- iOS: UIViewController.setup installs { NSThread.currentThread }, once, gated on
  Build.debug (Platform.isDebugBinary).

Deliberately NOT enabled on JVM/jvmSsr: SSR legitimately mutates the graph from
Dispatchers.Default worker threads per request, so the guard would false-positive
there. JS has no real threads. Release builds stay inert (hook left null).

Verified: :library:testDebugUnitTest / :example-app:testDebugUnitTest,
:library:jvmSsrTest (proves JVM unaffected), iOS + JS compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Font has identity equality and systemDefaultFont/systemDefaultFixedWidthFont were
`get() = Font { ... }`, handing out a fresh instance per access. Since FontAndStyle
is a data class comparing its Font by value, two themes built from the default font
compared unequal on Android/iOS — so Theme.Debugger.structurallyEquals flagged the
NORMAL re-derivation case as a collision (ThemeTest.debugger_collidingId_with
EqualContent_doesNotThrow failed under Robolectric; it passed under jvmSsr only
because the commonHtml Font actual is value-comparable — which is why the TH1 pass
missed it).

Make the Android and iOS system-default fonts stable `val` singletons. The Font
lambda defers all platform (Typeface/UIFont) access, so val init is pure and safe;
also removes a wasteful per-access allocation.

Verified: :library:testDebugUnitTest ThemeTest green under Robolectric; iOS compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
The quiescence-based settle waits on in-flight reactive WORK, so a page binding
that starts an async block which never completes would hang the SSR request
forever. Wrap awaitQuiescence() in a 30s withTimeout that throws a descriptive
IllegalStateException including the tracker's pendingWorkCount, so the page bug
surfaces loudly (as the surrounding comment already promised) instead of hanging.
Generous bound so slow-but-legitimate chained loads still finish.

Verified: :library:jvmSsrTest, :example-app:jvmSsrTest (quiescence + golden green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
The reactive-vocabulary migration is done in code but the docs still taught the
deprecated names, so new code kept minting them. Verified the old->new mapping
from reactive's deprecated.kt before editing (Property->Signal, shared->remember,
SharedReadable->Remember, LazyProperty->MutableRemember, LateInitProperty->
LateInitSignal, Readable/Writable->Reactive/MutableReactive, readableState->
reactiveState, reactiveScope->reactive). Swept CLAUDE.md, AGENTS.md,
GoodKiteuiCode.md, docs/TESTING_GUIDE.md, the two SSR plan docs, the tracked
.claude/skills/kiteui.md, and .junie/guidelines.md. Prose/examples only; semantics
unchanged. CheatSheet.kt already used Signal (no change).

Note: LateInitProperty's own @deprecated message in reactive points at a
nonexistent "LateInitReactiveValue"; the real replacement is LateInitSignal
(used here) — worth fixing the reactive deprecation message separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Update internal callers to the non-deprecated replacements named by each
@deprecated(ReplaceWith), removing the deprecation warnings without any behavior
change:
- reactiveScope { } -> reactive { } (navComponents, wsretry, Navigator.js, ...)
- deprecated ambient helpers -> context form: toast/alert/confirmDanger,
  closePopovers, pageNavigator (the deprecation's own "use directly through
  context" target — not a nav-API redesign)
- onNext(semantic) -> themed(semantic); dynamicTheme { } -> dynamicThemed { }
- RawVideoView.time -> currentTime via a Duration<->Double(seconds) lens
  (MediaView, VideoView)
- CalculationContext -> CoroutineScope (SoundEffectPool.backgroundAudio, wsretry)
- ContainingView -> ContainerElement (navComponents setup receivers)
- kiteui encode/decodeURIComponent forwarders -> kotlinx.serialization.uri
- kotlinx-datetime monthNumber/dayOfMonth -> month.number/day (ExternalServices)

The nav-API DESIGN (navigate/goBack return-type asymmetry, the ambient accessor
matrix's existence) is deliberately left unchanged. Deleting the now-possibly-dead
deprecated shims (PageNavigatorBehavior, encodeToStringMap) is a separate
follow-up, not done here.

Verified: :library:jvmSsrTest, :example-app:jvmSsrTest, :example-app:
testDebugUnitTest, :library:compileKotlinJs, :library:compileKotlinIosSimulatorArm64
all BUILD SUCCESSFUL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
…r task name

- ARCHITECTURE-SUGGESTIONS.md: add an implementation-status section (section 0)
  mapping each finding to its landed commit, the maintainer's deliberately-skipped
  items (R2/R3/V3/N4), and the deferred follow-ups (TH2-iOS, N7 part-2, reactive
  version/tag + remember-hole + LateInitProperty deprecation-message, SSR preload
  consolidation, remaining reactiveScope uses, browser-only JS hydration tests).
  Also folds in the prior maintainer Q&A pass.
- CLAUDE.md: the JS dev server task is `jsViteDev`, not the stale `viteRun`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
Reverts the QuiescenceTracker work (kiteui commits 54bd8d7 + 1d2531f, and the
reactive QuiescenceTracker commit dropped from reactive-bugfixes) at the
maintainer's request.

Reason: the tracker duplicated instrumentation with the existing StatusListener.
Both are CoroutineContext.Element begin/end trackers of reactive activity, wired at
adjacent/identical call sites (e.g. load{} is announced to both, 13 lines apart).
They answer subtly different questions - work-in-flight vs value-readiness, which
matters only for the never-ready case - so a proper implementation would extend
StatusListener rather than run parallel to it. Since SSR is not yet in production,
we defer rather than carry two parallel mechanisms.

- SSR awaitAllResources() is back to delay(1).
- Removes SsrContext/SsrResource/ElementContext/NativeElement/root.kt quiescence
  wiring, the settle timeout, and SsrQuiescenceTest.
- Re-pins reactive to 6.0.0-prerelease-52-local (bug-fixes build, no quiescence).
- ARCHITECTURE-SUGGESTIONS.md section 0 records the revert + the StatusListener-based
  path if it's ever revisited.

Verified: builds green (see follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qeb63y8jqUWG1TiJ9nthH
# Conflicts:
#	library/src/commonMain/kotlin/com/lightningkite/kiteui/views/NativeContainerElement.kt
#	library/src/commonMain/kotlin/com/lightningkite/kiteui/views/NativeElement.kt
@UnknownJoe796
UnknownJoe796 requested a review from iHoonter July 28, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants