@onekeyfe/react-native-native-list 3.0.157-alpha.248 → 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
package/src/validation.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ROW_BOX_STYLE_KEYS_BY_TYPE } from './models';
1
2
  import type {
2
3
  ActivityRow,
3
4
  IdentityRow,
@@ -5,8 +6,11 @@ import type {
5
6
  LeadingVisual,
6
7
  MarketRow,
7
8
  MarketRowStyle,
8
- MarketTextStyle,
9
+ NativeListListStyle,
9
10
  NativeListSnapshot,
11
+ NativeListTextStyle,
12
+ NativeListTypographyToken,
13
+ RowBoxStyle,
10
14
  RowModel,
11
15
  RowPatch,
12
16
  WalletGroupRow,
@@ -127,11 +131,119 @@ function assertBoundedStyleNumber(
127
131
  }
128
132
  }
129
133
 
130
- function assertMarketTextStyle(
131
- style: MarketTextStyle | undefined,
132
- path: string
134
+ /**
135
+ * The named typography scale from docs/STYLE_SPEC.md §3.2. Tokens are resolved
136
+ * to numbers here, before serialization, so the iOS, Android, and Web renderers
137
+ * never learn the vocabulary and cannot drift from it.
138
+ */
139
+ const TYPOGRAPHY_TOKENS: Readonly<
140
+ Record<
141
+ NativeListTypographyToken,
142
+ Readonly<{
143
+ fontSize: number;
144
+ lineHeight: number;
145
+ fontWeight: NonNullable<NativeListTextStyle['fontWeight']>;
146
+ }>
147
+ >
148
+ > = {
149
+ $headingXl: { fontSize: 24, lineHeight: 32, fontWeight: 'semibold' },
150
+ $headingLg: { fontSize: 20, lineHeight: 28, fontWeight: 'semibold' },
151
+ $headingMd: { fontSize: 18, lineHeight: 24, fontWeight: 'semibold' },
152
+ $headingSm: { fontSize: 16, lineHeight: 24, fontWeight: 'medium' },
153
+ $headingXs: { fontSize: 14, lineHeight: 20, fontWeight: 'semibold' },
154
+ $bodyLg: { fontSize: 16, lineHeight: 24, fontWeight: 'regular' },
155
+ $bodyMd: { fontSize: 14, lineHeight: 20, fontWeight: 'regular' },
156
+ $bodySm: { fontSize: 12, lineHeight: 16, fontWeight: 'regular' },
157
+ $bodyXs: { fontSize: 11, lineHeight: 16, fontWeight: 'regular' },
158
+ };
159
+
160
+ /** Style keys name model fields, never views. See docs/STYLE_SPEC.md §4. */
161
+ const TEXT_STYLE_KEYS_BY_ROW_TYPE: Readonly<
162
+ Record<RowModel['type'], readonly string[]>
163
+ > = {
164
+ identity: [
165
+ 'title',
166
+ 'subtitle',
167
+ 'tertiary',
168
+ 'badge',
169
+ 'value',
170
+ 'valueSecondary',
171
+ ],
172
+ walletGroup: [],
173
+ rail: ['title', 'badge', 'status'],
174
+ activity: [
175
+ 'title',
176
+ 'description',
177
+ 'status',
178
+ 'primaryAmount',
179
+ 'secondaryAmount',
180
+ ],
181
+ message: ['title', 'body', 'time'],
182
+ dataRow: ['columns', 'columnSecondary', 'index'],
183
+ market: ['title', 'subtitle', 'price', 'change'],
184
+ mediaTile: ['title', 'subtitle', 'badge'],
185
+ metricCard: ['title', 'value', 'subtitle', 'trend'],
186
+ sectionHeader: ['title', 'subtitle', 'value'],
187
+ action: ['title', 'value'],
188
+ system: ['title', 'message', 'actionText'],
189
+ };
190
+
191
+ const EXTRA_STYLE_KEYS_BY_ROW_TYPE: Readonly<
192
+ Partial<Record<RowModel['type'], readonly string[]>>
193
+ > = {
194
+ market: [
195
+ 'titleBadgeLayout',
196
+ 'contentTrailingGap',
197
+ 'subtitleTrailingPadding',
198
+ 'changeWidth',
199
+ 'changeHeight',
200
+ 'changeCornerRadius',
201
+ ],
202
+ };
203
+
204
+ function assertTextStyle(
205
+ style: NativeListTextStyle | undefined,
206
+ path: string,
207
+ additionalKeys: readonly string[] = []
133
208
  ): void {
134
- if (!style) return;
209
+ if (style === undefined) return;
210
+ if (typeof style !== 'object' || style === null || Array.isArray(style)) {
211
+ fail(path, 'must be an object');
212
+ }
213
+ assertUnknownKeys(
214
+ style as Record<string, unknown>,
215
+ [
216
+ 'token',
217
+ 'fontSize',
218
+ 'fontWeight',
219
+ 'color',
220
+ 'lineHeight',
221
+ 'lines',
222
+ 'truncate',
223
+ 'alignment',
224
+ 'verticalAlignment',
225
+ 'offsetY',
226
+ ...additionalKeys,
227
+ ],
228
+ path,
229
+ 'text style key'
230
+ );
231
+ if (
232
+ style.color !== undefined &&
233
+ (typeof style.color !== 'string' ||
234
+ !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(style.color))
235
+ ) {
236
+ fail(`${path}.color`, 'must be #RRGGBB or #RRGGBBAA');
237
+ }
238
+ if (
239
+ style.token !== undefined &&
240
+ !Object.prototype.hasOwnProperty.call(TYPOGRAPHY_TOKENS, style.token)
241
+ ) {
242
+ fail(
243
+ `${path}.token`,
244
+ `must be one of ${Object.keys(TYPOGRAPHY_TOKENS).join(', ')}`
245
+ );
246
+ }
135
247
  assertBoundedStyleNumber(style.fontSize, `${path}.fontSize`, 8, 48);
136
248
  assertBoundedStyleNumber(style.lineHeight, `${path}.lineHeight`, 8, 64);
137
249
  if (
@@ -146,44 +258,117 @@ function assertMarketTextStyle(
146
258
  ) {
147
259
  fail(`${path}.alignment`, 'must be start, center, or end');
148
260
  }
149
- if (style.lines !== undefined && ![1, 2].includes(style.lines)) {
150
- fail(`${path}.lines`, 'must be 1 or 2');
261
+ if (style.lines !== undefined && ![1, 2, 3].includes(style.lines)) {
262
+ fail(`${path}.lines`, 'must be 1, 2, or 3');
151
263
  }
264
+ if (
265
+ style.truncate !== undefined &&
266
+ !['tail', 'clip'].includes(style.truncate)
267
+ ) {
268
+ fail(`${path}.truncate`, 'must be tail or clip');
269
+ }
270
+ if (
271
+ style.verticalAlignment !== undefined &&
272
+ !['top', 'center', 'bottom'].includes(style.verticalAlignment)
273
+ ) {
274
+ fail(`${path}.verticalAlignment`, 'must be top, center, or bottom');
275
+ }
276
+ assertBoundedStyleNumber(style.offsetY, `${path}.offsetY`, -8, 8);
152
277
  }
153
278
 
154
- function assertMarketStyle(
155
- style: MarketRowStyle | undefined,
156
- path: string
157
- ): void {
158
- if (!style) return;
279
+ function assertBoxStyle(style: RowBoxStyle, path: string): void {
280
+ const container = style.container;
281
+ if (container !== undefined) {
282
+ const containerPath = `${path}.container`;
283
+ if (
284
+ typeof container !== 'object' ||
285
+ container === null ||
286
+ Array.isArray(container)
287
+ ) {
288
+ fail(containerPath, 'must be an object');
289
+ }
290
+ assertUnknownKeys(
291
+ container as Record<string, unknown>,
292
+ [
293
+ 'height',
294
+ 'backgroundColor',
295
+ 'opacity',
296
+ 'cornerRadius',
297
+ 'borderWidth',
298
+ 'borderColor',
299
+ 'contentVerticalAlignment',
300
+ ],
301
+ containerPath,
302
+ 'row container style key'
303
+ );
304
+ for (const key of ['backgroundColor', 'borderColor'] as const) {
305
+ const value = container[key];
306
+ if (
307
+ value !== undefined &&
308
+ (typeof value !== 'string' ||
309
+ !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value))
310
+ ) {
311
+ fail(`${containerPath}.${key}`, 'must be #RRGGBB or #RRGGBBAA');
312
+ }
313
+ }
314
+ assertBoundedStyleNumber(
315
+ container.height,
316
+ `${containerPath}.height`,
317
+ 0,
318
+ 4096
319
+ );
320
+ assertBoundedStyleNumber(
321
+ container.opacity,
322
+ `${containerPath}.opacity`,
323
+ 0,
324
+ 1
325
+ );
326
+ assertBoundedStyleNumber(
327
+ container.cornerRadius,
328
+ `${containerPath}.cornerRadius`,
329
+ 0,
330
+ 80
331
+ );
332
+ assertBoundedStyleNumber(
333
+ container.borderWidth,
334
+ `${containerPath}.borderWidth`,
335
+ 0,
336
+ 8
337
+ );
338
+ if (
339
+ container.contentVerticalAlignment !== undefined &&
340
+ !['top', 'center', 'bottom'].includes(container.contentVerticalAlignment)
341
+ ) {
342
+ fail(
343
+ `${containerPath}.contentVerticalAlignment`,
344
+ 'must be top, center, or bottom'
345
+ );
346
+ }
347
+ }
159
348
  for (const field of [
160
349
  'horizontalPadding',
161
350
  'verticalPadding',
162
351
  'leadingGap',
163
352
  'titleBadgeGap',
164
353
  'trailingGap',
165
- 'contentTrailingGap',
166
- 'subtitleTrailingPadding',
167
354
  ] as const) {
168
355
  assertBoundedStyleNumber(style[field], `${path}.${field}`, 0, 64);
169
356
  }
170
- // OneKey patch: validate the opt-in Market badge layout.
171
- if (
172
- style.titleBadgeLayout !== undefined &&
173
- style.titleBadgeLayout !== 'inline'
174
- ) {
175
- fail(`${path}.titleBadgeLayout`, 'must be inline when provided');
176
- }
177
357
  assertBoundedStyleNumber(style.lineGap, `${path}.lineGap`, 0, 16);
178
- assertBoundedStyleNumber(style.changeWidth, `${path}.changeWidth`, 1, 160);
179
- assertBoundedStyleNumber(style.changeHeight, `${path}.changeHeight`, 1, 160);
180
- assertBoundedStyleNumber(
181
- style.changeCornerRadius,
182
- `${path}.changeCornerRadius`,
183
- 0,
184
- 80
185
- );
186
- if (style.image) {
358
+ if (style.image !== undefined) {
359
+ if (
360
+ typeof style.image !== 'object' ||
361
+ style.image === null ||
362
+ Array.isArray(style.image)
363
+ ) {
364
+ fail(`${path}.image`, 'must be an object');
365
+ }
366
+ assertUnknownKeys(
367
+ style.image as Record<string, unknown>,
368
+ ['width', 'height', 'shape', 'cornerRadius', 'contentFit'],
369
+ `${path}.image`,
370
+ 'image style key'
371
+ );
187
372
  assertBoundedStyleNumber(style.image.width, `${path}.image.width`, 1, 160);
188
373
  assertBoundedStyleNumber(
189
374
  style.image.height,
@@ -208,10 +393,205 @@ function assertMarketStyle(
208
393
  );
209
394
  }
210
395
  }
211
- assertMarketTextStyle(style.title, `${path}.title`);
212
- assertMarketTextStyle(style.subtitle, `${path}.subtitle`);
213
- assertMarketTextStyle(style.price, `${path}.price`);
214
- assertMarketTextStyle(style.change, `${path}.change`);
396
+ }
397
+
398
+ function assertMarketStyle(
399
+ style: MarketRowStyle | undefined,
400
+ path: string
401
+ ): void {
402
+ if (!style) return;
403
+ assertBoxStyle(style, path);
404
+ for (const field of [
405
+ 'contentTrailingGap',
406
+ 'subtitleTrailingPadding',
407
+ ] as const) {
408
+ assertBoundedStyleNumber(style[field], `${path}.${field}`, 0, 64);
409
+ }
410
+ // OneKey patch: validate the opt-in Market badge layout.
411
+ if (
412
+ style.titleBadgeLayout !== undefined &&
413
+ style.titleBadgeLayout !== 'inline'
414
+ ) {
415
+ fail(`${path}.titleBadgeLayout`, 'must be inline when provided');
416
+ }
417
+ assertBoundedStyleNumber(style.changeWidth, `${path}.changeWidth`, 1, 160);
418
+ assertBoundedStyleNumber(style.changeHeight, `${path}.changeHeight`, 1, 160);
419
+ assertBoundedStyleNumber(
420
+ style.changeCornerRadius,
421
+ `${path}.changeCornerRadius`,
422
+ 0,
423
+ 80
424
+ );
425
+ assertTextStyle(style.title, `${path}.title`);
426
+ assertTextStyle(style.subtitle, `${path}.subtitle`);
427
+ assertTextStyle(style.price, `${path}.price`);
428
+ assertTextStyle(style.change, `${path}.change`);
429
+ }
430
+
431
+ /**
432
+ * Rejects keys the template does not declare, so a style written for one
433
+ * template cannot reach a shared view through another. See docs/STYLE_SPEC.md
434
+ * §7 rule 1.
435
+ */
436
+ function assertRowStyle(row: RowModel, path: string): void {
437
+ const style = (row as { style?: unknown }).style;
438
+ if (style === undefined) return;
439
+ if (typeof style !== 'object' || style === null || Array.isArray(style)) {
440
+ fail(`${path}.style`, 'must be an object');
441
+ }
442
+ const textKeys = TEXT_STYLE_KEYS_BY_ROW_TYPE[row.type] ?? [];
443
+ const allowed = new Set([
444
+ 'container',
445
+ ...ROW_BOX_STYLE_KEYS_BY_TYPE[row.type],
446
+ ...textKeys,
447
+ ...(EXTRA_STYLE_KEYS_BY_ROW_TYPE[row.type] ?? []),
448
+ ]);
449
+ Object.keys(style as Record<string, unknown>).forEach((key) => {
450
+ if (!allowed.has(key)) {
451
+ fail(
452
+ `${path}.style.${key}`,
453
+ `is not a style key of the "${row.type}" template`
454
+ );
455
+ }
456
+ });
457
+ // Market keeps its own richer assertions, driven from assertMarketRow.
458
+ if (row.type === 'market') return;
459
+ assertBoxStyle(style as RowBoxStyle, `${path}.style`);
460
+ const slots = style as Record<string, NativeListTextStyle | undefined>;
461
+ textKeys.forEach((key) =>
462
+ assertTextStyle(slots[key], `${path}.style.${key}`)
463
+ );
464
+ }
465
+
466
+ const LIST_STYLE_KEYS: readonly string[] = ['separator', 'groupCornerRadius'];
467
+ const SEPARATOR_STYLE_KEYS: readonly string[] = ['inset', 'color'];
468
+
469
+ function assertUnknownKeys(
470
+ value: Record<string, unknown>,
471
+ allowed: readonly string[],
472
+ path: string,
473
+ subject: string
474
+ ): void {
475
+ Object.keys(value).forEach((key) => {
476
+ if (!allowed.includes(key)) fail(`${path}.${key}`, `is not a ${subject}`);
477
+ });
478
+ }
479
+
480
+ function assertListStyle(
481
+ listStyle: NativeListListStyle | undefined,
482
+ path: string
483
+ ): void {
484
+ if (listStyle === undefined) return;
485
+ if (
486
+ typeof listStyle !== 'object' ||
487
+ listStyle === null ||
488
+ Array.isArray(listStyle)
489
+ ) {
490
+ fail(path, 'must be an object');
491
+ }
492
+ assertUnknownKeys(
493
+ listStyle as Record<string, unknown>,
494
+ LIST_STYLE_KEYS,
495
+ path,
496
+ 'list style key'
497
+ );
498
+ assertBoundedStyleNumber(
499
+ listStyle.groupCornerRadius,
500
+ `${path}.groupCornerRadius`,
501
+ 0,
502
+ 40
503
+ );
504
+ const separator = listStyle.separator;
505
+ if (separator === undefined) return;
506
+ if (
507
+ typeof separator !== 'object' ||
508
+ separator === null ||
509
+ Array.isArray(separator)
510
+ ) {
511
+ fail(`${path}.separator`, 'must be an object');
512
+ }
513
+ assertUnknownKeys(
514
+ separator as Record<string, unknown>,
515
+ SEPARATOR_STYLE_KEYS,
516
+ `${path}.separator`,
517
+ 'separator style key'
518
+ );
519
+ assertBoundedStyleNumber(separator.inset, `${path}.separator.inset`, 0, 64);
520
+ // Same format as every row style color; renderers parse it directly.
521
+ if (
522
+ separator.color !== undefined &&
523
+ (typeof separator.color !== 'string' ||
524
+ !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(separator.color))
525
+ ) {
526
+ fail(`${path}.separator.color`, 'must be #RRGGBB or #RRGGBBAA');
527
+ }
528
+ }
529
+
530
+ /** Token resolution is idempotent: a resolved style carries no token. */
531
+ function resolveTextStyle(
532
+ style: NativeListTextStyle | undefined
533
+ ): NativeListTextStyle | undefined {
534
+ if (!style?.token) return style;
535
+ const { token, ...overrides } = style;
536
+ return { ...TYPOGRAPHY_TOKENS[token], ...overrides };
537
+ }
538
+
539
+ function resolveStyleTokens(
540
+ style: Record<string, unknown>,
541
+ rowType: RowModel['type']
542
+ ): Record<string, unknown> {
543
+ const textKeys = TEXT_STYLE_KEYS_BY_ROW_TYPE[rowType] ?? [];
544
+ let changed = false;
545
+ const next: Record<string, unknown> = { ...style };
546
+ textKeys.forEach((key) => {
547
+ const slot = style[key] as NativeListTextStyle | undefined;
548
+ const resolved = resolveTextStyle(slot);
549
+ if (resolved !== slot) {
550
+ next[key] = resolved;
551
+ changed = true;
552
+ }
553
+ });
554
+ return changed ? next : style;
555
+ }
556
+
557
+ function normalizeRowStyles<T extends RowModel>(row: T): T {
558
+ // Narrowing a generic does not reach the walletGroup members; go through the
559
+ // union type instead.
560
+ const model: RowModel = row;
561
+ const style = (row as { style?: Record<string, unknown> }).style;
562
+ const nextStyle = style ? resolveStyleTokens(style, row.type) : style;
563
+ const prefix = model.type === 'market' ? model.subtitlePrefix : undefined;
564
+ const nextPrefixStyle = resolveTextStyle(prefix?.style);
565
+ let nextParent: IdentityRow | undefined;
566
+ let nextChildren: readonly IdentityRow[] | undefined;
567
+ if (model.type === 'walletGroup') {
568
+ const parent = normalizeRowStyles(model.parent);
569
+ const children = model.children.map(normalizeRowStyles);
570
+ if (
571
+ parent !== model.parent ||
572
+ children.some((child, index) => child !== model.children[index])
573
+ ) {
574
+ nextParent = parent;
575
+ nextChildren = children;
576
+ }
577
+ }
578
+ if (
579
+ nextStyle === style &&
580
+ nextPrefixStyle === prefix?.style &&
581
+ nextParent === undefined
582
+ ) {
583
+ return row;
584
+ }
585
+ const next: Record<string, unknown> = { ...row };
586
+ if (nextStyle !== style) next.style = nextStyle;
587
+ if (prefix && nextPrefixStyle !== prefix.style) {
588
+ next.subtitlePrefix = { ...prefix, style: nextPrefixStyle };
589
+ }
590
+ if (nextParent !== undefined) {
591
+ next.parent = nextParent;
592
+ next.children = nextChildren;
593
+ }
594
+ return next as T;
215
595
  }
216
596
 
217
597
  function assertMarketRow(row: MarketRow, path: string): void {
@@ -235,10 +615,7 @@ function assertMarketRow(row: MarketRow, path: string): void {
235
615
  1,
236
616
  320
237
617
  );
238
- assertMarketTextStyle(
239
- row.subtitlePrefix.style,
240
- `${path}.subtitlePrefix.style`
241
- );
618
+ assertTextStyle(row.subtitlePrefix.style, `${path}.subtitlePrefix.style`);
242
619
  }
243
620
  assertText(row.price, `${path}.price`);
244
621
  assertText(row.change.text, `${path}.change.text`);
@@ -286,7 +663,17 @@ function assertMarketRow(row: MarketRow, path: string): void {
286
663
  badgeKeys.add(badge.key);
287
664
  assertText(badge.text, `${badgePath}.text`);
288
665
  // OneKey patch: share typography bounds with Market text styles.
289
- assertMarketTextStyle(badge.style, `${badgePath}.style`);
666
+ assertTextStyle(badge.style, `${badgePath}.style`, [
667
+ 'height',
668
+ 'horizontalPadding',
669
+ ]);
670
+ if (badge.style)
671
+ assertUnknownKeys(
672
+ badge.style,
673
+ ['fontSize', 'fontWeight', 'lineHeight', 'height', 'horizontalPadding'],
674
+ `${badgePath}.style`,
675
+ 'Market badge style key'
676
+ );
290
677
  assertBoundedStyleNumber(
291
678
  badge.style?.height,
292
679
  `${badgePath}.style.height`,
@@ -571,18 +958,31 @@ function assertWalletGroup(row: WalletGroupRow, path: string): void {
571
958
  });
572
959
  }
573
960
 
961
+ function assertRowType(
962
+ type: unknown,
963
+ path: string
964
+ ): asserts type is RowModel['type'] {
965
+ if (
966
+ typeof type !== 'string' ||
967
+ !Object.prototype.hasOwnProperty.call(ROW_BOX_STYLE_KEYS_BY_TYPE, type)
968
+ ) {
969
+ fail(`${path}.type`, 'is not supported');
970
+ }
971
+ }
972
+
574
973
  function assertRow(
575
974
  row: RowModel,
576
975
  index: number,
577
976
  path = `rows[${index}]`
578
977
  ): void {
978
+ assertRowType(row.type, path);
579
979
  assertKey(row.key, `${path}.key`);
580
980
  // OneKey patch: explicit dimensions and opacity cannot corrupt list layout.
581
981
  if (row.height !== undefined && (row.height < 0 || row.height > 4096))
582
982
  fail(`${path}.height`, 'must be within 0...4096');
583
983
  if (
584
984
  row.heightRounding !== undefined &&
585
- (row.height === undefined ||
985
+ ((row.height === undefined && row.style?.container?.height === undefined) ||
586
986
  !['floor', 'nearest'].includes(row.heightRounding))
587
987
  )
588
988
  fail(
@@ -597,6 +997,7 @@ function assertRow(
597
997
  if (!row.groupId && row.groupPosition) {
598
998
  fail(`${path}.groupId`, 'is required when groupPosition is present');
599
999
  }
1000
+ assertRowStyle(row, path);
600
1001
  assertVisual(row, path);
601
1002
 
602
1003
  switch (row.type) {
@@ -685,8 +1086,8 @@ function assertRow(
685
1086
  case 'message':
686
1087
  assertText(row.title, `${path}.title`);
687
1088
  assertText(row.body, `${path}.body`);
688
- if ((row.bodyLines ?? 3) < 1 || (row.bodyLines ?? 3) > 3) {
689
- fail(`${path}.bodyLines`, 'must be within 1...3');
1089
+ if (![1, 2, 3].includes(row.bodyLines ?? 3)) {
1090
+ fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
690
1091
  }
691
1092
  assertImage(row.thumbnail, `${path}.thumbnail`);
692
1093
  break;
@@ -964,6 +1365,8 @@ export function validateSnapshot(
964
1365
  }
965
1366
  }
966
1367
 
1368
+ assertListStyle(snapshot.listStyle, 'snapshot.listStyle');
1369
+
967
1370
  const rowKeys = new Set<string>();
968
1371
  const sectionIndexTitles = new Set<string>();
969
1372
  snapshot.rows.forEach((row, index) => {
@@ -1016,11 +1419,44 @@ export function validateSnapshot(
1016
1419
  'supports at most one key in single mode'
1017
1420
  );
1018
1421
  }
1019
- return snapshot;
1422
+ return normalizeSnapshotStyles(snapshot);
1423
+ }
1424
+
1425
+ /**
1426
+ * Resolves typography tokens to numbers. Returns the original snapshot when
1427
+ * nothing needs resolving, so the common no-style path keeps object identity.
1428
+ */
1429
+ function normalizeSnapshotStyles(
1430
+ snapshot: NativeListSnapshot
1431
+ ): NativeListSnapshot {
1432
+ let changed = false;
1433
+ const rows = snapshot.rows.map((row) => {
1434
+ const next = normalizeRowStyles(row);
1435
+ if (next !== row) changed = true;
1436
+ return next;
1437
+ });
1438
+ const emptyState = snapshot.emptyState
1439
+ ? normalizeRowStyles(snapshot.emptyState)
1440
+ : snapshot.emptyState;
1441
+ const fixedFooter = snapshot.fixedFooter
1442
+ ? normalizeRowStyles(snapshot.fixedFooter)
1443
+ : snapshot.fixedFooter;
1444
+ if (
1445
+ !changed &&
1446
+ emptyState === snapshot.emptyState &&
1447
+ fixedFooter === snapshot.fixedFooter
1448
+ ) {
1449
+ return snapshot;
1450
+ }
1451
+ return { ...snapshot, rows, emptyState, fixedFooter };
1020
1452
  }
1021
1453
 
1022
1454
  function assertPatchChanges(patch: RowPatch, index: number): void {
1023
1455
  const path = `patches[${index}].changes`;
1456
+ const patchedStyle = (patch.changes as { style?: unknown }).style;
1457
+ if (patchedStyle !== undefined) {
1458
+ assertRowStyle({ type: patch.type, style: patchedStyle } as RowModel, path);
1459
+ }
1024
1460
  // OneKey patch: partial balance updates retain a valid, current accessibility label.
1025
1461
  if ('accessibilityLabel' in patch.changes) {
1026
1462
  assertText(patch.changes.accessibilityLabel, `${path}.accessibilityLabel`);
@@ -1095,9 +1531,9 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
1095
1531
  assertText(patch.changes.time, `${path}.time`);
1096
1532
  if (
1097
1533
  patch.changes.bodyLines !== undefined &&
1098
- (patch.changes.bodyLines < 1 || patch.changes.bodyLines > 3)
1534
+ ![1, 2, 3].includes(patch.changes.bodyLines)
1099
1535
  ) {
1100
- fail(`${path}.bodyLines`, 'must be within 1...3');
1536
+ fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
1101
1537
  }
1102
1538
  assertImage(patch.changes.thumbnail, `${path}.thumbnail`);
1103
1539
  break;
@@ -1236,7 +1672,26 @@ export function validatePatches(
1236
1672
  }
1237
1673
  assertPatchChanges(patch, index);
1238
1674
  });
1239
- return patches;
1675
+ return normalizePatchStyles(patches);
1676
+ }
1677
+
1678
+ /** Patches reach the native side without passing through a snapshot. */
1679
+ function normalizePatchStyles(
1680
+ patches: readonly RowPatch[]
1681
+ ): readonly RowPatch[] {
1682
+ let changed = false;
1683
+ const next = patches.map((patch) => {
1684
+ const style = (patch.changes as { style?: Record<string, unknown> }).style;
1685
+ if (!style) return patch;
1686
+ const resolved = resolveStyleTokens(style, patch.type);
1687
+ if (resolved === style) return patch;
1688
+ changed = true;
1689
+ return {
1690
+ ...patch,
1691
+ changes: { ...patch.changes, style: resolved },
1692
+ } as RowPatch;
1693
+ });
1694
+ return changed ? next : patches;
1240
1695
  }
1241
1696
 
1242
1697
  export function applyRowPatches(