From cceeb51689f8e73587541968fa7e565cf8a08950 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 22 Oct 2026 17:47:09 +0800 Subject: [PATCH 1/7] fix: use a native-compatible dynamic anchor Mount nested dynamic branches in a transparent box so native renderers accept their display property. --- src/lui_elements.ml | 4 ++-- test/test_lui.ml | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lui_elements.ml b/src/lui_elements.ml index e099ed7a..83c36877 100644 --- a/src/lui_elements.ml +++ b/src/lui_elements.ml @@ -438,8 +438,8 @@ let dynamic mount : t = node | None -> (* A dynamic rendered as the branch of another dynamic has no anchor of - its own: mount under a transparent stack so it still has a home. *) - let anchor = Lui_ui.stack context in + its own: mount under a transparent box supported by every backend. *) + let anchor = Lui_ui.box context in Lui_ui.string_property context anchor DisplayValue "contents"; ignore (mount context anchor); anchor diff --git a/test/test_lui.ml b/test/test_lui.ml index cf6592eb..00ee4670 100644 --- a/test/test_lui.ml +++ b/test/test_lui.ml @@ -423,6 +423,14 @@ let test_dyn_nested_bare () = flush_app app; Alcotest.(check bool) "nested dyn mounts text" true (creates_text (all_ops ())); + let kinds = Hashtbl.create 8 in + List.iter (function + | Lui_protocol.CreateNode (id, kind) -> Hashtbl.replace kinds id kind + | Lui_protocol.SetProp (id, Lui_protocol.DisplayValue, _) -> + Alcotest.(check bool) "transparent anchors support native display properties" true + (List.mem (Hashtbl.find kinds id) + [Lui_protocol.Box; Lui_protocol.Column; Lui_protocol.Row]) + | _ -> ()) (all_ops ()); batches := []; ignore (Lui_app.send app (`Inner "updated")); flush_app app; From 791950e4f2dba24f891a377143b72a3b88e7b725 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 22 Oct 2026 18:45:06 +0800 Subject: [PATCH 2/7] fix(android): support scrollable native tabs --- .../src/main/kotlin/dev/lui/LuiNodeView.kt | 11 ++++-- .../test/kotlin/dev/lui/LuiScreenshotTest.kt | 33 ++++++++++++++++++ ...crollableTabsDark_scrollable-tabs-dark.png | Bin 0 -> 5678 bytes ...ollableTabsLight_scrollable-tabs-light.png | Bin 0 -> 5423 bytes 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_scrollableTabsDark_scrollable-tabs-dark.png create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_scrollableTabsLight_scrollable-tabs-light.png diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt index 79e43436..7efce1e9 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -74,6 +74,7 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.ModalBottomSheet @@ -590,8 +591,14 @@ private fun LuiNodeContent( if (children.isNotEmpty()) { val selectedIndex = children.indexOfFirst { backend.node(it)?.propBool("selected") == true } .coerceAtLeast(0) - PrimaryTabRow(selectedTabIndex = selectedIndex, modifier = modifier) { - children.forEachNode { LuiNodeView(backend, it) } + if (node.propString("style-class").orEmpty().split(' ').contains("scrollable")) { + PrimaryScrollableTabRow(selectedTabIndex = selectedIndex, modifier = modifier, edgePadding = 0.dp) { + children.forEachNode { LuiNodeView(backend, it) } + } + } else { + PrimaryTabRow(selectedTabIndex = selectedIndex, modifier = modifier) { + children.forEachNode { LuiNodeView(backend, it) } + } } } } diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt index 453881ff..6284a339 100644 --- a/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt @@ -237,4 +237,37 @@ class LuiScreenshotTest { @Test fun controlsDark() = controlsScreen(dark = true) + + private fun scrollableTabs(dark: Boolean) { + val backend = backendFor("""{"generation":1,"ops":[ + {"op":"create-node","id":1,"kind":"root"}, + {"op":"create-node","id":2,"kind":"column"}, + {"op":"set-prop","id":2,"property":"width","value":220}, + {"op":"insert-child","parent":1,"child":2,"index":0}, + {"op":"create-node","id":3,"kind":"tabs"}, + {"op":"set-prop","id":3,"property":"style-class","value":"scrollable"}, + {"op":"insert-child","parent":2,"child":3,"index":0}, + {"op":"create-node","id":4,"kind":"button"}, + {"op":"set-prop","id":4,"property":"text","value":"List"}, + {"op":"set-prop","id":4,"property":"press-enabled","value":true}, + {"op":"insert-child","parent":3,"child":4,"index":0}, + {"op":"create-node","id":5,"kind":"button"}, + {"op":"set-prop","id":5,"property":"text","value":"Table"}, + {"op":"set-prop","id":5,"property":"selected","value":true}, + {"op":"set-prop","id":5,"property":"press-enabled","value":true}, + {"op":"insert-child","parent":3,"child":5,"index":1}, + {"op":"create-node","id":6,"kind":"button"}, + {"op":"set-prop","id":6,"property":"text","value":"Gallery"}, + {"op":"set-prop","id":6,"property":"press-enabled","value":true}, + {"op":"insert-child","parent":3,"child":6,"index":2}, + {"op":"create-node","id":7,"kind":"button"}, + {"op":"set-prop","id":7,"property":"text","value":"Projects"}, + {"op":"set-prop","id":7,"property":"press-enabled","value":true}, + {"op":"insert-child","parent":3,"child":7,"index":3} + ]}""") + snapshot(if (dark) "scrollable-tabs-dark" else "scrollable-tabs-light", backend, dark) + } + + @Test fun scrollableTabsLight() = scrollableTabs(dark = false) + @Test fun scrollableTabsDark() = scrollableTabs(dark = true) } diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_scrollableTabsDark_scrollable-tabs-dark.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_scrollableTabsDark_scrollable-tabs-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5873ebc89ea4d776ad9deb32cbd383021670876f GIT binary patch literal 5678 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 literal 0 HcmV?d00001 diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_scrollableTabsLight_scrollable-tabs-light.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_scrollableTabsLight_scrollable-tabs-light.png new file mode 100644 index 0000000000000000000000000000000000000000..61dad84f385a714a48510ca9003831216b7bacbd GIT binary patch literal 5423 zcmeI0X*ApE9>=58P8SrVgHn`Q=4z`5)grdED8aN{>o)-iGMVW;=l1BlxcBvS&iBpp{QuAQ#rJ%k-}jsc>EvK7 ze&EOf001CvYXiOn0EmzPfNy5@eJk8)Z7Hb*0AyBe!4_8#BWzk!r1Oyci^;g-W~uM} zv{e)8GmRXbp*~f0&9hhT49$jm^zAEB*{8QhDz`()|U3`ATK#}QCmbF$ozG^{9f*m5E z-3*73>M}$YPBR>TFh#@F9=Nwo-aF01lJWp|nl1xDTLPg9o9|+O@{8^-r?zz&?C~i1l}7PT#9Jof2JG^hSdXza-*S$9m@+KKT`)Hy+FsgLYuvWn zm%7}+>mA6w9NQg#=oa=LcmcMt=9w)Qr%cAqOGC68;YuGuRB0jX*5%CsS7umUq8wdMOgmpyRzmrlKb1=HzUMap&?P;^KihK_HX^v6gwx4D&$! z6@u1t)9!crc^t0fjI)1l+`EUx8==3O4}1KelS@nweJ3t@d1bs*Y9dX*p|77nL?^=& z;Q8^P>#RJs70mb&1zYelr!_FHnIlm!W!Ap=p(5sL)UQSAq@L;*$vR$b8*k4YYXwtS z@LAg+f)gg5Gd>ZIn%`t^Ex#bC`_7H1sTD;DAO&TN=@|XHVoK<%*%7yoDe7RPKBj;t z8C}uw3;fK%ff3lxM3q3@E~BlJEr9g2my|Ul{B2y{n>r04QHo}mFr0b!eq&ViEoww~ z#4Q4Ma}P7a=l%3AX|o6OFY1_W1@M<5f-3FAsYus-IuIaX6CN4&Xpu_5m(ho+n0@oz zCUIG$8`vO+@4@;B_)o=d%raAmFKu}55uQ zn)9^7blvrZmf1q}&=wyHpw2l=7ReP`5Uhr-jgi%yt6MSfwf3!w#ymNTSxI(atPP!m z-s?AKc(nhrT0pKL!xW@Wg7u58UAu8~<|30me1=hj6wRwGYKpOl;`78Ou88ySn7Xyp znzp6>hefWXZY`h8`CP$z1IY@OmWS(|W*@zD^@ffR4!U)iv+b+Cw^J}3RH-wJMRxpP zxXXhR#n5?t{k7yVE>%n$ zmLIb|x`fh9v+!?Eoe*%t$Rr?v*Q#n7#dX=xTg@9L*)7oqc2lrg7~Q#)3@K4ujp+hW zePc}->z-{`Rq-9ESCMq>0AX%(gE?gF;w`Iv*mvN~>~epiwnn=5TNS!Nn%US{Z{S2s zgd3k{6sU_+tp3z?v^+9_VO4YC0omGQ?Z!HfSaA{R8CgM{$ewSy`J)iHOl8ssnL~C- zB*CbZf(Q*)4w{gZ5n*!_73E2tMN#EN@}7dos^Qwot@r8t0HixEA~VdV zpqW!-|8CR9jz#}~sh&dk_~X1jd?hF*;_4Mj=qd%NAX_@!oT*zZ&ygmpfntaIV-6q7 zyzU8!ZWnW{T|rQklF?M69;=t?51LoUp9yDVtygZ;=@Af85Umb)ce0tKkt-I_;<%R4 zWCCrj_nY~Vo~HYWL9m0H)**?;p2K6sX3caa*b?9Inucd3{ZaOF8>F&m z77}bDDvV|EV}15kbcmR0JuJSCk&uP$dgb8%ZK_+}LSNk5E23G|tc}`sc$SsxQ{jDw zf~IwGF}GH;V_2%c>Vih!zej4w*a1C}zUvMzCQ&*k$Y=7pD)srM-sx)jH%Dn`H?JT& zPFtPs$xRO*(5PbDO|Q;6D<-eW?32QSq+mbQ`?~YR$`uj|$+j{4!0UW1uMIA}1HVMvb}F}hID-k|P`%6t7Jlzhr~)l2^=kJ-X{p5E>v&pEtS;knd%<~rEZ zdlrFpgY*zhxB^pDo!t{>FHhRsBkAAqSO6a-Y^QDA^j87hwfvvUt6JD0Qo^dEB-x(F zl{Z=go3wa&iRI0$-?K*$K=u~@jT9ljlZzQJAO0sDh=d#7mVJkJAbf7p%fx%$X89vHDU2Np+xjoX*<(HY- zdgwi{_~zb_Bc0_1*0k)NQ&=A|hW%$7d&7txewAnOVCZFPc0?d`uk?*t_?>$*sBCQ= zwD6WnkhN@|q5LDU-N8?{)>Gj5XAM*^r>XGnP)PLUiGN8@a+kxxW$kAW;XW1RaJ!*s zXpParTq>-94t&VccVBNUFo{@=NQZaG6rJtMS&YstjvN$mshKV|Ehp$BHS7&AQARik zGE+L%LBnbHjoEvbVB99!tD^mv)4eUmCggZ>2ZF;>h<3Nu{3}n13&zqk?Da6t+=C^Q zgD97h#kxdaV(FWDHaE4tL}tBVq`6QvCt##tHPHB{gRH8Hp0U$qG~i$PK@tIgqW#;I zq5=3?Dj2{v-G}e`THC$;boW&Om;*(&2|(HXHv&rown-8K_*&b%J}>_(JE8m6c9J_e z{5$0x9zNr-lfzC9Uk-{L9(H)x;o-{!c6j(Qjh*@M|H4D*4B^gCC+_Pmxw;4+LIJkt L9l+I=9tnQ{mA+>; literal 0 HcmV?d00001 From 3391c4a56051a0b31eb0b2734ab50337deb5a28b Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 22 Oct 2026 19:03:28 +0800 Subject: [PATCH 3/7] fix(android): fill native search field containers --- .../lui/src/main/kotlin/dev/lui/LuiNodeView.kt | 2 +- .../test/kotlin/dev/lui/LuiScreenshotTest.kt | 16 ++++++++++++++++ ...t_searchFieldWidthDark_search-width-dark.png | Bin 0 -> 6595 bytes ...searchFieldWidthLight_search-width-light.png | Bin 0 -> 6567 bytes 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_searchFieldWidthDark_search-width-dark.png create mode 100644 platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_searchFieldWidthLight_search-width-light.png diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt index 7efce1e9..1d2cd961 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -1224,7 +1224,7 @@ private fun LuiTextControl( } val fieldModifier = modifier.then( - if (grouped || borderless || formField) Modifier.fillMaxWidth() else Modifier.width(240.dp), + if (grouped || borderless || formField || search) Modifier.fillMaxWidth() else Modifier.width(240.dp), ).focusRequester(focusRequester) val fieldTextStyle = (if (borderless) MaterialTheme.typography.bodyLarge else MaterialTheme.typography.bodyMedium).copy( diff --git a/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt b/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt index 6284a339..b5f4c6c2 100644 --- a/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt +++ b/platform/android/lui/src/test/kotlin/dev/lui/LuiScreenshotTest.kt @@ -270,4 +270,20 @@ class LuiScreenshotTest { @Test fun scrollableTabsLight() = scrollableTabs(dark = false) @Test fun scrollableTabsDark() = scrollableTabs(dark = true) + + private fun searchFieldWidth(dark: Boolean) { + val backend = backendFor("""{"generation":1,"ops":[ + {"op":"create-node","id":1,"kind":"root"}, + {"op":"create-node","id":2,"kind":"column"}, + {"op":"set-prop","id":2,"property":"width","value":320}, + {"op":"insert-child","parent":1,"child":2,"index":0}, + {"op":"create-node","id":3,"kind":"search-field"}, + {"op":"set-prop","id":3,"property":"placeholder","value":"Search values"}, + {"op":"insert-child","parent":2,"child":3,"index":0} + ]}""") + snapshot(if (dark) "search-width-dark" else "search-width-light", backend, dark) + } + + @Test fun searchFieldWidthLight() = searchFieldWidth(dark = false) + @Test fun searchFieldWidthDark() = searchFieldWidth(dark = true) } diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_searchFieldWidthDark_search-width-dark.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_searchFieldWidthDark_search-width-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a493bcaa048469dd67dd5a94c6761801aa02902c GIT binary patch literal 6595 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 Z86EUrWv8TGi!WyYR_3;5HRpf3`!89tGDZLZ literal 0 HcmV?d00001 diff --git a/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_searchFieldWidthLight_search-width-light.png b/platform/android/lui/src/test/snapshots/images/dev.lui_LuiScreenshotTest_searchFieldWidthLight_search-width-light.png new file mode 100644 index 0000000000000000000000000000000000000000..9af23d40f2268ddeff0b0af518e90f8c5ae5c5b9 GIT binary patch literal 6567 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 Date: Thu, 22 Oct 2026 19:49:53 +0800 Subject: [PATCH 4/7] fix(apple): render inline search as one native field --- platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift index 7b9974a8..05985ec3 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift @@ -7447,7 +7447,11 @@ private struct LUITextControlView: View { .submitLabel(model.property(.submitOnEnter)?.boolValue == true ? .send : .return) } else if model.kind == .searchField { #if os(iOS) - iosSearchFieldRow.textFieldStyle(.roundedBorder) + iosSearchFieldRow + .textFieldStyle(.plain) + .padding(.horizontal, 10) + .padding(.vertical, 8) + .background(.quaternary, in: RoundedRectangle(cornerRadius: 10)) #else HStack(spacing: 6) { Image(systemName: "magnifyingglass") From 45d173e2b6181bd57eb6aad8b821b881cca13dcc Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 22 Oct 2026 20:07:42 +0800 Subject: [PATCH 5/7] fix: preserve scoped themes in native sheets --- platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift index 05985ec3..4c58eda5 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift @@ -2849,7 +2849,9 @@ private struct LUIModalSurfaceContent: View { var body: some View { surfaceContent + .environment(\.luiSemanticColors, effectiveSemanticColors) .modifier(LUIModalPresentationStyle(model: model)) + .presentationBackground(modalBackground) .sheet(item: Binding( get: { backend.modalPresentation.nestedSheets[model.id] }, set: { value in @@ -2936,7 +2938,7 @@ private struct LUIModalSurfaceContent: View { if LUIModalBackgroundPolicy.usesGroupedSystemBackground( styleClass: model.derived.styleClass ) { - return modalGroupedSystemBackground + return effectiveSemanticColors["background"] ?? modalGroupedSystemBackground } return LUIModalBackgroundPolicy.color( semanticColors: effectiveSemanticColors, From 3de59f985ac1214d01fbc12dd29fd4438d7f498e Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 22 Oct 2026 20:10:16 +0800 Subject: [PATCH 6/7] fix: resolve modal themes through native extension parents --- .../LUIAppleBackend/LUISwiftUIRoot.swift | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift index 4c58eda5..1e1eba5a 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift @@ -2850,6 +2850,7 @@ private struct LUIModalSurfaceContent: View { var body: some View { surfaceContent .environment(\.luiSemanticColors, effectiveSemanticColors) + .preferredColorScheme(effectiveColorScheme) .modifier(LUIModalPresentationStyle(model: model)) .presentationBackground(modalBackground) .sheet(item: Binding( @@ -2864,7 +2865,7 @@ private struct LUIModalSurfaceContent: View { LUIModalSurfaceContent(model: presentation.model, backend: backend) .luiSheetScope( semanticColors: effectiveSemanticColors, - colorScheme: colorScheme + colorScheme: effectiveColorScheme ) } } @@ -2873,18 +2874,31 @@ private struct LUIModalSurfaceContent: View { /// ancestors never reach it through the environment — walk the chain and /// apply each ancestor's commit-decoded tokens the same way /// `LUIThemeScopeModifier` does (outermost-first, inner wins). - private var effectiveSemanticColors: [String: Color] { + private var themeAncestors: [LUINodeModel] { var ancestors: [LUINodeModel] = [] var cursor = model.parent - while let parentID = cursor, - let parent = backend.model(id: parentID) { - ancestors.append(parent) - cursor = parent.parent + while let parentID = cursor { + if let parent = backend.model(id: parentID) { + ancestors.append(parent) + cursor = parent.parent + } else { + cursor = backend.extensionModel(id: parentID)?.parent + } } + return ancestors.reversed() + } + + private var effectiveColorScheme: ColorScheme { + themeAncestors.reduce(colorScheme) { $1.derived.themeScope.scheme ?? $0 } + } + + private var effectiveSemanticColors: [String: Color] { var colors = semanticColors - for ancestor in ancestors.reversed() { + var scheme = colorScheme + for ancestor in themeAncestors { + scheme = ancestor.derived.themeScope.scheme ?? scheme colors.merge( - ancestor.derived.themeScope.colors(dark: colorScheme == .dark) + ancestor.derived.themeScope.colors(dark: scheme == .dark) ) { _, new in new } } return colors From c1a098f2f7893b7714c3a60118413b1c6d24cd50 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 22 Oct 2026 20:13:20 +0800 Subject: [PATCH 7/7] fix: use native grouped inputs and expandable mobile sheets --- platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt | 6 +++++- platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt index 1d2cd961..08caa238 100644 --- a/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt +++ b/platform/android/lui/src/main/kotlin/dev/lui/LuiNodeView.kt @@ -2195,7 +2195,11 @@ private fun LuiModalSurface(backend: LuiBackend, node: LuiNodeLike, id: Long) { ) LuiNodeKind.sheet -> ModalBottomSheet( onDismissRequest = onDismiss, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = node.prop("detents")?.stringValue + ?.split(",")?.map { it.trim() }?.contains("medium") != true, + ), + containerColor = LocalLuiSemanticColors.current["background"] ?: MaterialTheme.colorScheme.surface, ) { Column(Modifier.fillMaxWidth().heightIn(max = node.prop("height")?.intValue?.dp ?: Dp.Infinity)) { if (node.text().isNotEmpty()) { diff --git a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift index 1e1eba5a..f1c964be 100644 --- a/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift +++ b/platform/apple/Sources/LUIAppleBackend/LUISwiftUIRoot.swift @@ -6727,6 +6727,7 @@ private struct LUIListView: View { .modifier(LUIListRowSeparatorModifier( visibility: backend.model(id: childID)?.separatorVisibility )) + .environment(\.luiIsNativeFormRow, true) .listRowBackground(semanticColors["surface"]) } }