@onekeyfe/react-native-native-list 3.0.105

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 (181) hide show
  1. package/README.md +342 -0
  2. package/android/CMakeLists.txt +20 -0
  3. package/android/build.gradle +105 -0
  4. package/android/gradle.properties +4 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  7. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +125 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +165 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +147 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListPackage.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +2939 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1794 -0
  13. package/common/fonts/Roobert-Bold.ttf +0 -0
  14. package/common/fonts/Roobert-Medium.ttf +0 -0
  15. package/common/fonts/Roobert-Regular.ttf +0 -0
  16. package/common/fonts/Roobert-SemiBold.ttf +0 -0
  17. package/docs/DESIGN.md +129 -0
  18. package/ios/HybridNativeList.swift +131 -0
  19. package/ios/NativeListCell.swift +2858 -0
  20. package/ios/NativeListDesignAssets.swift +110 -0
  21. package/ios/NativeListModels.swift +215 -0
  22. package/ios/RNCNativeListView.swift +1737 -0
  23. package/ios/Resources/NativeListIcons.xcassets/Contents.json +1 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/Contents.json +1 -0
  25. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/icon.svg +1 -0
  26. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/Contents.json +1 -0
  27. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/icon.svg +1 -0
  28. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/Contents.json +1 -0
  29. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/icon.svg +1 -0
  30. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/Contents.json +1 -0
  31. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/icon.svg +1 -0
  32. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/Contents.json +1 -0
  33. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/icon.svg +1 -0
  34. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/Contents.json +1 -0
  35. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/icon.svg +1 -0
  36. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/Contents.json +1 -0
  37. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/icon.svg +1 -0
  38. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/Contents.json +1 -0
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/icon.svg +1 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/Contents.json +1 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/icon.svg +1 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/Contents.json +1 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/icon.svg +1 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/Contents.json +1 -0
  45. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/icon.svg +1 -0
  46. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/Contents.json +1 -0
  47. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/icon.svg +1 -0
  48. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/Contents.json +1 -0
  49. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/icon.svg +1 -0
  50. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/Contents.json +1 -0
  51. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/icon.svg +1 -0
  52. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/Contents.json +1 -0
  53. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/icon.svg +1 -0
  54. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/Contents.json +1 -0
  55. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/icon.svg +1 -0
  56. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/Contents.json +1 -0
  57. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/icon.svg +1 -0
  58. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/Contents.json +1 -0
  59. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/icon.svg +1 -0
  60. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/Contents.json +1 -0
  61. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/icon.svg +1 -0
  62. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/Contents.json +1 -0
  63. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/icon.svg +1 -0
  64. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/Contents.json +1 -0
  65. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/icon.svg +1 -0
  66. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/Contents.json +1 -0
  67. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/icon.svg +1 -0
  68. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/Contents.json +1 -0
  69. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/icon.svg +1 -0
  70. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/Contents.json +1 -0
  71. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/icon.svg +1 -0
  72. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/Contents.json +1 -0
  73. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/icon.svg +1 -0
  74. package/lib/module/NativeList.js +183 -0
  75. package/lib/module/NativeList.nitro.js +4 -0
  76. package/lib/module/NativeList.types.js +4 -0
  77. package/lib/module/NativeList.web.js +167 -0
  78. package/lib/module/index.js +6 -0
  79. package/lib/module/models.js +2 -0
  80. package/lib/module/package.json +1 -0
  81. package/lib/module/scrolling.js +121 -0
  82. package/lib/module/selection.js +85 -0
  83. package/lib/module/validation.js +562 -0
  84. package/lib/module/web/NativeListWebEngine.js +2238 -0
  85. package/lib/nitrogen/generated/.gitattributes +1 -0
  86. package/lib/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  87. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +201 -0
  88. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +84 -0
  89. package/lib/nitrogen/generated/android/c++/JNativeListScrollAlignment.hpp +64 -0
  90. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +104 -0
  91. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.hpp +54 -0
  92. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_std__string.kt +78 -0
  93. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +177 -0
  94. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListScrollAlignment.kt +25 -0
  95. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/nativelistOnLoad.kt +35 -0
  96. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +96 -0
  97. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListStateUpdater.kt +23 -0
  98. package/lib/nitrogen/generated/android/nativelist+autolinking.cmake +83 -0
  99. package/lib/nitrogen/generated/android/nativelist+autolinking.gradle +27 -0
  100. package/lib/nitrogen/generated/android/nativelistOnLoad.cpp +58 -0
  101. package/lib/nitrogen/generated/android/nativelistOnLoad.hpp +34 -0
  102. package/lib/nitrogen/generated/ios/NativeListModule+autolinking.rb +62 -0
  103. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +41 -0
  104. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +91 -0
  105. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +49 -0
  106. package/lib/nitrogen/generated/ios/NativeListModuleAutolinking.mm +33 -0
  107. package/lib/nitrogen/generated/ios/NativeListModuleAutolinking.swift +26 -0
  108. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.cpp +11 -0
  109. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +180 -0
  110. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +180 -0
  111. package/lib/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  112. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +69 -0
  113. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +447 -0
  114. package/lib/nitrogen/generated/ios/swift/NativeListScrollAlignment.swift +48 -0
  115. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +43 -0
  116. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +87 -0
  117. package/lib/nitrogen/generated/shared/c++/NativeListScrollAlignment.hpp +84 -0
  118. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +46 -0
  119. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +103 -0
  120. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +16 -0
  121. package/lib/typescript/package.json +1 -0
  122. package/lib/typescript/src/NativeList.d.ts +18 -0
  123. package/lib/typescript/src/NativeList.nitro.d.ts +29 -0
  124. package/lib/typescript/src/NativeList.types.d.ts +87 -0
  125. package/lib/typescript/src/NativeList.web.d.ts +19 -0
  126. package/lib/typescript/src/index.d.ts +7 -0
  127. package/lib/typescript/src/models.d.ts +456 -0
  128. package/lib/typescript/src/scrolling.d.ts +32 -0
  129. package/lib/typescript/src/selection.d.ts +27 -0
  130. package/lib/typescript/src/validation.d.ts +7 -0
  131. package/lib/typescript/src/web/NativeListWebEngine.d.ts +203 -0
  132. package/nitro.json +23 -0
  133. package/nitrogen/generated/.gitattributes +1 -0
  134. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  135. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +201 -0
  136. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +84 -0
  137. package/nitrogen/generated/android/c++/JNativeListScrollAlignment.hpp +64 -0
  138. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +104 -0
  139. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.hpp +54 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_std__string.kt +78 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +177 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListScrollAlignment.kt +25 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/nativelistOnLoad.kt +35 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +96 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListStateUpdater.kt +23 -0
  146. package/nitrogen/generated/android/nativelist+autolinking.cmake +83 -0
  147. package/nitrogen/generated/android/nativelist+autolinking.gradle +27 -0
  148. package/nitrogen/generated/android/nativelistOnLoad.cpp +58 -0
  149. package/nitrogen/generated/android/nativelistOnLoad.hpp +34 -0
  150. package/nitrogen/generated/ios/NativeListModule+autolinking.rb +62 -0
  151. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +41 -0
  152. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +91 -0
  153. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +49 -0
  154. package/nitrogen/generated/ios/NativeListModuleAutolinking.mm +33 -0
  155. package/nitrogen/generated/ios/NativeListModuleAutolinking.swift +26 -0
  156. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.cpp +11 -0
  157. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +180 -0
  158. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +180 -0
  159. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  160. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +69 -0
  161. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +447 -0
  162. package/nitrogen/generated/ios/swift/NativeListScrollAlignment.swift +48 -0
  163. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +43 -0
  164. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +87 -0
  165. package/nitrogen/generated/shared/c++/NativeListScrollAlignment.hpp +84 -0
  166. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +46 -0
  167. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +103 -0
  168. package/nitrogen/generated/shared/json/NativeListConfig.json +16 -0
  169. package/package.json +126 -0
  170. package/react-native-native-list.podspec +38 -0
  171. package/react-native.config.js +19 -0
  172. package/src/NativeList.nitro.ts +48 -0
  173. package/src/NativeList.tsx +298 -0
  174. package/src/NativeList.types.ts +125 -0
  175. package/src/NativeList.web.tsx +258 -0
  176. package/src/index.ts +35 -0
  177. package/src/models.ts +604 -0
  178. package/src/scrolling.ts +189 -0
  179. package/src/selection.ts +159 -0
  180. package/src/validation.ts +877 -0
  181. package/src/web/NativeListWebEngine.ts +3767 -0
@@ -0,0 +1,877 @@
1
+ import type {
2
+ ActivityRow,
3
+ IdentityRow,
4
+ ImageSource,
5
+ LeadingVisual,
6
+ NativeListSnapshot,
7
+ RowModel,
8
+ RowPatch,
9
+ WalletGroupRow,
10
+ } from './models';
11
+
12
+ const MAX_KEY_LENGTH = 256;
13
+ const MAX_TEXT_LENGTH = 4096;
14
+ const MAX_BADGES = 2;
15
+ const MAX_TRAILING_ACCESSORIES = 2;
16
+ const MAX_FOOTER_ACTIONS = 3;
17
+ const MAX_IMAGE_EDGE = 4096;
18
+ const MAX_IMAGE_HEADERS = 32;
19
+ const MAX_HEADER_LENGTH = 4096;
20
+ const MAX_SPACER_HEIGHT = 512;
21
+ const MAX_SECTION_INDEX_TITLE_LENGTH = 8;
22
+
23
+ function fail(path: string, message: string): never {
24
+ throw new Error(`NativeList ${path}: ${message}`);
25
+ }
26
+
27
+ function assertPlainSerializable(
28
+ value: unknown,
29
+ path: string,
30
+ seen: Set<object>
31
+ ): void {
32
+ if (value === null || value === undefined) return;
33
+ const valueType = typeof value;
34
+ if (valueType === 'string' || valueType === 'boolean') return;
35
+ if (valueType === 'number') {
36
+ if (!Number.isFinite(value)) fail(path, 'numbers must be finite');
37
+ return;
38
+ }
39
+ if (valueType !== 'object')
40
+ fail(path, 'must contain only plain serializable data');
41
+
42
+ const objectValue = value as object;
43
+ if (seen.has(objectValue)) fail(path, 'must not contain circular references');
44
+ seen.add(objectValue);
45
+ if (Array.isArray(value)) {
46
+ value.forEach((item, index) =>
47
+ assertPlainSerializable(item, `${path}[${index}]`, seen)
48
+ );
49
+ } else {
50
+ const prototype = Object.getPrototypeOf(value);
51
+ if (prototype !== Object.prototype && prototype !== null) {
52
+ fail(path, 'must contain plain objects only');
53
+ }
54
+ Object.entries(value as Record<string, unknown>).forEach(([key, item]) =>
55
+ assertPlainSerializable(item, `${path}.${key}`, seen)
56
+ );
57
+ }
58
+ seen.delete(objectValue);
59
+ }
60
+
61
+ function assertKey(key: string, path: string): void {
62
+ if (
63
+ typeof key !== 'string' ||
64
+ key.length === 0 ||
65
+ key.length > MAX_KEY_LENGTH
66
+ ) {
67
+ fail(
68
+ path,
69
+ `must be a non-empty string no longer than ${MAX_KEY_LENGTH} characters`
70
+ );
71
+ }
72
+ if (key.trim() !== key)
73
+ fail(path, 'must not have leading or trailing whitespace');
74
+ }
75
+
76
+ function assertText(text: string | undefined, path: string): void {
77
+ if (text === undefined) return;
78
+ if (typeof text !== 'string' || text.length > MAX_TEXT_LENGTH) {
79
+ fail(path, `must be a string no longer than ${MAX_TEXT_LENGTH} characters`);
80
+ }
81
+ }
82
+
83
+ function assertSectionIndexTitle(
84
+ title: string | undefined,
85
+ path: string
86
+ ): void {
87
+ if (title === undefined) return;
88
+ if (
89
+ typeof title !== 'string' ||
90
+ title.length === 0 ||
91
+ Array.from(title).length > MAX_SECTION_INDEX_TITLE_LENGTH
92
+ ) {
93
+ fail(
94
+ path,
95
+ `must be a non-empty string no longer than ${MAX_SECTION_INDEX_TITLE_LENGTH} Unicode code points`
96
+ );
97
+ }
98
+ if (title.trim() !== title) {
99
+ fail(path, 'must not have leading or trailing whitespace');
100
+ }
101
+ }
102
+
103
+ function assertTextTone(tone: string | undefined, path: string): void {
104
+ if (
105
+ tone !== undefined &&
106
+ !['primary', 'secondary', 'positive', 'negative'].includes(tone)
107
+ ) {
108
+ fail(path, 'must be primary, secondary, positive, or negative');
109
+ }
110
+ }
111
+
112
+ function assertSectionHeaderVariant(
113
+ variant: string | undefined,
114
+ path: string
115
+ ): void {
116
+ if (
117
+ variant !== undefined &&
118
+ !['summary', 'gallery', 'history'].includes(variant)
119
+ ) {
120
+ fail(path, 'must be summary, gallery, or history when provided');
121
+ }
122
+ }
123
+
124
+ function assertNonNegativeNumber(
125
+ value: number | undefined,
126
+ path: string
127
+ ): void {
128
+ if (value !== undefined && value < 0) {
129
+ fail(path, 'must be non-negative');
130
+ }
131
+ }
132
+
133
+ function assertVisualShape(shape: string | undefined, path: string): void {
134
+ if (shape !== undefined && !['circle', 'rounded', 'square'].includes(shape)) {
135
+ fail(path, 'must be circle, rounded, or square');
136
+ }
137
+ }
138
+
139
+ function assertTrailingAccessories(
140
+ accessories: IdentityRow['trailing'] | undefined,
141
+ path: string
142
+ ): void {
143
+ accessories?.forEach((accessory, index) => {
144
+ const accessoryPath = `${path}[${index}]`;
145
+ if (
146
+ accessory.kind === 'progress' &&
147
+ (accessory.value < 0 || accessory.value > 1)
148
+ ) {
149
+ fail(`${accessoryPath}.value`, 'must be within 0...1');
150
+ }
151
+ if (accessory.kind === 'valuePair') {
152
+ assertTextTone(accessory.primaryTone, `${accessoryPath}.primaryTone`);
153
+ assertTextTone(accessory.secondaryTone, `${accessoryPath}.secondaryTone`);
154
+ }
155
+ });
156
+ }
157
+
158
+ function assertImage(image: ImageSource | undefined, path: string): void {
159
+ if (!image) return;
160
+ if (!image.uri.trim()) fail(`${path}.uri`, 'must not be empty');
161
+ if (
162
+ !Number.isFinite(image.width) ||
163
+ !Number.isFinite(image.height) ||
164
+ image.width <= 0 ||
165
+ image.height <= 0 ||
166
+ image.width > MAX_IMAGE_EDGE ||
167
+ image.height > MAX_IMAGE_EDGE
168
+ ) {
169
+ fail(path, `width and height must be within 1...${MAX_IMAGE_EDGE}`);
170
+ }
171
+ if (
172
+ image.overscan !== undefined &&
173
+ (image.overscan < 1 || image.overscan > 4)
174
+ ) {
175
+ fail(`${path}.overscan`, 'must be within 1...4');
176
+ }
177
+ if (image.headers) {
178
+ const entries = Object.entries(image.headers);
179
+ if (entries.length > MAX_IMAGE_HEADERS) {
180
+ fail(`${path}.headers`, `supports at most ${MAX_IMAGE_HEADERS} headers`);
181
+ }
182
+ entries.forEach(([key, value]) => {
183
+ if (
184
+ !key.trim() ||
185
+ key.length > MAX_HEADER_LENGTH ||
186
+ typeof value !== 'string' ||
187
+ value.length > MAX_HEADER_LENGTH
188
+ ) {
189
+ fail(
190
+ `${path}.headers`,
191
+ `keys and values must be within 1...${MAX_HEADER_LENGTH} characters`
192
+ );
193
+ }
194
+ });
195
+ }
196
+ }
197
+
198
+ function assertLeadingVisual(
199
+ visual: LeadingVisual | undefined,
200
+ path: string
201
+ ): void {
202
+ if (!visual) return;
203
+ if (visual.kind === 'stackedImages') {
204
+ if (visual.images.length < 2 || visual.images.length > 3) {
205
+ fail(`${path}.images`, 'must contain 2 or 3 images');
206
+ }
207
+ visual.images.forEach((image, index) =>
208
+ assertImage(image, `${path}.images[${index}]`)
209
+ );
210
+ } else if (visual.kind === 'image') {
211
+ assertImage(visual.image, `${path}.image`);
212
+ assertVisualShape(visual.shape, `${path}.shape`);
213
+ } else if (visual.kind !== 'icon') {
214
+ assertImage(visual.image, `${path}.image`);
215
+ assertVisualShape(visual.shape, `${path}.shape`);
216
+ assertText(visual.cornerIcon?.name, `${path}.cornerIcon.name`);
217
+ if (visual.kind === 'token') {
218
+ assertImage(visual.networkImage, `${path}.networkImage`);
219
+ }
220
+ }
221
+ }
222
+
223
+ function assertVisual(row: RowModel, path: string): void {
224
+ const visuals =
225
+ row.type === 'identity'
226
+ ? [row.leading]
227
+ : row.type === 'rail'
228
+ ? [row.visual]
229
+ : row.type === 'activity'
230
+ ? [row.leading, row.secondaryLeading]
231
+ : row.type === 'message'
232
+ ? [row.leading]
233
+ : row.type === 'dataRow'
234
+ ? [row.leading]
235
+ : row.type === 'metricCard'
236
+ ? [row.visual]
237
+ : [];
238
+
239
+ visuals.forEach((visual, index) => {
240
+ assertLeadingVisual(visual, `${path}.visual[${index}]`);
241
+ });
242
+ }
243
+
244
+ function assertActivity(row: ActivityRow, path: string): void {
245
+ if ((row.footerActions?.length ?? 0) > MAX_FOOTER_ACTIONS) {
246
+ fail(
247
+ `${path}.footerActions`,
248
+ `supports at most ${MAX_FOOTER_ACTIONS} actions`
249
+ );
250
+ }
251
+ const keys = new Set<string>();
252
+ row.footerActions?.forEach((action, index) => {
253
+ assertKey(action.key, `${path}.footerActions[${index}].key`);
254
+ if (keys.has(action.key))
255
+ fail(`${path}.footerActions`, `duplicate action key "${action.key}"`);
256
+ keys.add(action.key);
257
+ });
258
+ }
259
+
260
+ function assertWalletGroup(row: WalletGroupRow, path: string): void {
261
+ if (row.parent.key !== row.key) {
262
+ fail(`${path}.parent.key`, 'must match the wallet group key');
263
+ }
264
+ if (row.children.length === 0) {
265
+ fail(`${path}.children`, 'must contain at least one child wallet');
266
+ }
267
+ const members = [row.parent, ...row.children];
268
+ const keys = new Set<string>();
269
+ members.forEach((member, memberIndex) => {
270
+ const memberPath =
271
+ memberIndex === 0
272
+ ? `${path}.parent`
273
+ : `${path}.children[${memberIndex - 1}]`;
274
+ if (member.type !== 'identity' || member.presentation !== 'walletSidebar') {
275
+ fail(memberPath, 'must be a walletSidebar identity row');
276
+ }
277
+ assertRow(member, memberIndex, memberPath);
278
+ if (keys.has(member.key)) {
279
+ fail(memberPath, `duplicate wallet member key "${member.key}"`);
280
+ }
281
+ keys.add(member.key);
282
+ });
283
+ }
284
+
285
+ function assertRow(
286
+ row: RowModel,
287
+ index: number,
288
+ path = `rows[${index}]`
289
+ ): void {
290
+ assertKey(row.key, `${path}.key`);
291
+ if (row.groupId && !row.groupPosition) {
292
+ fail(`${path}.groupPosition`, 'is required when groupId is present');
293
+ }
294
+ if (!row.groupId && row.groupPosition) {
295
+ fail(`${path}.groupId`, 'is required when groupPosition is present');
296
+ }
297
+ assertVisual(row, path);
298
+
299
+ switch (row.type) {
300
+ case 'walletGroup':
301
+ assertWalletGroup(row, path);
302
+ break;
303
+ case 'identity':
304
+ if (
305
+ row.presentation !== undefined &&
306
+ row.presentation !== 'walletSidebar' &&
307
+ row.presentation !== 'accountSelector' &&
308
+ row.presentation !== 'networkSelector'
309
+ ) {
310
+ fail(
311
+ `${path}.presentation`,
312
+ 'must be walletSidebar, accountSelector, or networkSelector when provided'
313
+ );
314
+ }
315
+ assertText(row.title, `${path}.title`);
316
+ assertText(row.subtitle, `${path}.subtitle`);
317
+ assertText(row.tertiary, `${path}.tertiary`);
318
+ if (
319
+ row.tertiaryTone !== undefined &&
320
+ !['secondary', 'info'].includes(row.tertiaryTone)
321
+ ) {
322
+ fail(`${path}.tertiaryTone`, 'must be secondary or info');
323
+ }
324
+ if ((row.badges?.length ?? 0) > MAX_BADGES) {
325
+ fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
326
+ }
327
+ if ((row.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
328
+ fail(
329
+ `${path}.trailing`,
330
+ `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`
331
+ );
332
+ }
333
+ assertTrailingAccessories(row.trailing, `${path}.trailing`);
334
+ assertTrailingAccessories(
335
+ row.leadingAction ? [row.leadingAction] : undefined,
336
+ `${path}.leadingAction`
337
+ );
338
+ break;
339
+ case 'rail':
340
+ assertText(row.title, `${path}.title`);
341
+ break;
342
+ case 'activity':
343
+ assertActivity(row, path);
344
+ break;
345
+ case 'message':
346
+ assertText(row.title, `${path}.title`);
347
+ assertText(row.body, `${path}.body`);
348
+ if ((row.bodyLines ?? 3) < 1 || (row.bodyLines ?? 3) > 3) {
349
+ fail(`${path}.bodyLines`, 'must be within 1...3');
350
+ }
351
+ assertImage(row.thumbnail, `${path}.thumbnail`);
352
+ break;
353
+ case 'dataRow':
354
+ if (row.columns.length < 2 || row.columns.length > 4) {
355
+ fail(`${path}.columns`, 'must contain 2...4 columns');
356
+ }
357
+ row.columns.forEach((column, columnIndex) => {
358
+ assertKey(column.key, `${path}.columns[${columnIndex}].key`);
359
+ assertText(column.text, `${path}.columns[${columnIndex}].text`);
360
+ assertText(
361
+ column.secondaryLeadingText,
362
+ `${path}.columns[${columnIndex}].secondaryLeadingText`
363
+ );
364
+ assertText(
365
+ column.secondaryText,
366
+ `${path}.columns[${columnIndex}].secondaryText`
367
+ );
368
+ assertTextTone(
369
+ column.secondaryTone,
370
+ `${path}.columns[${columnIndex}].secondaryTone`
371
+ );
372
+ });
373
+ if ((row.badges?.length ?? 0) > MAX_BADGES) {
374
+ fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
375
+ }
376
+ break;
377
+ case 'mediaTile':
378
+ assertImage(row.image, `${path}.image`);
379
+ if (
380
+ row.imageState !== undefined &&
381
+ !['empty', 'error'].includes(row.imageState)
382
+ ) {
383
+ fail(`${path}.imageState`, 'must be empty or error when provided');
384
+ }
385
+ if (row.imageState === undefined && row.image === undefined) {
386
+ fail(
387
+ `${path}.image`,
388
+ 'is required unless imageState is empty or error'
389
+ );
390
+ }
391
+ assertImage(row.networkImage, `${path}.networkImage`);
392
+ assertText(row.title, `${path}.title`);
393
+ assertText(row.subtitle, `${path}.subtitle`);
394
+ break;
395
+ case 'metricCard':
396
+ assertText(row.title, `${path}.title`);
397
+ assertText(row.value, `${path}.value`);
398
+ assertText(row.subtitle, `${path}.subtitle`);
399
+ assertText(row.trend, `${path}.trend`);
400
+ if (
401
+ row.variant !== undefined &&
402
+ !['standard', 'activity', 'performance'].includes(row.variant)
403
+ ) {
404
+ fail(`${path}.variant`, 'must be standard, activity, or performance');
405
+ }
406
+ if (row.metrics && (row.metrics.length < 2 || row.metrics.length > 5)) {
407
+ fail(`${path}.metrics`, 'must contain 2...5 metrics');
408
+ }
409
+ row.metrics?.forEach((metric, metricIndex) => {
410
+ assertKey(metric.key, `${path}.metrics[${metricIndex}].key`);
411
+ assertText(metric.label, `${path}.metrics[${metricIndex}].label`);
412
+ assertText(metric.value, `${path}.metrics[${metricIndex}].value`);
413
+ assertTextTone(metric.tone, `${path}.metrics[${metricIndex}].tone`);
414
+ assertLeadingVisual(
415
+ metric.visual,
416
+ `${path}.metrics[${metricIndex}].visual`
417
+ );
418
+ });
419
+ if (
420
+ row.progress !== undefined &&
421
+ (row.progress < 0 || row.progress > 1)
422
+ ) {
423
+ fail(`${path}.progress`, 'must be within 0...1');
424
+ }
425
+ break;
426
+ case 'sectionHeader':
427
+ assertKey(row.sectionKey, `${path}.sectionKey`);
428
+ if (
429
+ row.presentation !== undefined &&
430
+ row.presentation !== 'networkSelector'
431
+ ) {
432
+ fail(`${path}.presentation`, 'must be networkSelector when provided');
433
+ }
434
+ assertSectionIndexTitle(row.indexTitle, `${path}.indexTitle`);
435
+ assertSectionHeaderVariant(row.variant, `${path}.variant`);
436
+ assertText(row.title, `${path}.title`);
437
+ assertText(row.subtitle, `${path}.subtitle`);
438
+ assertText(row.value, `${path}.value`);
439
+ if (row.valueActionKey !== undefined) {
440
+ assertKey(row.valueActionKey, `${path}.valueActionKey`);
441
+ }
442
+ assertTrailingAccessories(
443
+ row.titleIcon ? [row.titleIcon] : undefined,
444
+ `${path}.titleIcon`
445
+ );
446
+ assertTrailingAccessories(
447
+ row.valueIcon ? [row.valueIcon] : undefined,
448
+ `${path}.valueIcon`
449
+ );
450
+ if (row.variant === 'summary' && row.checkbox !== undefined) {
451
+ fail(`${path}.checkbox`, 'is not supported by summary headers');
452
+ }
453
+ break;
454
+ case 'action':
455
+ if (
456
+ row.presentation !== undefined &&
457
+ row.presentation !== 'accountSelector'
458
+ ) {
459
+ fail(`${path}.presentation`, 'must be accountSelector when provided');
460
+ }
461
+ assertKey(row.actionKey, `${path}.actionKey`);
462
+ assertText(row.title, `${path}.title`);
463
+ if ((row.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
464
+ fail(
465
+ `${path}.trailing`,
466
+ `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`
467
+ );
468
+ }
469
+ assertTrailingAccessories(row.trailing, `${path}.trailing`);
470
+ break;
471
+ case 'system':
472
+ if (
473
+ !['loading', 'retry', 'noMatch', 'end', 'spacer'].includes(row.variant)
474
+ ) {
475
+ fail(
476
+ `${path}.variant`,
477
+ 'must be loading, retry, noMatch, end, or spacer'
478
+ );
479
+ }
480
+ if (row.variant !== 'spacer') {
481
+ assertText(row.message, `${path}.message`);
482
+ }
483
+ if (
484
+ row.variant === 'spacer' &&
485
+ (row.height < 0 || row.height > MAX_SPACER_HEIGHT)
486
+ ) {
487
+ fail(`${path}.height`, `must be within 0...${MAX_SPACER_HEIGHT}`);
488
+ }
489
+ if (row.variant === 'retry') {
490
+ assertKey(row.actionKey, `${path}.actionKey`);
491
+ }
492
+ break;
493
+ }
494
+ }
495
+
496
+ function assertGroups(rows: readonly RowModel[]): void {
497
+ let index = 0;
498
+ while (index < rows.length) {
499
+ const groupId = rows[index]?.groupId;
500
+ if (!groupId) {
501
+ index += 1;
502
+ continue;
503
+ }
504
+ const start = index;
505
+ while (index < rows.length && rows[index]?.groupId === groupId) index += 1;
506
+ const group = rows.slice(start, index);
507
+ const expected =
508
+ group.length === 1
509
+ ? ['single']
510
+ : group.map((_, groupIndex) =>
511
+ groupIndex === 0
512
+ ? 'first'
513
+ : groupIndex === group.length - 1
514
+ ? 'last'
515
+ : 'middle'
516
+ );
517
+ group.forEach((row, groupIndex) => {
518
+ if (row.groupPosition !== expected[groupIndex]) {
519
+ fail(
520
+ `rows[${start + groupIndex}].groupPosition`,
521
+ `must be "${expected[groupIndex]}"`
522
+ );
523
+ }
524
+ });
525
+ if (rows.slice(index).some((row) => row.groupId === groupId)) {
526
+ fail(`rows[${start}].groupId`, `group "${groupId}" must be contiguous`);
527
+ }
528
+ }
529
+ }
530
+
531
+ export function validateSnapshot(
532
+ snapshot: NativeListSnapshot
533
+ ): NativeListSnapshot {
534
+ assertPlainSerializable(snapshot, 'snapshot', new Set<object>());
535
+ if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
536
+ if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0) {
537
+ fail('snapshot.generation', 'must be a non-negative safe integer');
538
+ }
539
+ if (snapshot.layout.kind === 'grid' && !snapshot.layout.gridColumns) {
540
+ fail('snapshot.layout.gridColumns', 'is required for grid layout');
541
+ }
542
+ assertNonNegativeNumber(
543
+ snapshot.layout.contentPadding,
544
+ 'snapshot.layout.contentPadding'
545
+ );
546
+ assertNonNegativeNumber(
547
+ snapshot.layout.contentPaddingHorizontal,
548
+ 'snapshot.layout.contentPaddingHorizontal'
549
+ );
550
+ assertNonNegativeNumber(
551
+ snapshot.layout.contentPaddingTop,
552
+ 'snapshot.layout.contentPaddingTop'
553
+ );
554
+ assertNonNegativeNumber(
555
+ snapshot.layout.contentPaddingBottom,
556
+ 'snapshot.layout.contentPaddingBottom'
557
+ );
558
+ assertNonNegativeNumber(
559
+ snapshot.layout.itemSpacing,
560
+ 'snapshot.layout.itemSpacing'
561
+ );
562
+ if (
563
+ snapshot.capabilities?.endReachedThreshold !== undefined &&
564
+ (snapshot.capabilities.endReachedThreshold < 0 ||
565
+ snapshot.capabilities.endReachedThreshold > 1)
566
+ ) {
567
+ fail('snapshot.capabilities.endReachedThreshold', 'must be within 0...1');
568
+ }
569
+ const sectionIndex = snapshot.capabilities?.sectionIndex;
570
+ if (sectionIndex !== undefined) {
571
+ if (typeof sectionIndex.enabled !== 'boolean') {
572
+ fail('snapshot.capabilities.sectionIndex.enabled', 'must be a boolean');
573
+ }
574
+ if (
575
+ sectionIndex.hapticsEnabled !== undefined &&
576
+ typeof sectionIndex.hapticsEnabled !== 'boolean'
577
+ ) {
578
+ fail(
579
+ 'snapshot.capabilities.sectionIndex.hapticsEnabled',
580
+ 'must be a boolean'
581
+ );
582
+ }
583
+ if (
584
+ sectionIndex.enabled &&
585
+ (snapshot.layout.kind !== 'sectioned' ||
586
+ snapshot.layout.orientation === 'horizontal')
587
+ ) {
588
+ fail(
589
+ 'snapshot.capabilities.sectionIndex',
590
+ 'requires a vertical sectioned layout'
591
+ );
592
+ }
593
+ }
594
+
595
+ const rowKeys = new Set<string>();
596
+ const sectionIndexTitles = new Set<string>();
597
+ snapshot.rows.forEach((row, index) => {
598
+ assertRow(row, index);
599
+ if (rowKeys.has(row.key))
600
+ fail(`rows[${index}].key`, `duplicate key "${row.key}"`);
601
+ rowKeys.add(row.key);
602
+ if (row.type === 'sectionHeader' && row.indexTitle !== undefined) {
603
+ const normalizedTitle = row.indexTitle.normalize('NFC');
604
+ if (sectionIndexTitles.has(normalizedTitle)) {
605
+ fail(
606
+ `rows[${index}].indexTitle`,
607
+ `duplicate index title "${row.indexTitle}"`
608
+ );
609
+ }
610
+ sectionIndexTitles.add(normalizedTitle);
611
+ }
612
+ });
613
+ if (snapshot.emptyState) assertRow(snapshot.emptyState, 0, 'emptyState');
614
+ if (snapshot.fixedFooter) assertRow(snapshot.fixedFooter, 0, 'fixedFooter');
615
+ assertGroups(snapshot.rows);
616
+
617
+ const selectedKeys = new Set<string>();
618
+ snapshot.selection?.selectedKeys.forEach((key, index) => {
619
+ assertKey(key, `snapshot.selection.selectedKeys[${index}]`);
620
+ if (!rowKeys.has(key))
621
+ fail('snapshot.selection.selectedKeys', `unknown row key "${key}"`);
622
+ const selectedRow = snapshot.rows.find((row) => row.key === key);
623
+ if (
624
+ selectedRow?.disabled ||
625
+ selectedRow?.type === 'sectionHeader' ||
626
+ selectedRow?.type === 'action' ||
627
+ selectedRow?.type === 'system'
628
+ ) {
629
+ fail('snapshot.selection.selectedKeys', `row "${key}" is not selectable`);
630
+ }
631
+ if (selectedKeys.has(key))
632
+ fail('snapshot.selection.selectedKeys', `duplicate key "${key}"`);
633
+ selectedKeys.add(key);
634
+ });
635
+ if (snapshot.selection?.mode === 'none' && selectedKeys.size > 0) {
636
+ fail(
637
+ 'snapshot.selection.selectedKeys',
638
+ 'must be empty when selection mode is none'
639
+ );
640
+ }
641
+ if (snapshot.selection?.mode === 'single' && selectedKeys.size > 1) {
642
+ fail(
643
+ 'snapshot.selection.selectedKeys',
644
+ 'supports at most one key in single mode'
645
+ );
646
+ }
647
+ return snapshot;
648
+ }
649
+
650
+ function assertPatchChanges(patch: RowPatch, index: number): void {
651
+ const path = `patches[${index}].changes`;
652
+ if (
653
+ patch.changes.revision !== undefined &&
654
+ (!Number.isSafeInteger(patch.changes.revision) ||
655
+ patch.changes.revision < 0)
656
+ ) {
657
+ fail(`${path}.revision`, 'must be a non-negative safe integer');
658
+ }
659
+
660
+ switch (patch.type) {
661
+ case 'identity':
662
+ assertText(patch.changes.title, `${path}.title`);
663
+ assertText(patch.changes.subtitle, `${path}.subtitle`);
664
+ assertText(patch.changes.tertiary, `${path}.tertiary`);
665
+ if (
666
+ patch.changes.tertiaryTone !== undefined &&
667
+ !['secondary', 'info'].includes(patch.changes.tertiaryTone)
668
+ ) {
669
+ fail(`${path}.tertiaryTone`, 'must be secondary or info');
670
+ }
671
+ assertLeadingVisual(patch.changes.leading, `${path}.leading`);
672
+ assertTrailingAccessories(
673
+ patch.changes.leadingAction ? [patch.changes.leadingAction] : undefined,
674
+ `${path}.leadingAction`
675
+ );
676
+ if ((patch.changes.badges?.length ?? 0) > MAX_BADGES) {
677
+ fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
678
+ }
679
+ if ((patch.changes.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
680
+ fail(
681
+ `${path}.trailing`,
682
+ `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`
683
+ );
684
+ }
685
+ assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
686
+ break;
687
+ case 'rail':
688
+ assertText(patch.changes.title, `${path}.title`);
689
+ assertLeadingVisual(patch.changes.visual, `${path}.visual`);
690
+ break;
691
+ case 'activity':
692
+ assertText(patch.changes.title, `${path}.title`);
693
+ assertText(patch.changes.description, `${path}.description`);
694
+ assertText(patch.changes.status, `${path}.status`);
695
+ assertText(patch.changes.primaryAmount, `${path}.primaryAmount`);
696
+ assertText(patch.changes.secondaryAmount, `${path}.secondaryAmount`);
697
+ assertLeadingVisual(patch.changes.leading, `${path}.leading`);
698
+ assertLeadingVisual(
699
+ patch.changes.secondaryLeading,
700
+ `${path}.secondaryLeading`
701
+ );
702
+ if ((patch.changes.footerActions?.length ?? 0) > MAX_FOOTER_ACTIONS) {
703
+ fail(
704
+ `${path}.footerActions`,
705
+ `supports at most ${MAX_FOOTER_ACTIONS} actions`
706
+ );
707
+ }
708
+ break;
709
+ case 'message':
710
+ assertText(patch.changes.title, `${path}.title`);
711
+ assertText(patch.changes.body, `${path}.body`);
712
+ assertText(patch.changes.time, `${path}.time`);
713
+ if (
714
+ patch.changes.bodyLines !== undefined &&
715
+ (patch.changes.bodyLines < 1 || patch.changes.bodyLines > 3)
716
+ ) {
717
+ fail(`${path}.bodyLines`, 'must be within 1...3');
718
+ }
719
+ assertImage(patch.changes.thumbnail, `${path}.thumbnail`);
720
+ break;
721
+ case 'dataRow':
722
+ if (
723
+ patch.changes.columns &&
724
+ (patch.changes.columns.length < 2 || patch.changes.columns.length > 4)
725
+ ) {
726
+ fail(`${path}.columns`, 'must contain 2...4 columns');
727
+ }
728
+ patch.changes.columns?.forEach((column, columnIndex) => {
729
+ assertKey(column.key, `${path}.columns[${columnIndex}].key`);
730
+ assertText(column.text, `${path}.columns[${columnIndex}].text`);
731
+ assertText(
732
+ column.secondaryText,
733
+ `${path}.columns[${columnIndex}].secondaryText`
734
+ );
735
+ assertTextTone(
736
+ column.secondaryTone,
737
+ `${path}.columns[${columnIndex}].secondaryTone`
738
+ );
739
+ });
740
+ if ((patch.changes.badges?.length ?? 0) > MAX_BADGES) {
741
+ fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
742
+ }
743
+ break;
744
+ case 'mediaTile':
745
+ assertImage(patch.changes.image, `${path}.image`);
746
+ if (
747
+ patch.changes.imageState !== undefined &&
748
+ !['empty', 'error'].includes(patch.changes.imageState)
749
+ ) {
750
+ fail(`${path}.imageState`, 'must be empty or error when provided');
751
+ }
752
+ assertImage(patch.changes.networkImage, `${path}.networkImage`);
753
+ assertText(patch.changes.title, `${path}.title`);
754
+ assertText(patch.changes.subtitle, `${path}.subtitle`);
755
+ break;
756
+ case 'metricCard':
757
+ assertText(patch.changes.title, `${path}.title`);
758
+ assertText(patch.changes.value, `${path}.value`);
759
+ assertText(patch.changes.subtitle, `${path}.subtitle`);
760
+ assertText(patch.changes.trend, `${path}.trend`);
761
+ assertLeadingVisual(patch.changes.visual, `${path}.visual`);
762
+ if (
763
+ patch.changes.variant !== undefined &&
764
+ !['standard', 'activity', 'performance'].includes(patch.changes.variant)
765
+ ) {
766
+ fail(`${path}.variant`, 'must be standard, activity, or performance');
767
+ }
768
+ if (
769
+ patch.changes.metrics &&
770
+ (patch.changes.metrics.length < 2 || patch.changes.metrics.length > 5)
771
+ ) {
772
+ fail(`${path}.metrics`, 'must contain 2...5 metrics');
773
+ }
774
+ patch.changes.metrics?.forEach((metric, metricIndex) => {
775
+ assertKey(metric.key, `${path}.metrics[${metricIndex}].key`);
776
+ assertText(metric.label, `${path}.metrics[${metricIndex}].label`);
777
+ assertText(metric.value, `${path}.metrics[${metricIndex}].value`);
778
+ assertTextTone(metric.tone, `${path}.metrics[${metricIndex}].tone`);
779
+ assertLeadingVisual(
780
+ metric.visual,
781
+ `${path}.metrics[${metricIndex}].visual`
782
+ );
783
+ });
784
+ if (
785
+ patch.changes.progress !== undefined &&
786
+ (patch.changes.progress < 0 || patch.changes.progress > 1)
787
+ ) {
788
+ fail(`${path}.progress`, 'must be within 0...1');
789
+ }
790
+ break;
791
+ case 'sectionHeader':
792
+ assertSectionHeaderVariant(patch.changes.variant, `${path}.variant`);
793
+ assertText(patch.changes.title, `${path}.title`);
794
+ assertText(patch.changes.subtitle, `${path}.subtitle`);
795
+ assertText(patch.changes.value, `${path}.value`);
796
+ if (patch.changes.valueActionKey !== undefined) {
797
+ assertKey(patch.changes.valueActionKey, `${path}.valueActionKey`);
798
+ }
799
+ assertTrailingAccessories(
800
+ patch.changes.titleIcon ? [patch.changes.titleIcon] : undefined,
801
+ `${path}.titleIcon`
802
+ );
803
+ assertTrailingAccessories(
804
+ patch.changes.valueIcon ? [patch.changes.valueIcon] : undefined,
805
+ `${path}.valueIcon`
806
+ );
807
+ break;
808
+ case 'action':
809
+ assertText(patch.changes.title, `${path}.title`);
810
+ if ((patch.changes.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
811
+ fail(
812
+ `${path}.trailing`,
813
+ `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`
814
+ );
815
+ }
816
+ assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
817
+ break;
818
+ case 'system':
819
+ assertText(patch.changes.message, `${path}.message`);
820
+ break;
821
+ default:
822
+ fail(`patches[${index}].type`, 'is not supported');
823
+ }
824
+ }
825
+
826
+ export function validatePatches(
827
+ patches: readonly RowPatch[]
828
+ ): readonly RowPatch[] {
829
+ assertPlainSerializable(patches, 'patches', new Set<object>());
830
+ const keys = new Set<string>();
831
+ patches.forEach((patch, index) => {
832
+ assertKey(patch.key, `patches[${index}].key`);
833
+ if (keys.has(patch.key))
834
+ fail(`patches[${index}].key`, `duplicate key "${patch.key}"`);
835
+ keys.add(patch.key);
836
+ if (Object.keys(patch.changes).length === 0) {
837
+ fail(`patches[${index}].changes`, 'must contain at least one field');
838
+ }
839
+ assertPatchChanges(patch, index);
840
+ });
841
+ return patches;
842
+ }
843
+
844
+ export function applyRowPatches(
845
+ snapshot: NativeListSnapshot,
846
+ patches: readonly RowPatch[]
847
+ ): NativeListSnapshot {
848
+ validatePatches(patches);
849
+ const patchByKey = new Map(patches.map((patch) => [patch.key, patch]));
850
+ const knownKeys = new Set(snapshot.rows.map((row) => row.key));
851
+ patches.forEach((patch) => {
852
+ if (!knownKeys.has(patch.key))
853
+ fail('patches', `unknown row key "${patch.key}"`);
854
+ });
855
+ const rows = snapshot.rows.map((row): RowModel => {
856
+ const patch = patchByKey.get(row.key);
857
+ if (!patch) return row;
858
+ if (patch.type !== row.type) {
859
+ fail('patches', `row "${row.key}" is ${row.type}, not ${patch.type}`);
860
+ }
861
+ return {
862
+ ...row,
863
+ ...patch.changes,
864
+ key: row.key,
865
+ type: row.type,
866
+ } as RowModel;
867
+ });
868
+ return validateSnapshot({ ...snapshot, rows });
869
+ }
870
+
871
+ export function serializeSnapshot(snapshot: NativeListSnapshot): string {
872
+ return JSON.stringify(validateSnapshot(snapshot));
873
+ }
874
+
875
+ export function serializePatches(patches: readonly RowPatch[]): string {
876
+ return JSON.stringify(validatePatches(patches));
877
+ }