@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.
- package/README.md +21 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
- package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
- package/docs/DESIGN.md +518 -9
- package/docs/MIGRATION_BASELINE.md +117 -0
- package/docs/ROW_TEMPLATES.md +359 -0
- package/docs/SPEC.md +358 -0
- package/docs/STYLE_SPEC.md +978 -0
- package/docs/images/list-capabilities.svg +39 -0
- package/docs/images/row-action.svg +17 -0
- package/docs/images/row-activity.svg +24 -0
- package/docs/images/row-data.svg +24 -0
- package/docs/images/row-identity.svg +25 -0
- package/docs/images/row-market.svg +23 -0
- package/docs/images/row-media-tile.svg +26 -0
- package/docs/images/row-message.svg +20 -0
- package/docs/images/row-metric-card.svg +33 -0
- package/docs/images/row-rail.svg +18 -0
- package/docs/images/row-section-header.svg +19 -0
- package/docs/images/row-style-regions.svg +30 -0
- package/docs/images/row-system.svg +30 -0
- package/docs/images/row-wallet-group.svg +26 -0
- package/ios/NativeListAccessoryStack.swift +680 -0
- package/ios/NativeListActionRenderer.swift +108 -0
- package/ios/NativeListActivityRenderer.swift +179 -0
- package/ios/NativeListDataRowRenderer.swift +444 -0
- package/ios/NativeListIdentityRenderer.swift +685 -0
- package/ios/NativeListImageSlot.swift +140 -0
- package/ios/NativeListLeadingVisual.swift +372 -0
- package/ios/NativeListMarketRenderer.swift +698 -0
- package/ios/NativeListMediaTileRenderer.swift +184 -0
- package/ios/NativeListMessageRenderer.swift +168 -0
- package/ios/NativeListMetricCardRenderer.swift +420 -0
- package/ios/NativeListModels.swift +32 -0
- package/ios/NativeListRailRenderer.swift +152 -0
- package/ios/NativeListRendererCell.swift +311 -0
- package/ios/NativeListRendererRegistry.swift +50 -0
- package/ios/NativeListResolvedText.swift +122 -0
- package/ios/NativeListRowHost.swift +63 -0
- package/ios/NativeListSectionHeaderRenderer.swift +708 -0
- package/ios/NativeListSystemRenderer.swift +568 -0
- package/ios/NativeListTextLabel.swift +42 -0
- package/ios/NativeListTextStyles.swift +236 -0
- package/ios/NativeListWalletGroupRenderer.swift +255 -0
- package/ios/RNCNativeListView.swift +111 -180
- package/lib/module/models.js +54 -0
- package/lib/module/validation.js +324 -26
- package/lib/module/web/NativeListWebEngine.js +148 -936
- package/lib/module/web/templates/ActionRowRenderer.js +97 -0
- package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
- package/lib/module/web/templates/DataRowRenderer.js +145 -0
- package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
- package/lib/module/web/templates/MarketRowRenderer.js +310 -0
- package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
- package/lib/module/web/templates/MessageRowRenderer.js +165 -0
- package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
- package/lib/module/web/templates/RailRowRenderer.js +98 -0
- package/lib/module/web/templates/RowContainerStyle.js +100 -0
- package/lib/module/web/templates/RowElements.js +93 -0
- package/lib/module/web/templates/RowRendererRegistry.js +70 -0
- package/lib/module/web/templates/RowText.js +121 -0
- package/lib/module/web/templates/RowVisual.js +43 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
- package/lib/module/web/templates/SystemRowRenderer.js +161 -0
- package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
- package/lib/typescript/src/models.d.ts +166 -13
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
- package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
- package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
- package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
- package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
- package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
- package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
- package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
- package/package.json +3 -3
- package/src/models.ts +281 -19
- package/src/validation.ts +501 -46
- package/src/web/NativeListWebEngine.ts +361 -1932
- package/src/web/templates/ActionRowRenderer.ts +123 -0
- package/src/web/templates/ActivityRowRenderer.ts +159 -0
- package/src/web/templates/DataRowRenderer.ts +212 -0
- package/src/web/templates/IdentityRowRenderer.ts +527 -0
- package/src/web/templates/MarketRowRenderer.ts +467 -0
- package/src/web/templates/MediaTileRowRenderer.ts +182 -0
- package/src/web/templates/MessageRowRenderer.ts +306 -0
- package/src/web/templates/MetricCardRowRenderer.ts +337 -0
- package/src/web/templates/RailRowRenderer.ts +136 -0
- package/src/web/templates/RowContainerStyle.ts +131 -0
- package/src/web/templates/RowElements.ts +160 -0
- package/src/web/templates/RowRendererRegistry.ts +111 -0
- package/src/web/templates/RowText.ts +181 -0
- package/src/web/templates/RowVisual.ts +115 -0
- package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
- package/src/web/templates/SystemRowRenderer.ts +282 -0
- package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
- 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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 (
|
|
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
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
689
|
-
fail(`${path}.bodyLines`, 'must be
|
|
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
|
-
|
|
1534
|
+
![1, 2, 3].includes(patch.changes.bodyLines)
|
|
1099
1535
|
) {
|
|
1100
|
-
fail(`${path}.bodyLines`, 'must be
|
|
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(
|