@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.250

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 (130) hide show
  1. package/README.md +21 -5
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
  5. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
  6. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
  16. package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
  18. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
  19. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
  20. package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
  21. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
  22. package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
  23. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
  24. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
  25. package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
  26. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
  27. package/docs/DESIGN.md +518 -9
  28. package/docs/MIGRATION_BASELINE.md +117 -0
  29. package/docs/ROW_TEMPLATES.md +359 -0
  30. package/docs/SPEC.md +358 -0
  31. package/docs/STYLE_SPEC.md +978 -0
  32. package/docs/images/list-capabilities.svg +39 -0
  33. package/docs/images/row-action.svg +17 -0
  34. package/docs/images/row-activity.svg +24 -0
  35. package/docs/images/row-data.svg +24 -0
  36. package/docs/images/row-identity.svg +25 -0
  37. package/docs/images/row-market.svg +23 -0
  38. package/docs/images/row-media-tile.svg +26 -0
  39. package/docs/images/row-message.svg +20 -0
  40. package/docs/images/row-metric-card.svg +33 -0
  41. package/docs/images/row-rail.svg +18 -0
  42. package/docs/images/row-section-header.svg +19 -0
  43. package/docs/images/row-style-regions.svg +30 -0
  44. package/docs/images/row-system.svg +30 -0
  45. package/docs/images/row-wallet-group.svg +26 -0
  46. package/ios/NativeListAccessoryStack.swift +680 -0
  47. package/ios/NativeListActionRenderer.swift +108 -0
  48. package/ios/NativeListActivityRenderer.swift +179 -0
  49. package/ios/NativeListDataRowRenderer.swift +444 -0
  50. package/ios/NativeListIdentityRenderer.swift +685 -0
  51. package/ios/NativeListImageSlot.swift +140 -0
  52. package/ios/NativeListLeadingVisual.swift +372 -0
  53. package/ios/NativeListMarketRenderer.swift +698 -0
  54. package/ios/NativeListMediaTileRenderer.swift +184 -0
  55. package/ios/NativeListMessageRenderer.swift +168 -0
  56. package/ios/NativeListMetricCardRenderer.swift +420 -0
  57. package/ios/NativeListModels.swift +32 -0
  58. package/ios/NativeListRailRenderer.swift +152 -0
  59. package/ios/NativeListRendererCell.swift +311 -0
  60. package/ios/NativeListRendererRegistry.swift +50 -0
  61. package/ios/NativeListResolvedText.swift +122 -0
  62. package/ios/NativeListRowHost.swift +63 -0
  63. package/ios/NativeListSectionHeaderRenderer.swift +708 -0
  64. package/ios/NativeListSystemRenderer.swift +568 -0
  65. package/ios/NativeListTextLabel.swift +42 -0
  66. package/ios/NativeListTextStyles.swift +236 -0
  67. package/ios/NativeListWalletGroupRenderer.swift +255 -0
  68. package/ios/RNCNativeListView.swift +111 -180
  69. package/lib/module/models.js +54 -0
  70. package/lib/module/validation.js +324 -26
  71. package/lib/module/web/NativeListWebEngine.js +148 -936
  72. package/lib/module/web/templates/ActionRowRenderer.js +97 -0
  73. package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
  74. package/lib/module/web/templates/DataRowRenderer.js +145 -0
  75. package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
  76. package/lib/module/web/templates/MarketRowRenderer.js +310 -0
  77. package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
  78. package/lib/module/web/templates/MessageRowRenderer.js +165 -0
  79. package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
  80. package/lib/module/web/templates/RailRowRenderer.js +98 -0
  81. package/lib/module/web/templates/RowContainerStyle.js +100 -0
  82. package/lib/module/web/templates/RowElements.js +93 -0
  83. package/lib/module/web/templates/RowRendererRegistry.js +70 -0
  84. package/lib/module/web/templates/RowText.js +121 -0
  85. package/lib/module/web/templates/RowVisual.js +43 -0
  86. package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
  87. package/lib/module/web/templates/SystemRowRenderer.js +161 -0
  88. package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
  89. package/lib/typescript/src/models.d.ts +166 -13
  90. package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
  91. package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
  92. package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
  93. package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
  94. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
  95. package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
  96. package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
  97. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
  98. package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
  99. package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
  100. package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
  101. package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
  102. package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
  103. package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
  104. package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
  105. package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
  106. package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
  107. package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
  108. package/package.json +3 -3
  109. package/src/models.ts +281 -19
  110. package/src/validation.ts +501 -46
  111. package/src/web/NativeListWebEngine.ts +361 -1932
  112. package/src/web/templates/ActionRowRenderer.ts +123 -0
  113. package/src/web/templates/ActivityRowRenderer.ts +159 -0
  114. package/src/web/templates/DataRowRenderer.ts +212 -0
  115. package/src/web/templates/IdentityRowRenderer.ts +527 -0
  116. package/src/web/templates/MarketRowRenderer.ts +467 -0
  117. package/src/web/templates/MediaTileRowRenderer.ts +182 -0
  118. package/src/web/templates/MessageRowRenderer.ts +306 -0
  119. package/src/web/templates/MetricCardRowRenderer.ts +337 -0
  120. package/src/web/templates/RailRowRenderer.ts +136 -0
  121. package/src/web/templates/RowContainerStyle.ts +131 -0
  122. package/src/web/templates/RowElements.ts +160 -0
  123. package/src/web/templates/RowRendererRegistry.ts +111 -0
  124. package/src/web/templates/RowText.ts +181 -0
  125. package/src/web/templates/RowVisual.ts +115 -0
  126. package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
  127. package/src/web/templates/SystemRowRenderer.ts +282 -0
  128. package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
  129. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
  130. package/ios/NativeListCell.swift +0 -4300
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
+ import { ROW_BOX_STYLE_KEYS_BY_TYPE } from "./models.js";
3
4
  const MAX_KEY_LENGTH = 256;
4
5
  const MAX_TEXT_LENGTH = 4096;
5
6
  const MAX_BADGES = 2;
@@ -68,8 +69,90 @@ function assertBoundedStyleNumber(value, path, min, max) {
68
69
  fail(path, `must be within ${min}...${max}`);
69
70
  }
70
71
  }
71
- function assertMarketTextStyle(style, path) {
72
- if (!style) return;
72
+
73
+ /**
74
+ * The named typography scale from docs/STYLE_SPEC.md §3.2. Tokens are resolved
75
+ * to numbers here, before serialization, so the iOS, Android, and Web renderers
76
+ * never learn the vocabulary and cannot drift from it.
77
+ */
78
+ const TYPOGRAPHY_TOKENS = {
79
+ $headingXl: {
80
+ fontSize: 24,
81
+ lineHeight: 32,
82
+ fontWeight: 'semibold'
83
+ },
84
+ $headingLg: {
85
+ fontSize: 20,
86
+ lineHeight: 28,
87
+ fontWeight: 'semibold'
88
+ },
89
+ $headingMd: {
90
+ fontSize: 18,
91
+ lineHeight: 24,
92
+ fontWeight: 'semibold'
93
+ },
94
+ $headingSm: {
95
+ fontSize: 16,
96
+ lineHeight: 24,
97
+ fontWeight: 'medium'
98
+ },
99
+ $headingXs: {
100
+ fontSize: 14,
101
+ lineHeight: 20,
102
+ fontWeight: 'semibold'
103
+ },
104
+ $bodyLg: {
105
+ fontSize: 16,
106
+ lineHeight: 24,
107
+ fontWeight: 'regular'
108
+ },
109
+ $bodyMd: {
110
+ fontSize: 14,
111
+ lineHeight: 20,
112
+ fontWeight: 'regular'
113
+ },
114
+ $bodySm: {
115
+ fontSize: 12,
116
+ lineHeight: 16,
117
+ fontWeight: 'regular'
118
+ },
119
+ $bodyXs: {
120
+ fontSize: 11,
121
+ lineHeight: 16,
122
+ fontWeight: 'regular'
123
+ }
124
+ };
125
+
126
+ /** Style keys name model fields, never views. See docs/STYLE_SPEC.md §4. */
127
+ const TEXT_STYLE_KEYS_BY_ROW_TYPE = {
128
+ identity: ['title', 'subtitle', 'tertiary', 'badge', 'value', 'valueSecondary'],
129
+ walletGroup: [],
130
+ rail: ['title', 'badge', 'status'],
131
+ activity: ['title', 'description', 'status', 'primaryAmount', 'secondaryAmount'],
132
+ message: ['title', 'body', 'time'],
133
+ dataRow: ['columns', 'columnSecondary', 'index'],
134
+ market: ['title', 'subtitle', 'price', 'change'],
135
+ mediaTile: ['title', 'subtitle', 'badge'],
136
+ metricCard: ['title', 'value', 'subtitle', 'trend'],
137
+ sectionHeader: ['title', 'subtitle', 'value'],
138
+ action: ['title', 'value'],
139
+ system: ['title', 'message', 'actionText']
140
+ };
141
+ const EXTRA_STYLE_KEYS_BY_ROW_TYPE = {
142
+ market: ['titleBadgeLayout', 'contentTrailingGap', 'subtitleTrailingPadding', 'changeWidth', 'changeHeight', 'changeCornerRadius']
143
+ };
144
+ function assertTextStyle(style, path, additionalKeys = []) {
145
+ if (style === undefined) return;
146
+ if (typeof style !== 'object' || style === null || Array.isArray(style)) {
147
+ fail(path, 'must be an object');
148
+ }
149
+ assertUnknownKeys(style, ['token', 'fontSize', 'fontWeight', 'color', 'lineHeight', 'lines', 'truncate', 'alignment', 'verticalAlignment', 'offsetY', ...additionalKeys], path, 'text style key');
150
+ if (style.color !== undefined && (typeof style.color !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(style.color))) {
151
+ fail(`${path}.color`, 'must be #RRGGBB or #RRGGBBAA');
152
+ }
153
+ if (style.token !== undefined && !Object.prototype.hasOwnProperty.call(TYPOGRAPHY_TOKENS, style.token)) {
154
+ fail(`${path}.token`, `must be one of ${Object.keys(TYPOGRAPHY_TOKENS).join(', ')}`);
155
+ }
73
156
  assertBoundedStyleNumber(style.fontSize, `${path}.fontSize`, 8, 48);
74
157
  assertBoundedStyleNumber(style.lineHeight, `${path}.lineHeight`, 8, 64);
75
158
  if (style.fontWeight !== undefined && !['regular', 'medium', 'semibold', 'bold'].includes(style.fontWeight)) {
@@ -78,36 +161,191 @@ function assertMarketTextStyle(style, path) {
78
161
  if (style.alignment !== undefined && !['start', 'center', 'end'].includes(style.alignment)) {
79
162
  fail(`${path}.alignment`, 'must be start, center, or end');
80
163
  }
81
- if (style.lines !== undefined && ![1, 2].includes(style.lines)) {
82
- fail(`${path}.lines`, 'must be 1 or 2');
164
+ if (style.lines !== undefined && ![1, 2, 3].includes(style.lines)) {
165
+ fail(`${path}.lines`, 'must be 1, 2, or 3');
166
+ }
167
+ if (style.truncate !== undefined && !['tail', 'clip'].includes(style.truncate)) {
168
+ fail(`${path}.truncate`, 'must be tail or clip');
169
+ }
170
+ if (style.verticalAlignment !== undefined && !['top', 'center', 'bottom'].includes(style.verticalAlignment)) {
171
+ fail(`${path}.verticalAlignment`, 'must be top, center, or bottom');
172
+ }
173
+ assertBoundedStyleNumber(style.offsetY, `${path}.offsetY`, -8, 8);
174
+ }
175
+ function assertBoxStyle(style, path) {
176
+ const container = style.container;
177
+ if (container !== undefined) {
178
+ const containerPath = `${path}.container`;
179
+ if (typeof container !== 'object' || container === null || Array.isArray(container)) {
180
+ fail(containerPath, 'must be an object');
181
+ }
182
+ assertUnknownKeys(container, ['height', 'backgroundColor', 'opacity', 'cornerRadius', 'borderWidth', 'borderColor', 'contentVerticalAlignment'], containerPath, 'row container style key');
183
+ for (const key of ['backgroundColor', 'borderColor']) {
184
+ const value = container[key];
185
+ if (value !== undefined && (typeof value !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value))) {
186
+ fail(`${containerPath}.${key}`, 'must be #RRGGBB or #RRGGBBAA');
187
+ }
188
+ }
189
+ assertBoundedStyleNumber(container.height, `${containerPath}.height`, 0, 4096);
190
+ assertBoundedStyleNumber(container.opacity, `${containerPath}.opacity`, 0, 1);
191
+ assertBoundedStyleNumber(container.cornerRadius, `${containerPath}.cornerRadius`, 0, 80);
192
+ assertBoundedStyleNumber(container.borderWidth, `${containerPath}.borderWidth`, 0, 8);
193
+ if (container.contentVerticalAlignment !== undefined && !['top', 'center', 'bottom'].includes(container.contentVerticalAlignment)) {
194
+ fail(`${containerPath}.contentVerticalAlignment`, 'must be top, center, or bottom');
195
+ }
196
+ }
197
+ for (const field of ['horizontalPadding', 'verticalPadding', 'leadingGap', 'titleBadgeGap', 'trailingGap']) {
198
+ assertBoundedStyleNumber(style[field], `${path}.${field}`, 0, 64);
199
+ }
200
+ assertBoundedStyleNumber(style.lineGap, `${path}.lineGap`, 0, 16);
201
+ if (style.image !== undefined) {
202
+ if (typeof style.image !== 'object' || style.image === null || Array.isArray(style.image)) {
203
+ fail(`${path}.image`, 'must be an object');
204
+ }
205
+ assertUnknownKeys(style.image, ['width', 'height', 'shape', 'cornerRadius', 'contentFit'], `${path}.image`, 'image style key');
206
+ assertBoundedStyleNumber(style.image.width, `${path}.image.width`, 1, 160);
207
+ assertBoundedStyleNumber(style.image.height, `${path}.image.height`, 1, 160);
208
+ assertBoundedStyleNumber(style.image.cornerRadius, `${path}.image.cornerRadius`, 0, 80);
209
+ assertVisualShape(style.image.shape, `${path}.image.shape`);
210
+ if (style.image.contentFit !== undefined && !['cover', 'contain', 'fill', 'center'].includes(style.image.contentFit)) {
211
+ fail(`${path}.image.contentFit`, 'must be cover, contain, fill, or center');
212
+ }
83
213
  }
84
214
  }
85
215
  function assertMarketStyle(style, path) {
86
216
  if (!style) return;
87
- for (const field of ['horizontalPadding', 'verticalPadding', 'leadingGap', 'titleBadgeGap', 'trailingGap', 'contentTrailingGap', 'subtitleTrailingPadding']) {
217
+ assertBoxStyle(style, path);
218
+ for (const field of ['contentTrailingGap', 'subtitleTrailingPadding']) {
88
219
  assertBoundedStyleNumber(style[field], `${path}.${field}`, 0, 64);
89
220
  }
90
221
  // OneKey patch: validate the opt-in Market badge layout.
91
222
  if (style.titleBadgeLayout !== undefined && style.titleBadgeLayout !== 'inline') {
92
223
  fail(`${path}.titleBadgeLayout`, 'must be inline when provided');
93
224
  }
94
- assertBoundedStyleNumber(style.lineGap, `${path}.lineGap`, 0, 16);
95
225
  assertBoundedStyleNumber(style.changeWidth, `${path}.changeWidth`, 1, 160);
96
226
  assertBoundedStyleNumber(style.changeHeight, `${path}.changeHeight`, 1, 160);
97
227
  assertBoundedStyleNumber(style.changeCornerRadius, `${path}.changeCornerRadius`, 0, 80);
98
- if (style.image) {
99
- assertBoundedStyleNumber(style.image.width, `${path}.image.width`, 1, 160);
100
- assertBoundedStyleNumber(style.image.height, `${path}.image.height`, 1, 160);
101
- assertBoundedStyleNumber(style.image.cornerRadius, `${path}.image.cornerRadius`, 0, 80);
102
- assertVisualShape(style.image.shape, `${path}.image.shape`);
103
- if (style.image.contentFit !== undefined && !['cover', 'contain', 'fill', 'center'].includes(style.image.contentFit)) {
104
- fail(`${path}.image.contentFit`, 'must be cover, contain, fill, or center');
228
+ assertTextStyle(style.title, `${path}.title`);
229
+ assertTextStyle(style.subtitle, `${path}.subtitle`);
230
+ assertTextStyle(style.price, `${path}.price`);
231
+ assertTextStyle(style.change, `${path}.change`);
232
+ }
233
+
234
+ /**
235
+ * Rejects keys the template does not declare, so a style written for one
236
+ * template cannot reach a shared view through another. See docs/STYLE_SPEC.md
237
+ * §7 rule 1.
238
+ */
239
+ function assertRowStyle(row, path) {
240
+ const style = row.style;
241
+ if (style === undefined) return;
242
+ if (typeof style !== 'object' || style === null || Array.isArray(style)) {
243
+ fail(`${path}.style`, 'must be an object');
244
+ }
245
+ const textKeys = TEXT_STYLE_KEYS_BY_ROW_TYPE[row.type] ?? [];
246
+ const allowed = new Set(['container', ...ROW_BOX_STYLE_KEYS_BY_TYPE[row.type], ...textKeys, ...(EXTRA_STYLE_KEYS_BY_ROW_TYPE[row.type] ?? [])]);
247
+ Object.keys(style).forEach(key => {
248
+ if (!allowed.has(key)) {
249
+ fail(`${path}.style.${key}`, `is not a style key of the "${row.type}" template`);
105
250
  }
251
+ });
252
+ // Market keeps its own richer assertions, driven from assertMarketRow.
253
+ if (row.type === 'market') return;
254
+ assertBoxStyle(style, `${path}.style`);
255
+ const slots = style;
256
+ textKeys.forEach(key => assertTextStyle(slots[key], `${path}.style.${key}`));
257
+ }
258
+ const LIST_STYLE_KEYS = ['separator', 'groupCornerRadius'];
259
+ const SEPARATOR_STYLE_KEYS = ['inset', 'color'];
260
+ function assertUnknownKeys(value, allowed, path, subject) {
261
+ Object.keys(value).forEach(key => {
262
+ if (!allowed.includes(key)) fail(`${path}.${key}`, `is not a ${subject}`);
263
+ });
264
+ }
265
+ function assertListStyle(listStyle, path) {
266
+ if (listStyle === undefined) return;
267
+ if (typeof listStyle !== 'object' || listStyle === null || Array.isArray(listStyle)) {
268
+ fail(path, 'must be an object');
269
+ }
270
+ assertUnknownKeys(listStyle, LIST_STYLE_KEYS, path, 'list style key');
271
+ assertBoundedStyleNumber(listStyle.groupCornerRadius, `${path}.groupCornerRadius`, 0, 40);
272
+ const separator = listStyle.separator;
273
+ if (separator === undefined) return;
274
+ if (typeof separator !== 'object' || separator === null || Array.isArray(separator)) {
275
+ fail(`${path}.separator`, 'must be an object');
276
+ }
277
+ assertUnknownKeys(separator, SEPARATOR_STYLE_KEYS, `${path}.separator`, 'separator style key');
278
+ assertBoundedStyleNumber(separator.inset, `${path}.separator.inset`, 0, 64);
279
+ // Same format as every row style color; renderers parse it directly.
280
+ if (separator.color !== undefined && (typeof separator.color !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(separator.color))) {
281
+ fail(`${path}.separator.color`, 'must be #RRGGBB or #RRGGBBAA');
106
282
  }
107
- assertMarketTextStyle(style.title, `${path}.title`);
108
- assertMarketTextStyle(style.subtitle, `${path}.subtitle`);
109
- assertMarketTextStyle(style.price, `${path}.price`);
110
- assertMarketTextStyle(style.change, `${path}.change`);
283
+ }
284
+
285
+ /** Token resolution is idempotent: a resolved style carries no token. */
286
+ function resolveTextStyle(style) {
287
+ if (!style?.token) return style;
288
+ const {
289
+ token,
290
+ ...overrides
291
+ } = style;
292
+ return {
293
+ ...TYPOGRAPHY_TOKENS[token],
294
+ ...overrides
295
+ };
296
+ }
297
+ function resolveStyleTokens(style, rowType) {
298
+ const textKeys = TEXT_STYLE_KEYS_BY_ROW_TYPE[rowType] ?? [];
299
+ let changed = false;
300
+ const next = {
301
+ ...style
302
+ };
303
+ textKeys.forEach(key => {
304
+ const slot = style[key];
305
+ const resolved = resolveTextStyle(slot);
306
+ if (resolved !== slot) {
307
+ next[key] = resolved;
308
+ changed = true;
309
+ }
310
+ });
311
+ return changed ? next : style;
312
+ }
313
+ function normalizeRowStyles(row) {
314
+ // Narrowing a generic does not reach the walletGroup members; go through the
315
+ // union type instead.
316
+ const model = row;
317
+ const style = row.style;
318
+ const nextStyle = style ? resolveStyleTokens(style, row.type) : style;
319
+ const prefix = model.type === 'market' ? model.subtitlePrefix : undefined;
320
+ const nextPrefixStyle = resolveTextStyle(prefix?.style);
321
+ let nextParent;
322
+ let nextChildren;
323
+ if (model.type === 'walletGroup') {
324
+ const parent = normalizeRowStyles(model.parent);
325
+ const children = model.children.map(normalizeRowStyles);
326
+ if (parent !== model.parent || children.some((child, index) => child !== model.children[index])) {
327
+ nextParent = parent;
328
+ nextChildren = children;
329
+ }
330
+ }
331
+ if (nextStyle === style && nextPrefixStyle === prefix?.style && nextParent === undefined) {
332
+ return row;
333
+ }
334
+ const next = {
335
+ ...row
336
+ };
337
+ if (nextStyle !== style) next.style = nextStyle;
338
+ if (prefix && nextPrefixStyle !== prefix.style) {
339
+ next.subtitlePrefix = {
340
+ ...prefix,
341
+ style: nextPrefixStyle
342
+ };
343
+ }
344
+ if (nextParent !== undefined) {
345
+ next.parent = nextParent;
346
+ next.children = nextChildren;
347
+ }
348
+ return next;
111
349
  }
112
350
  function assertMarketRow(row, path) {
113
351
  if (!['token', 'stock', 'perp'].includes(row.variant)) {
@@ -120,7 +358,7 @@ function assertMarketRow(row, path) {
120
358
  assertText(row.subtitlePrefix.text, `${path}.subtitlePrefix.text`);
121
359
  assertBoundedStyleNumber(row.subtitlePrefix.gap, `${path}.subtitlePrefix.gap`, 0, 64);
122
360
  assertBoundedStyleNumber(row.subtitlePrefix.maxWidth, `${path}.subtitlePrefix.maxWidth`, 1, 320);
123
- assertMarketTextStyle(row.subtitlePrefix.style, `${path}.subtitlePrefix.style`);
361
+ assertTextStyle(row.subtitlePrefix.style, `${path}.subtitlePrefix.style`);
124
362
  }
125
363
  assertText(row.price, `${path}.price`);
126
364
  assertText(row.change.text, `${path}.change.text`);
@@ -159,7 +397,8 @@ function assertMarketRow(row, path) {
159
397
  badgeKeys.add(badge.key);
160
398
  assertText(badge.text, `${badgePath}.text`);
161
399
  // OneKey patch: share typography bounds with Market text styles.
162
- assertMarketTextStyle(badge.style, `${badgePath}.style`);
400
+ assertTextStyle(badge.style, `${badgePath}.style`, ['height', 'horizontalPadding']);
401
+ if (badge.style) assertUnknownKeys(badge.style, ['fontSize', 'fontWeight', 'lineHeight', 'height', 'horizontalPadding'], `${badgePath}.style`, 'Market badge style key');
163
402
  assertBoundedStyleNumber(badge.style?.height, `${badgePath}.style.height`, 1, 64);
164
403
  assertBoundedStyleNumber(badge.style?.horizontalPadding, `${badgePath}.style.horizontalPadding`, 0, 32);
165
404
  if (badge.iconName !== undefined && badge.iconName !== 'verified') {
@@ -309,11 +548,17 @@ function assertWalletGroup(row, path) {
309
548
  keys.add(member.key);
310
549
  });
311
550
  }
551
+ function assertRowType(type, path) {
552
+ if (typeof type !== 'string' || !Object.prototype.hasOwnProperty.call(ROW_BOX_STYLE_KEYS_BY_TYPE, type)) {
553
+ fail(`${path}.type`, 'is not supported');
554
+ }
555
+ }
312
556
  function assertRow(row, index, path = `rows[${index}]`) {
557
+ assertRowType(row.type, path);
313
558
  assertKey(row.key, `${path}.key`);
314
559
  // OneKey patch: explicit dimensions and opacity cannot corrupt list layout.
315
560
  if (row.height !== undefined && (row.height < 0 || row.height > 4096)) fail(`${path}.height`, 'must be within 0...4096');
316
- if (row.heightRounding !== undefined && (row.height === undefined || !['floor', 'nearest'].includes(row.heightRounding))) fail(`${path}.heightRounding`, 'requires an explicit height and must be floor or nearest');
561
+ if (row.heightRounding !== undefined && (row.height === undefined && row.style?.container?.height === undefined || !['floor', 'nearest'].includes(row.heightRounding))) fail(`${path}.heightRounding`, 'requires an explicit height and must be floor or nearest');
317
562
  if (row.opacity !== undefined && (row.opacity < 0 || row.opacity > 1)) fail(`${path}.opacity`, 'must be within 0...1');
318
563
  if (row.groupId && !row.groupPosition) {
319
564
  fail(`${path}.groupPosition`, 'is required when groupId is present');
@@ -321,6 +566,7 @@ function assertRow(row, index, path = `rows[${index}]`) {
321
566
  if (!row.groupId && row.groupPosition) {
322
567
  fail(`${path}.groupId`, 'is required when groupPosition is present');
323
568
  }
569
+ assertRowStyle(row, path);
324
570
  assertVisual(row, path);
325
571
  switch (row.type) {
326
572
  case 'walletGroup':
@@ -364,8 +610,8 @@ function assertRow(row, index, path = `rows[${index}]`) {
364
610
  case 'message':
365
611
  assertText(row.title, `${path}.title`);
366
612
  assertText(row.body, `${path}.body`);
367
- if ((row.bodyLines ?? 3) < 1 || (row.bodyLines ?? 3) > 3) {
368
- fail(`${path}.bodyLines`, 'must be within 1...3');
613
+ if (![1, 2, 3].includes(row.bodyLines ?? 3)) {
614
+ fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
369
615
  }
370
616
  assertImage(row.thumbnail, `${path}.thumbnail`);
371
617
  break;
@@ -532,6 +778,7 @@ export function validateSnapshot(snapshot) {
532
778
  fail('snapshot.capabilities.sectionIndex', 'requires a vertical sectioned layout');
533
779
  }
534
780
  }
781
+ assertListStyle(snapshot.listStyle, 'snapshot.listStyle');
535
782
  const rowKeys = new Set();
536
783
  const sectionIndexTitles = new Set();
537
784
  snapshot.rows.forEach((row, index) => {
@@ -566,10 +813,41 @@ export function validateSnapshot(snapshot) {
566
813
  if (snapshot.selection?.mode === 'single' && selectedKeys.size > 1) {
567
814
  fail('snapshot.selection.selectedKeys', 'supports at most one key in single mode');
568
815
  }
569
- return snapshot;
816
+ return normalizeSnapshotStyles(snapshot);
817
+ }
818
+
819
+ /**
820
+ * Resolves typography tokens to numbers. Returns the original snapshot when
821
+ * nothing needs resolving, so the common no-style path keeps object identity.
822
+ */
823
+ function normalizeSnapshotStyles(snapshot) {
824
+ let changed = false;
825
+ const rows = snapshot.rows.map(row => {
826
+ const next = normalizeRowStyles(row);
827
+ if (next !== row) changed = true;
828
+ return next;
829
+ });
830
+ const emptyState = snapshot.emptyState ? normalizeRowStyles(snapshot.emptyState) : snapshot.emptyState;
831
+ const fixedFooter = snapshot.fixedFooter ? normalizeRowStyles(snapshot.fixedFooter) : snapshot.fixedFooter;
832
+ if (!changed && emptyState === snapshot.emptyState && fixedFooter === snapshot.fixedFooter) {
833
+ return snapshot;
834
+ }
835
+ return {
836
+ ...snapshot,
837
+ rows,
838
+ emptyState,
839
+ fixedFooter
840
+ };
570
841
  }
571
842
  function assertPatchChanges(patch, index) {
572
843
  const path = `patches[${index}].changes`;
844
+ const patchedStyle = patch.changes.style;
845
+ if (patchedStyle !== undefined) {
846
+ assertRowStyle({
847
+ type: patch.type,
848
+ style: patchedStyle
849
+ }, path);
850
+ }
573
851
  // OneKey patch: partial balance updates retain a valid, current accessibility label.
574
852
  if ('accessibilityLabel' in patch.changes) {
575
853
  assertText(patch.changes.accessibilityLabel, `${path}.accessibilityLabel`);
@@ -617,8 +895,8 @@ function assertPatchChanges(patch, index) {
617
895
  assertText(patch.changes.title, `${path}.title`);
618
896
  assertText(patch.changes.body, `${path}.body`);
619
897
  assertText(patch.changes.time, `${path}.time`);
620
- if (patch.changes.bodyLines !== undefined && (patch.changes.bodyLines < 1 || patch.changes.bodyLines > 3)) {
621
- fail(`${path}.bodyLines`, 'must be within 1...3');
898
+ if (patch.changes.bodyLines !== undefined && ![1, 2, 3].includes(patch.changes.bodyLines)) {
899
+ fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
622
900
  }
623
901
  assertImage(patch.changes.thumbnail, `${path}.thumbnail`);
624
902
  break;
@@ -723,7 +1001,27 @@ export function validatePatches(patches) {
723
1001
  }
724
1002
  assertPatchChanges(patch, index);
725
1003
  });
726
- return patches;
1004
+ return normalizePatchStyles(patches);
1005
+ }
1006
+
1007
+ /** Patches reach the native side without passing through a snapshot. */
1008
+ function normalizePatchStyles(patches) {
1009
+ let changed = false;
1010
+ const next = patches.map(patch => {
1011
+ const style = patch.changes.style;
1012
+ if (!style) return patch;
1013
+ const resolved = resolveStyleTokens(style, patch.type);
1014
+ if (resolved === style) return patch;
1015
+ changed = true;
1016
+ return {
1017
+ ...patch,
1018
+ changes: {
1019
+ ...patch.changes,
1020
+ style: resolved
1021
+ }
1022
+ };
1023
+ });
1024
+ return changed ? next : patches;
727
1025
  }
728
1026
  export function applyRowPatches(snapshot, patches) {
729
1027
  validatePatches(patches);