jui-tools-mcp-server 2.13.0 → 2.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/attribute_definitions.json +129 -79
- package/data/attribute_semantics.json +18 -1
- package/data/component_metadata.json +36 -7
- package/data/type_synonyms.json +167 -0
- package/dist/cli_runner.js +7 -1
- package/dist/spec_loader.d.ts +25 -0
- package/dist/spec_loader.js +71 -0
- package/dist/tools/jui/jui_generate_converter.js +10 -2
- package/dist/tools/jui/jui_generate_project.js +11 -2
- package/dist/tools/spec/get_data_source.js +5 -2
- package/package.json +1 -1
- package/scripts/fetch-definitions.js +1 -1
- package/scripts/fetch-manifest.js +6 -0
- package/src/cli_runner.ts +7 -1
- package/src/spec_loader.ts +85 -0
- package/src/tools/jui/jui_generate_converter.ts +10 -2
- package/src/tools/jui/jui_generate_project.ts +13 -2
- package/src/tools/spec/get_data_source.ts +5 -2
- package/tests/cli_runner.test.ts +38 -1
- package/tests/snapshot_completeness.test.ts +1 -0
- package/tests/staleness_is_content_not_mtime.test.ts +1 -1
- package/tests/tools_cli.test.ts +43 -0
- package/tests/tools_test.test.ts +4 -4
- package/tests/type_synonyms.test.ts +178 -0
|
@@ -214,14 +214,14 @@
|
|
|
214
214
|
"string",
|
|
215
215
|
"binding"
|
|
216
216
|
],
|
|
217
|
-
"description": "Background color
|
|
217
|
+
"description": "Background color while pressed, on a node with a tap (onClick / onclick) and on a Button: it replaces the background until the press ends. On web a Button also shows it on hover (the web has hover; iOS and Android do not). A node without a tap draws nothing for it. Hex string or color name from colors.json (binding supported)"
|
|
218
218
|
},
|
|
219
219
|
"highlightBackground": {
|
|
220
220
|
"type": [
|
|
221
221
|
"string",
|
|
222
222
|
"binding"
|
|
223
223
|
],
|
|
224
|
-
"description": "Background color
|
|
224
|
+
"description": "Background color while `highlighted` is true (a View; drawn only with both declared). Not the pressed colour, which is tapBackground, and not a focused text field's background. On a Button it is the pressed colour (Button.highlightBackground). Hex string or color name from colors.json (binding supported)"
|
|
225
225
|
},
|
|
226
226
|
"disabledBackground": {
|
|
227
227
|
"type": [
|
|
@@ -701,7 +701,7 @@
|
|
|
701
701
|
},
|
|
702
702
|
"onClick": {
|
|
703
703
|
"type": "binding",
|
|
704
|
-
"description": "Click handler (camelCase) - binding only (@{functionName})"
|
|
704
|
+
"description": "Click handler (camelCase) - binding only (@{functionName}). On a type the SSoT does not declare (an app's own component) JsonUI gives the tap no screen-reader role; the component carries its own."
|
|
705
705
|
},
|
|
706
706
|
"onLongPress": {
|
|
707
707
|
"type": "binding",
|
|
@@ -725,14 +725,14 @@
|
|
|
725
725
|
"boolean",
|
|
726
726
|
"binding"
|
|
727
727
|
],
|
|
728
|
-
"description": "
|
|
728
|
+
"description": "SwiftUI / Compose: a gate on the tap — false (or a binding that resolves false) turns onClick / onclick off; absent, there is no gate and the handler alone makes the tap. UIKit: whether the view shows its pressed state (tapBackgroundColor, an image's highlight filter); the tap itself comes from onclick either way."
|
|
729
729
|
},
|
|
730
730
|
"tintColor": {
|
|
731
731
|
"type": [
|
|
732
732
|
"string",
|
|
733
733
|
"binding"
|
|
734
734
|
],
|
|
735
|
-
"description": "Tint color - hex string or color name from colors.json (binding supported)"
|
|
735
|
+
"description": "Tint color - hex string or color name from colors.json (binding supported). It is the accent of the node's operable parts, SwiftUI `.tint` / CSS `accent-color`: a control's accent, a link's colour, the text cursor - never the text colour (jsonui-cli 1.9.0 ruling). From 1.9.0 Compose no longer tints the text or icons inside a tinted node (the LocalContentColor wrap is gone), as iOS and web never did. The per-type declarations say what the accent is on that type (TextField: the cursor; Switch / Slider / Progress: the tint; Segment / TabView: the selected item). A container's tintColor reaches the controls inside it (included views, cells and embedded screens too), as SwiftUI's `.tint` does: on Compose, from the release after jsonui-cli 1.9.1, through the library's LocalJsonUITint; a control's own tintColor (or a more specific colour such as onTintColor) wins. Images and icons do not take it."
|
|
736
736
|
},
|
|
737
737
|
"binding": {
|
|
738
738
|
"type": [
|
|
@@ -745,7 +745,25 @@
|
|
|
745
745
|
"bind": {
|
|
746
746
|
"type": "binding",
|
|
747
747
|
"binding_direction": "two-way",
|
|
748
|
-
"description": "Two-way binding for the component's primary value — Switch/Check isOn, Slider value, Segment selectedIndex, SelectBox selectedValue, Progress progress,
|
|
748
|
+
"description": "Two-way binding for the component's primary value — Switch/Check isOn, Slider value, Segment selectedIndex, SelectBox selectedValue (a Date SelectBox: selectedDate), Progress progress, Radio selectedValue, TextField/TextView text. An alternative spelling to each component's own value attribute, which takes precedence when both are set; it is folded on the node a renderer draws, after its style is merged and its responsive branch resolved (shared/core/bind_fold.rb), so a value a style gives counts as set. Not a Collection's data source (Table is a Collection): that is `items`, and the validator says so. `primaryValue` lists, per section, the attribute `bind` stands for (first) and the other spellings of that value: the layout normalizer rewrites a lone `bind` to the first, and drops `bind` with a warning when any of them is set. A section whose value depends on another attribute gives an object instead of a list: `by` names that attribute, `lists` the list for each of its values, and `whenAbsent` the value to take when it is not set (SelectBox by selectItemType: a Date SelectBox's value is selectedDate, and its selectedValue / selectedItem / selectedIndex are read by no path — the validator names each).",
|
|
749
|
+
"primaryValue": {
|
|
750
|
+
"Switch": ["isOn", "value", "checked"],
|
|
751
|
+
"CheckBox": ["isOn", "checked"],
|
|
752
|
+
"Slider": ["value"],
|
|
753
|
+
"Segment": ["selectedIndex"],
|
|
754
|
+
"SelectBox": {
|
|
755
|
+
"by": "selectItemType",
|
|
756
|
+
"whenAbsent": "Normal",
|
|
757
|
+
"lists": {
|
|
758
|
+
"Normal": ["selectedValue", "selectedItem", "selectedIndex"],
|
|
759
|
+
"Date": ["selectedDate"]
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
"Progress": ["progress"],
|
|
763
|
+
"Radio": ["selectedValue"],
|
|
764
|
+
"TextField": ["text"],
|
|
765
|
+
"TextView": ["text"]
|
|
766
|
+
}
|
|
749
767
|
},
|
|
750
768
|
"include": {
|
|
751
769
|
"type": "string",
|
|
@@ -883,7 +901,7 @@
|
|
|
883
901
|
"systemChromeMaterial": "Chrome"
|
|
884
902
|
},
|
|
885
903
|
"default": "Regular",
|
|
886
|
-
"description": "Visual effect (blur) style.
|
|
904
|
+
"description": "Visual effect (blur) style. A value is its declared spelling, case and all (jsonui-cli 1.9.0): no converter downcases before matching, so `thick` or `light` is an undeclared spelling, which the validator names and which draws the default. A node that declares its own effectStyle is judged on that declaration: a Blur takes only Blur.effectStyle's Light / Dark / ExtraLight, so Thick on a Blur draws the default too. The spellings here apply to every other node. The `system*Material` spellings are valueAliases of the material names (systemMaterial -> Regular, systemUltraThinMaterial -> UltraThin, and so on). `Regular` is the fallback every path uses for an unrecognised or absent value, hence the declared default. Enumerated because it was declared as a bare string while Blur.effectStyle — the same concept on the component that owns it — carried the enum: the split let the common spelling reach the render stage unvalidated, which is what the codegen-effect gate reported once the mode audit put these fixtures back in scope. Declared on `common`, so it applies on all three platforms and not only on Blur; the ios material/tint table is pinned in attribute_semantics.json -> effectStyle, which is also where the system* valueAliases and the tint layer order are ruled."
|
|
887
905
|
},
|
|
888
906
|
"tag": {
|
|
889
907
|
"type": [
|
|
@@ -944,8 +962,16 @@
|
|
|
944
962
|
},
|
|
945
963
|
"touchDisabledState": {
|
|
946
964
|
"type": "string",
|
|
947
|
-
"
|
|
948
|
-
|
|
965
|
+
"enum": [
|
|
966
|
+
"none",
|
|
967
|
+
"onlyMe",
|
|
968
|
+
"viewsWithoutTouchEnabled",
|
|
969
|
+
"viewsWithoutInList"
|
|
970
|
+
],
|
|
971
|
+
"description": "The hit-test mode of SJUIView (UIKit). none — as usual; onlyMe — the view itself lets a touch through to what is behind it, its subviews still take theirs; viewsWithoutTouchEnabled — only subviews with isUserInteractionEnabled take a touch; viewsWithoutInList — only the subviews whose id is in touchEnabledViewIds do. SwiftUI, Compose and web do not read it: to stop a view and everything in it, use userInteractionEnabled: false.",
|
|
972
|
+
"platform": "swift",
|
|
973
|
+
"mode": "uikit",
|
|
974
|
+
"warn_outside_mode": true
|
|
949
975
|
},
|
|
950
976
|
"touchEnabledViewIds": {
|
|
951
977
|
"type": "array",
|
|
@@ -977,16 +1003,16 @@
|
|
|
977
1003
|
"centerHorizontal",
|
|
978
1004
|
"center"
|
|
979
1005
|
],
|
|
980
|
-
"description": "Content gravity/alignment. A single value names ONE axis; the axis it does not name falls to the container default (top vertically, start horizontally), so in LTR `left` and `top` both resolve to (start, top) and render identically. Use the array form to name both axes. Full ruling in attribute_semantics.json -> gravityDefaults; do not restate it in toolchain comments."
|
|
1006
|
+
"description": "Content gravity/alignment. A single value names ONE axis; the axis it does not name falls to the container default (top vertically, start horizontally), so in LTR `left` and `top` both resolve to (start, top) and render identically. Use the array form to name both axes. On a Button and a TextField textAlign owns the horizontal: their gravity positions the content on the vertical axis only (see their textAlign). On a Label textAlign places the text across when it is declared; gravity's horizontal part places it only when textAlign is not (see Label.textAlign). Full ruling in attribute_semantics.json -> gravityDefaults; do not restate it in toolchain comments."
|
|
981
1007
|
},
|
|
982
1008
|
"onAppear": {
|
|
983
1009
|
"type": "string",
|
|
984
|
-
"description": "Lifecycle callback when view appears (SwiftUI/Compose only)",
|
|
1010
|
+
"description": "Lifecycle callback when view appears (SwiftUI/Compose only). The handler's name (e.g. \"screenAppeared\"); written as a binding (`@{screenAppeared}`) or with UIKit's sender mark (`screenAppeared:`, which means nothing in SwiftUI or Compose) it is read as the same name, as the other event handlers are. Called as its declared closure type asks: `()` with nothing, `(String)` with the viewId.",
|
|
985
1011
|
"mode": ["swiftui", "compose"]
|
|
986
1012
|
},
|
|
987
1013
|
"onDisappear": {
|
|
988
1014
|
"type": "string",
|
|
989
|
-
"description": "Lifecycle callback when view disappears (SwiftUI/Compose only)",
|
|
1015
|
+
"description": "Lifecycle callback when view disappears (SwiftUI/Compose only). The handler's name (e.g. \"screenDisappeared\"); written as a binding (`@{screenDisappeared}`) or with UIKit's sender mark (`screenDisappeared:`, which means nothing in SwiftUI or Compose) it is read as the same name, as the other event handlers are. Called as its declared closure type asks: `()` with nothing, `(String)` with the viewId.",
|
|
990
1016
|
"mode": ["swiftui", "compose"]
|
|
991
1017
|
},
|
|
992
1018
|
"className": {
|
|
@@ -1207,7 +1233,7 @@
|
|
|
1207
1233
|
"string",
|
|
1208
1234
|
"binding"
|
|
1209
1235
|
],
|
|
1210
|
-
"description": "Text content (can be data binding, supports interpolation). PLAIN TEXT: markdown is not interpreted, so \"[label](/path)\", \"**bold**\" and backticks render literally, brackets and all. To emphasise or link part of the string, use partialAttributes ('range' plus font/color/underline, and '
|
|
1236
|
+
"description": "Text content (can be data binding, supports interpolation). PLAIN TEXT: markdown is not interpreted, so \"[label](/path)\", \"**bold**\" and backticks render literally, brackets and all. To emphasise or link part of the string, use partialAttributes ('range' plus font/color/underline, and 'onClick' for a tappable span). Rendering real markdown is out of scope for Label — write a custom component."
|
|
1211
1237
|
},
|
|
1212
1238
|
"hint": {
|
|
1213
1239
|
"type": "string",
|
|
@@ -1281,7 +1307,7 @@
|
|
|
1281
1307
|
"center",
|
|
1282
1308
|
"right"
|
|
1283
1309
|
],
|
|
1284
|
-
"description": "
|
|
1310
|
+
"description": "Where the label's text sits across the label's own width: Left the start, Center the middle, Right the end (binding supported). Without textAlign, the horizontal part of gravity places it (left / right / centerHorizontal, center); without either, the start. When gravity names both a center part (center / centerHorizontal) and right, center wins, on every platform. Vertically the text sits at the vertical gravity names, else the middle (gravityDefaults -> leafOwnFrameChannel). The lines of a multi-line or wrapped label follow the same rule across. (Ruled 2026-09-27; for iOS: align to start.) The web draws it so from jsonui-cli 1.9.0, its lines too (rjui gives gravity's text-center / text-right when textAlign is not declared; measured in Chromium, a wrapped label's lines stayed at the start until then); Compose from jsonui-cli 1.9.0, except the lines of a wrapContent-width label whose text wraps (by reading, they stayed at the start) and a gravity naming both center and right (by reading, it took right), both from the release after jsonui-cli 1.9.1; iOS from jsonui-cli 1.9.0 and SwiftJsonUI 10.29.0 (round 6) - until then, measured on sjui codegen and SwiftJsonUI Dynamic, a fixed-width label with neither drew its text in the middle (\"Mon\" in 50, a 200 x 44 label), one with gravity right or center at the start, and the lines of a multi-line one at the start whatever its gravity."
|
|
1285
1311
|
},
|
|
1286
1312
|
"lines": {
|
|
1287
1313
|
"type": [
|
|
@@ -1394,7 +1420,7 @@
|
|
|
1394
1420
|
"boolean",
|
|
1395
1421
|
"binding"
|
|
1396
1422
|
],
|
|
1397
|
-
"description": "Enable URL/phone detection. Auto-links BARE http(s) URLs and phone numbers found in the text. It does not parse markdown, and relative paths are deliberately never linked: a path like \"/guides/x\" only means something to a web router, so linkifying it would produce a control that is dead on iOS and Android. Express a cross-document reference with partialAttributes 'range' + '
|
|
1423
|
+
"description": "Enable URL/phone detection. Auto-links BARE http(s) URLs and phone numbers found in the text. It does not parse markdown, and relative paths are deliberately never linked: a path like \"/guides/x\" only means something to a web router, so linkifying it would produce a control that is dead on iOS and Android. Express a cross-document reference with partialAttributes 'range' + 'onClick', where the handler performs the navigation itself — including scrolling to the target position, which an href cannot express on mobile anyway."
|
|
1398
1424
|
},
|
|
1399
1425
|
"textShadow": {
|
|
1400
1426
|
"type": [
|
|
@@ -1450,7 +1476,7 @@
|
|
|
1450
1476
|
},
|
|
1451
1477
|
"partialAttributes": {
|
|
1452
1478
|
"type": "array",
|
|
1453
|
-
"description": "Partial text styling: apply font/size/color/underline/shadow to a substring selected by 'range' (a [start, end] pair, a text pattern, or a binding), and optionally make it tappable with 'onclick'. This is the supported way to get emphasis or a link inside a Label, since 'text' is plain text and markdown is not interpreted, and it is the portable way to express a cross-reference: the handler navigates (and scrolls to the target) in host code, so the same layout works everywhere, where a URL-based link would only work on web. All three platforms resolve partials at RUNTIME against the resolved string, so a pattern range and a localized or bound 'text' both work. Semantics, identical across platforms and verified against each runtime: an array range is [start, end) with the end exclusive; a string range is the FIRST occurrence and the partial is skipped (not an error) when the pattern is absent; a range that is out of bounds or inverted is skipped; partials apply in declaration order and MERGE where they overlap, later declarations winning per property.",
|
|
1479
|
+
"description": "Partial text styling: apply font/size/color/underline/shadow to a substring selected by 'range' (a [start, end] pair, a text pattern, or a binding), and optionally make it tappable with 'onClick' (alias 'onclick'). This is the supported way to get emphasis or a link inside a Label, since 'text' is plain text and markdown is not interpreted, and it is the portable way to express a cross-reference: the handler navigates (and scrolls to the target) in host code, so the same layout works everywhere, where a URL-based link would only work on web. All three platforms resolve partials at RUNTIME against the resolved string, so a pattern range and a localized or bound 'text' both work. Semantics, identical across platforms and verified against each runtime: an array range is [start, end) with the end exclusive; a string range is the FIRST occurrence and the partial is skipped (not an error) when the pattern is absent; a range that is out of bounds or inverted is skipped; partials apply in declaration order and MERGE where they overlap, later declarations winning per property.",
|
|
1454
1480
|
"items": {
|
|
1455
1481
|
"type": "object",
|
|
1456
1482
|
"properties": {
|
|
@@ -1529,13 +1555,10 @@
|
|
|
1529
1555
|
],
|
|
1530
1556
|
"description": "Range [start, end], text pattern string, or binding expression"
|
|
1531
1557
|
},
|
|
1532
|
-
"onclick": {
|
|
1533
|
-
"type": "string",
|
|
1534
|
-
"description": "Click handler for this range (selector-based, lowercase)"
|
|
1535
|
-
},
|
|
1536
1558
|
"onClick": {
|
|
1537
|
-
"type": "binding",
|
|
1538
|
-
"
|
|
1559
|
+
"type": ["binding", "string"],
|
|
1560
|
+
"aliases": ["onclick"],
|
|
1561
|
+
"description": "Click handler for this range: a binding (@{functionName}), or the method's name. `onclick` is its alias — the selector spelling, a method name — and the normalizer folds it here (both set: onClick is kept); a raw layout's `onclick` is read the same way on every platform. A name is called as the component's `onclick` selector calls it; a binding as its `onClick` does."
|
|
1539
1562
|
}
|
|
1540
1563
|
}
|
|
1541
1564
|
}
|
|
@@ -1669,7 +1692,7 @@
|
|
|
1669
1692
|
"center",
|
|
1670
1693
|
"right"
|
|
1671
1694
|
],
|
|
1672
|
-
"description": "
|
|
1695
|
+
"description": "Where the field's text sits across it: Left the start, Center the middle, Right the end; default Left (the start). A TextField's text is placed horizontally by textAlign alone - its gravity positions its content only on the vertical axis, the one textAlign does not own (4f ruling 2026-09-27: iOS, Compose and the web each put a TextField's text at the start whatever its gravity)."
|
|
1673
1696
|
},
|
|
1674
1697
|
"borderStyle": {
|
|
1675
1698
|
"type": "string",
|
|
@@ -2393,11 +2416,11 @@
|
|
|
2393
2416
|
},
|
|
2394
2417
|
"tapBackground": {
|
|
2395
2418
|
"type": "string",
|
|
2396
|
-
"description": "Background
|
|
2419
|
+
"description": "Background while pressed. On web it also shows on hover (the web has hover; iOS and Android do not). Hex string or color name from colors.json"
|
|
2397
2420
|
},
|
|
2398
2421
|
"highlightBackground": {
|
|
2399
2422
|
"type": "string",
|
|
2400
|
-
"description": "Background
|
|
2423
|
+
"description": "Background while pressed: UIButton's highlighted state, the same colour as tapBackground under its older name. tapBackground wins when both are set. Hex string or color name from colors.json"
|
|
2401
2424
|
},
|
|
2402
2425
|
"image": {
|
|
2403
2426
|
"type": [
|
|
@@ -2413,7 +2436,7 @@
|
|
|
2413
2436
|
"Center",
|
|
2414
2437
|
"Right"
|
|
2415
2438
|
],
|
|
2416
|
-
"description": "
|
|
2439
|
+
"description": "Where the button's text sits across it: Left the start, Center the middle, Right the end; default Center. A Button's text is placed horizontally by textAlign alone - its gravity positions its content only on the vertical axis, the one textAlign does not own (4f ruling 2026-09-27: iOS, Compose and the web each put a Button's text in the middle whatever its gravity; iOS drew Left and Right in the middle too until jsonui-cli 1.9.0 / SwiftJsonUI 10.29.0). On Compose the text takes the button's width to be placed when the button has a width of its own (declared, not wrapContent, or a weight) - a wrap-width button is its text's width - on kjui codegen and KotlinJsonUI Dynamic alike (round 17; it sat in the middle whatever textAlign said)."
|
|
2417
2440
|
},
|
|
2418
2441
|
"config": {
|
|
2419
2442
|
"type": "object",
|
|
@@ -2448,7 +2471,7 @@
|
|
|
2448
2471
|
},
|
|
2449
2472
|
"partialAttributes": {
|
|
2450
2473
|
"type": "array",
|
|
2451
|
-
"description": "Partial text styling: apply font/size/color/underline/shadow to a substring selected by 'range' (a [start, end] pair, a text pattern, or a binding), and optionally make it tappable with 'onclick'. This is the supported way to get emphasis or a link inside a Label, since 'text' is plain text and markdown is not interpreted, and it is the portable way to express a cross-reference: the handler navigates (and scrolls to the target) in host code, so the same layout works everywhere, where a URL-based link would only work on web. All three platforms resolve partials at RUNTIME against the resolved string, so a pattern range and a localized or bound 'text' both work. Semantics, identical across platforms and verified against each runtime: an array range is [start, end) with the end exclusive; a string range is the FIRST occurrence and the partial is skipped (not an error) when the pattern is absent; a range that is out of bounds or inverted is skipped; partials apply in declaration order and MERGE where they overlap, later declarations winning per property. Declared from the implementation, which already read it: sjui button_converter.rb:68-129 (plan 51-E).",
|
|
2474
|
+
"description": "Partial text styling: apply font/size/color/underline/shadow to a substring selected by 'range' (a [start, end] pair, a text pattern, or a binding), and optionally make it tappable with 'onClick' (alias 'onclick'). This is the supported way to get emphasis or a link inside a Label, since 'text' is plain text and markdown is not interpreted, and it is the portable way to express a cross-reference: the handler navigates (and scrolls to the target) in host code, so the same layout works everywhere, where a URL-based link would only work on web. All three platforms resolve partials at RUNTIME against the resolved string, so a pattern range and a localized or bound 'text' both work. Semantics, identical across platforms and verified against each runtime: an array range is [start, end) with the end exclusive; a string range is the FIRST occurrence and the partial is skipped (not an error) when the pattern is absent; a range that is out of bounds or inverted is skipped; partials apply in declaration order and MERGE where they overlap, later declarations winning per property. Declared from the implementation, which already read it: sjui button_converter.rb:68-129 (plan 51-E).",
|
|
2452
2475
|
"items": {
|
|
2453
2476
|
"type": "object",
|
|
2454
2477
|
"properties": {
|
|
@@ -2527,13 +2550,10 @@
|
|
|
2527
2550
|
],
|
|
2528
2551
|
"description": "Range [start, end], text pattern string, or binding expression"
|
|
2529
2552
|
},
|
|
2530
|
-
"onclick": {
|
|
2531
|
-
"type": "string",
|
|
2532
|
-
"description": "Click handler for this range (selector-based, lowercase)"
|
|
2533
|
-
},
|
|
2534
2553
|
"onClick": {
|
|
2535
|
-
"type": "binding",
|
|
2536
|
-
"
|
|
2554
|
+
"type": ["binding", "string"],
|
|
2555
|
+
"aliases": ["onclick"],
|
|
2556
|
+
"description": "Click handler for this range: a binding (@{functionName}), or the method's name. `onclick` is its alias — the selector spelling, a method name — and the normalizer folds it here (both set: onClick is kept); a raw layout's `onclick` is read the same way on every platform. A name is called as the component's `onclick` selector calls it; a binding as its `onClick` does."
|
|
2537
2557
|
}
|
|
2538
2558
|
}
|
|
2539
2559
|
}
|
|
@@ -2545,7 +2565,7 @@
|
|
|
2545
2565
|
"string",
|
|
2546
2566
|
"binding"
|
|
2547
2567
|
],
|
|
2548
|
-
"description": "Image source name (can be data binding)"
|
|
2568
|
+
"description": "Image source name (can be data binding). On web it is passed to `<img src>` as a path, since web has no asset catalog, so a bare image name belongs in srcName there (rjui warns)."
|
|
2549
2569
|
},
|
|
2550
2570
|
"srcName": {
|
|
2551
2571
|
"type": [
|
|
@@ -2612,9 +2632,15 @@
|
|
|
2612
2632
|
]
|
|
2613
2633
|
},
|
|
2614
2634
|
"alt": {
|
|
2615
|
-
"type":
|
|
2616
|
-
|
|
2617
|
-
|
|
2635
|
+
"type": [
|
|
2636
|
+
"string",
|
|
2637
|
+
"binding"
|
|
2638
|
+
],
|
|
2639
|
+
"description": "What screen readers say for the image (VoiceOver, TalkBack, web alt): a strings.json key or text, localized like `text`, or a binding. \"\" marks the image decorative (skipped). With no alt the image is decorative too, unless it operates a control (a tap handler on the image, or the only content of a tappable with no text): then it keeps the id / asset name each platform read before, and the build names it (INFO). Decorative is the default, so give every image that carries meaning (a logo, a photo, an icon that is the only content of a button) an alt.",
|
|
2640
|
+
"aliases": [
|
|
2641
|
+
"accessibilityLabel",
|
|
2642
|
+
"contentDescription"
|
|
2643
|
+
]
|
|
2618
2644
|
},
|
|
2619
2645
|
"loading": {
|
|
2620
2646
|
"type": "string",
|
|
@@ -2641,12 +2667,6 @@
|
|
|
2641
2667
|
"kotlin"
|
|
2642
2668
|
]
|
|
2643
2669
|
},
|
|
2644
|
-
"canTap": {
|
|
2645
|
-
"type": "boolean",
|
|
2646
|
-
"description": "Enable tap gesture independent of onClick.",
|
|
2647
|
-
"platform": "swift",
|
|
2648
|
-
"mode": "swiftui"
|
|
2649
|
-
},
|
|
2650
2670
|
"systemIcon": {
|
|
2651
2671
|
"type": "boolean",
|
|
2652
2672
|
"description": "Interpret `src` as an SF Symbol name.",
|
|
@@ -2748,9 +2768,15 @@
|
|
|
2748
2768
|
"mode": "uikit"
|
|
2749
2769
|
},
|
|
2750
2770
|
"alt": {
|
|
2751
|
-
"type":
|
|
2752
|
-
|
|
2753
|
-
|
|
2771
|
+
"type": [
|
|
2772
|
+
"string",
|
|
2773
|
+
"binding"
|
|
2774
|
+
],
|
|
2775
|
+
"description": "What screen readers say for the image (VoiceOver, TalkBack, web alt): a strings.json key or text, localized like `text`, or a binding. \"\" marks the image decorative (skipped). With no alt the image is decorative too, unless it operates a control (a tap handler on the image, or the only content of a tappable with no text): then it keeps the id / asset name each platform read before, and the build names it (INFO). Decorative is the default, so give every image that carries meaning (a logo, a photo, an icon that is the only content of a button) an alt.",
|
|
2776
|
+
"aliases": [
|
|
2777
|
+
"accessibilityLabel",
|
|
2778
|
+
"contentDescription"
|
|
2779
|
+
]
|
|
2754
2780
|
},
|
|
2755
2781
|
"loading": {
|
|
2756
2782
|
"type": "string",
|
|
@@ -2854,7 +2880,7 @@
|
|
|
2854
2880
|
"valueAliases": {
|
|
2855
2881
|
"datetime": "dateAndTime"
|
|
2856
2882
|
},
|
|
2857
|
-
"description": "Date picker mode. datetime is a declared synonym of dateAndTime and the normalizer folds it: sjui and rjui accept both spellings (selectbox_converter.rb case arms; rjui
|
|
2883
|
+
"description": "Date picker mode. datetime is a declared synonym of dateAndTime and the normalizer folds it: sjui and rjui accept both spellings as declared, case and all (sjui matches them literally in selectbox_converter.rb's case arms; rjui's select_box_converter.rb takes a value only when it is a declared spelling, through EnumSpelling.lowered, and lowercases it just for its own case arms), but KotlinJsonUI's DateSelectBox matches \"dateAndTime\" literally and datetime would silently fall to the date-only branch — so the UIKit spelling is canonical, same reasoning as gradientDirection's capitalised canon."
|
|
2858
2884
|
},
|
|
2859
2885
|
"datePickerStyle": {
|
|
2860
2886
|
"type": "string",
|
|
@@ -3068,7 +3094,7 @@
|
|
|
3068
3094
|
},
|
|
3069
3095
|
"onValueChange": {
|
|
3070
3096
|
"type": "binding",
|
|
3071
|
-
"description": "Value change handler - binding only (@{functionName}). The handler receives the new value of the selection binding, the same on every platform: with selectedIndex bound it is the Int index (declare ((Int) -> Void)?, or ((String, Int) -> Void)? for viewId + index); with selectedItem / selectedValue bound it is the String item (declare ((String, String) -> Void)? for viewId + item). A lone (String) parameter is
|
|
3097
|
+
"description": "Value change handler - binding only (@{functionName}). The handler receives the new value of the selection binding, the same on every platform: with selectedIndex bound it is the Int index (declare ((Int) -> Void)?, or ((String, Int) -> Void)? for viewId + index); with selectedItem / selectedValue bound it is the String item (declare ((String, String) -> Void)? for viewId + item). A lone (String) parameter is the selected item as a String, with selectedIndex bound too; the viewId comes only as the first of two parameters. (() -> Void)? receives nothing and the ViewModel reads the bound value back."
|
|
3072
3098
|
},
|
|
3073
3099
|
"onValueChanged": {
|
|
3074
3100
|
"type": "binding",
|
|
@@ -3538,7 +3564,7 @@
|
|
|
3538
3564
|
"Diagonal": "Oblique"
|
|
3539
3565
|
},
|
|
3540
3566
|
"default": "Vertical",
|
|
3541
|
-
"description": "Gradient direction. Canon copied from GradientView.gradientDirection, which declared the enum for the same concept while this spelling stayed a bare string. Vertical is the fallback on all three.
|
|
3567
|
+
"description": "Gradient direction. Canon copied from GradientView.gradientDirection, which declared the enum for the same concept while this spelling stayed a bare string. Vertical is the fallback on all three. A value is its declared spelling, case and all (jsonui-cli 1.9.0): sjui matches the capitalised spellings literally (modifier_helper.rb gradient_expression), so 'horizontal' is an undeclared spelling that draws the Vertical fallback; the aliases normalise to the capitalised canon before any converter sees the value. Compose and web do not read this declaration (platform swift); their GradientView converters read GradientView.gradientDirection by the same rule (EnumSpelling.lowered: a declared spelling only, lowercased just for their own case arms). RightToLeft and BottomToTop are declared without an alias on purpose: they are reversed directions (sjui draws them trailing to leading and bottom to top), so folding them into Horizontal/Vertical would silently reverse the gradient rather than preserve it.",
|
|
3542
3568
|
"platform": "swift"
|
|
3543
3569
|
},
|
|
3544
3570
|
"locations": {
|
|
@@ -3828,7 +3854,7 @@
|
|
|
3828
3854
|
"none"
|
|
3829
3855
|
],
|
|
3830
3856
|
"default": "lazy",
|
|
3831
|
-
"description": "Outer container shape for the Collection (single-column section path uses CollectionStackView/CollectionStack). Accepts: 'lazy' (default) -> ScrollView+LazyVStack/LazyHStack on iOS, LazyColumn/LazyRow on Android, with virtualized cell rendering. 'eager' -> ScrollView+VStack/HStack on iOS, Column(verticalScroll)/Row(horizontalScroll) on Android — no virtualization, smooth scrolling for heavy cells (markdown / images / attributed text) that suffer from LazyVStack re-evaluation. 'none' -> VStack/HStack only, no scroll container, parent must already be scrollable (for layout:'flow' this is the only value that removes the Collection's own vertical scroll). Bindings (@{prop}) are resolved at runtime via the wrapper's mode parameter so toggles preserve view identity. Sticky headers and paging require 'lazy'."
|
|
3857
|
+
"description": "Outer container shape for the Collection (single-column section path uses CollectionStackView/CollectionStack). Accepts: 'lazy' (default) -> ScrollView+LazyVStack/LazyHStack on iOS, LazyColumn/LazyRow on Android, with virtualized cell rendering. 'eager' -> ScrollView+VStack/HStack on iOS, Column(verticalScroll)/Row(horizontalScroll) on Android — no virtualization, smooth scrolling for heavy cells (markdown / images / attributed text) that suffer from LazyVStack re-evaluation. 'none' -> VStack/HStack only, no scroll container, parent must already be scrollable (for layout:'flow' this is the only value that removes the Collection's own vertical scroll). With no scroll container of its own, a 'none' Collection's scrollTo and defaultScrollAnchor move nothing, on every path - the parent owns the scrolling (declared 4f round 11, jsonui-cli 1.9.0; sjui, rjui and both Dynamic renderers already drew nothing for them). A vertical Collection whose height wraps its content (`height: wrapContent`, lazy or eager) scrolls inside the height its parent bounds it to, and under a parent that does not bound it (a scrolling ancestor) it is its content's height, with nothing of its own to scroll - its scrollTo and defaultScrollAnchor then move nothing. So iOS draws it (a ScrollView as tall as its parent lets it be) and the web (a fit-content box a flex parent shrinks, overflow auto), measured 2026-09-27 on sjui codegen, SwiftJsonUI Dynamic and rjui; both Compose paths draw it so from jsonui-cli 1.9.0 (4f ruling 2026-09-27, round 12) - until then it was a Column that never scrolled, its cells running past a bounded parent. With fewer cells than the parent's height every path fits the cells - iOS from jsonui-cli 1.9.0 (SwiftJsonUI's CollectionContentFit; the user's ruling 2026-09-27), it filled that height until then - except an iOS List-route Collection (the class-list List: cellClasses and no `sections`; a sectioned Collection with listStyle), which fills it: a SwiftUI List reports no content height to fit (measured 2026-09-27: fitted, it was 0pt tall). Bindings (@{prop}) are resolved at runtime via the wrapper's mode parameter so toggles preserve view identity. Sticky headers and paging require 'lazy'."
|
|
3832
3858
|
},
|
|
3833
3859
|
"columns": {
|
|
3834
3860
|
"type": [
|
|
@@ -3836,19 +3862,19 @@
|
|
|
3836
3862
|
"binding"
|
|
3837
3863
|
],
|
|
3838
3864
|
"min": 1,
|
|
3839
|
-
"description": "Number of columns. Literal int -> static GridCells.Fixed(N) / Array(repeating: ..., count: N). Binding @{prop} -> runtime-resolved data.prop; the Collection always renders on the multi-column grid path (LazyVerticalGrid / LazyHorizontalGrid on Android, LazyVGrid / LazyHGrid on iOS) even when the binding resolves to 1, so the grid layout stays stable across runtime column changes."
|
|
3865
|
+
"description": "Number of columns. Literal int -> static GridCells.Fixed(N) / Array(repeating: ..., count: N). Binding @{prop} -> runtime-resolved data.prop; the Collection always renders on the multi-column grid path (LazyVerticalGrid / LazyHorizontalGrid on Android, LazyVGrid / LazyHGrid on iOS) even when the binding resolves to 1, so the grid layout stays stable across runtime column changes. On a horizontal Collection `columns` is the number of lanes (jsonui-cli 1.9.0; until then every horizontal route drew one lane), and a section's own `columns` is that section block's lanes. A paging Collection draws no lanes: paging is not a lane route, whatever `columns` says."
|
|
3840
3866
|
},
|
|
3841
3867
|
"columnSpacing": {
|
|
3842
3868
|
"type": "number",
|
|
3843
|
-
"description": "Spacing between columns"
|
|
3869
|
+
"description": "Spacing between columns (on a horizontal Collection, between lanes). Undeclared: itemSpacing, else 0 on every route, flow included (attribute_semantics.json -> collectionSpacing)."
|
|
3844
3870
|
},
|
|
3845
3871
|
"itemSpacing": {
|
|
3846
3872
|
"type": "number",
|
|
3847
|
-
"description": "Spacing between items (used for both grid spacing and list item spacing)"
|
|
3873
|
+
"description": "Spacing between items (used for both grid spacing and list item spacing): the fallback for lineSpacing and columnSpacing. Undeclared: 0 on every route, flow included (attribute_semantics.json -> collectionSpacing)."
|
|
3848
3874
|
},
|
|
3849
3875
|
"lineSpacing": {
|
|
3850
3876
|
"type": "number",
|
|
3851
|
-
"description": "Spacing between rows
|
|
3877
|
+
"description": "Spacing between rows (on a horizontal Collection, along the scroll axis); `sectionSpacing` folds here. Undeclared: itemSpacing, else 0 on every route, flow included (attribute_semantics.json -> collectionSpacing).",
|
|
3852
3878
|
"aliases": [
|
|
3853
3879
|
"sectionSpacing"
|
|
3854
3880
|
]
|
|
@@ -3887,15 +3913,15 @@
|
|
|
3887
3913
|
"array",
|
|
3888
3914
|
"string"
|
|
3889
3915
|
],
|
|
3890
|
-
"description": "Content insets"
|
|
3916
|
+
"description": "Content insets: padding around the cells, inside the Collection's scroll - 1, 2 or 4 values, an array or a string separated by `|`, read as `paddings` reads them: one, every side; two, [vertical, horizontal]; four, [top, right, bottom, left] (right the end, left the start); any other value pads nothing. A value in the array may be a binding, bound as the data holds it (a number; unset, 0). Both Compose paths read it so from jsonui-cli 1.9.0 (4f rulings 2026-09-27, rounds 16 and 17; kjui read four as [top, left, bottom, right], only an array of four and no string, and KotlinJsonUI Dynamic no array of one or two, nor an array with a binding). It is ADDED to insetHorizontal / insetVertical and to the safe area contentInsetAdjustmentBehavior asks for, side by side, on iOS (measured 2026-09-27 on sjui codegen and SwiftJsonUI Dynamic: insets [8, 0, 0, 0] with insetVertical 8 at the top of a 62pt safe area put the first cell at 78) and on both Compose paths from jsonui-cli 1.9.0 (4f ruling, round 17; a declared insets replaced both until then), and on the web from jsonui-cli 1.9.0 (4f round 6: rjui adds them per edge on the Collection's own box, its scroll container, with exact values; until then it rounded them to Tailwind's spacing scale - 30 became 28 -, four values replaced insetHorizontal / insetVertical, two lost to insetHorizontal, the string form padded nothing and a bound value in the array stopped the layout, measured in Chromium; a pager's snap points keep the insets, as scroll-padding). On iOS every route pads inside its scroll from jsonui-cli 1.9.0 and SwiftJsonUI 10.29.0 (round 6): a List (a sectioned Collection with a listStyle, and the class-list shape) pads its rows with safe-area padding on the List, on top of its own row insets, and a pager pads the cell on every page, a page being the pager's size - until then sjui's Lists read no insets, and SwiftJsonUI Dynamic's Lists and both pagers were padded from outside, which narrowed the scroll (a 300-wide pager with insets [0, 0, 0, 30] turned its pages in a 270-wide scroll, measured). With insetHorizontal / insetVertical it pads every route's content on Compose - the lazy lists and grids, and from jsonui-cli 1.9.0 the containers that are not lazy lists too: the wrapContent and `lazy: none` Column and Row, the flow and CollectionStack's NONE container, as KotlinJsonUI Dynamic already drew them (4f ruling 2026-09-27, round 15; kjui codegen and CollectionStack applied none there)."
|
|
3891
3917
|
},
|
|
3892
3918
|
"insetHorizontal": {
|
|
3893
3919
|
"type": "number",
|
|
3894
|
-
"description": "Horizontal inset"
|
|
3920
|
+
"description": "Horizontal inset: padding on the left and right of the cells, inside the Collection's scroll. Added to the safe area contentInsetAdjustmentBehavior asks for, on every platform - as iOS adds its safe area (measured 2026-09-27 on sjui codegen and SwiftJsonUI Dynamic), and on both Compose paths from jsonui-cli 1.9.0 (4f ruling, round 16; kjui dropped the safe area for it, KotlinJsonUI Dynamic dropped it for the safe area). A declared insets is added to it too, on iOS and from jsonui-cli 1.9.0 on both Compose paths (round 17; it replaced it), and on the web from jsonui-cli 1.9.0 (round 6; a declared insets replaced it there, and it was rounded to Tailwind's spacing scale - 13 became 12). On a horizontal Collection kjui's codegen keeps it in the content padding with the rest (round 17; handed to CollectionStack's insetLeading / insetTrailing, it dropped insetVertical and the safe area there)."
|
|
3895
3921
|
},
|
|
3896
3922
|
"insetVertical": {
|
|
3897
3923
|
"type": "number",
|
|
3898
|
-
"description": "Vertical inset"
|
|
3924
|
+
"description": "Vertical inset: padding above and below the cells, inside the Collection's scroll. Added to the safe area contentInsetAdjustmentBehavior asks for, on every platform - iOS measured 2026-09-27 on sjui codegen and SwiftJsonUI Dynamic: a Collection at the top of a 62pt safe area with insetVertical 8 put its first cell at 70, `always` alike, and at 8 with `never` - and on both Compose paths from jsonui-cli 1.9.0 (4f ruling, round 16; kjui dropped the safe area for it, KotlinJsonUI Dynamic dropped it for the safe area). A declared insets is added to it too, on iOS (78 = 62 + 8 + 8), from jsonui-cli 1.9.0 on both Compose paths (round 17; it replaced it) and on the web (round 6; a four-value insets replaced it there)."
|
|
3899
3925
|
},
|
|
3900
3926
|
"contentInsets": {
|
|
3901
3927
|
"type": [
|
|
@@ -3908,7 +3934,7 @@
|
|
|
3908
3934
|
},
|
|
3909
3935
|
"contentInsetAdjustmentBehavior": {
|
|
3910
3936
|
"type": "string",
|
|
3911
|
-
"description": "Content inset adjustment"
|
|
3937
|
+
"description": "Content inset adjustment: whether the cells clear the safe area (the system bars). On iOS the system adjusts by default and `never` stops it. On Compose `always` / `automatic` pad the cells by the safe area, `scrollableAxes` by its sides on the scroll axis, and `never` - or no declaration - by nothing, Compose's own default. There is no precedence: the safe area, a declared insets (contentPadding) and insetHorizontal / insetVertical are all added, side by side, on iOS and on both Compose paths (4f rulings 2026-09-27, rounds 16 and 17; until jsonui-cli 1.9.0 a declared insets replaced the other two there, and insetHorizontal / insetVertical and the safe area replaced each other)."
|
|
3912
3938
|
},
|
|
3913
3939
|
"itemWeight": {
|
|
3914
3940
|
"type": "number",
|
|
@@ -3968,11 +3994,8 @@
|
|
|
3968
3994
|
}
|
|
3969
3995
|
},
|
|
3970
3996
|
"items": {
|
|
3971
|
-
"type":
|
|
3972
|
-
|
|
3973
|
-
"binding"
|
|
3974
|
-
],
|
|
3975
|
-
"description": "Data source binding"
|
|
3997
|
+
"type": "binding",
|
|
3998
|
+
"description": "Data source binding (@{…}). A binding only: the array form was declared and drawn by no platform (codegen or Dynamic) and used by no face, so it was taken out on 2026-09-26; a literal array is named by the validator. The bound value is a CollectionDataSource or an array; an array is one section, every element drawn with cellClasses[0] (a Collection without `sections`; the layout's data declaration decides on the codegen paths, the value's shape on the Dynamic ones)."
|
|
3976
3999
|
},
|
|
3977
4000
|
"keyboardAvoidance": {
|
|
3978
4001
|
"type": "boolean",
|
|
@@ -3981,13 +4004,15 @@
|
|
|
3981
4004
|
},
|
|
3982
4005
|
"setTargetAsDelegate": {
|
|
3983
4006
|
"type": "boolean",
|
|
3984
|
-
"description": "Set target as delegate",
|
|
3985
|
-
"platform": "swift"
|
|
4007
|
+
"description": "Set target as delegate (UIKit: SJUICollectionView reads it; the SwiftUI paths have no delegate to set)",
|
|
4008
|
+
"platform": "swift",
|
|
4009
|
+
"mode": "uikit"
|
|
3986
4010
|
},
|
|
3987
4011
|
"setTargetAsDataSource": {
|
|
3988
4012
|
"type": "boolean",
|
|
3989
|
-
"description": "Set target as data source",
|
|
3990
|
-
"platform": "swift"
|
|
4013
|
+
"description": "Set target as data source (UIKit: SJUICollectionView reads it; the SwiftUI paths have no data source to set)",
|
|
4014
|
+
"platform": "swift",
|
|
4015
|
+
"mode": "uikit"
|
|
3991
4016
|
},
|
|
3992
4017
|
"columnCount": {
|
|
3993
4018
|
"type": "number",
|
|
@@ -3996,7 +4021,7 @@
|
|
|
3996
4021
|
},
|
|
3997
4022
|
"scrollTo": {
|
|
3998
4023
|
"type": "binding",
|
|
3999
|
-
"description": "Binding that requests a programmatic scroll. The declared DATA CLASS is a plain value
|
|
4024
|
+
"description": "Binding that requests a programmatic scroll. The declared DATA CLASS is a plain value, and it decides what the value names, on every path (4f ruling 2026-09-27, round 14; jsonui-cli 1.9.0): an `Int` is a cell's index, a `String` a cell's key. cellIdProperty decides what a cell's key is — its `cellId`, else its cellIdProperty value — and not the class: a Collection with cellIdProperty may declare an `Int`, and one without it a `String` (its cells' cellIds are the keys). What it names across sections (4f ruling 2026-09-27, jsonui-cli 1.9.0; one rule on every path — sjui, kjui and rjui codegen, SwiftJsonUI and KotlinJsonUI Dynamic): an `Int` is the index of a CELL counted across all the drawn sections in order, cells only — a section's header or footer, and a grid's filler, is not counted (the same n as the paging address `<id>_item_<n>`); a `String` is the FIRST cell, in section order, whose key (its `cellId`, else its cellIdProperty value) it is, when two cells share the key — in two sections or in one; a cell with no key has none, and no String names it. A value that names no cell scrolls nowhere. Kotlin only (legacy): a String that is no cell's key and reads `<digits>` or `<digits>#<anything>` scrolls to lazy item <digits>, with a DEBUG warning, on the lazy lists; a container that is not one (the flow, the EAGER one, the wrapContent Column) has no lazy item and does not read it, and iOS and web do not read it. (A consuming Android screen asks its keyed, reverse-laid-out list for its bottom-most item with `\"0#<time>\"`; read as a cell, 0 would be section 0's first cell, which is not the bottom-most item when a later section has cells.) The request is a CHANGE of the value (4f ruling 2026-09-27, jsonui-cli 1.9.0): the value a Collection is drawn with scrolls nowhere, a value arriving where there was none is a change like any other, and sending the same value again does not re-scroll. It is deliberately NOT `PassthroughSubject<Int, Never>`, which this description used to prescribe: PassthroughSubject is a Combine type, and naming a Swift transport in the cross-platform SSoT is what made consumers write it into their data section, where kjui's map_to_kotlin_type passes unknown classes through verbatim and the Kotlin build dies on it. How the request is delivered is each platform's own business — iOS keys `.onChange(of:)` on the value inside a ScrollViewReader, Compose a LaunchedEffect, web a useEffect that compares the value with the one it last saw. STATUS 2026-09-27 (jsonui-cli 1.9.0): every path reads the plain value — sjui and SwiftJsonUI Dynamic, kjui and KotlinJsonUI Dynamic, rjui. On iOS and web it reaches the cells of every route with a scroll container of its own: the lists (the class-list shape's included), the grids, the flows, the horizontal Collections and the pagers — a pager turns to the page the value names, its currentPage when one is bound. On Compose (kjui codegen and KotlinJsonUI Dynamic) it reaches the lazy lists and grids, the horizontal rows, the pagers and a flow that scrolls in its own bounds (4f ruling 2026-09-27, round 11), and the EAGER container (kjui's CollectionStack EAGER, KotlinJsonUI Dynamic's Column and Row) and the Column of a vertical Collection whose height wraps its content (see `lazy`), by the same rule (4f ruling 2026-09-27, round 12); until jsonui-cli 1.9.0 neither scrolled to a cell. Until 1.9.0 kjui codegen read every value as its text and scrolled to that LAZY ITEM index - headers, footers and grid fillers counted -, with cellIdProperty looked an Int up as a key first (so an Int never became a cell there) and read a String without cellIdProperty as the index, scrolled on the value the list was first drawn with, landed bottom and center with the target's top edge at the viewport's bottom edge and middle, drew no scroll on the flow and the pager, and a key two cells shared took the lazy list down (\"Key ... was already used\"); KotlinJsonUI Dynamic read only a SharedFlow<Int>, so the plain value never scrolled, landed center and bottom at the top, and drew no scroll on the flow and the pager. Until 1.9.0 web's grid was no scroll container (a grid of a declared height drew its rows past its box and a scrollTo moved nothing); on iOS the flow (sjui codegen and SwiftJsonUI Dynamic), SwiftJsonUI Dynamic's sectioned List, the class-list List and the pager drew no scroll at all; and a String on a Collection with no cellIdProperty never met a cellId — web read a string of digits there as an index. Until 1.9.0 sjui codegen took what the value names from cellIdProperty, not from the declared class: an `Int` with cellIdProperty did not compile on the class-list List and the pager and scrolled nowhere on the other routes; and its cell ids with cellIdProperty let a String reach a cell with no key (its id was \"\\(index)\"), and gave two cells of one section with one key one id, the cell it reached SwiftUI's choice. A Collection with `lazy: none` has no scroll container of its own and scrolls nowhere. Until 2026-09-27 this paragraph named the SwiftUI Dynamic renderer as the one that could not read a plain value; it was the KotlinJsonUI Dynamic renderer, which reads it today."
|
|
4000
4025
|
},
|
|
4001
4026
|
"scrollAnchor": {
|
|
4002
4027
|
"type": "string",
|
|
@@ -4006,7 +4031,7 @@
|
|
|
4006
4031
|
"bottom"
|
|
4007
4032
|
],
|
|
4008
4033
|
"default": "bottom",
|
|
4009
|
-
"description": "Anchor point for the scrollTo target. Measured 2026-08-05: ios (sjui collection_converter.rb:1138), web (rjui build_command.rb:768) and the Compose GRID path
|
|
4034
|
+
"description": "Anchor point for the scrollTo target: where the target lands along the scroll axis. top - its leading edge at the viewport's leading edge; center - its middle at the viewport's middle; bottom (the default) - its trailing edge at the viewport's trailing edge. Drawn so by SwiftUI's ScrollViewReader (vertical), the web (scrollCollectionToCell) and, since jsonui-cli 1.9.0, both Compose paths (4f ruling 2026-09-27, round 11): kjui codegen offset the target by the viewport (bottom) or half of it (center), which put the target's top edge at the viewport's bottom edge or middle, and KotlinJsonUI Dynamic landed center and bottom at the top. A reverseLayout list (Compose) lands top and bottom on the edges they name on screen. Measured 2026-08-05: ios (sjui collection_converter.rb:1138), web (rjui build_command.rb:768) and the Compose GRID path all fall back to bottom, so bottom is the declared default. On a horizontal Collection, top / center / bottom mean the start / centre / end along the scroll axis, in reading direction, on every platform (4f ruling 2026-09-27)."
|
|
4010
4035
|
},
|
|
4011
4036
|
"defaultScrollAnchor": {
|
|
4012
4037
|
"type": "string",
|
|
@@ -4015,7 +4040,7 @@
|
|
|
4015
4040
|
"center",
|
|
4016
4041
|
"bottom"
|
|
4017
4042
|
],
|
|
4018
|
-
"description": "Initial scroll position anchor (iOS 17+). Sets where the scroll view starts."
|
|
4043
|
+
"description": "Initial scroll position anchor (iOS 17+). Sets where the scroll view starts. On Compose (kjui codegen and KotlinJsonUI Dynamic) it is applied once, when the list first has cells, by scrolling to the middle (center) or last (bottom) CELL counted as scrollTo counts them - across the drawn sections, headers, footers and grid fillers not counted (4f ruling 2026-09-27, round 11); until jsonui-cli 1.9.0 it counted the first data section only and scrolled to that lazy item index. Under reverseLayout a list rests at its visual bottom, so top and bottom trade places, as they do for scrollAnchor (4f ruling 2026-09-27, round 12 - where iOS lands, which draws the same order from content anchored at its bottom): bottom is where the list rests and moves nothing; top goes to the cell drawn at the visual top, the last one the content emits. Until jsonui-cli 1.9.0 bottom went to the last cell, which a reversed list draws at its visual top. A container that is not a lazy list and draws no reverseLayout (the wrapContent Column, the horizontal EAGER one) takes the middle or last cell to its top edge (round 12); the vertical EAGER one draws reverseLayout and follows the lazy list (round 13). It sets the initial position only: how a list keeps its position when its data changes with no scrollTo is each platform's own - iOS keeps the anchored edge while the list rests at it and, once a scrollTo has moved it, stays where it scrolled; Compose keeps the first visible item - so to follow new content, send scrollTo (4f ruling 2026-09-27, round 13; the iOS half measured round 14: a list resting at its bottom showed an appended cell there, and one a scrollTo had left above a cell kept it below the edge when more arrived). On Compose a list that is not reversed, with defaultScrollAnchor bottom and content shorter than its viewport, sits at its bottom, as iOS draws it (4f ruling 2026-09-27, round 14; it sat at the top). On a horizontal Collection, top / center / bottom mean the start / centre / end along the scroll axis, in reading direction, on every platform (4f ruling 2026-09-27); a horizontal list whose content is shorter than its row sits at the row's centre (center) or end (bottom) on Compose too, on the lazy and eager routes, as iOS draws it - measured on sjui codegen and SwiftJsonUI Dynamic - and a `lazy: none` row, which does not scroll, at its start, as sjui codegen draws it (4f ruling 2026-09-27, round 15; every Compose row sat at its start until jsonui-cli 1.9.0). A vertical list shorter than its viewport sits in its middle for center, on Compose too - the lazy and eager routes, reversed or not, as iOS draws them (measured 2026-09-27 on sjui codegen and SwiftJsonUI Dynamic) - and a `lazy: none` Column, which does not scroll, at its top (4f ruling 2026-09-27, round 16; it sat at the top, the bottom when reversed, until jsonui-cli 1.9.0)."
|
|
4019
4044
|
},
|
|
4020
4045
|
"scrollAnimated": {
|
|
4021
4046
|
"type": "boolean",
|
|
@@ -4023,7 +4048,7 @@
|
|
|
4023
4048
|
},
|
|
4024
4049
|
"cellIdProperty": {
|
|
4025
4050
|
"type": "string",
|
|
4026
|
-
"description": "Cell data property key to use as unique ID for ForEach identity.
|
|
4051
|
+
"description": "Cell data property key to use as unique ID for ForEach identity: a cell's key is its `cellId`, else this property's value. It decides what a scrollTo String names, not the scrollTo's type — the declared data class does (see scrollTo; 4f ruling 2026-09-27, round 14; until jsonui-cli 1.9.0 this said a set cellIdProperty made scrollTo a String)."
|
|
4027
4052
|
},
|
|
4028
4053
|
"autoChangeTrackingId": {
|
|
4029
4054
|
"type": "boolean",
|
|
@@ -4031,7 +4056,7 @@
|
|
|
4031
4056
|
},
|
|
4032
4057
|
"reverseLayout": {
|
|
4033
4058
|
"type": "boolean",
|
|
4034
|
-
"description": "Reverse the layout direction of the Collection grid. When true, section order in the generated code is also reversed so that JSON definition order (index 0 first) matches the visual top-to-bottom order on iOS (which does not support reverseLayout). Compose's reverseLayout then flips the entire list so index 0 appears at the bottom.",
|
|
4059
|
+
"description": "Reverse the layout direction of the Collection grid. When true, section order in the generated code is also reversed so that JSON definition order (index 0 first) matches the visual top-to-bottom order on iOS (which does not support reverseLayout). Compose's reverseLayout then flips the entire list so index 0 appears at the bottom. A reversed list whose content is shorter than its viewport sits at its bottom (its end, horizontally) - where iOS draws such a list, bottom-anchored - and the vertical EAGER container draws reverseLayout as the lazy list does (its first cell at the bottom, resting there), on both Compose paths (4f rulings 2026-09-27, round 13; jsonui-cli 1.9.0). Until then a short reversed list sat at its top and EAGER drew no reverseLayout. The Column of a wrapContent or `lazy: none` Collection and the horizontal EAGER container draw no reverseLayout.",
|
|
4035
4060
|
"mode": "compose"
|
|
4036
4061
|
},
|
|
4037
4062
|
"scrollEnabled": {
|
|
@@ -4039,7 +4064,7 @@
|
|
|
4039
4064
|
"boolean",
|
|
4040
4065
|
"binding"
|
|
4041
4066
|
],
|
|
4042
|
-
"description": "Enable/disable scrolling (binding supported)"
|
|
4067
|
+
"description": "Enable/disable scrolling (binding supported). It stops the user's scrolling only: a programmatic scroll (scrollTo, defaultScrollAnchor) still moves the Collection, as iOS's scrollDisabled does, on every route with a scroll container of its own (4f ruling 2026-09-27, round 13; jsonui-cli 1.9.0). Until then Compose's EAGER container dropped its scroll with it (kjui codegen), KotlinJsonUI Dynamic read it on the grids only, and neither Compose path read it on the flow, the pager or the single-lane row."
|
|
4043
4068
|
},
|
|
4044
4069
|
"onItemAppear": {
|
|
4045
4070
|
"type": [
|
|
@@ -4085,7 +4110,7 @@
|
|
|
4085
4110
|
},
|
|
4086
4111
|
"hideSeparator": {
|
|
4087
4112
|
"type": "boolean",
|
|
4088
|
-
"description": "Hide the row separators a list draws between cells. A NO-OP where the container draws no separators (a grid-shaped Collection has none) — that is the contract, not an unimplemented gap, and it does not license switching the container to a List.
|
|
4113
|
+
"description": "Hide the row separators a list draws between cells. A NO-OP where the container draws no separators (a grid-shaped Collection has none) — that is the contract, not an unimplemented gap, and it does not license switching the container to a List. Full ruling in attribute_semantics.json -> collectionSeparators."
|
|
4089
4114
|
},
|
|
4090
4115
|
"listStyle": {
|
|
4091
4116
|
"type": "string",
|
|
@@ -4096,7 +4121,7 @@
|
|
|
4096
4121
|
"sidebar"
|
|
4097
4122
|
],
|
|
4098
4123
|
"default": "plain",
|
|
4099
|
-
"description": "Which list chrome the collection is drawn with
|
|
4124
|
+
"description": "Which list chrome the collection is drawn with: plain (the default), grouped, insetGrouped or sidebar; an unrecognised value falls back to plain. ORTHOGONAL to hideSeparator — that one hides the separators, this one picks the chrome, and neither overrides the other. Full ruling in attribute_semantics.json -> collectionSeparators."
|
|
4100
4125
|
}
|
|
4101
4126
|
},
|
|
4102
4127
|
"Radio": {
|
|
@@ -4401,12 +4426,20 @@
|
|
|
4401
4426
|
"indicatorStyle": {
|
|
4402
4427
|
"type": "string",
|
|
4403
4428
|
"enum": [
|
|
4429
|
+
"small",
|
|
4404
4430
|
"medium",
|
|
4405
|
-
"large"
|
|
4431
|
+
"large",
|
|
4432
|
+
"linear"
|
|
4406
4433
|
],
|
|
4407
|
-
"description": "Indicator style",
|
|
4408
|
-
"platform":
|
|
4409
|
-
|
|
4434
|
+
"description": "Indicator style: `small` / `medium` / `large` are the spinner's size (`medium` the platform's own) and `linear` a bar instead of a spinner. A declared width / height wins over the style's size. The legacy spellings `style` (naming one of these values) and `size` (a length) are folded by the layout normalizer, with a warning: `style` into this attribute, `size` into width and height.",
|
|
4435
|
+
"platform": [
|
|
4436
|
+
"swift",
|
|
4437
|
+
"kotlin"
|
|
4438
|
+
],
|
|
4439
|
+
"mode": [
|
|
4440
|
+
"swiftui",
|
|
4441
|
+
"compose"
|
|
4442
|
+
]
|
|
4410
4443
|
},
|
|
4411
4444
|
"color": {
|
|
4412
4445
|
"type": [
|
|
@@ -4630,6 +4663,23 @@
|
|
|
4630
4663
|
"type": "string",
|
|
4631
4664
|
"description": "Iframe allow attribute for feature policy",
|
|
4632
4665
|
"platform": "react"
|
|
4666
|
+
},
|
|
4667
|
+
"onLoadFailed": {
|
|
4668
|
+
"type": "binding",
|
|
4669
|
+
"description": "Handler called when the MAIN-FRAME load fails - binding only (@{functionName}), called with no arguments. A failure is either the navigation itself failing (no connection, timeout, DNS, TLS) or the main-frame response carrying HTTP 4xx/5xx. Never called for a subresource (image, CSS, script) or a subframe, and never for a navigation that was cancelled because another one replaced it - a reloadToken change during a load is exactly that (iOS reports it as NSURLErrorCancelled, which is excluded; Android excludes net::ERR_ABORTED should it be reported). At most one call per load. The page a server returns with a 4xx/5xx is still displayed; to show an error face instead, hide the Web from the handler. iOS: WKNavigationDelegate didFail / didFailProvisionalNavigation and the status in decidePolicyFor(navigationResponse:). Android: WebViewClient onReceivedError / onReceivedHttpError, both gated on request.isForMainFrame. Not declared for react: an iframe reports neither the HTTP status of its document nor a cross-origin load failure (its load event fires for the browser's error page too, and error never fires for a document), so a web face could not honour this declaration.",
|
|
4670
|
+
"binding_direction": "one-way",
|
|
4671
|
+
"platform": [
|
|
4672
|
+
"swift",
|
|
4673
|
+
"kotlin"
|
|
4674
|
+
]
|
|
4675
|
+
},
|
|
4676
|
+
"reloadToken": {
|
|
4677
|
+
"type": "binding",
|
|
4678
|
+
"description": "Binding to a plain value (declare it Int) that reloads the component's own source each time the value CHANGES - the current `url`, or `html` when there is no url. Increment it in the ViewModel to retry after onLoadFailed. The value present when the view is created loads nothing extra; only a later change does. Writing the same url back does not reload (a url is compared with the last one loaded), which is why this exists. It reloads the declared source, not a page the user navigated to inside the view. Not declared for react: an iframe could be re-keyed, but the attribute exists to retry what onLoadFailed reports and the web face has no onLoadFailed.",
|
|
4679
|
+
"platform": [
|
|
4680
|
+
"swift",
|
|
4681
|
+
"kotlin"
|
|
4682
|
+
]
|
|
4633
4683
|
}
|
|
4634
4684
|
},
|
|
4635
4685
|
"SafeAreaView": {
|