jui-tools-mcp-server 2.5.0 → 2.5.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.
@@ -101,8 +101,7 @@
101
101
  "binding"
102
102
  ],
103
103
  "description": "Width weight for flexible layouts (binding supported)",
104
- "platform": "swift",
105
- "mode": "uikit"
104
+ "platform": "swift"
106
105
  },
107
106
  "heightWeight": {
108
107
  "type": [
@@ -110,8 +109,10 @@
110
109
  "binding"
111
110
  ],
112
111
  "description": "Height weight for flexible layouts (binding supported)",
113
- "platform": "swift",
114
- "mode": "uikit"
112
+ "platform": [
113
+ "swift",
114
+ "kotlin"
115
+ ]
115
116
  },
116
117
  "maxWidthWeight": {
117
118
  "type": [
@@ -231,7 +232,7 @@
231
232
  "string",
232
233
  "binding"
233
234
  ],
234
- "description": "Border color - hex string or color name from colors.json (binding supported)"
235
+ "description": "Border color - hex string or color name from colors.json (binding supported). There is deliberately NO default: a border is drawn only when borderWidth AND borderColor are both declared, and neither half summons one on its own (2026-08-03 user ruling, recorded in attribute_semantics.json#semantics.border and gated by `jui conformance gate --cross-effect` against the five `observable` entries there). Declaring a default here would make borderWidth alone draw, which is the direction d2c8628 took once and the ruling superseded."
235
236
  },
236
237
  "borderStyle": {
237
238
  "type": "string",
@@ -243,15 +244,6 @@
243
244
  "default": "solid",
244
245
  "description": "Border line style - solid (default), dashed, or dotted"
245
246
  },
246
- "alpha": {
247
- "type": [
248
- "number",
249
- "binding"
250
- ],
251
- "min": 0,
252
- "max": 1,
253
- "description": "Opacity (0-1) - can be data binding"
254
- },
255
247
  "opacity": {
256
248
  "type": [
257
249
  "number",
@@ -259,7 +251,7 @@
259
251
  ],
260
252
  "min": 0,
261
253
  "max": 1,
262
- "description": "Opacity (0-1), alias for alpha - can be data binding",
254
+ "description": "Opacity (0-1) - can be data binding. `alpha` is an accepted alias spelling.",
263
255
  "aliases": [
264
256
  "alpha"
265
257
  ]
@@ -734,7 +726,7 @@
734
726
  },
735
727
  "variables": {
736
728
  "type": "object",
737
- "description": "Variables for include",
729
+ "description": "Variables for include The SwiftUI codegen DOES read this (base_view_converter.rb), but only to echo it back as a `// variables: ...` comment — nothing reaches the rendered view, so `mode: uikit` stands. Recorded because the 2026-08-05 mode audit flags the read and the next auditor would otherwise re-derive it: the read is real, the effect is not.",
738
730
  "platform": "swift",
739
731
  "mode": "uikit"
740
732
  },
@@ -778,7 +770,7 @@
778
770
  },
779
771
  "binding_id": {
780
772
  "type": "string",
781
- "description": "Binding ID",
773
+ "description": "Binding ID The SwiftUI codegen DOES read this (base_view_converter.rb), but only to echo it back as a `// binding_id: ...` comment — nothing reaches the rendered view, so `mode: uikit` stands. Recorded because the 2026-08-05 mode audit flags the read and the next auditor would otherwise re-derive it: the read is real, the effect is not.",
782
774
  "platform": "swift",
783
775
  "mode": "uikit"
784
776
  },
@@ -836,24 +828,46 @@
836
828
  },
837
829
  "events": {
838
830
  "type": "object",
839
- "description": "Complex event handlers",
840
- "platform": "swift",
841
- "mode": "uikit"
831
+ "description": "Complex event handlers"
842
832
  },
843
833
  "effectStyle": {
844
- "type": "string",
845
- "description": "Visual effect style",
846
- "platform": "swift",
847
- "mode": "uikit"
848
- },
834
+ "type": "string",
835
+ "enum": [
836
+ "Light",
837
+ "Dark",
838
+ "ExtraLight",
839
+ "Regular",
840
+ "Prominent",
841
+ "UltraThin",
842
+ "Thin",
843
+ "Thick",
844
+ "Chrome",
845
+ "systemMaterial",
846
+ "systemUltraThinMaterial",
847
+ "systemThinMaterial",
848
+ "systemThickMaterial",
849
+ "systemChromeMaterial"
850
+ ],
851
+ "valueAliases": {
852
+ "systemMaterial": "Regular",
853
+ "systemUltraThinMaterial": "UltraThin",
854
+ "systemThinMaterial": "Thin",
855
+ "systemThickMaterial": "Thick",
856
+ "systemChromeMaterial": "Chrome"
857
+ },
858
+ "default": "Regular",
859
+ "description": "Visual effect (blur) style. Measured across the three converters 2026-08-05: the UIKit trio Light/Dark/ExtraLight is what all three map (sjui blur_converter.rb:71, kjui blurview_component.rb:38, rjui blur_converter.rb:98), and web additionally maps the SwiftUI material names. Every converter downcases before matching, so the casing here is presentational. `Regular` is the fallback all three already use 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."
860
+ },
849
861
  "tag": {
850
862
  "type": [
851
863
  "number",
852
864
  "binding"
853
865
  ],
854
866
  "description": "View tag for identification (binding supported)",
855
- "platform": "swift",
856
- "mode": "uikit"
867
+ "platform": [
868
+ "swift",
869
+ "react"
870
+ ]
857
871
  },
858
872
  "userInteractionEnabled": {
859
873
  "type": [
@@ -904,8 +918,7 @@
904
918
  "touchDisabledState": {
905
919
  "type": "string",
906
920
  "description": "Touch disable mode",
907
- "platform": "swift",
908
- "mode": "uikit"
921
+ "platform": "swift"
909
922
  },
910
923
  "touchEnabledViewIds": {
911
924
  "type": "array",
@@ -919,7 +932,7 @@
919
932
  },
920
933
  "bindingScript": {
921
934
  "type": "string",
922
- "description": "Custom Swift binding code",
935
+ "description": "Custom Swift binding code The SwiftUI codegen DOES read this (base_view_converter.rb), but only to echo it back as a `// bindingScript: ...` comment — nothing reaches the rendered view, so `mode: uikit` stands. Recorded because the 2026-08-05 mode audit flags the read and the next auditor would otherwise re-derive it: the read is real, the effect is not.",
923
936
  "platform": "swift",
924
937
  "mode": "uikit"
925
938
  },
@@ -1221,9 +1234,7 @@
1221
1234
  "array",
1222
1235
  "string"
1223
1236
  ],
1224
- "description": "Edge insets as array or pipe-separated string",
1225
- "deprecated": "kotlin",
1226
- "deprecation_note": "Compose Text has no edgeInset; use padding* instead."
1237
+ "description": "Edge insets as array or pipe-separated string. Applied on every platform including Compose, where it maps to .padding() (kjui text_component.rb / DynamicTextComponent.kt) - the former \"Compose Text has no edgeInset\" deprecation was contradicted by that implementation and was retracted 2026-08-05."
1227
1238
  },
1228
1239
  "underline": {
1229
1240
  "type": [
@@ -1442,7 +1453,7 @@
1442
1453
  },
1443
1454
  "hintAttributes": {
1444
1455
  "type": "object",
1445
- "description": "Hint text styling attributes",
1456
+ "description": "Hint text styling attributes. The nested keys win over the flat spellings (hintColor / hintFont / hintFontSize): a bag scoped to the hint is the more specific statement. That is the ordinary cascade rule and what all four readers do (rjui label_converter, kjui text_component, sjui Label and SelectBox) - it was unwritten until 2026-08-05, when the one converter that had it backwards was found by contradicting its own comment.",
1446
1457
  "properties": {
1447
1458
  "font": {
1448
1459
  "type": "string",
@@ -1492,27 +1503,23 @@
1492
1503
  "string",
1493
1504
  "binding"
1494
1505
  ],
1495
- "description": "Placeholder color - hex string or color name from colors.json (binding supported)",
1496
- "deprecated": "swiftui",
1497
- "deprecation_note": "SwiftUI TextField placeholder cannot be color-styled."
1506
+ "description": "Placeholder color - hex string or color name from colors.json (binding supported). `placeholderColor` is an accepted alias spelling.",
1507
+ "aliases": [
1508
+ "placeholderColor"
1509
+ ]
1498
1510
  },
1499
1511
  "hintFont": {
1500
1512
  "type": "string",
1501
- "description": "Placeholder font",
1502
- "deprecated": "swiftui",
1503
- "deprecation_note": "SwiftUI TextField placeholder cannot be custom-font-styled."
1513
+ "description": "Placeholder font"
1504
1514
  },
1505
1515
  "hintFontSize": {
1506
1516
  "type": "number",
1507
- "description": "Placeholder font size",
1508
- "deprecated": "swiftui",
1509
- "deprecation_note": "SwiftUI TextField placeholder cannot be custom-size-styled."
1517
+ "description": "Placeholder font size"
1510
1518
  },
1511
1519
  "hintAttributes": {
1512
1520
  "type": "object",
1513
- "description": "Hint text attributes (font, color, etc.)",
1514
- "platform": "swift",
1515
- "mode": "uikit"
1521
+ "description": "Hint text attributes (font, fontSize, fontColor). The nested keys win over the flat spellings (hintColor / hintFont / hintFontSize): a bag scoped to the hint is the more specific statement. That is the ordinary cascade rule and what all four readers do (rjui label_converter, kjui text_component, sjui Label and SelectBox) - it was unwritten until 2026-08-05, when the one converter that had it backwards was found by contradicting its own comment.",
1522
+ "platform": "swift"
1516
1523
  },
1517
1524
  "font": {
1518
1525
  "type": [
@@ -1577,7 +1584,22 @@
1577
1584
  },
1578
1585
  "inputType": {
1579
1586
  "type": "string",
1580
- "description": "Input type for Android (Android-only; `input` is the cross-platform attribute)",
1587
+ "enum": [
1588
+ "text",
1589
+ "number",
1590
+ "numberDecimal",
1591
+ "phone",
1592
+ "email",
1593
+ "password",
1594
+ "multiline",
1595
+ "textEmailAddress",
1596
+ "textPassword"
1597
+ ],
1598
+ "valueAliases": {
1599
+ "textEmailAddress": "email",
1600
+ "textPassword": "password"
1601
+ },
1602
+ "description": "Input type for Android (Android-only; `input` is the cross-platform attribute). Both the JsonUI spellings and the raw android:inputType names the frozen XML mapper passed through are accepted; the latter normalize to the former.",
1581
1603
  "platform": "kotlin"
1582
1604
  },
1583
1605
  "returnKeyType": {
@@ -1602,7 +1624,31 @@
1602
1624
  "string",
1603
1625
  "binding"
1604
1626
  ],
1605
- "description": "Content type for autofill (binding supported)"
1627
+ "enum": [
1628
+ "username",
1629
+ "password",
1630
+ "newPassword",
1631
+ "oneTimeCode",
1632
+ "email",
1633
+ "emailAddress",
1634
+ "name",
1635
+ "givenName",
1636
+ "familyName",
1637
+ "telephoneNumber",
1638
+ "tel",
1639
+ "phone",
1640
+ "streetAddress",
1641
+ "postalCode",
1642
+ "country",
1643
+ "creditCardNumber",
1644
+ "URL"
1645
+ ],
1646
+ "valueAliases": {
1647
+ "emailAddress": "email",
1648
+ "tel": "telephoneNumber",
1649
+ "phone": "telephoneNumber"
1650
+ },
1651
+ "description": "Content type for autofill (binding supported). The enum is the set of tokens at least one platform actually maps: iOS to .textContentType, web to the autoComplete attribute, Android to the keyboard type. `email` (not emailAddress) and `telephoneNumber` (not tel) are the canonical spellings — the web converter only matches those two, so the alternates are normalized before conversion. Tokens no platform maps are deliberately absent: declaring UIKit's full UITextContentType list would enumerate values that cannot reach any output."
1606
1652
  },
1607
1653
  "secure": {
1608
1654
  "type": [
@@ -1663,21 +1709,21 @@
1663
1709
  },
1664
1710
  "accessoryBackground": {
1665
1711
  "type": "string",
1666
- "description": "Input accessory background - hex string or color name from colors.json",
1712
+ "description": "Input accessory toolbar background - hex string or color name from colors.json. UIKit only: read by SJUITextField's accessory toolbar; SwiftUI has no input-accessory surface.",
1667
1713
  "platform": "swift",
1668
- "mode": "swiftui"
1714
+ "mode": "uikit"
1669
1715
  },
1670
1716
  "accessoryTextColor": {
1671
1717
  "type": "string",
1672
- "description": "Input accessory text color - hex string or color name from colors.json",
1718
+ "description": "Input accessory toolbar text color - hex string or color name from colors.json. UIKit only (see accessoryBackground).",
1673
1719
  "platform": "swift",
1674
- "mode": "swiftui"
1720
+ "mode": "uikit"
1675
1721
  },
1676
1722
  "doneText": {
1677
1723
  "type": "string",
1678
- "description": "Done button text",
1724
+ "description": "Text of the input accessory toolbar's Done button. UIKit only (see accessoryBackground).",
1679
1725
  "platform": "swift",
1680
- "mode": "swiftui"
1726
+ "mode": "uikit"
1681
1727
  },
1682
1728
  "tintColor": {
1683
1729
  "type": "string",
@@ -1691,9 +1737,7 @@
1691
1737
  "type": "string",
1692
1738
  "description": "Caret color - hex string or color name from colors.json"
1693
1739
  }
1694
- },
1695
- "deprecated": "swiftui",
1696
- "deprecation_note": "SwiftUI cannot style the caret (iOS 17 has partial .tint API)."
1740
+ }
1697
1741
  },
1698
1742
  "onTextChange": {
1699
1743
  "type": "string",
@@ -1701,24 +1745,36 @@
1701
1745
  },
1702
1746
  "autocapitalizationType": {
1703
1747
  "type": "string",
1704
- "description": "Auto-capitalization type"
1748
+ "enum": [
1749
+ "none",
1750
+ "words",
1751
+ "sentences",
1752
+ "allCharacters",
1753
+ "characters"
1754
+ ],
1755
+ "valueAliases": {
1756
+ "characters": "allCharacters"
1757
+ },
1758
+ "description": "Auto-capitalization type. UIKit's vocabulary, honoured on every platform (.textInputAutocapitalization / KeyboardCapitalization / the HTML autocapitalize attribute). `characters` is an accepted alias spelling of allCharacters."
1705
1759
  },
1706
1760
  "autocorrectionType": {
1707
1761
  "type": "string",
1708
- "description": "Auto-correction type"
1709
- },
1710
- "spellCheckingType": {
1711
- "type": "string",
1712
- "description": "Spell checking type",
1713
- "platform": "swift",
1714
- "deprecated": "swift",
1715
- "deprecation_note": "Compose does not expose spell-checking control."
1716
- },
1717
- "keyboardAppearance": {
1718
- "type": "string",
1719
- "description": "Keyboard appearance: dark, light",
1720
- "deprecated": "kotlin",
1721
- "deprecation_note": "Compose keyboard appearance is system-controlled."
1762
+ "enum": [
1763
+ "default",
1764
+ "yes",
1765
+ "no",
1766
+ "on",
1767
+ "off",
1768
+ "true",
1769
+ "false"
1770
+ ],
1771
+ "valueAliases": {
1772
+ "on": "yes",
1773
+ "true": "yes",
1774
+ "off": "no",
1775
+ "false": "no"
1776
+ },
1777
+ "description": "Auto-correction type. UIKit's vocabulary; `on`/`true` and `off`/`false` are accepted alias spellings of yes/no. `default` means \"leave it to the platform\" — web deliberately emits nothing for it."
1722
1778
  },
1723
1779
  "clearButtonMode": {
1724
1780
  "type": "string",
@@ -1846,12 +1902,6 @@
1846
1902
  "description": "Hide other elements while the field is focused.",
1847
1903
  "platform": "swift",
1848
1904
  "mode": "swiftui"
1849
- },
1850
- "placeholderColor": {
1851
- "type": "string",
1852
- "description": "Deprecated on swift.",
1853
- "deprecated": "swift",
1854
- "deprecation_note": "SwiftUI TextField placeholder cannot be color-styled."
1855
1905
  }
1856
1906
  },
1857
1907
  "TextView": {
@@ -1892,7 +1942,7 @@
1892
1942
  },
1893
1943
  "hintAttributes": {
1894
1944
  "type": "object",
1895
- "description": "Hint styling attributes"
1945
+ "description": "Hint styling attributes. The nested keys win over the flat spellings (hintColor / hintFont / hintFontSize): a bag scoped to the hint is the more specific statement. That is the ordinary cascade rule and what all four readers do (rjui label_converter, kjui text_component, sjui Label and SelectBox) - it was unwritten until 2026-08-05, when the one converter that had it backwards was found by contradicting its own comment."
1896
1946
  },
1897
1947
  "hideOnFocused": {
1898
1948
  "type": "boolean",
@@ -1985,7 +2035,15 @@
1985
2035
  },
1986
2036
  "lineBreakMode": {
1987
2037
  "type": "string",
1988
- "description": "Line break mode"
2038
+ "enum": [
2039
+ "Char",
2040
+ "Clip",
2041
+ "Word",
2042
+ "Head",
2043
+ "Middle",
2044
+ "Tail"
2045
+ ],
2046
+ "description": "Truncation mode. Same vocabulary as Label.lineBreakMode — the two were the same concept declared twice, once enumerated and once as a bare string."
1989
2047
  },
1990
2048
  "dataDetectorTypes": {
1991
2049
  "type": "string",
@@ -2001,7 +2059,41 @@
2001
2059
  },
2002
2060
  "keyboardType": {
2003
2061
  "type": "string",
2004
- "description": "Keyboard type"
2062
+ "enum": [
2063
+ "default",
2064
+ "asciiCapable",
2065
+ "alphabet",
2066
+ "number",
2067
+ "numberPad",
2068
+ "decimal",
2069
+ "decimalPad",
2070
+ "phone",
2071
+ "phonePad",
2072
+ "email",
2073
+ "emailAddress",
2074
+ "URL",
2075
+ "webURL",
2076
+ "webSearch",
2077
+ "search",
2078
+ "namePhonePad",
2079
+ "twitter",
2080
+ "text",
2081
+ "numeric",
2082
+ "numbersAndPunctuation"
2083
+ ],
2084
+ "valueAliases": {
2085
+ "alphabet": "asciiCapable",
2086
+ "numberPad": "number",
2087
+ "decimalPad": "decimal",
2088
+ "phonePad": "phone",
2089
+ "numeric": "phone",
2090
+ "numbersAndPunctuation": "number",
2091
+ "emailAddress": "email",
2092
+ "webURL": "URL",
2093
+ "search": "webSearch",
2094
+ "text": "default"
2095
+ },
2096
+ "description": "Soft-keyboard type. The canonical token of each pair is the one all three converters accept: `number` rather than UIKit's numberPad (Compose only matches `number` and would fall back to a plain text keyboard), likewise decimal/phone/email. `namePhonePad` and `twitter` reach iOS only; the other platforms fall back to their default keyboard."
2005
2097
  },
2006
2098
  "onTextChange": {
2007
2099
  "type": "string",
@@ -2097,10 +2189,11 @@
2097
2189
  "platform": "react"
2098
2190
  },
2099
2191
  "edgeInset": {
2100
- "type": "string",
2101
- "description": "Deprecated on kotlin.",
2102
- "deprecated": "kotlin",
2103
- "deprecation_note": "Compose Text has no edgeInset; use padding* instead."
2192
+ "type": [
2193
+ "number",
2194
+ "array"
2195
+ ],
2196
+ "description": "The UIKit spelling of containerInset (content inset). Same accepted shapes: a single number, or an array of 1/2/4 numbers. Was declared deprecated on kotlin (\"Compose Text has no edgeInset\"); retracted 2026-08-05 because Label.edgeInset maps to .padding() on Compose in both the codegen and the dynamic renderer. Unimplemented on the Compose TextView path, not impossible."
2104
2197
  }
2105
2198
  },
2106
2199
  "EditText": {
@@ -2151,10 +2244,6 @@
2151
2244
  "hilightColor"
2152
2245
  ]
2153
2246
  },
2154
- "hilightColor": {
2155
- "type": "string",
2156
- "description": "Highlight color - hex string or color name from colors.json (typo alias)"
2157
- },
2158
2247
  "tapBackground": {
2159
2248
  "type": "string",
2160
2249
  "description": "Background when tapped - hex string or color name from colors.json"
@@ -2259,7 +2348,10 @@
2259
2348
  "Left",
2260
2349
  "Right"
2261
2350
  ],
2262
- "description": "Content mode (binding supported)"
2351
+ "valueAliases": {
2352
+ "ScaleToFill": "fill"
2353
+ },
2354
+ "description": "Content mode (binding supported). ScaleToFill is a declared synonym of fill (the stretch — see attribute_semantics.json#semantics.image); the normalizer folds it, fixtures are generated for the canonical spelling only."
2263
2355
  },
2264
2356
  "renderingMode": {
2265
2357
  "type": "string",
@@ -2443,14 +2535,6 @@
2443
2535
  ],
2444
2536
  "description": "Hint text color - hex string or color name from colors.json (binding supported)"
2445
2537
  },
2446
- "text": {
2447
- "type": [
2448
- "string",
2449
- "binding"
2450
- ],
2451
- "description": "Display text (binding supported)",
2452
- "platform": "swift"
2453
- },
2454
2538
  "items": {
2455
2539
  "type": [
2456
2540
  "array",
@@ -2481,8 +2565,10 @@
2481
2565
  "binding"
2482
2566
  ],
2483
2567
  "description": "Selected item value (two-way binding)",
2484
- "platform": "swift",
2485
- "mode": "uikit"
2568
+ "platform": [
2569
+ "swift",
2570
+ "kotlin"
2571
+ ]
2486
2572
  },
2487
2573
  "selectedDate": {
2488
2574
  "type": [
@@ -2500,7 +2586,10 @@
2500
2586
  "dateAndTime",
2501
2587
  "countDown"
2502
2588
  ],
2503
- "description": "Date picker mode"
2589
+ "valueAliases": {
2590
+ "datetime": "dateAndTime"
2591
+ },
2592
+ "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 downcases), 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."
2504
2593
  },
2505
2594
  "datePickerStyle": {
2506
2595
  "type": "string",
@@ -2807,10 +2896,6 @@
2807
2896
  "platform": "swift",
2808
2897
  "mode": "uikit"
2809
2898
  },
2810
- "onToggle": {
2811
- "type": "binding",
2812
- "description": "Toggle handler function - binding only (@{functionName}). Alias of onValueChange."
2813
- },
2814
2899
  "onValueChange": {
2815
2900
  "type": "binding",
2816
2901
  "description": "Value change handler - binding only (@{functionName})",
@@ -2854,9 +2939,7 @@
2854
2939
  },
2855
2940
  "trackTintColor": {
2856
2941
  "type": "string",
2857
- "description": "Deprecated on swift.",
2858
- "deprecated": "swift",
2859
- "deprecation_note": "SwiftUI Toggle uses unified tint only."
2942
+ "description": "Colour of the switch track - hex string or color name from colors.json. Deprecation retracted with the Slider pair; a SwiftUI Toggle is not limited to a unified tint any more than Progress is."
2860
2943
  }
2861
2944
  },
2862
2945
  "Toggle": {
@@ -2879,25 +2962,21 @@
2879
2962
  },
2880
2963
  "tintColor": {
2881
2964
  "type": "string",
2882
- "description": "Tint color - hex string or color name from colors.json"
2965
+ "description": "Accent color of the SELECTED segment - a background fill on ios/web, the indicator on Compose. Hex string or color name from colors.json"
2883
2966
  },
2884
- "normalColor": {
2885
- "type": [
2886
- "string",
2887
- "binding"
2888
- ],
2889
- "description": "Normal state color - hex string or color name from colors.json (binding supported)",
2890
- "platform": "swift",
2891
- "mode": "swiftui"
2967
+ "fontColor": {
2968
+ "type": "string",
2969
+ "description": "Label color of the UNSELECTED segments - hex string or color name from colors.json",
2970
+ "aliases": [
2971
+ "normalColor"
2972
+ ]
2892
2973
  },
2893
- "selectedColor": {
2894
- "type": [
2895
- "string",
2896
- "binding"
2897
- ],
2898
- "description": "Selected state color - hex string or color name from colors.json (binding supported)",
2899
- "platform": "swift",
2900
- "mode": "swiftui"
2974
+ "selectedFontColor": {
2975
+ "type": "string",
2976
+ "description": "Label color of the selected segment, falling back to fontColor - hex string or color name from colors.json",
2977
+ "aliases": [
2978
+ "selectedColor"
2979
+ ]
2901
2980
  },
2902
2981
  "valueChange": {
2903
2982
  "type": "string",
@@ -2929,37 +3008,23 @@
2929
3008
  "description": "Current value (binding for two-way)",
2930
3009
  "binding_direction": "two-way"
2931
3010
  },
2932
- "minValue": {
2933
- "type": [
2934
- "number",
2935
- "binding"
2936
- ],
2937
- "description": "Minimum value (binding supported)"
2938
- },
2939
3011
  "minimum": {
2940
3012
  "type": [
2941
3013
  "number",
2942
3014
  "binding"
2943
3015
  ],
2944
- "description": "Minimum value (alias for minValue) (binding supported)",
3016
+ "description": "Minimum value (binding supported). `minValue` / `minimumValue` are accepted alias spellings.",
2945
3017
  "aliases": [
2946
3018
  "minimumValue",
2947
3019
  "minValue"
2948
3020
  ]
2949
3021
  },
2950
- "maxValue": {
2951
- "type": [
2952
- "number",
2953
- "binding"
2954
- ],
2955
- "description": "Maximum value (binding supported)"
2956
- },
2957
3022
  "maximum": {
2958
3023
  "type": [
2959
3024
  "number",
2960
3025
  "binding"
2961
3026
  ],
2962
- "description": "Maximum value (alias for maxValue) (binding supported)",
3027
+ "description": "Maximum value (binding supported). `maxValue` / `maximumValue` are accepted alias spellings.",
2963
3028
  "aliases": [
2964
3029
  "maximumValue",
2965
3030
  "maxValue"
@@ -2995,15 +3060,11 @@
2995
3060
  },
2996
3061
  "progressTintColor": {
2997
3062
  "type": "string",
2998
- "description": "Deprecated on swift.",
2999
- "deprecated": "swift",
3000
- "deprecation_note": "SwiftUI Slider uses unified tint only."
3063
+ "description": "Colour of the filled portion of the track - hex string or color name from colors.json. Was declared deprecated on swift (\"SwiftUI Slider uses unified tint only\"); retracted 2026-08-05 because Progress, also SwiftUI, maps the same attribute to .tint(). Unimplemented on the Slider path, not impossible."
3001
3064
  },
3002
3065
  "trackTintColor": {
3003
3066
  "type": "string",
3004
- "description": "Deprecated on swift.",
3005
- "deprecated": "swift",
3006
- "deprecation_note": "SwiftUI Slider uses unified tint only."
3067
+ "description": "Colour of the unfilled track - hex string or color name from colors.json. Deprecation retracted with progressTintColor: SwiftUI Progress reaches the same result through .background()."
3007
3068
  }
3008
3069
  },
3009
3070
  "Progress": {
@@ -3137,15 +3198,29 @@
3137
3198
  "gradient": {
3138
3199
  "type": "array",
3139
3200
  "description": "Gradient colors",
3140
- "platform": "swift",
3141
- "mode": "uikit"
3201
+ "platform": "swift"
3142
3202
  },
3143
3203
  "gradientDirection": {
3144
- "type": "string",
3145
- "description": "Gradient direction",
3146
- "platform": "swift",
3147
- "mode": "uikit"
3148
- },
3204
+ "type": "string",
3205
+ "enum": [
3206
+ "Vertical",
3207
+ "Horizontal",
3208
+ "Oblique",
3209
+ "LeftToRight",
3210
+ "TopToBottom",
3211
+ "Diagonal",
3212
+ "RightToLeft",
3213
+ "BottomToTop"
3214
+ ],
3215
+ "valueAliases": {
3216
+ "LeftToRight": "Horizontal",
3217
+ "TopToBottom": "Vertical",
3218
+ "Diagonal": "Oblique"
3219
+ },
3220
+ "default": "Vertical",
3221
+ "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. CASE MATTERS on ios: sjui matches 'Horizontal'/'Oblique' literally (modifier_helper.rb:16) without downcasing, while Compose and web downcase — so the capitalised spellings are canonical and the aliases normalise to them before any converter sees the value. RightToLeft and BottomToTop are declared without an alias on purpose: they are reversed directions web implements (gradient_view_converter.rb:88,90) and the other two have no equivalent, so folding them into Horizontal/Vertical would silently reverse the gradient rather than preserve it.",
3222
+ "platform": "swift"
3223
+ },
3149
3224
  "locations": {
3150
3225
  "type": "array",
3151
3226
  "description": "Gradient color stop locations",
@@ -3154,7 +3229,7 @@
3154
3229
  },
3155
3230
  "safeAreaInsetPositions": {
3156
3231
  "type": "array",
3157
- "description": "Safe area inset positions (SafeAreaView only)",
3232
+ "description": "Which edges reserve the safe area. Declared here as well as on SafeAreaView on purpose: SafeAreaView is its own definition section and does not inherit View's, and every platform honours the attribute on a PLAIN view too (sjui base_view_converter.rb#apply_safe_area_insets_to_bag runs for every component; rjui view_converter.rb#safe_area_edges emits env(safe-area-inset-*) padding). The two declarations are the same concept reaching two routing paths, not a duplicate.",
3158
3233
  "items": {
3159
3234
  "type": "string",
3160
3235
  "enum": [
@@ -3216,15 +3291,11 @@
3216
3291
  "_comment": "Sparse by design, not an alias: only the Kotlin dynamic runtime dispatches CircleView (DynamicCircleViewComponent, CircleShape clip) - P18 audit 2026-08-01. Everything it renders beyond child/children comes from common. component_metadata.json carries the per-platform truth.",
3217
3292
  "child": {
3218
3293
  "type": "array",
3219
- "description": "Child component(s)",
3220
- "platform": "swift",
3221
- "mode": "uikit"
3294
+ "description": "Child component(s)"
3222
3295
  },
3223
3296
  "children": {
3224
3297
  "type": "array",
3225
- "description": "Child components (alias for child)",
3226
- "platform": "swift",
3227
- "mode": "uikit"
3298
+ "description": "Child components (alias for child)"
3228
3299
  }
3229
3300
  },
3230
3301
  "ScrollView": {
@@ -3579,7 +3650,7 @@
3579
3650
  },
3580
3651
  "scrollTo": {
3581
3652
  "type": "binding",
3582
- "description": "PassthroughSubject<Int, Never> for programmatic scrolling to cell index"
3653
+ "description": "Binding that requests a programmatic scroll. The declared DATA CLASS is a plain value — `String` when cellIdProperty is set (scroll to that cell id), `Int` otherwise (scroll to that index). 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 wraps the value in a publisher so a repeated send re-scrolls, Compose reads the String directly, web keys a useEffect on it. STATUS 2026-08-05, after B landed the derivation (jsonui-cli 8c41e3e): the CODEGEN side of all three platforms now takes the plain value — sjui emits `.onChange(of: data.<prop>)` with no publisher and no throttle, Compose keys a LaunchedEffect on it, web a useEffect. The remaining gap is the SwiftUI DYNAMIC renderer: CollectionConverter.swift:127 still resolves the binding as `data[propName] as? PassthroughSubject<Int, Never>` and gets nil for a plain value, so the scroll silently never fires there (it does not crash, it does nothing). Consequence for fixtures: a Collection.scrollTo fixture is now declarable and no longer kills the Kotlin build, and Collection.scrollAnchor is measurable on the codegen path — but ios codegen and ios dynamic will disagree until the dynamic resolver accepts the value, so expect a parity finding there rather than a render failure. Owner of the remainder: the SwiftJsonUI dynamic lane. Note also that the vendored CollectionAttributes.swift still carries the old publisher wording in its doc comment; it is generated from this description and will pick the correction up at the next re-vendor."
3583
3654
  },
3584
3655
  "scrollAnchor": {
3585
3656
  "type": "string",
@@ -3588,7 +3659,8 @@
3588
3659
  "center",
3589
3660
  "bottom"
3590
3661
  ],
3591
- "description": "Anchor point for scroll target (default: bottom)"
3662
+ "default": "bottom",
3663
+ "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 (kjui collection_component.rb:313) all fall back to bottom, so bottom is the declared default. The Compose LIST path emits no anchor offset unless the attribute is explicitly declared, which lands the target at top - that is a divergence in that one path, not a second default."
3592
3664
  },
3593
3665
  "defaultScrollAnchor": {
3594
3666
  "type": "string",
@@ -3624,17 +3696,16 @@
3624
3696
  "description": "Enable/disable scrolling (binding supported)"
3625
3697
  },
3626
3698
  "onItemAppear": {
3627
- "type": "callback",
3628
- "description": "Called with the cell index (Int) when a cell appears on screen. Use for pagination by checking index against total count in ViewModel."
3699
+ "type": [
3700
+ "string",
3701
+ "binding"
3702
+ ],
3703
+ "description": "Called with the cell index (Int) when a cell appears on screen. Use for pagination by checking index against total count in ViewModel. Declared `binding` because that is what a layout actually carries: the author writes `@{handlerName}`, a STRING, and every reader matches it as one (kjui collection_component.rb:364 `json_data['onItemAppear'].match(/@\\{([^}]+)\\}/)`, and the binding validators infer `((Int) -> Unit)?` from that spelling). It was `type: \"callback\"` — the only callback-typed attribute in the whole SSoT — and attr-codegen skips that type as \"function-valued, not extractable from JSON\". True of a function; not true of the `@{...}` string a JSON layout can hold, so the attribute had no row in any generated table and no platform could read it typed (2026-08-05, plan 49-E, raised by A)."
3629
3704
  },
3630
3705
  "currentPage": {
3631
3706
  "type": "binding",
3632
3707
  "description": "Current page index for paging collections (binding supported)"
3633
3708
  },
3634
- "onPageChanged": {
3635
- "type": "callback",
3636
- "description": "Called with the new page index (Int) when the user swipes to a different page"
3637
- },
3638
3709
  "containerInset": {
3639
3710
  "type": "array",
3640
3711
  "description": "Container-level insets applied outside of individual cells.",
@@ -3698,16 +3769,12 @@
3698
3769
  "selected_icon"
3699
3770
  ]
3700
3771
  },
3701
- "selected_icon": {
3702
- "type": "string",
3703
- "description": "Selected icon name (underscore alias)"
3704
- },
3705
3772
  "checked": {
3706
3773
  "type": [
3707
3774
  "boolean",
3708
3775
  "binding"
3709
3776
  ],
3710
- "description": "Initial checked state (can be binding)"
3777
+ "description": "Seeds the group's INITIAL selection — it is not the live state, and it is not two-way. The state a radio group carries is `selectedValue`; `checked` only says which option starts selected when nothing else has said. Precedence, and all three platforms implement it in this order: a BOUND `selectedValue` wins outright (the group's own two-way state), then a LITERAL `selectedValue` (a group-level statement naming the starting option), then this option's `checked`. So a radio group that binds selectedValue ignores `checked` entirely, which is what stops a seed from freezing a group that the user can no longer change. The bound form of `checked` seeds the glyph rather than the state, since a property initialiser cannot read the data map: it selects only while the group has made no choice yet (sjui radio_converter.rb:88-103, and the same rule in RadioConverter.swift). CONTRAST with CheckBox.checked and Switch.checked, which look identical and are not: those ARE the control's state and carry binding_direction two-way. A radio has no state of its own — the group does. Declared 2026-08-05 (plan 49-E) from the implementations; the previous description said only \"Initial checked state\", which was true and told nobody what happens when both are declared."
3711
3778
  },
3712
3779
  "value": {
3713
3780
  "type": "any",
@@ -3922,6 +3989,18 @@
3922
3989
  "_comment": "Component alias of CheckBox. Resolved by the L1 normalizer type rewrite and by every validator/codegen consumer - do not add attribute copies here."
3923
3990
  },
3924
3991
  "Indicator": {
3992
+ "animating": {
3993
+ "type": [
3994
+ "boolean",
3995
+ "binding"
3996
+ ],
3997
+ "default": true,
3998
+ "description": "Whether the indicator is spinning. Defaults to true: an Indicator with nothing declared animates. `false` stops it, and hidesWhenStopped then decides whether the stopped indicator keeps its space or collapses out of the layout \u2014 which is why hidesWhenStopped is only ever read on this branch (sjui indicator_converter.rb:23, kjui indicator_component.rb:13).",
3999
+ "platform": [
4000
+ "swift",
4001
+ "kotlin"
4002
+ ]
4003
+ },
3925
4004
  "indicatorStyle": {
3926
4005
  "type": "string",
3927
4006
  "enum": [
@@ -4070,8 +4149,8 @@
4070
4149
  "platform": "swift"
4071
4150
  },
4072
4151
  "sandbox": {
4073
- "type": "string",
4074
- "description": "Iframe sandbox attribute",
4152
+ "type": "boolean",
4153
+ "description": "Iframe sandboxing kill-switch. `false` emits no sandbox attribute at all; otherwise the permission list is built from the other Web attributes (javaScriptEnabled, javaScriptCanOpenWindowsAutomatically, allowPopupsToEscapeSandbox, allowModals, ...) rather than from a value written here.",
4075
4154
  "platform": "react"
4076
4155
  },
4077
4156
  "allow": {
@@ -4091,7 +4170,10 @@
4091
4170
  },
4092
4171
  "safeAreaInsetPositions": {
4093
4172
  "type": "array",
4094
- "description": "Safe area edges",
4173
+ "description": "Safe area edges. `edges` is an accepted alias spelling: the Compose SafeAreaView builder reads it (kjui compose_builder.rb:722 `json_data['edges'] || json_data['safeAreaInsetPositions']`) while no declaration named it, so iOS and web silently ignored a layout that used it. Declaring the alias normalizes it to the canonical spelling and makes it reach all three platforms.",
4174
+ "aliases": [
4175
+ "edges"
4176
+ ],
4095
4177
  "items": {
4096
4178
  "type": "string",
4097
4179
  "enum": [
@@ -4195,10 +4277,6 @@
4195
4277
  "default": true,
4196
4278
  "description": "Whether to show tab labels (default: true)"
4197
4279
  },
4198
- "onTabChange": {
4199
- "type": "binding",
4200
- "description": "Tab change handler - binding only (@{functionName})"
4201
- },
4202
4280
  "onValueChange": {
4203
4281
  "type": [
4204
4282
  "string",
@@ -6,25 +6,41 @@
6
6
  "border": {
7
7
  "widthAlone": "no-draw",
8
8
  "styleAlone": "no-draw",
9
- "ruling": "A border is drawn only when BOTH borderWidth and borderColor are declared; borderWidth alone renders nothing on every platform — there is no default border color (2026-08-03 user ruling; supersedes the transient gray-default direction of d2c8628, unified across toolchains in 3e87b96). borderStyle alone likewise renders nothing: it styles a border the width+color pair requests, it never summons one (2026-08-03 user ruling #2). NOTE: TextField.borderStyle is a DIFFERENT attribute (UIKit text-field chrome: roundedRect/line/bezel/none) and is outside this entry.",
9
+ "ruling": "A border is drawn only when BOTH borderWidth and borderColor are declared; borderWidth alone renders nothing on every platform — there is no default border color (2026-08-03 user ruling; supersedes the transient gray-default direction of d2c8628, unified across toolchains in 3e87b96). borderColor alone renders nothing for the same reason — the pair is what requests a border, and neither half requests one on its own (measured 2026-08-04, plan 34 re-measure: 0 px against the control on all three platforms; the android codegen does emit on the attribute's PRESENCE, which is a codegen-stage fact plan 41 tracks, not a rendered difference). borderStyle alone likewise renders nothing: it styles a border the width+color pair requests, it never summons one (2026-08-03 user ruling #2). NOTE: TextField.borderStyle is a DIFFERENT attribute (UIKit text-field chrome: roundedRect/line/bezel/none) and is outside this entry. The `__*_with_border` fixtures are the paired variants (borderStyle declared ALONGSIDE borderWidth + borderColor) that plan 49 added once three lanes had implemented the attribute with nothing measuring it — the lone-style fixtures above stay inert and keep testing the AND rule. `solid_with_border` is inert for a different reason from the lone ones: solid is the declared default of borderStyle, so stating it explicitly draws the same picture as omitting it (codegen_effect calls this class `value-is-default`). dashed/dotted are the two that must differ from the control. If the 3PF measurement comes back split rather than uniform, the honest record is a per-platform map like skinIdioms uses, not a forced uniform verdict: plan 34 caught ios emitting identical text for solid and dashed (C2/presence-only), and B/C/A implemented the three platforms separately in this wave. MEASURED 2026-08-05 (conformance-mobile run 30950551601 @ 5aada98), and the split the paragraph above anticipated is what happened: dashed and dotted are active on android and web and INERT on ios — the three borderStyle values render pixel-identical to each other and to the control on ios, which is the same symptom plan 34 recorded at the codegen stage (sjui emitting the same text for solid and dashed, C2/presence-only). They are therefore recorded as per-platform maps, not forced to a uniform verdict. This records today's reality; it does not bless it. If ios is supposed to draw a dashed border, the entry moves to uniformly-active once it does. solid_with_border stays uniformly-inert on all three because solid is borderStyle's declared default. RESOLVED 2026-08-05 on run 30990560599: ios draws both dashed and dotted, so the per-platform split is folded back to uniformly-active. The gap was never in sjui's codegen — that emitted StrokeStyle from dc35e82 — but in the SwiftUI dynamic renderer's applyBorder, which stroked with lineWidth only and so drew every style as solid. Recorded because the shape recurs: a codegen and a dynamic path disagreeing looks exactly like a platform not implementing an attribute.",
10
10
  "observable": {
11
11
  "common/borderWidth__static": "uniformly-inert",
12
+ "common/borderColor__static": "uniformly-inert",
12
13
  "common/borderStyle__solid": "uniformly-inert",
13
14
  "common/borderStyle__dashed": "uniformly-inert",
14
- "common/borderStyle__dotted": "uniformly-inert"
15
+ "common/borderStyle__dotted": "uniformly-inert",
16
+ "common/borderStyle__solid_with_border": "uniformly-inert",
17
+ "common/borderStyle__dashed_with_border": "uniformly-active",
18
+ "common/borderStyle__dotted_with_border": "uniformly-active"
19
+ }
20
+ },
21
+ "highlight": {
22
+ "highlightedAlone": "no-draw",
23
+ "highlightBackgroundAlone": "no-draw",
24
+ "ruling": "The highlighted appearance is drawn only when BOTH `highlighted` and a highlight colour are declared — the same pair-required shape as `border`, and ruled the same way. `highlighted: true` alone swaps the background to a colour that was never given, so nothing changes; `highlightBackground` alone names a colour for a state the view is never in. Every codegen already treats them as a pair (kjui modifier_builder.rb:490 combines highlight_cond with highlight_bg; sjui apply_highlighted_to_bag does the same), so a single-attribute fixture measures the absence of the companion rather than the converter. Established by measurement, not assumption: plan 41's paired probe carried View.highlighted as its one PROVISIONAL entry, and giving it `highlightBackground` made the ios/C0 and android/C0 findings disappear — the converters were correct and the fixture was missing the other half. This entry retires that PROVISIONAL: the pairing is now derived from the ledger like every other family (2026-08-05, plan 49-E, on plan 41's evidence). MEASURED 2026-08-05 (same run): web is active — A routed the highlight background through an inline style instead of a losing Tailwind class — while ios and android are still inert, so the verdict is recorded per platform. NOTE for anyone tidying this block: `common/highlightBackground__static` carries no control in the generated fixture set, so it is never compared and its verdict here is not checked against anything. It must NOT be deleted regardless — companions.py derives the pairing from this topic's `observable` KEYS, and a topic with fewer than two members stops producing companions at all. Same permanent-unverifiable note as networkImage's loadingImage applies to `common/highlightBackground__static`: rules.py scopes it to the paired form, so the bare fixture has no control and the entry can never be checked at the render stage. It stays because companions.py derives the View.highlighted pairing from these keys — deleting it would stop the pairing, not just the check. MEASURED 2026-08-06 on run 31068716186: android is now active (152,960 px) — the highlighted+highlightBackground pair landed on android; ios remains inert.",
25
+ "observable": {
26
+ "View/highlighted__true": {
27
+ "ios": "inert",
28
+ "android": "active",
29
+ "web": "active"
30
+ },
31
+ "common/highlightBackground__static": "uniformly-inert"
15
32
  }
16
33
  },
17
34
  "image": {
18
35
  "defaultContentMode": "fit",
19
36
  "fill": "stretch",
20
- "ruling": "2026-08-03 user rulings: (a) the default contentMode is fit on every platform — web's implicit cover default was drift (fixed in rjui, 22d96b4); (b) fill is the stretch (scaleToFill synonym) — AspectFill is the crop. All three platforms now implement the full mapping including the positional modes (unscaled + aligned + cropped; sjui/kjui/SwiftJsonUI/KotlinJsonUI, 33 wave). Observables: fit-valued fixtures equal the default rendering (uniformly inert); fill/scaleToFill differ from it everywhere (uniformly active). Verified on the 33 round-3 measurement.",
37
+ "ruling": "2026-08-03 user rulings: (a) the default contentMode is fit on every platform — web's implicit cover default was drift (fixed in rjui, 22d96b4); (b) fill is the stretch (scaleToFill synonym) — AspectFill is the crop. All three platforms now implement the full mapping including the positional modes (unscaled + aligned + cropped; sjui/kjui/SwiftJsonUI/KotlinJsonUI, 33 wave). Observables: fit-valued fixtures equal the default rendering (uniformly inert); fill/scaleToFill differ from it everywhere (uniformly active). Verified on the 33 round-3 measurement. REGRESSION on run 30990560599, recorded as measured and NOT as a semantics change: fill and scaleToFill went inert on ios between round 3 (44,640 px against the control) and round 4 (0 px), while the fixture declaration and the control image are byte-identical across the two runs — so the picture moved, not the probe. SwiftJsonUI fe74662 rewrote ImageViewConverter's declaredSize guard from `component.width/height` to `component.declaredWidth/declaredHeight`, and the stretch is applied through that size; it landed after round 3 and is in round 4. This entry is expected to fall back to uniformly-active once that is fixed — it is a progress meter, not a ruling that ios stretches differently. RESOLVED 2026-08-06 on run 31068716186 (SwiftJsonUI checkout 8b4c1b3): ios draws the stretch again — 44,640 px against the control, the exact round-3 magnitude — so both fill and scaleToFill fold back to uniformly-active, as the round-5 prediction registered. The per-platform split lived for exactly one round, which is what a progress meter is for.",
21
38
  "observable": {
22
39
  "Image/contentMode__fit": "uniformly-inert",
23
40
  "Image/contentMode__aspectfit": "uniformly-inert",
24
41
  "NetworkImage/contentMode__fit": "uniformly-inert",
25
42
  "NetworkImage/contentMode__aspectfit": "uniformly-inert",
26
- "Image/contentMode__fill": "uniformly-active",
27
- "Image/contentMode__scaletofill": "uniformly-active"
43
+ "Image/contentMode__fill": "uniformly-active"
28
44
  }
29
45
  },
30
46
  "textView": {
@@ -43,12 +59,19 @@
43
59
  },
44
60
  "networkImage": {
45
61
  "noSrc": "defaultImage",
46
- "ruling": "2026-08-03 user ruling: with no src, NetworkImage displays defaultImage; while loading it displays the first of hint/placeholder/loadingImage; on error it displays errorImage, falling back to defaultImage. Implemented on all platforms in the 33 wave (KotlinJsonUI defaultImage read + Coil fallback chain; ios placeholder no longer collapses over defaultImage; rjui defaultImage prop + template state machine). State-image fixtures use the DISTINCT conformance_sample_alt asset, so a hijacked no-src state renders visibly — the uniformly-inert observables verify the chain end-to-end.",
62
+ "ruling": "2026-08-03 user ruling: with no src, NetworkImage displays defaultImage; while loading it displays the first of hint/placeholder/loadingImage; on error it displays errorImage, falling back to defaultImage. Implemented on all platforms in the 33 wave (KotlinJsonUI defaultImage read + Coil fallback chain; ios placeholder no longer collapses over defaultImage; rjui defaultImage prop + template state machine). State-image fixtures use the DISTINCT conformance_sample_alt asset, so a hijacked no-src state renders visibly — the uniformly-inert observables verify the chain end-to-end. MEASURED 2026-08-05 (same run): placeholder is now active on all three — the conformance host never completes a network load, so the loading state persists and the placeholder is what the fixture shows; inert was the wrong expectation for it and the contract is updated. errorImage and loadingImage are active on android and inert on ios (they are out of scope on web), recorded per platform. OPEN QUESTION, deliberately not resolved by this entry: under the ruling above a state image should only appear in its own state, so android rendering a difference for errorImage — when the fixture never errors — reads like android showing a state image it should not, i.e. a defect rather than a contract. Recorded as measured because the gate cannot be satisfied by a ledger reason, but the android/ios split here needs adjudicating by the lanes that own those runtimes before it is treated as intended. NOTE on loadingImage's entry: rules.py deliberately gives that fixture no control (an in-flight image has no duration, so the loading face cannot be photographed however the fixture is shaped), which means the contract here is PERMANENTLY unverifiable at the render stage and will show up every run under \"could not be verified\". It is kept because the codegen probe still measures the spelling on both mobile converters — the entry states what the platforms should do, and the render stage simply is not the instrument that can check it. MEASURED 2026-08-06 on run 31068716186: the android/ios pair REVERSED — errorImage is now active on ios (631 px: ios draws the error face) and inert on android (0 px), and placeholder went inert on android in the same run. ios moving to active is the OPEN QUESTION's suspected defect being adjudicated by implementation; android going dark on BOTH state images at once reads as an android-side regression of the state-image chain, assigned to G+C (2026-08-06). placeholder's contract records the split as a progress meter and returns to uniformly-active when the android chain is fixed; errorImage's contract is deliberately left at its pre-run-5 value so the gate keeps a standing violation pointing at the open investigation — a contract edit would file the regression as intended. RESOLVED 2026-08-06 on run 31080217407: G's decisive-network interceptor made the error state deterministic, and errorImage measures ACTIVE on both mobile platforms (web stays out of scope) — the deliberately-stale contract folds to uniformly-active and the standing violation retires. placeholder's android:inert row REMAINS measured reality on run 6 and is consistent with the same interceptor: a load that now completes deterministically ends the loading state, so the loading face never shows on android — whether ios/web should match that is G+C's open half of #19, and the placeholder row stays a meter until they rule.",
47
63
  "observable": {
48
- "NetworkImage/placeholder__static": "uniformly-inert",
64
+ "NetworkImage/placeholder__static": {
65
+ "ios": "active",
66
+ "android": "inert",
67
+ "web": "active"
68
+ },
49
69
  "NetworkImage/hint__static": "uniformly-inert",
50
- "NetworkImage/errorImage__static": "uniformly-inert",
51
- "NetworkImage/loadingImage__static": "uniformly-inert"
70
+ "NetworkImage/errorImage__static": "uniformly-active",
71
+ "NetworkImage/loadingImage__static": {
72
+ "ios": "inert",
73
+ "android": "active"
74
+ }
52
75
  }
53
76
  },
54
77
  "margins": {
@@ -75,7 +98,7 @@
75
98
  },
76
99
  "skinIdioms": {
77
100
  "demarcation": "control chrome may differ per platform; the DIFFERENCE ITSELF is contract-pinned",
78
- "ruling": "2026-08-03 campaign adjudication: platform-owned control chrome is a legitimate difference, but only in its adjudicated SHAPE — the observable pins each accepted divergence as an expected-difference pattern, so both disappearing and mutating fail the gate. Members: Blur effectStyle (android material variants differ visibly, ios materials render alike on flat surfaces per the 31 saturation analysis, web CSS blur is single-variant), TextField.borderStyle (UIKit field chrome — each platform draws its own; web renders none for the UIKit-only bezel variants), TabView.showLabels (ios hides tab labels, android/web keep the labeled bar). The tabview glyph-source family is the codegen-side counterpart, pinned permanently in codegen_parity.json (2026-08-03 user ruling).",
101
+ "ruling": "2026-08-03 campaign adjudication: platform-owned control chrome is a legitimate difference, but only in its adjudicated SHAPE — the observable pins each accepted divergence as an expected-difference pattern, so both disappearing and mutating fail the gate. Members: Blur effectStyle (android material variants differ visibly, ios materials render alike on flat surfaces per the 31 saturation analysis, web CSS blur is single-variant), TextField.borderStyle (UIKit field chrome — each platform draws its own; web renders none for the UIKit-only bezel variants), TabView.showLabels WAS a member on the strength of a `__true` measurement — the DEFAULT value — and left the topic on 2026-08-05 when run 30990560599 measured both values on all three: `true` is inert everywhere (it is the default, so declaring it draws what omitting it draws) and `false` is active everywhere (ios 1491 px, android 8427, web 3019), with the two values distinguishable on every platform. There is no ios-only divergence to pin, so the entry moves to `__false` as a plain uniformly-active fact and the attribute is no longer a skin idiom. The original reading came from comparing the default value against its control, which cannot show what a non-default value does. The tabview glyph-source family is the codegen-side counterpart, pinned permanently in codegen_parity.json (2026-08-03 user ruling).",
79
102
  "observable": {
80
103
  "Blur/effectStyle__extralight": {
81
104
  "ios": "inert",
@@ -117,11 +140,7 @@
117
140
  "android": "active",
118
141
  "web": "inert"
119
142
  },
120
- "TabView/showLabels__true": {
121
- "ios": "active",
122
- "android": "inert",
123
- "web": "inert"
124
- }
143
+ "TabView/showLabels__false": "uniformly-active"
125
144
  }
126
145
  },
127
146
  "progressValue": {
@@ -132,6 +151,91 @@
132
151
  "observable": {
133
152
  "Progress/progress__static": "uniformly-active"
134
153
  }
154
+ },
155
+ "segmentLabelColors": {
156
+ "roleModel": "fontColor is the unselected label, selectedFontColor the selected one (falling back to fontColor), tintColor the SELECTED segment's accent — drawn as a background fill on ios/web and as the TabRow indicator on Compose",
157
+ "ruling": "2026-08-04 orchestrator adjudication (38, consumer field report): a Segment whose selected background is a brand colour had no way to declare a readable label — web hardcoded the unselected label and treated fontColor as the SELECTED label's fallback, so the name and the role were twisted, and neither spelling was declared for Segment at all. fontColor and selectedFontColor are now declared for Segment on every platform (the IconLabel.selectedFontColor precedent) and carry the roles above: rjui emits the unselected class from fontColor, sjui writes both UISegmentedControl title attributes (.normal and .selected), kjui fills contentColor / selectedContentColor. The swift-only normalColor and selectedColor keep resolving behind the new spellings — they are in live consumer use — and selectedBackground was dropped instead of declared: tintColor already means the selected background and a second spelling for it earns nothing. No activeness observable: the fixtures would need a selected-vs-unselected pair to separate the two roles, which the current single-value shape cannot express.",
158
+ "tintColorAlignment": "2026-08-04 orchestrator adjudication (42): android used to feed tintColor into the selected LABEL colour, so one declaration painted different things per platform — it now feeds the indicator, which is Compose's selected-state accent (its default is Configuration.Segment.defaultSelectedBackgroundColor). The rendering SHAPE stays platform-native: a filled segment on ios/web, a TabRow indicator on Compose; TabRow has no per-segment background and containerColor is the whole row. Measured before the change: of the two consumer screens declaring Segment colours, the only android one resolved its label colour from selectedColor (which precedes tintColor in the old chain), so no shipped screen changes colour — the missing accent simply appears. indicatorColor was an undeclared second spelling for the same slot and its read was dropped; normalColor / selectedColor became declared aliases of fontColor / selectedFontColor, so every converter reads canonical names only.",
159
+ "observable": {}
160
+ },
161
+ "boundSize": {
162
+ "unitModel": "a bound size attribute carries a runtime number in layout units: dp on android, points on ios, px on web",
163
+ "ruling": "2026-08-04 orchestrator adjudication (36, consumer field report): all six size attributes (width, height, minWidth, maxWidth, minHeight, maxHeight) declare \"binding\" in attribute_definitions, so a bound value MUST be honoured on every platform. kjui resolves it to `.height(data.v.dp)` (process_dimension) and sjui to `.frame(height: data.v)` (frame_helper is_binding?); rjui dropped it silently because the value reached the Tailwind arbitrary-value mapper, which returns '' for any string that is not a CSS unit — the declaration produced no class and no inline style, and jui build stayed warning-free. rjui now routes a bound dimension to the inline style as a px template literal, and treats it as an EXPLICIT size for the shrink and explicit-over-bounds rules above. No activeness observable: a bound fixture renders whatever the host viewmodel supplies, so fixture-vs-control cannot separate honouring from ignoring.",
164
+ "observable": {}
165
+ },
166
+ "distribution": {
167
+ "sizeValues": [
168
+ "fill",
169
+ "fillEqually"
170
+ ],
171
+ "gapValues": [
172
+ "equalSpacing",
173
+ "equalCentering"
174
+ ],
175
+ "spacingWins": "an explicit spacing pins the gap; distribution still governs size",
176
+ "ruling": "2026-08-05 (plan 49-E), raised by F/G and measured across all three toolchains. The vocabulary is UIStackView's, and its four values split into TWO KINDS that the current implementations conflate: `fill` and `fillEqually` distribute SIZE among the children, while `equalSpacing` and `equalCentering` distribute the FREE SPACE between them. fill = children grow to consume the axis, leaving no free space (proportional to their content size); fillEqually = every child gets the SAME size regardless of content; equalSpacing = equal gaps between adjacent children, with no leading or trailing gap; equalCentering = equal centre-to-centre distances. CONSEQUENCE, and this is the part every platform gets wrong today: the two size values are NOT an arrangement. They are `Modifier.weight(1f)` on each child in Compose, `.frame(maxWidth: .infinity)` (or equal weights) in SwiftUI, and `flex-1` in CSS — not Arrangement.* and not justify-content. Mapping `fill` to SpaceBetween/justify-between is precisely backwards: fill means there is no free space left to distribute. MEASURED STATE 2026-08-05 — all three disagree with the canon AND with each other, and each collapses a DIFFERENT pair of values into one output, so no fixture comparing two declared values can tell them apart: android (kjui container_component.rb:129-140) sends fillEqually and equalCentering both to Arrangement.SpaceEvenly; web (rjui view_converter.rb:184-195) sends the same pair both to justify-evenly; ios (sjui view_converter.rb:217,367,383) synthesises identical Spacer sets for equalSpacing and equalCentering, and gives fillEqually between-only spacers, which is space-between rather than equal sizes. `fill` emits nothing on ios (correct by accident) and a space-between arrangement on the other two (wrong). WHY NO GATE CAUGHT IT: --inert-complete asks whether a fixture differs from its CONTROL. Two declared values that both differ from the control but are identical to EACH OTHER pass that check, and android parity passes too because codegen and dynamic collapse them the same way. A value-vs-value comparison is the missing instrument — this is the 'both wrong in the same direction reads as green' shape. SPACING PRECEDENCE: `spacing` and `distribution` are not mutually exclusive and the current Compose behaviour (spacing != null wins outright, distribution never read) is wrong for the size values. An explicit `spacing` pins the GAP, so it overrides the gap that equalSpacing/equalCentering would compute; it says nothing about size, so fill/fillEqually still apply underneath it. Same shape as the size topic's explicit > bounds > fill ordering: the more specific declaration wins the axis it speaks about, and only that axis. No observable entries yet: the fixtures that would prove this need to compare two declared VALUES against each other rather than each against the control, which the current generator does not emit. That instrument is the follow-up this ruling asks for. PER-VALUE MAPPING added 2026-08-05 after A implemented the ruling on web (jsonui-cli 651403e) — recorded so B and C match that choice instead of re-deriving it, which is how the three drifted apart in the first place. The `perValueMapping` block above is the table; the two decisions worth calling out are that the SIZE values are carried to the CHILDREN (a per-child flex/weight instruction, not a container arrangement) and that equalCentering maps to SpaceAround / justify-around rather than SpaceEvenly — SpaceEvenly equalises the GAPS, which is equalSpacing's job, and collapsing the two is precisely the conflation this ruling exists to undo. web landed first and is the reference implementation; ios (sjui view_converter.rb, still synthesising Spacers for both gap values) and android (kjui container_component.rb:129, still mapping four values onto two Arrangements) are outstanding. CHILD SIZE PRECEDENCE, ruled 2026-08-05 after G measured android and found `fill` leaving 60dp unconsumed: an EXPLICIT width/height on the child wins over the size half of distribution, so `fill` and `fillEqually` do not override a declared child size. This is not a new principle — the `size` topic's 2026-08-03 campaign adjudication already ordered explicit > bounds > fill, and distribution's size half IS a fill instruction on the child's axis, so it sits at the bottom of that order like any other. It is also what `spacingWins` says one line up: the more specific declaration wins the axis it speaks about. CONSEQUENCE for the measurement, and the reason this had to be ruled rather than implemented: a fixture whose children declare `width: 40` cannot demonstrate `fill` at all — the 60dp G measured is the correct outcome, not a defect. The fixture has to omit the child dimension on the distribution axis for the attribute to have anything to do. That makes it the fourth case of a fixture shape hiding the attribute it is supposed to measure (after SafeAreaView's matchParent, alignTop/LeftOfView off-screen, and SelectBox with nothing to decorate). android's current implementation is correct as it stands; no three-platform change follows from this. FIRST VALUE-VS-VALUE MEASUREMENT 2026-08-05, on run 30972192150's artifacts once D's fixtures (0035321) and H's --value-discrimination gate (f54d3d5) existed, with control_diff.ignore_bands applied. It confirms the collapse this ruling was written from, platform by platform and by measurement rather than by reading converters: ios draws equalSpacing and equalCentering pixel-identically, android and web draw fillEqually and equalCentering pixel-identically. Three platforms, two different collapsed pairs, nobody agreeing — recorded in `collapsedPairs` so the fix can be checked off per platform. `fill` is ALSO inert against its control on ios while active on android and web, which is not a contradiction of explicitChildSizeWins: the fixture's children carry an explicit width, so no platform should be moving them, and the two that do are moving the TRACK rather than the child. The entry is per-platform for that reason and must not be flattened to uniformly-inert until the fixture drops the child width — at which point fill becomes measurable and this entry gets re-measured, not edited. RE-MEASURED on run 30990560599 after D removed the child width from the size-half fixtures (661bfba). web now distinguishes all four values — A's implementation is the first to satisfy the ruling end to end, and `collapsedPairs` loses its web row. ios still collapses equalSpacing with equalCentering and android now collapses fill with fillEqually (it had been fillEqually with equalCentering): the pair moved because the fixture changed, so android's remaining defect is in the SIZE half, where fill and fillEqually must differ — fill grows children from their content, fillEqually makes them equal regardless of it. `fill` stays inert on ios: with the child width gone the fixture can finally demonstrate it, and ios does not. Run 31068716186 (2026-08-06) measured android fill != fillEqually with both active against the control — C's 27b9b13 (codegen) and G's c7d2dfb (DistributionFillRow) closed the SIZE-half collapse, so the android collapsedPairs row is deleted BY MEASUREMENT, not by edit. The ios pair (equalSpacing == equalCentering) persists: run-5 geometry shows identical child positions and a 32 px gap for both values. Run 31080217407 (2026-08-06) measured ios distinguishing equalSpacing from equalCentering — B (b57e327/3dc3795) and F (e8f99c7) implemented the pinned iosGapConstruction, and the geometry is exactly the canon's: equalCentering shifts the first child right by the half-gap edge spacer (box_a 154 -> 168 px) while every child keeps its declared size (118 px). collapsedPairs is now EMPTY — all three platforms distinguish all four values, which is the state this ruling was written to reach. The empty map stays as the healthy-state marker, same convention as companions.py's emptied PROVISIONAL.",
177
+ "observable": {
178
+ "View/distribution__fill": {
179
+ "ios": "inert",
180
+ "android": "active",
181
+ "web": "active"
182
+ },
183
+ "View/distribution__fillequally": "uniformly-active",
184
+ "View/distribution__equalspacing": "uniformly-active",
185
+ "View/distribution__equalcentering": "uniformly-active"
186
+ },
187
+ "perValueMapping": {
188
+ "fill": {
189
+ "swiftui": ".frame(maxWidth/maxHeight: .infinity) on each child",
190
+ "compose": "grow from content — NOT expressible with Modifier.weight (weight(1f) IS fillEqually, the run-4 0px collapse; weight(1f, fill=false) packs by content and is inert). Dedicated measurement pass DistributionFillRow/Column in the base KotlinJsonUI library (c7d2dfb): content size plus an equal split of the leftover, one policy shared by the kjui codegen emit and the dynamic renderer. Device-verified: all 6 value pairs pairwise distinct, explicit child size never grown, spacing gaps pinned",
191
+ "web": "grow on each child"
192
+ },
193
+ "fillEqually": {
194
+ "swiftui": "equal frames on each child",
195
+ "compose": "Modifier.weight(1f) with a zero base on each child",
196
+ "web": "flex-1 min-w-0 min-h-0 on each child"
197
+ },
198
+ "equalSpacing": {
199
+ "swiftui": "Spacer between children only, flush ends (see iosGapConstruction)",
200
+ "compose": "Arrangement.SpaceBetween",
201
+ "web": "justify-between"
202
+ },
203
+ "equalCentering": {
204
+ "swiftui": "one Spacer at each end, two between each pair — the count ratio makes the ends half (see iosGapConstruction)",
205
+ "compose": "Arrangement.SpaceAround",
206
+ "web": "justify-around"
207
+ }
208
+ },
209
+ "iosGapConstruction": {
210
+ "equalSpacing": "[child] Spacer [child] Spacer [child] — between-children only, ends flush (justify-between)",
211
+ "equalCentering": "Spacer [child] Spacer Spacer [child] Spacer Spacer [child] Spacer — ONE spacer at each end, TWO between each pair; free space splits into 2(n-1)+2 equal units, so each end gap is exactly half a between gap (justify-around)",
212
+ "spacerMinLength": "every synthesised Spacer MUST be Spacer(minLength: 0) — SwiftUI's default minLength pollutes the computed gaps",
213
+ "spacingWins": "an explicit `spacing` pins the gap and suppresses this synthesis, same as the topic-level spacingWins",
214
+ "gravityStandsDown": "a declared gap distribution OWNS the free space on its axis: the gravity packing spacers (e.g. the leading spacer right-gravity emits) are NOT emitted alongside it — one extra packing spacer breaks the count ratio and drags equalSpacing toward equalCentering (F, e8f99c7, decision 1)",
215
+ "edgeSpacerGate": "equalCentering's edge spacers are suppressed ONLY on a wrapContent axis (declared size nil): with no free space a Spacer would expand the container and swallow the trailing padding (the measured round-3 bug). A FIXED numeric size has free space for the ratio and KEEPS its edge spacers — the older widthExpands gate (matchParent/-1/.infinity) was too narrow for it (F, e8f99c7, decision 2)",
216
+ "note": "Pinned 2026-08-06 (E's proposal, orchestrator-approved: implement this release, B codegen + F dynamic) so the two ios paths emit ONE counting instead of deriving Spacer arithmetic twice — two paths growing children separately is exactly what collapsed distribution's SIZE half before c7d2dfb. Spacer counting suffices because HStack/VStack split free space equally among unconstrained Spacers: the 1:2 end:between count ratio IS space-around's half-gap. No custom Layout, no measurement pass. Acceptance is already wired: when this lands, run 6 must delete collapsedPairs' ios row and turn the two value_discrimination receiving rows stale. Regression-test discipline from F's landing: a partial reversion (between 2->1 only) builds an equal-three-spacer structure the first sample points slipped past — pin BOTH sides of the computed boundary (55 in the 300pt fixture) so partial and full reversion both fail."
217
+ },
218
+ "explicitChildSizeWins": "an explicit child width/height beats distribution's size half — size topic's explicit > bounds > fill",
219
+ "collapsedPairs": {}
220
+ },
221
+ "trackColors": {
222
+ "sliderTrack": "trackTintColor is the UNFILLED track; progressTintColor is the filled one",
223
+ "switchTrack": "trackTintColor is the OFF track; onTintColor overrides it while on",
224
+ "notTheView": "neither paints the view's background",
225
+ "ruling": "2026-08-06 (plan 49-E), asked for by A after ios parity showed three implementations doing three different things. Ruled from the ones that already agree rather than from the name: Compose and web BOTH read trackTintColor as the unfilled Slider track (kjui slider_component.rb:102, rjui slider_converter.rb:85 -> ::-webkit-slider-runnable-track) and BOTH read it as the OFF Switch track with onTintColor winning while on (kjui switch_component.rb:99, rjui switch_converter.rb:30). sjui's SwiftUI Slider says the same thing in its comment. So: Slider = the unfilled side only, the filled side is progressTintColor, and tintColor stays the accent that colours the thumb and the active track when nothing more specific is declared. Switch = the OFF track, and onTintColor is not an alias of it but the state that overrides it. The view's BACKGROUND is wrong on both, and that is what the codegen emits — `.background(color)` paints the whole control, so a declared track colour swallows the component. It is also why the two ios parity findings exist: dynamic reaches the track, codegen reaches the view. RECORDED HERE BECAUSE IT WAS ALREADY CITED: kjui and rjui both carry comments pointing at `slider.trackColors` / `switch.trackColors` in this file, and neither entry existed — the ruling was referenced by three converters and written down nowhere, which is why three implementations could drift while each believed it was following the canon. IOS CONSTRAINT, measured on run 30990560599 and recorded 2026-08-06 after A implemented the ruling: SwiftUI's Toggle exposes no surface for the OFF track, so both ios paths degrade to .tint(), which is visible only while the switch is ON. The fixture declares no isOn and photographs the OFF state, so ios renders identically to its control (0 px) while android and web paint the OFF track (6788 / 909 px) — a platform constraint of the skinIdioms family (platform-owned control chrome), NOT implementation drift and NOT a progress meter: unlike the per-platform entries that fall when a fix lands, this split is expected to persist until Apple exposes the surface. Recorded here rather than in skinIdioms so the ruling and its one platform exception stay on the same page; recorded here rather than in inert_audit because the verdict is attributed, not unexplained. Slider's entry is uniformly-active by the same run's measurement (3067 / 14442 / 16236 px): the unfilled slider track IS paintable on ios.",
226
+ "observable": {
227
+ "Slider/trackTintColor__static": "uniformly-active",
228
+ "Switch/trackTintColor__static": {
229
+ "ios": "inert",
230
+ "android": "active",
231
+ "web": "active"
232
+ }
233
+ },
234
+ "iosSwitchConstraint": "SwiftUI's Toggle has no paintable OFF-track surface; both ios paths degrade to .tint(), visible only while ON"
235
+ },
236
+ "fontProvider": {
237
+ "ruling": "Configuration.Font.fontProvider is the app's typeface-policy injection point, and the presence of `fontFamily` must NOT change whether it is consulted: every font resolution routes through the provider as FontSpec(family:, weight:, size:), family nil included. A path that bypasses the provider when family is absent makes the same attribute on the same screen pass or skip the app's policy depending on mode — the checked/unchecked shape this wave kept finding. The codegen path is the conforming side; SwiftJsonUI's dynamic path (DynamicDecodingHelper.fontFromComponent) is being aligned to it with a regression test (F, plan 49). A provider that ignores family nil sees no change; one that returns a custom face was silently falling back to the system font on dynamic — either way the contract converges on: the typeface does not depend on mode. Preserving a consumer's current look is not a reason to keep the bypass (canonical semantics win). 2026-08-06 ruling.",
238
+ "contract": "family presence must not gate provider consultation — every implementer, current or future, routes every FontSpec through fontProvider"
135
239
  }
136
240
  }
137
241
  }
@@ -117,7 +117,12 @@
117
117
  },
118
118
  "Image": {
119
119
  "description": "Local image component",
120
- "aliases": [],
120
+ "aliases": [
121
+ "CircleImage",
122
+ "CircleImageView",
123
+ "ImageView",
124
+ "Img"
125
+ ],
121
126
  "platforms": {
122
127
  "swift_generated": true,
123
128
  "swift_dynamic": true,
@@ -233,7 +238,11 @@
233
238
  },
234
239
  "Collection": {
235
240
  "description": "List/grid collection with data binding",
236
- "aliases": [],
241
+ "aliases": [
242
+ "Table",
243
+ "TableView",
244
+ "RecyclerView"
245
+ ],
237
246
  "platforms": {
238
247
  "swift_generated": true,
239
248
  "swift_dynamic": true,
@@ -2,33 +2,6 @@
2
2
  "schemaVersion": 1,
3
3
  "_comment": "Declared attributes no platform converter reads. Hand-maintained: close a gap by implementing it (then delete the entry) or by narrowing platform/mode in attribute_definitions.json. reason is one of: dynamic-key, legacy, platform-na, runtime-only, unimplemented.",
4
4
  "entries": [
5
- {
6
- "component": "Button",
7
- "attribute": "hilightColor",
8
- "platforms": [
9
- "web"
10
- ],
11
- "reason": "legacy",
12
- "note": "alias — normalized to its canonical spelling before conversion"
13
- },
14
- {
15
- "component": "Collection",
16
- "attribute": "onPageChanged",
17
- "platforms": [
18
- "web"
19
- ],
20
- "reason": "legacy",
21
- "note": "alias — normalized to its canonical spelling before conversion"
22
- },
23
- {
24
- "component": "Radio",
25
- "attribute": "selected_icon",
26
- "platforms": [
27
- "android"
28
- ],
29
- "reason": "legacy",
30
- "note": "alias — normalized to its canonical spelling before conversion"
31
- },
32
5
  {
33
6
  "component": "ScrollView",
34
7
  "attribute": "minZoom",
@@ -38,77 +11,14 @@
38
11
  "reason": "unimplemented",
39
12
  "note": "codegen 未対応。ランタイム側に実装あり (ios: UIKit) — この検査は Ruby codegen しか走査しないので、実装の有無ではなく codegen 対応の有無を表す"
40
13
  },
41
- {
42
- "component": "Slider",
43
- "attribute": "maxValue",
44
- "platforms": [
45
- "web"
46
- ],
47
- "reason": "legacy",
48
- "note": "alias — normalized to its canonical spelling before conversion"
49
- },
50
- {
51
- "component": "Slider",
52
- "attribute": "minValue",
53
- "platforms": [
54
- "web"
55
- ],
56
- "reason": "legacy",
57
- "note": "alias — normalized to its canonical spelling before conversion"
58
- },
59
- {
60
- "component": "Switch",
61
- "attribute": "onToggle",
62
- "platforms": [
63
- "web"
64
- ],
65
- "reason": "legacy",
66
- "note": "alias — normalized to its canonical spelling before conversion"
67
- },
68
- {
69
- "component": "TextField",
70
- "attribute": "accessoryBackground",
71
- "platforms": [
72
- "ios"
73
- ],
74
- "reason": "unimplemented",
75
- "note": "codegen 未対応。ランタイム側に実装あり (ios: UIKit) — この検査は Ruby codegen しか走査しないので、実装の有無ではなく codegen 対応の有無を表す"
76
- },
77
- {
78
- "component": "TextField",
79
- "attribute": "accessoryTextColor",
80
- "platforms": [
81
- "ios"
82
- ],
83
- "reason": "unimplemented",
84
- "note": "codegen 未対応。ランタイム側に実装あり (ios: UIKit) — この検査は Ruby codegen しか走査しないので、実装の有無ではなく codegen 対応の有無を表す"
85
- },
86
- {
87
- "component": "TextField",
88
- "attribute": "doneText",
89
- "platforms": [
90
- "ios"
91
- ],
92
- "reason": "unimplemented",
93
- "note": "codegen 未対応。ランタイム側に実装あり (ios: UIKit) — この検査は Ruby codegen しか走査しないので、実装の有無ではなく codegen 対応の有無を表す"
94
- },
95
- {
96
- "component": "View",
97
- "attribute": "direction",
98
- "platforms": [
99
- "ios"
100
- ],
101
- "reason": "unimplemented",
102
- "note": "codegen 未対応。ランタイム側に実装あり (ios: UIKit) — この検査は Ruby codegen しか走査しないので、実装の有無ではなく codegen 対応の有無を表す"
103
- },
104
14
  {
105
15
  "component": "common",
106
- "attribute": "alpha",
16
+ "attribute": "binding",
107
17
  "platforms": [
108
- "web"
18
+ "ios"
109
19
  ],
110
20
  "reason": "legacy",
111
- "note": "alias — normalized to its canonical spelling before conversion"
21
+ "note": "UIKit-only KVC binding (SJUIViewCreator sets view.binding / bindingSet). The last Ruby reader was sjui's table_converter.rb, folded into the Collection route when 49-E ruled Table is a Collection — so this became unread by that ruling, not by anything failing. The declaration says it is superseded by `@{...}` in the attribute value itself; the UIKit runtime still honours it for layouts that predate that, which is why it is not deleted. Reason `legacy` rather than `unimplemented`: nothing is missing."
112
22
  }
113
23
  ]
114
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jui-tools-mcp-server",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "MCP server for JsonUI project management - wraps jui and jsonui-doc CLIs plus component spec lookup",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -44,7 +44,7 @@ const DATA_DIR = join(PROJECT_ROOT, "data");
44
44
  // 3. `npm run fetch-definitions` and commit the data/ diff together with
45
45
  // this pin in one commit
46
46
  // JSONUI_CLI_RAW_BASE still overrides the whole base URL for manual testing.
47
- const JSONUI_CLI_TAG = "v1.4.0";
47
+ const JSONUI_CLI_TAG = "v1.4.1";
48
48
  const BASE_URL =
49
49
  process.env.JSONUI_CLI_RAW_BASE ||
50
50
  `https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/${JSONUI_CLI_TAG}`;