@onekeyfe/react-native-native-list 6.7.0-alpha.378 → 6.7.0-alpha.383

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.
Files changed (116) hide show
  1. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
  5. package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
  6. package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
  8. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
  12. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
  16. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
  18. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
  19. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
  20. package/docs/ROW_TEMPLATES.md +60 -9
  21. package/docs/SPEC.md +450 -5
  22. package/docs/STYLE_SPEC.md +6 -0
  23. package/ios/HybridNativeList.swift +21 -2
  24. package/ios/NativeListAccessoryStack.swift +227 -39
  25. package/ios/NativeListActivityDetailsView.swift +18 -8
  26. package/ios/NativeListActivityRenderer.swift +8 -2
  27. package/ios/NativeListButtonGroupRenderer.swift +89 -0
  28. package/ios/NativeListDataRowRenderer.swift +155 -27
  29. package/ios/NativeListDesignAssets.swift +3 -0
  30. package/ios/NativeListIdentityRenderer.swift +166 -30
  31. package/ios/NativeListLeadingVisual.swift +49 -14
  32. package/ios/NativeListMessageRenderer.swift +34 -1
  33. package/ios/NativeListModels.swift +42 -2
  34. package/ios/NativeListRendererRegistry.swift +1 -0
  35. package/ios/NativeListResolvedText.swift +2 -1
  36. package/ios/NativeListRowHost.swift +14 -0
  37. package/ios/NativeListSectionHeaderRenderer.swift +88 -0
  38. package/ios/RNCNativeListView.swift +1013 -55
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
  45. package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
  46. package/ios/tests/NativeListLifecycleTests.swift +2720 -3
  47. package/lib/module/NativeList.js +175 -28
  48. package/lib/module/NativeList.web.js +129 -30
  49. package/lib/module/index.js +1 -1
  50. package/lib/module/models.js +1 -0
  51. package/lib/module/selection.js +1 -1
  52. package/lib/module/validation.js +421 -8
  53. package/lib/module/web/NativeListWebEngine.js +507 -19
  54. package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
  55. package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
  56. package/lib/module/web/templates/DataRowRenderer.js +40 -5
  57. package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
  58. package/lib/module/web/templates/MessageRowRenderer.js +18 -2
  59. package/lib/module/web/templates/RowContainerStyle.js +1 -0
  60. package/lib/module/web/templates/RowRendererRegistry.js +3 -0
  61. package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
  62. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  63. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  64. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  65. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  66. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  67. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  68. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  69. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  70. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  71. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  72. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  73. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  74. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  75. package/lib/typescript/src/NativeList.d.ts +1 -1
  76. package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
  77. package/lib/typescript/src/NativeList.types.d.ts +18 -0
  78. package/lib/typescript/src/index.d.ts +2 -2
  79. package/lib/typescript/src/models.d.ts +105 -9
  80. package/lib/typescript/src/validation.d.ts +12 -0
  81. package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
  82. package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
  83. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
  84. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
  85. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  86. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  87. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  89. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  90. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  91. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  92. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  93. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  94. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  95. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  96. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  97. package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  98. package/package.json +3 -3
  99. package/react-native-native-list.podspec +1 -0
  100. package/src/NativeList.nitro.ts +2 -0
  101. package/src/NativeList.tsx +313 -44
  102. package/src/NativeList.types.ts +22 -0
  103. package/src/NativeList.web.tsx +204 -48
  104. package/src/index.ts +2 -0
  105. package/src/models.ts +117 -3
  106. package/src/selection.ts +1 -0
  107. package/src/validation.ts +852 -6
  108. package/src/web/NativeListWebEngine.ts +803 -20
  109. package/src/web/templates/ActivityRowRenderer.ts +14 -6
  110. package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
  111. package/src/web/templates/DataRowRenderer.ts +93 -20
  112. package/src/web/templates/IdentityRowRenderer.ts +150 -5
  113. package/src/web/templates/MessageRowRenderer.ts +23 -7
  114. package/src/web/templates/RowContainerStyle.ts +2 -0
  115. package/src/web/templates/RowRendererRegistry.ts +11 -1
  116. package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
@@ -167,10 +167,13 @@ const TEXT_STYLE_KEYS_BY_ROW_TYPE = {
167
167
  metricCard: ['title', 'value', 'subtitle', 'trend'],
168
168
  sectionHeader: ['title', 'subtitle', 'value'],
169
169
  action: ['title', 'value'],
170
+ buttonGroup: [],
170
171
  system: ['title', 'message', 'actionText']
171
172
  };
172
173
  const EXTRA_STYLE_KEYS_BY_ROW_TYPE = {
173
- market: ['titleBadgeLayout', 'contentTrailingGap', 'subtitleTrailingPadding', 'changeWidth', 'changeHeight', 'changeCornerRadius']
174
+ identity: ['leadingContentMaxWidthRatio'],
175
+ market: ['titleBadgeLayout', 'contentTrailingGap', 'subtitleTrailingPadding', 'changeWidth', 'changeHeight', 'changeCornerRadius'],
176
+ sectionHeader: ['valuePlacement', 'titleDecoration']
174
177
  };
175
178
  function assertTextStyle(style, path, additionalKeys = []) {
176
179
  if (style === undefined) return;
@@ -283,6 +286,15 @@ function assertRowStyle(row, path) {
283
286
  // Market keeps its own richer assertions, driven from assertMarketRow.
284
287
  if (row.type === 'market') return;
285
288
  assertBoxStyle(style, `${path}.style`);
289
+ if (row.type === 'identity') {
290
+ const ratio = style?.leadingContentMaxWidthRatio;
291
+ if (ratio !== undefined && (!Number.isFinite(ratio) || ratio <= 0 || ratio > 1)) fail(`${path}.style.leadingContentMaxWidthRatio`, 'must be within (0, 1]');
292
+ }
293
+ if (row.type === 'sectionHeader') {
294
+ const sectionStyle = style;
295
+ if (sectionStyle?.valuePlacement !== undefined && sectionStyle.valuePlacement !== 'afterTitle') fail(`${path}.style.valuePlacement`, 'must be afterTitle');
296
+ if (sectionStyle?.titleDecoration !== undefined && !['none', 'dashed'].includes(sectionStyle.titleDecoration)) fail(`${path}.style.titleDecoration`, 'must be none or dashed');
297
+ }
286
298
  const slots = style;
287
299
  textKeys.forEach(key => assertTextStyle(slots[key], `${path}.style.${key}`));
288
300
  }
@@ -572,15 +584,88 @@ function assertVisualShape(shape, path) {
572
584
  function assertTrailingAccessories(accessories, path) {
573
585
  accessories?.forEach((accessory, index) => {
574
586
  const accessoryPath = `${path}[${index}]`;
587
+ if (accessory.kind === 'button') {
588
+ assertUnknownKeys(accessory, ['kind', 'label', 'actionKey', 'disabled', 'testID', 'accessibilityLabel', 'textColor', 'backgroundColor', 'pressedBackgroundColor', 'hoverBackgroundColor', 'focusRingColor'], accessoryPath, 'button key');
589
+ if (typeof accessory.label !== 'string' || !accessory.label.trim()) fail(`${accessoryPath}.label`, 'must be non-blank');
590
+ assertKey(accessory.actionKey, `${accessoryPath}.actionKey`);
591
+ if (accessory.disabled !== undefined && typeof accessory.disabled !== 'boolean') fail(`${accessoryPath}.disabled`, 'must be boolean');
592
+ for (const field of ['textColor', 'backgroundColor', 'pressedBackgroundColor', 'hoverBackgroundColor', 'focusRingColor']) {
593
+ const value = accessory[field];
594
+ if (value !== undefined && (typeof value !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value))) fail(`${accessoryPath}.${field}`, 'must be #RRGGBB or #RRGGBBAA');
595
+ }
596
+ }
597
+ if (accessory.kind === 'icon' && accessory.buttonStyle !== undefined && accessory.buttonStyle !== 'tertiary') fail(`${accessoryPath}.buttonStyle`, 'must be tertiary');
575
598
  if (accessory.kind === 'progress' && (accessory.value < 0 || accessory.value > 1)) {
576
599
  fail(`${accessoryPath}.value`, 'must be within 0...1');
577
600
  }
578
601
  if (accessory.kind === 'valuePair') {
602
+ if (accessory.primaryFontWeight !== undefined && accessory.primaryFontWeight !== 'medium') fail(`${accessoryPath}.primaryFontWeight`, 'must be medium when provided');
603
+ assertTextHelp(accessory.primaryHelp, `${accessoryPath}.primaryHelp`);
579
604
  assertTextTone(accessory.primaryTone, `${accessoryPath}.primaryTone`);
580
605
  assertTextTone(accessory.secondaryTone, `${accessoryPath}.secondaryTone`);
606
+ for (const field of ['primaryTextSegments', 'secondaryTextSegments']) {
607
+ const segments = accessory[field];
608
+ if (segments === undefined) continue;
609
+ const segmentPath = `${accessoryPath}.${field}`;
610
+ if (!Array.isArray(segments) || segments.length > 64) fail(segmentPath, 'must be an array of at most 64 segments');
611
+ let textLength = 0;
612
+ segments.forEach((segment, segmentIndex) => {
613
+ const path = `${segmentPath}[${segmentIndex}]`;
614
+ const entry = protocolObject(segment, path);
615
+ assertUnknownKeys(entry, ['text', 'style'], path, 'segment key');
616
+ if (typeof entry.text !== 'string' || entry.text.length > MAX_TEXT_LENGTH) fail(`${path}.text`, `must be a string no longer than ${MAX_TEXT_LENGTH} characters`);
617
+ if (entry.style !== undefined && entry.style !== 'subscript') fail(`${path}.style`, 'must be subscript when provided');
618
+ textLength += entry.text.length;
619
+ });
620
+ if (textLength > MAX_TEXT_LENGTH) fail(segmentPath, `total text must be at most ${MAX_TEXT_LENGTH} characters`);
621
+ }
622
+ }
623
+ });
624
+ }
625
+ function assertTextHelp(help, path) {
626
+ if (help === undefined) return;
627
+ const object = protocolObject(help, path);
628
+ assertUnknownKeys(object, ['actionKey', 'presentation', 'underline'], path, 'help key');
629
+ assertKey(help.actionKey, `${path}.actionKey`);
630
+ if (!['tooltip', 'popover'].includes(help.presentation)) fail(`${path}.presentation`, 'must be tooltip or popover');
631
+ if (help.underline !== undefined && help.underline !== 'dashed') fail(`${path}.underline`, 'must be dashed');
632
+ }
633
+ function assertButtonGroup(row, path) {
634
+ if (!Array.isArray(row.buttons) || row.buttons.length === 0) fail(`${path}.buttons`, 'must contain at least one button');
635
+ const keys = new Set();
636
+ row.buttons.forEach((button, index) => {
637
+ const itemPath = `${path}.buttons[${index}]`;
638
+ const object = protocolObject(button, itemPath);
639
+ assertUnknownKeys(object, ['key', 'label', 'actionKey', 'disabled', 'loading', 'testID', 'accessibilityLabel'], itemPath, 'button key');
640
+ assertKey(button.key, `${itemPath}.key`);
641
+ if (keys.has(button.key)) fail(`${itemPath}.key`, 'duplicate button key');
642
+ keys.add(button.key);
643
+ if (typeof button.label !== 'string' || !button.label.trim() || button.label.length > MAX_TEXT_LENGTH) fail(`${itemPath}.label`, 'must be non-blank bounded text');
644
+ assertKey(button.actionKey, `${itemPath}.actionKey`);
645
+ for (const field of ['disabled', 'loading']) {
646
+ if (button[field] !== undefined && typeof button[field] !== 'boolean') fail(`${itemPath}.${field}`, 'must be boolean');
647
+ }
648
+ for (const field of ['testID', 'accessibilityLabel']) {
649
+ if (button[field] !== undefined && typeof button[field] !== 'string') fail(`${itemPath}.${field}`, 'must be a string');
581
650
  }
582
651
  });
583
652
  }
653
+ function assertIdentityExtras(row, path) {
654
+ for (const field of ['titleIcon', 'secondaryTitleIcon']) {
655
+ const titleIcon = row[field];
656
+ if (titleIcon === undefined) continue;
657
+ const iconPath = `${path}.${field}`;
658
+ const icon = protocolObject(titleIcon, iconPath);
659
+ assertUnknownKeys(icon, ['name', 'tintColor', 'accessibilityLabel', 'hoverActionKey'], iconPath, 'title icon key');
660
+ if (typeof titleIcon.name !== 'string' || !titleIcon.name.trim()) fail(`${iconPath}.name`, 'must be non-blank');
661
+ if (titleIcon.tintColor !== undefined && (typeof titleIcon.tintColor !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(titleIcon.tintColor))) fail(`${iconPath}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
662
+ if (titleIcon.accessibilityLabel !== undefined && typeof titleIcon.accessibilityLabel !== 'string') fail(`${iconPath}.accessibilityLabel`, 'must be a string');
663
+ if (titleIcon.hoverActionKey !== undefined) assertKey(titleIcon.hoverActionKey, `${iconPath}.hoverActionKey`);
664
+ }
665
+ const ratio = row.style?.leadingContentMaxWidthRatio;
666
+ if (ratio !== undefined && (typeof ratio !== 'number' || !Number.isFinite(ratio) || ratio <= 0 || ratio > 1)) fail(`${path}.style.leadingContentMaxWidthRatio`, 'must be within (0, 1]');
667
+ assertTrailingAccessories(row.trailing, `${path}.trailing`);
668
+ }
584
669
  function assertImage(image, path) {
585
670
  if (!image) return;
586
671
  if (!image.uri.trim()) fail(`${path}.uri`, 'must not be empty');
@@ -625,6 +710,9 @@ function assertLeadingVisual(visual, path) {
625
710
  assertText(visual.cornerIcon?.name, `${path}.cornerIcon.name`);
626
711
  // OneKey patch: validate optional selector overlays at the JSON boundary.
627
712
  assertText(visual.fallbackIcon?.name, `${path}.fallbackIcon.name`);
713
+ if (visual.fallbackTextColor !== undefined && (typeof visual.fallbackTextColor !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(visual.fallbackTextColor))) fail(`${path}.fallbackTextColor`, 'must be a hex color');
714
+ if (visual.fallbackIcon?.size !== undefined && (!Number.isFinite(visual.fallbackIcon.size) || visual.fallbackIcon.size <= 0 || visual.fallbackIcon.size > 40)) fail(`${path}.fallbackIcon.size`, 'must be within (0, 40]');
715
+ if (visual.fallbackIcon?.backgroundColor !== undefined && !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(visual.fallbackIcon.backgroundColor)) fail(`${path}.fallbackIcon.backgroundColor`, 'must be a hex color');
628
716
  if ((visual.overlays?.length ?? 0) > 2) fail(`${path}.overlays`, 'supports at most two overlays');
629
717
  visual.overlays?.forEach((overlay, index) => {
630
718
  if (!['topLeft', 'bottomRight'].includes(overlay.position)) fail(`${path}.overlays[${index}].position`, 'must be topLeft or bottomRight');
@@ -641,7 +729,11 @@ function assertLeadingVisual(visual, path) {
641
729
  }
642
730
  if (overlay.padding !== undefined && overlay.padding * 2 >= Math.min(overlay.width ?? overlay.size ?? 20, overlay.height ?? overlay.size ?? 20)) fail(`${path}.overlays[${index}].padding`, 'must fit inside the overlay');
643
731
  assertImage(overlay.image, `${path}.overlays[${index}].image`);
732
+ if (overlay.fallbackIcon !== undefined && !overlay.image) fail(`${path}.overlays[${index}].fallbackIcon`, 'requires image');
733
+ if (overlay.fallbackIcon !== undefined && (typeof overlay.fallbackIcon.name !== 'string' || !overlay.fallbackIcon.name.trim())) fail(`${path}.overlays[${index}].fallbackIcon.name`, 'must be non-blank');
734
+ if (overlay.fallbackIcon?.tintColor !== undefined && !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(overlay.fallbackIcon.tintColor)) fail(`${path}.overlays[${index}].fallbackIcon.tintColor`, 'must be a hex color');
644
735
  assertText(overlay.text, `${path}.overlays[${index}].text`);
736
+ if (overlay.fontWeight !== undefined && !['regular', 'medium', 'semibold', 'bold'].includes(overlay.fontWeight)) fail(`${path}.overlays[${index}].fontWeight`, 'must be regular, medium, semibold, or bold');
645
737
  });
646
738
  if (visual.kind === 'token') {
647
739
  assertImage(visual.networkImage, `${path}.networkImage`);
@@ -655,6 +747,7 @@ function assertVisual(row, path) {
655
747
  });
656
748
  }
657
749
  function assertActivity(row, path) {
750
+ if (row.hideLeading !== undefined && typeof row.hideLeading !== 'boolean') fail(`${path}.hideLeading`, 'must be boolean');
658
751
  assertText(row.title, `${path}.title`);
659
752
  assertText(row.description, `${path}.description`);
660
753
  if (row.presentation !== undefined && !['stacked', 'table'].includes(row.presentation)) fail(`${path}.presentation`, 'must be stacked or table');
@@ -751,10 +844,26 @@ function assertRow(row, index, path = `rows[${index}]`) {
751
844
  assertWalletGroup(row, path);
752
845
  break;
753
846
  case 'identity':
847
+ if (row.contentHeight !== undefined && row.contentHeight !== 'auto') {
848
+ fail(`${path}.contentHeight`, 'must be auto');
849
+ }
850
+ if (row.subtitleLines !== undefined && ![0, 1, 2].includes(row.subtitleLines)) {
851
+ fail(`${path}.subtitleLines`, 'must be 0, 1, or 2');
852
+ }
853
+ if (row.longPressActionKey !== undefined) {
854
+ assertKey(row.longPressActionKey, `${path}.longPressActionKey`);
855
+ }
754
856
  if (row.presentation !== undefined && row.presentation !== 'walletSidebar' && row.presentation !== 'accountSelector' && row.presentation !== 'networkSelector') {
755
857
  fail(`${path}.presentation`, 'must be walletSidebar, accountSelector, or networkSelector when provided');
756
858
  }
757
859
  assertText(row.title, `${path}.title`);
860
+ if ((row.titleSuffixes?.length ?? 0) > 3) fail(`${path}.titleSuffixes`, 'supports at most 3 suffixes');
861
+ row.titleSuffixes?.forEach((suffix, index) => {
862
+ assertText(suffix.text, `${path}.titleSuffixes[${index}].text`);
863
+ if (!['secondary', 'disabled'].includes(suffix.tone)) fail(`${path}.titleSuffixes[${index}].tone`, 'must be secondary or disabled');
864
+ if (suffix.maxWidth !== undefined && (!Number.isFinite(suffix.maxWidth) || suffix.maxWidth < 0 || suffix.maxWidth > 4096)) fail(`${path}.titleSuffixes[${index}].maxWidth`, 'must be finite within 0...4096');
865
+ });
866
+ assertIdentityExtras(row, path);
758
867
  assertText(row.subtitle, `${path}.subtitle`);
759
868
  // OneKey patch: selector text segments truncate independently.
760
869
  row.subtitleSegments?.forEach((segment, segmentIndex) => {
@@ -786,10 +895,13 @@ function assertRow(row, index, path = `rows[${index}]`) {
786
895
  assertActivity(row, path);
787
896
  break;
788
897
  case 'message':
898
+ if (row.presentation !== undefined && row.presentation !== 'inlineWarning') {
899
+ fail(`${path}.presentation`, 'must be inlineWarning');
900
+ }
789
901
  assertText(row.title, `${path}.title`);
790
902
  assertText(row.body, `${path}.body`);
791
- if (![1, 2, 3].includes(row.bodyLines ?? 3)) {
792
- fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
903
+ if (![0, 1, 2, 3].includes(row.bodyLines ?? 3)) {
904
+ fail(`${path}.bodyLines`, 'must be 0, 1, 2, or 3');
793
905
  }
794
906
  assertImage(row.thumbnail, `${path}.thumbnail`);
795
907
  break;
@@ -800,6 +912,13 @@ function assertRow(row, index, path = `rows[${index}]`) {
800
912
  row.columns.forEach((column, columnIndex) => {
801
913
  assertKey(column.key, `${path}.columns[${columnIndex}].key`);
802
914
  assertText(column.text, `${path}.columns[${columnIndex}].text`);
915
+ assertIdentityExtras(column, `${path}.columns[${columnIndex}]`);
916
+ for (const field of ['textSegments', 'secondaryTextSegments']) {
917
+ column[field]?.forEach((segment, segmentIndex) => {
918
+ assertText(segment.text, `${path}.columns[${columnIndex}].${field}[${segmentIndex}].text`);
919
+ if (segment.style !== undefined && segment.style !== 'subscript') fail(`${path}.columns[${columnIndex}].${field}[${segmentIndex}].style`, 'must be subscript');
920
+ });
921
+ }
803
922
  assertText(column.secondaryLeadingText, `${path}.columns[${columnIndex}].secondaryLeadingText`);
804
923
  assertText(column.secondaryText, `${path}.columns[${columnIndex}].secondaryText`);
805
924
  assertTextTone(column.secondaryTone, `${path}.columns[${columnIndex}].secondaryTone`);
@@ -807,6 +926,10 @@ function assertRow(row, index, path = `rows[${index}]`) {
807
926
  if ((row.badges?.length ?? 0) > MAX_BADGES) {
808
927
  fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
809
928
  }
929
+ if ((row.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
930
+ fail(`${path}.trailing`, `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`);
931
+ }
932
+ assertTrailingAccessories(row.trailing, `${path}.trailing`);
810
933
  break;
811
934
  case 'market':
812
935
  assertMarketRow(row, path);
@@ -857,6 +980,11 @@ function assertRow(row, index, path = `rows[${index}]`) {
857
980
  assertText(row.title, `${path}.title`);
858
981
  assertText(row.subtitle, `${path}.subtitle`);
859
982
  assertText(row.value, `${path}.value`);
983
+ if (row.leadingBadge) {
984
+ assertKey(row.leadingBadge.key, `${path}.leadingBadge.key`);
985
+ assertText(row.leadingBadge.text, `${path}.leadingBadge.text`);
986
+ }
987
+ assertTextHelp(row.valueHelp, `${path}.valueHelp`);
860
988
  if (row.valueActionKey !== undefined) {
861
989
  assertKey(row.valueActionKey, `${path}.valueActionKey`);
862
990
  }
@@ -877,6 +1005,9 @@ function assertRow(row, index, path = `rows[${index}]`) {
877
1005
  }
878
1006
  assertTrailingAccessories(row.trailing, `${path}.trailing`);
879
1007
  break;
1008
+ case 'buttonGroup':
1009
+ assertButtonGroup(row, path);
1010
+ break;
880
1011
  case 'system':
881
1012
  const presentation = 'presentation' in row ? row.presentation : undefined;
882
1013
  if (presentation !== undefined && presentation !== 'market') {
@@ -938,9 +1069,22 @@ function assertMediaPreview(value, path) {
938
1069
  }
939
1070
  }
940
1071
  function assertSnapshotContract(snapshot) {
1072
+ if (snapshot.disclosure !== undefined) {
1073
+ if (typeof snapshot.disclosure.expanded !== 'boolean') fail('snapshot.disclosure.expanded', 'must be a boolean');
1074
+ const inset = snapshot.disclosure.expandedTrailingInset;
1075
+ if (inset !== undefined && (!Number.isFinite(inset) || inset < 0)) fail('snapshot.disclosure.expandedTrailingInset', 'must be finite and nonnegative');
1076
+ }
1077
+ if (snapshot.structuralTransition) {
1078
+ assertKey(snapshot.structuralTransition.id, 'snapshot.structuralTransition.id');
1079
+ if (snapshot.structuralTransition.anchorKey !== undefined) assertKey(snapshot.structuralTransition.anchorKey, 'snapshot.structuralTransition.anchorKey');
1080
+ const duration = snapshot.structuralTransition.durationMs;
1081
+ if (duration !== undefined && (!Number.isFinite(duration) || duration < 0 || duration > 2000)) fail('snapshot.structuralTransition.durationMs', 'must be finite within 0...2000');
1082
+ }
941
1083
  if (snapshot.capabilities?.refreshTriggerDistance !== undefined && (!Number.isFinite(snapshot.capabilities.refreshTriggerDistance) || snapshot.capabilities.refreshTriggerDistance <= 0)) fail('capabilities.refreshTriggerDistance', 'must be a positive finite number');
942
1084
  assertPlainSerializable(snapshot, 'snapshot', new Set());
943
1085
  assertText(snapshot.theme?.imageFallbackBackground, 'theme.imageFallbackBackground');
1086
+ if (snapshot.theme?.successBackground !== undefined && (typeof snapshot.theme.successBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.successBackground))) fail('theme.successBackground', 'must be a hex color');
1087
+ if (snapshot.theme?.rowHoverBackground !== undefined && (typeof snapshot.theme.rowHoverBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.rowHoverBackground))) fail('theme.rowHoverBackground', 'must be a hex color');
944
1088
  if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
945
1089
  if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0) {
946
1090
  fail('snapshot.generation', 'must be a non-negative safe integer');
@@ -1018,8 +1162,8 @@ function protocolObject(value, path) {
1018
1162
  function protocolKey(value, path) {
1019
1163
  if (typeof value !== 'string' || value.trim().length === 0) fail(path, 'must be a non-blank string');
1020
1164
  }
1021
- const PROTOCOL_ARRAY_FIELDS = new Set(['columns', 'cells', 'points', 'images', 'overlays', 'badges', 'trailing', 'footerActions', 'amounts', 'metrics', 'children', 'subtitleSegments', 'titleMatch', 'selectedKeys', 'segments', 'hoverSecondary', 'hoverImages', 'secondaryBadges', 'fallbackUris', 'probeOrder', 'priceSegments', 'textSegments', 'secondaryTextSegments', 'primaryTextSegments']);
1022
- const PROTOCOL_NUMBER_FIELDS = new Set(['height', 'width', 'fontSize', 'lineHeight', 'opacity', 'offsetY', 'offsetX', 'cornerRadius', 'borderWidth', 'horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'trailingGap', 'start', 'end', 'padding', 'offset', 'overscan', 'minWidth', 'flex', 'paddingStart', 'paddingEnd', 'changeWidth', 'changeHeight', 'changeCornerRadius', 'contentTrailingGap', 'subtitleTrailingPadding', 'partGap', 'contentPadding', 'contentPaddingHorizontal', 'contentPaddingTop', 'contentPaddingBottom', 'itemSpacing', 'gridColumns']);
1165
+ const PROTOCOL_ARRAY_FIELDS = new Set(['columns', 'titleSuffixes', 'cells', 'points', 'images', 'overlays', 'badges', 'trailing', 'footerActions', 'buttons', 'amounts', 'metrics', 'children', 'subtitleSegments', 'titleMatch', 'selectedKeys', 'segments', 'hoverSecondary', 'hoverImages', 'secondaryBadges', 'fallbackUris', 'probeOrder', 'priceSegments', 'textSegments', 'secondaryTextSegments', 'primaryTextSegments']);
1166
+ const PROTOCOL_NUMBER_FIELDS = new Set(['leadingContentMaxWidthRatio', 'height', 'width', 'fontSize', 'lineHeight', 'opacity', 'offsetY', 'offsetX', 'cornerRadius', 'borderWidth', 'horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'trailingGap', 'start', 'end', 'padding', 'offset', 'overscan', 'minWidth', 'flex', 'paddingStart', 'paddingEnd', 'changeWidth', 'changeHeight', 'changeCornerRadius', 'contentTrailingGap', 'subtitleTrailingPadding', 'partGap', 'contentPadding', 'contentPaddingHorizontal', 'contentPaddingTop', 'contentPaddingBottom', 'itemSpacing', 'gridColumns']);
1023
1167
 
1024
1168
  /** Validate only shapes that renderers dereference, not display budgets. */
1025
1169
  function assertProtocolFields(value, path, parentField = '') {
@@ -1037,6 +1181,15 @@ function assertProtocolFields(value, path, parentField = '') {
1037
1181
  return;
1038
1182
  }
1039
1183
  const object = value;
1184
+ if (parentField === 'fallbackIcon') {
1185
+ protocolKey(object.name, `${path}.name`);
1186
+ if (object.size !== undefined && (typeof object.size !== 'number' || !Number.isFinite(object.size) || object.size <= 0 || object.size > 40)) fail(`${path}.size`, 'must be within (0, 40]');
1187
+ for (const field of ['tintColor', 'backgroundColor']) {
1188
+ const color = object[field];
1189
+ if (color !== undefined && (typeof color !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(color))) fail(`${path}.${field}`, 'must be a hex color');
1190
+ }
1191
+ }
1192
+ if (parentField === 'overlays' && object.fallbackIcon !== undefined && !object.image) fail(`${path}.fallbackIcon`, 'requires image');
1040
1193
  if (!path.includes('.style.') && ['image', 'networkImage', 'thumbnail', 'images', 'hoverImages'].includes(parentField) && typeof object.uri !== 'string') fail(`${path}.uri`, 'must be a string');
1041
1194
  Object.entries(value).forEach(([field, item]) => {
1042
1195
  if (item === undefined) return;
@@ -1044,6 +1197,9 @@ function assertProtocolFields(value, path, parentField = '') {
1044
1197
  if (!path.endsWith('.style') && PROTOCOL_ARRAY_FIELDS.has(field) && !Array.isArray(item)) fail(fieldPath, 'must be an array');
1045
1198
  if (PROTOCOL_NUMBER_FIELDS.has(field) && typeof item !== 'number') fail(fieldPath, 'must be a finite number');
1046
1199
  if (PROTOCOL_NUMBER_FIELDS.has(field) && typeof item === 'number' && item < 0 && !['offsetX', 'offsetY'].includes(field)) fail(fieldPath, 'must be non-negative');
1200
+ if (parentField === 'overlays' && field === 'fontWeight' && !['regular', 'medium', 'semibold', 'bold'].includes(item)) fail(fieldPath, 'must be regular, medium, semibold, or bold');
1201
+ if (field === 'buttonStyle' && item !== 'tertiary') fail(fieldPath, 'must be tertiary');
1202
+ if (field === 'hideLeading' && typeof item !== 'boolean') fail(fieldPath, 'must be boolean');
1047
1203
  if (field === 'key' || field === 'sectionKey' || field === 'actionKey' || field.endsWith('ActionKey')) protocolKey(item, fieldPath);
1048
1204
  if ((field === 'uri' || field === 'fallbackUri') && typeof item !== 'string') fail(fieldPath, 'must be a string');
1049
1205
  if (field === 'style' && !(item === 'subscript' && /[Ss]egments\[/.test(path)) || field === 'container') protocolObject(item, fieldPath);
@@ -1069,12 +1225,21 @@ function assertRowProtocol(row, path) {
1069
1225
  }
1070
1226
  if (row.height !== undefined && row.height < 0) fail(`${path}.height`, 'must be non-negative');
1071
1227
  if (row.type === 'identity') {
1228
+ assertIdentityExtras(row, path);
1229
+ if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes)) fail(`${path}.titleSuffixes`, 'must be an array');
1230
+ row.titleSuffixes?.forEach((suffix, index) => {
1231
+ protocolObject(suffix, `${path}.titleSuffixes[${index}]`);
1232
+ if (typeof suffix.text !== 'string' || !['secondary', 'disabled'].includes(suffix.tone)) fail(`${path}.titleSuffixes[${index}]`, 'invalid title suffix');
1233
+ if (suffix.maxWidth !== undefined && (!Number.isFinite(suffix.maxWidth) || suffix.maxWidth < 0 || suffix.maxWidth > 4096)) fail(`${path}.titleSuffixes[${index}].maxWidth`, 'must be finite within 0...4096');
1234
+ });
1072
1235
  let end = 0;
1073
1236
  row.titleMatch?.forEach(match => {
1074
1237
  if (!Number.isInteger(match.start) || !Number.isInteger(match.end) || match.start < end || match.end <= match.start || match.end > row.title.length) fail(`${path}.titleMatch`, 'invalid text range');
1075
1238
  end = match.end;
1076
1239
  });
1077
1240
  }
1241
+ if (row.type === 'buttonGroup') assertButtonGroup(row, path);
1242
+ if (row.type === 'sectionHeader') assertTextHelp(row.valueHelp, `${path}.valueHelp`);
1078
1243
  if (row.type === 'mediaTile' && row.media) {
1079
1244
  protocolObject(row.media.source, `${path}.media.source`);
1080
1245
  if (!Array.isArray(row.media.probeOrder)) fail(`${path}.media.probeOrder`, 'must be an array');
@@ -1091,7 +1256,15 @@ function assertRowProtocol(row, path) {
1091
1256
  keys.add(member.key);
1092
1257
  });
1093
1258
  }
1094
- if (row.type === 'dataRow' && !Array.isArray(row.columns)) fail(`${path}.columns`, 'must be an array');
1259
+ if (row.type === 'dataRow') {
1260
+ if (!Array.isArray(row.columns)) fail(`${path}.columns`, 'must be an array');
1261
+ row.columns.forEach((column, index) => {
1262
+ const columnPath = `${path}.columns[${index}]`;
1263
+ protocolObject(column, columnPath);
1264
+ assertIdentityExtras(column, columnPath);
1265
+ });
1266
+ assertTrailingAccessories(row.trailing, `${path}.trailing`);
1267
+ }
1095
1268
  if (row.type === 'market') {
1096
1269
  protocolObject(row.change, `${path}.change`);
1097
1270
  if (row.webTable) {
@@ -1118,6 +1291,21 @@ function assertRowProtocol(row, path) {
1118
1291
  }
1119
1292
  function assertSnapshotProtocol(snapshot) {
1120
1293
  protocolObject(snapshot, 'snapshot');
1294
+ if (snapshot.disclosure !== undefined) {
1295
+ protocolObject(snapshot.disclosure, 'snapshot.disclosure');
1296
+ if (typeof snapshot.disclosure.expanded !== 'boolean') fail('snapshot.disclosure.expanded', 'must be a boolean');
1297
+ const inset = snapshot.disclosure.expandedTrailingInset;
1298
+ if (inset !== undefined && (!Number.isFinite(inset) || inset < 0)) fail('snapshot.disclosure.expandedTrailingInset', 'must be finite and nonnegative');
1299
+ }
1300
+ if (snapshot.structuralTransition) {
1301
+ protocolObject(snapshot.structuralTransition, 'snapshot.structuralTransition');
1302
+ protocolKey(snapshot.structuralTransition.id, 'snapshot.structuralTransition.id');
1303
+ if (snapshot.structuralTransition.anchorKey !== undefined) protocolKey(snapshot.structuralTransition.anchorKey, 'snapshot.structuralTransition.anchorKey');
1304
+ const duration = snapshot.structuralTransition.durationMs;
1305
+ if (duration !== undefined && (!Number.isFinite(duration) || duration < 0 || duration > 2000)) fail('snapshot.structuralTransition.durationMs', 'must be finite within 0...2000');
1306
+ }
1307
+ if (snapshot.theme?.successBackground !== undefined && (typeof snapshot.theme.successBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.successBackground))) fail('theme.successBackground', 'must be a hex color');
1308
+ if (snapshot.theme?.rowHoverBackground !== undefined && (typeof snapshot.theme.rowHoverBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.rowHoverBackground))) fail('theme.rowHoverBackground', 'must be a hex color');
1121
1309
  if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
1122
1310
  if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0) fail('snapshot.generation', 'must be a non-negative safe integer');
1123
1311
  protocolObject(snapshot.layout, 'snapshot.layout');
@@ -1174,6 +1362,53 @@ export function validateSnapshot(snapshot) {
1174
1362
  assertSnapshotProtocol(normalized);
1175
1363
  return normalizeSnapshotStyles(normalized);
1176
1364
  }
1365
+ const ownedNativeSnapshots = new WeakMap();
1366
+ function capturePlainData(value, seen = new Map()) {
1367
+ if (value === null || typeof value !== 'object') return value;
1368
+ const previous = seen.get(value);
1369
+ if (previous) return previous;
1370
+ const copy = Array.isArray(value) ? new Array(value.length) : Object.create(Object.getPrototypeOf(value));
1371
+ seen.set(value, copy);
1372
+ const captureKey = key => {
1373
+ Object.defineProperty(copy, key, {
1374
+ value: capturePlainData(value[key], seen),
1375
+ enumerable: true,
1376
+ writable: true,
1377
+ configurable: true
1378
+ });
1379
+ };
1380
+ if (Array.isArray(value)) {
1381
+ for (let index = 0; index < value.length; index += 1) {
1382
+ if (index in value) captureKey(String(index));
1383
+ }
1384
+ Object.keys(value).forEach(key => {
1385
+ const index = Number(key);
1386
+ if (Number.isInteger(index) && index >= 0 && index < value.length && String(index) === key) return;
1387
+ captureKey(key);
1388
+ });
1389
+ } else {
1390
+ Object.keys(value).forEach(captureKey);
1391
+ }
1392
+ return copy;
1393
+ }
1394
+ function deepFreeze(value, seen = new WeakSet()) {
1395
+ if (value === null || typeof value !== 'object' || seen.has(value)) return value;
1396
+ seen.add(value);
1397
+ Object.values(value).forEach(item => deepFreeze(item, seen));
1398
+ return Object.freeze(value);
1399
+ }
1400
+
1401
+ /** Capture caller data once before validation, including accessor values. */
1402
+ export function captureNativeListInput(value) {
1403
+ return capturePlainData(value);
1404
+ }
1405
+
1406
+ /** Private native-wrapper ownership; exported validators keep their public identity. */
1407
+ export function ownValidatedNativeSnapshot(snapshot) {
1408
+ const owned = deepFreeze(snapshot);
1409
+ ownedNativeSnapshots.set(owned, new Map(owned.rows.map((row, index) => [row.key, index])));
1410
+ return owned;
1411
+ }
1177
1412
 
1178
1413
  /**
1179
1414
  * Resolves typography tokens to numbers. Returns the original snapshot when
@@ -1218,7 +1453,23 @@ function assertPatchChanges(patch, index) {
1218
1453
  }
1219
1454
  switch (patch.type) {
1220
1455
  case 'identity':
1456
+ if (patch.changes.contentHeight !== undefined && patch.changes.contentHeight !== 'auto') {
1457
+ fail(`${path}.contentHeight`, 'must be auto');
1458
+ }
1459
+ if (patch.changes.subtitleLines !== undefined && ![0, 1, 2].includes(patch.changes.subtitleLines)) {
1460
+ fail(`${path}.subtitleLines`, 'must be 0, 1, or 2');
1461
+ }
1462
+ if (patch.changes.longPressActionKey !== undefined) {
1463
+ assertKey(patch.changes.longPressActionKey, `${path}.longPressActionKey`);
1464
+ }
1221
1465
  assertText(patch.changes.title, `${path}.title`);
1466
+ if ((patch.changes.titleSuffixes?.length ?? 0) > 3) fail(`${path}.titleSuffixes`, 'supports at most 3 suffixes');
1467
+ patch.changes.titleSuffixes?.forEach((suffix, index) => {
1468
+ assertText(suffix.text, `${path}.titleSuffixes[${index}].text`);
1469
+ if (!['secondary', 'disabled'].includes(suffix.tone)) fail(`${path}.titleSuffixes[${index}].tone`, 'must be secondary or disabled');
1470
+ if (suffix.maxWidth !== undefined && (!Number.isFinite(suffix.maxWidth) || suffix.maxWidth < 0 || suffix.maxWidth > 4096)) fail(`${path}.titleSuffixes[${index}].maxWidth`, 'must be finite within 0...4096');
1471
+ });
1472
+ assertIdentityExtras(patch.changes, path);
1222
1473
  assertText(patch.changes.subtitle, `${path}.subtitle`);
1223
1474
  assertText(patch.changes.tertiary, `${path}.tertiary`);
1224
1475
  if (patch.changes.tertiaryTone !== undefined && !['secondary', 'info'].includes(patch.changes.tertiaryTone)) {
@@ -1252,11 +1503,14 @@ function assertPatchChanges(patch, index) {
1252
1503
  }
1253
1504
  break;
1254
1505
  case 'message':
1506
+ if (patch.changes.presentation !== undefined && patch.changes.presentation !== 'inlineWarning') {
1507
+ fail(`${path}.presentation`, 'must be inlineWarning');
1508
+ }
1255
1509
  assertText(patch.changes.title, `${path}.title`);
1256
1510
  assertText(patch.changes.body, `${path}.body`);
1257
1511
  assertText(patch.changes.time, `${path}.time`);
1258
- if (patch.changes.bodyLines !== undefined && ![1, 2, 3].includes(patch.changes.bodyLines)) {
1259
- fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
1512
+ if (patch.changes.bodyLines !== undefined && ![0, 1, 2, 3].includes(patch.changes.bodyLines)) {
1513
+ fail(`${path}.bodyLines`, 'must be 0, 1, 2, or 3');
1260
1514
  }
1261
1515
  assertImage(patch.changes.thumbnail, `${path}.thumbnail`);
1262
1516
  break;
@@ -1267,12 +1521,23 @@ function assertPatchChanges(patch, index) {
1267
1521
  patch.changes.columns?.forEach((column, columnIndex) => {
1268
1522
  assertKey(column.key, `${path}.columns[${columnIndex}].key`);
1269
1523
  assertText(column.text, `${path}.columns[${columnIndex}].text`);
1524
+ assertIdentityExtras(column, `${path}.columns[${columnIndex}]`);
1525
+ for (const field of ['textSegments', 'secondaryTextSegments']) {
1526
+ column[field]?.forEach((segment, segmentIndex) => {
1527
+ assertText(segment.text, `${path}.columns[${columnIndex}].${field}[${segmentIndex}].text`);
1528
+ if (segment.style !== undefined && segment.style !== 'subscript') fail(`${path}.columns[${columnIndex}].${field}[${segmentIndex}].style`, 'must be subscript');
1529
+ });
1530
+ }
1270
1531
  assertText(column.secondaryText, `${path}.columns[${columnIndex}].secondaryText`);
1271
1532
  assertTextTone(column.secondaryTone, `${path}.columns[${columnIndex}].secondaryTone`);
1272
1533
  });
1273
1534
  if ((patch.changes.badges?.length ?? 0) > MAX_BADGES) {
1274
1535
  fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
1275
1536
  }
1537
+ if ((patch.changes.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
1538
+ fail(`${path}.trailing`, `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`);
1539
+ }
1540
+ assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
1276
1541
  break;
1277
1542
  case 'market':
1278
1543
  assertMarketRow({
@@ -1331,6 +1596,11 @@ function assertPatchChanges(patch, index) {
1331
1596
  assertText(patch.changes.title, `${path}.title`);
1332
1597
  assertText(patch.changes.subtitle, `${path}.subtitle`);
1333
1598
  assertText(patch.changes.value, `${path}.value`);
1599
+ if (patch.changes.leadingBadge) {
1600
+ assertKey(patch.changes.leadingBadge.key, `${path}.leadingBadge.key`);
1601
+ assertText(patch.changes.leadingBadge.text, `${path}.leadingBadge.text`);
1602
+ }
1603
+ assertTextHelp(patch.changes.valueHelp, `${path}.valueHelp`);
1334
1604
  if (patch.changes.valueActionKey !== undefined) {
1335
1605
  assertKey(patch.changes.valueActionKey, `${path}.valueActionKey`);
1336
1606
  }
@@ -1344,6 +1614,11 @@ function assertPatchChanges(patch, index) {
1344
1614
  }
1345
1615
  assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
1346
1616
  break;
1617
+ case 'buttonGroup':
1618
+ if (patch.changes.buttons !== undefined) assertButtonGroup({
1619
+ buttons: patch.changes.buttons
1620
+ }, path);
1621
+ break;
1347
1622
  case 'system':
1348
1623
  assertText(patch.changes.message, `${path}.message`);
1349
1624
  break;
@@ -1363,6 +1638,7 @@ export function validatePatches(patches) {
1363
1638
  protocolObject(patch.changes, `patches[${index}].changes`);
1364
1639
  assertRowType(patch.type, `patches[${index}]`);
1365
1640
  assertProtocolFields(patch.changes, `patches[${index}].changes`);
1641
+ if (patch.type === 'identity') assertIdentityExtras(patch.changes, `patches[${index}].changes`);
1366
1642
  if (!isDevelopment()) continue;
1367
1643
  assertKey(patch.key, `patches[${index}].key`);
1368
1644
  if (Object.keys(patch.changes).length === 0) {
@@ -1393,6 +1669,96 @@ function normalizePatchStyles(patches) {
1393
1669
  });
1394
1670
  return changed ? next : patches;
1395
1671
  }
1672
+ const EMPTY_ROW_PATCHES = Object.freeze([]);
1673
+ function sameJsonValue(left, right) {
1674
+ if (left === right) return true;
1675
+ if (left === null || right === null) return false;
1676
+ if (typeof left !== 'object' || typeof right !== 'object') return false;
1677
+ if (Array.isArray(left) || Array.isArray(right)) {
1678
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => sameJsonValue(value, right[index]));
1679
+ }
1680
+ const leftObject = left;
1681
+ const rightObject = right;
1682
+ const leftKeys = Object.keys(leftObject).filter(key => leftObject[key] !== undefined);
1683
+ const rightKeys = Object.keys(rightObject).filter(key => rightObject[key] !== undefined);
1684
+ return leftKeys.length === rightKeys.length && leftKeys.every(key => sameJsonValue(leftObject[key], rightObject[key]));
1685
+ }
1686
+ function sameOptionalPresence(left, right) {
1687
+ return left === undefined === (right === undefined);
1688
+ }
1689
+
1690
+ /** Plan value-only patches; callers own valid immutable snapshots and commit state. */
1691
+ export function planNativeListPatches(previous, next) {
1692
+ if (previous === next) return EMPTY_ROW_PATCHES;
1693
+ const {
1694
+ rows: previousRows,
1695
+ ...previousMetadata
1696
+ } = previous;
1697
+ const {
1698
+ rows: nextRows,
1699
+ ...nextMetadata
1700
+ } = next;
1701
+ if (previousRows.length !== nextRows.length || !sameJsonValue(previousMetadata, nextMetadata)) return undefined;
1702
+ if (previousRows === nextRows) return EMPTY_ROW_PATCHES;
1703
+ const patches = [];
1704
+ for (let index = 0; index < nextRows.length; index += 1) {
1705
+ const oldRow = previousRows[index];
1706
+ const row = nextRows[index];
1707
+ if (oldRow.key !== row.key || oldRow.type !== row.type) return undefined;
1708
+ if (oldRow === row) continue;
1709
+ if (oldRow.type === 'identity' && row.type === 'identity') {
1710
+ const {
1711
+ subtitle: oldSubtitle,
1712
+ subtitleSegments: oldSubtitleSegments,
1713
+ trailing: oldTrailing,
1714
+ ...oldMetadata
1715
+ } = oldRow;
1716
+ const {
1717
+ subtitle,
1718
+ subtitleSegments,
1719
+ trailing,
1720
+ ...metadata
1721
+ } = row;
1722
+ if (!sameOptionalPresence(oldSubtitle, subtitle) || !sameOptionalPresence(oldSubtitleSegments, subtitleSegments) || !sameOptionalPresence(oldTrailing, trailing) || !sameJsonValue(oldMetadata, metadata)) return undefined;
1723
+ if (!sameJsonValue(oldSubtitle, subtitle) || !sameJsonValue(oldSubtitleSegments, subtitleSegments) || !sameJsonValue(oldTrailing, trailing)) {
1724
+ patches.push({
1725
+ type: 'identity',
1726
+ key: row.key,
1727
+ changes: {
1728
+ subtitle,
1729
+ subtitleSegments,
1730
+ trailing
1731
+ }
1732
+ });
1733
+ }
1734
+ } else if (oldRow.type === 'dataRow' && row.type === 'dataRow') {
1735
+ const {
1736
+ columns: oldColumns,
1737
+ trailing: oldTrailing,
1738
+ ...oldMetadata
1739
+ } = oldRow;
1740
+ const {
1741
+ columns,
1742
+ trailing,
1743
+ ...metadata
1744
+ } = row;
1745
+ if (!sameOptionalPresence(oldTrailing, trailing) || !sameJsonValue(oldMetadata, metadata) || !sameJsonValue(oldColumns[0], columns[0])) return undefined;
1746
+ if (!sameJsonValue(oldColumns.slice(1), columns.slice(1)) || !sameJsonValue(oldTrailing, trailing)) {
1747
+ patches.push({
1748
+ type: 'dataRow',
1749
+ key: row.key,
1750
+ changes: {
1751
+ columns,
1752
+ trailing
1753
+ }
1754
+ });
1755
+ }
1756
+ } else if (!sameJsonValue(oldRow, row)) {
1757
+ return undefined;
1758
+ }
1759
+ }
1760
+ return patches.length === 0 ? EMPTY_ROW_PATCHES : patches;
1761
+ }
1396
1762
  export function applyRowPatches(snapshot, patches) {
1397
1763
  const normalizedPatches = validatePatches(patches);
1398
1764
  const patchByKey = new Map(normalizedPatches.map(patch => [patch.key, patch]));
@@ -1419,6 +1785,53 @@ export function applyRowPatches(snapshot, patches) {
1419
1785
  rows
1420
1786
  });
1421
1787
  }
1788
+
1789
+ /** Native wrapper only: preserve full public validation for unsupported patches. */
1790
+ export function applyOwnedNativePatches(snapshot, patches) {
1791
+ const acceptedPatches = deepFreeze(validatePatches(capturePlainData(patches)));
1792
+ const rowIndexes = ownedNativeSnapshots.get(snapshot);
1793
+ const canPatchRows = rowIndexes !== undefined && acceptedPatches.every(patch => {
1794
+ const index = rowIndexes.get(patch.key);
1795
+ if (index === undefined || snapshot.rows[index].type !== patch.type) return false;
1796
+ const allowed = patch.type === 'identity' ? ['subtitle', 'subtitleSegments', 'trailing'] : patch.type === 'dataRow' ? ['columns', 'trailing'] : undefined;
1797
+ return allowed !== undefined && Object.entries(patch.changes).every(([key, value]) => allowed.includes(key) && (value !== undefined || snapshot.rows[index][key] === undefined));
1798
+ });
1799
+ if (!canPatchRows) {
1800
+ const baseline = rowIndexes ? snapshot : capturePlainData(snapshot);
1801
+ return {
1802
+ patches: acceptedPatches,
1803
+ snapshot: ownValidatedNativeSnapshot(applyRowPatches(baseline, acceptedPatches)),
1804
+ json: JSON.stringify(acceptedPatches)
1805
+ };
1806
+ }
1807
+ const nextRows = snapshot.rows.slice();
1808
+ for (const patch of acceptedPatches) {
1809
+ const index = rowIndexes.get(patch.key);
1810
+ const previous = nextRows[index];
1811
+ const merged = {
1812
+ ...previous,
1813
+ ...patch.changes,
1814
+ key: previous.key,
1815
+ type: previous.type
1816
+ };
1817
+ const path = `rows[${index}]`;
1818
+ assertPlainSerializable(merged, path, new Set());
1819
+ if (isDevelopment()) assertRow(merged, index);
1820
+ const normalized = isDevelopment() ? merged : normalizeEmptyImages(merged);
1821
+ assertRowProtocol(normalized, path);
1822
+ nextRows[index] = deepFreeze(normalizeRowStyles(normalized));
1823
+ }
1824
+ const nextSnapshot = Object.freeze({
1825
+ ...snapshot,
1826
+ rows: Object.freeze(nextRows)
1827
+ });
1828
+ ownedNativeSnapshots.set(nextSnapshot, rowIndexes);
1829
+ return {
1830
+ patches: acceptedPatches,
1831
+ snapshot: nextSnapshot,
1832
+ json: JSON.stringify(acceptedPatches)
1833
+ };
1834
+ }
1422
1835
  export function serializeSnapshot(snapshot) {
1423
1836
  return JSON.stringify(validateSnapshot(snapshot));
1424
1837
  }