@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.
- 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/lib/module/validation.js
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
368
|
-
fail(`${path}.bodyLines`, 'must be
|
|
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 &&
|
|
621
|
-
fail(`${path}.bodyLines`, 'must be
|
|
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);
|