jui-tools-mcp-server 2.13.0 → 2.14.0
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 +124 -74
- 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,7 +725,7 @@
|
|
|
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": [
|
|
@@ -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",
|
|
@@ -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. 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. (4f ruling 2026-09-27, and the user's ruling for iOS, round 6: align iOS 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 (4f round 17); 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",
|
|
@@ -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",
|
|
@@ -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": [
|
|
@@ -3840,15 +3866,15 @@
|
|
|
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": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"omittedEntirely": "same as the default: top | start. Omitting gravity must render identically to naming the default explicitly",
|
|
86
86
|
"leafOwnFrameChannel": {
|
|
87
87
|
"default": "center",
|
|
88
|
-
"ruling": "2026-08-08 adjudication (gravity leaf-scope hotfix, v1.5.2/10.15.1): the container default above scopes to CONTAINERS — nodes whose content the framework itself lays out. A childless node's own-frame content (a fit image's bitmap inside its declared box, a custom component's internals, a text block smaller than its fixed box) is a SEPARATE channel whose default is Center: Compose's Image/extension alignment default, which kjui never overrides, and the position the android face measurably draws a shrunken autoShrink line in its fixed box. ios converged in SwiftJsonUI 245d059+4d41bf9 (codegen gravity_to_frame_alignment leaf scope; dynamic fixed-frame text drop). A DECLARED gravity or textAlign still applies to any node — this entry names only the omitted-spelling default. Implementations were aligned BEFORE this text landed; the entry records the ruling the code already obeys."
|
|
88
|
+
"ruling": "2026-08-08 adjudication (gravity leaf-scope hotfix, v1.5.2/10.15.1): the container default above scopes to CONTAINERS — nodes whose content the framework itself lays out. A childless node's own-frame content (a fit image's bitmap inside its declared box, a custom component's internals, a text block smaller than its fixed box) is a SEPARATE channel whose default is Center: Compose's Image/extension alignment default, which kjui never overrides, and the position the android face measurably draws a shrunken autoShrink line in its fixed box. ios converged in SwiftJsonUI 245d059+4d41bf9 (codegen gravity_to_frame_alignment leaf scope; dynamic fixed-frame text drop). A DECLARED gravity or textAlign still applies to any node — this entry names only the omitted-spelling default. Implementations were aligned BEFORE this text landed; the entry records the ruling the code already obeys. 2026-09-27 (4f ruling, round 17): a Label's text in a frame taller than it (a numeric, matchParent or minHeight height, or a weight along its column) had not been: Compose drew it at the frame's top whatever the gravity — kjui for a numeric height, KotlinJsonUI Dynamic for every frame. From jsonui-cli 1.9.0 both Compose paths place it by the gravity's vertical part (top, bottom, the centre for center / centerVertical) and in the centre when the gravity names none, inside the Label's background and padding, as iOS does (sjui 19bf7b94, SwiftJsonUI f776b88)."
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"shadow": {
|
|
@@ -352,6 +352,7 @@
|
|
|
352
352
|
],
|
|
353
353
|
"noOpWhereNoSeparators": "a container that draws no separators satisfies hideSeparator vacuously — that is a no-op by contract, not an unimplemented gap",
|
|
354
354
|
"orthogonality": "listStyle picks the chrome, hideSeparator hides the separators; neither overrides the other",
|
|
355
|
+
"update_2026_09_26": "The ruling below describes 2026-08-07. Two of its facts have moved: TableConverter no longer exists (Table, List and ListView draw as Collection — shared/core/type_synonyms.json), and the attribute descriptions no longer say which faces read these attributes or that the coverage rows are runtime-only — which faces implement them is the coverage ledger's to say, measured, not a declaration's. The contract itself (the no-op where there are no separators, the orthogonality, the vocabulary) is unchanged.",
|
|
355
356
|
"ruling": "2026-08-07 (plan 51-E), raised by B: the ios probe path is CollectionStackView, not SwiftUI's List, and a container that is not a list has no separators to hide — so what should `hideSeparator` draw there? NOTHING, and that is the contract, not a gap. 'Hide the separators' is vacuously satisfied where there are none. The scope is deliberately NOT narrowed: the axis here is the container's rendered SHAPE (Collection's own `layout`, `columns` and `orientation` decide whether it is list-shaped), and `platform` / `mode` cannot express that — narrowing by platform would say something false. More importantly, 'implement it' must NOT mean 'switch the container to a List': an attribute that reshapes the thing it modifies is the defect family this wave exists to remove. hideSeparator modifies a list; it does not create one. MEASUREMENT CONSEQUENCE, so the ledgers do not mis-file it: the fixture must use a list-shaped Collection to observe this at all. Inert on a non-list container is the CONTRACT being honoured, and the coverage rows for both attributes are `runtime-only` (ios dynamic implements them; no codegen face does) rather than `unimplemented`. listStyle: enumerated here from the only implementation that reads it (SwiftJsonUI TableConverter.applyListStyle) as plain / grouped / insetGrouped / sidebar with plain the default — B was right that a bare string could not be validated or discriminated, and B should not replace the hardcoded PlainListStyle() until the vocabulary exists, which it now does. THE TWO ARE ORTHOGONAL. TableConverter.swift:45-49 currently lets `hideSeparator: true` win and forces .plain, which makes a declared `listStyle: \"grouped\"` silently inert whenever separators are hidden — the same silently-inert-declaration shape again. `.listRowSeparator(.hidden)` applies to any list style, so there is no need to force plain. Owner: F.",
|
|
356
357
|
"observable": {}
|
|
357
358
|
},
|
|
@@ -378,6 +379,22 @@
|
|
|
378
379
|
"defaultRange": "0 .. 1",
|
|
379
380
|
"ruling": "2026-08-07 (plan 51-E), raised by E2 with all three converters measured: none of them had a declared default to read, so each invented one — sjui `max || 1`, kjui and rjui `max || 100`. A layout writing `value: 0.5` therefore sits mid-track on ios and at half a percent on android and web. IOS IS CANON: the default range is 0 .. 1. The ground is this SSoT's own convention for a unitless value on a track, not a head count. `Progress.progress` declares `min: 0, max: 1` and `common.opacity` declares `min: 0, max: 1`; Progress is the Slider's closest sibling — a number along a track — and 100 would make Slider the only such attribute in the SSoT that is not normalised. It also keeps the round-trip coherent: `Slider.value` is declared two-way, so under a 0..100 default the control writes 0..100 back into a viewmodel whose author was writing 0..1. Declared as `default` on minimum and maximum so no converter has to invent it again; `range` is the [min, max] shorthand, so its absence means [0, 1] too. CONSEQUENCE FOR WEB, so that fixing the range does not break the control: `Slider.step` is declared platform:react with no default, and E2 measured web rounding 0.5 up to 1 under the old 0..100 range. With a 0..1 range an implicit step of 1 collapses the slider to two positions, so the web default step must be continuous (HTML `step=\"any\"`) unless the layout declares one. Owner: A for the step, A and C for the range.",
|
|
380
381
|
"observable": {}
|
|
382
|
+
},
|
|
383
|
+
"collectionSpacing": {
|
|
384
|
+
"attributes": [
|
|
385
|
+
"Collection.lineSpacing",
|
|
386
|
+
"Collection.columnSpacing",
|
|
387
|
+
"Collection.itemSpacing"
|
|
388
|
+
],
|
|
389
|
+
"undeclaredDefault": "0 on every route (vertical list and stack, grid, horizontal lanes and pages, flow) and on every path (sjui, kjui and rjui codegen; SwiftJsonUI and KotlinJsonUI Dynamic). A declared value is drawn as declared, 0 included.",
|
|
390
|
+
"chains": {
|
|
391
|
+
"vertical": "between rows: lineSpacing, else itemSpacing, else 0",
|
|
392
|
+
"grid": "between rows: lineSpacing, else itemSpacing, else 0; between columns: columnSpacing, else itemSpacing, else 0 — columnSpacing never spaces the rows",
|
|
393
|
+
"horizontal": "along the scroll axis, pages included: lineSpacing, else itemSpacing, else 0; between lanes: columnSpacing, else itemSpacing, else 0",
|
|
394
|
+
"flow": "between the cells of a line: columnSpacing, else itemSpacing, else 0; between lines, and between the section blocks, each section its own wrap: lineSpacing, else itemSpacing, else 0"
|
|
395
|
+
},
|
|
396
|
+
"ruling": "2026-09-26 (4f, round 5 of the Collection audit). Until jsonui-cli 1.9.0 no declaration said what an undeclared Collection spacing draws, and the paths had each picked one: 0 everywhere except the flow route on iOS — sjui codegen 8 between cells, lines and section blocks; SwiftJsonUI Dynamic 8 on the lazy flow (its section blocks the ScrollView's own stack spacing) and `x > 0 ? x : 8` on the `lazy: none` flow, so a declared 0 drew 8. Android and web drew 0. The ruling: 0, flow included, on every route and every path. Declared HERE and not as a `default` in attribute_definitions.json, on purpose: a `default` there changes the attribute's kind — the conformance fixture generator writes the declared default as the fixture's value, the inert audit lists it, and attr codegen carries it into the generated tables — and the rule is per route, which one scalar cannot say. `sectionSpacing` is lineSpacing's alias (definitions `aliases`). Toolchain comments point here.",
|
|
397
|
+
"observable": {}
|
|
381
398
|
}
|
|
382
399
|
}
|
|
383
400
|
}
|