@onekeyfe/react-native-native-list 6.7.0-alpha.375 → 6.7.0-alpha.382
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/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
- package/docs/ROW_TEMPLATES.md +60 -9
- package/docs/SPEC.md +450 -5
- package/docs/STYLE_SPEC.md +6 -0
- package/ios/HybridNativeList.swift +21 -2
- package/ios/NativeListAccessoryStack.swift +227 -39
- package/ios/NativeListActivityDetailsView.swift +18 -8
- package/ios/NativeListActivityRenderer.swift +8 -2
- package/ios/NativeListButtonGroupRenderer.swift +89 -0
- package/ios/NativeListDataRowRenderer.swift +155 -27
- package/ios/NativeListDesignAssets.swift +3 -0
- package/ios/NativeListIdentityRenderer.swift +166 -30
- package/ios/NativeListLeadingVisual.swift +49 -14
- package/ios/NativeListMessageRenderer.swift +34 -1
- package/ios/NativeListModels.swift +42 -2
- package/ios/NativeListRendererRegistry.swift +1 -0
- package/ios/NativeListResolvedText.swift +2 -1
- package/ios/NativeListRowHost.swift +14 -0
- package/ios/NativeListSectionHeaderRenderer.swift +88 -0
- package/ios/RNCNativeListView.swift +1013 -55
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
- package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
- package/ios/tests/NativeListLifecycleTests.swift +2720 -3
- package/lib/module/NativeList.js +175 -28
- package/lib/module/NativeList.web.js +129 -30
- package/lib/module/index.js +1 -1
- package/lib/module/models.js +1 -0
- package/lib/module/selection.js +1 -1
- package/lib/module/validation.js +421 -8
- package/lib/module/web/NativeListWebEngine.js +507 -19
- package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
- package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
- package/lib/module/web/templates/DataRowRenderer.js +40 -5
- package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
- package/lib/module/web/templates/MessageRowRenderer.js +18 -2
- package/lib/module/web/templates/RowContainerStyle.js +1 -0
- package/lib/module/web/templates/RowRendererRegistry.js +3 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/lib/typescript/src/NativeList.d.ts +1 -1
- package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
- package/lib/typescript/src/NativeList.types.d.ts +18 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/models.d.ts +105 -9
- package/lib/typescript/src/validation.d.ts +12 -0
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
- package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/package.json +3 -3
- package/react-native-native-list.podspec +1 -0
- package/src/NativeList.nitro.ts +2 -0
- package/src/NativeList.tsx +313 -44
- package/src/NativeList.types.ts +22 -0
- package/src/NativeList.web.tsx +204 -48
- package/src/index.ts +2 -0
- package/src/models.ts +117 -3
- package/src/selection.ts +1 -0
- package/src/validation.ts +852 -6
- package/src/web/NativeListWebEngine.ts +803 -20
- package/src/web/templates/ActivityRowRenderer.ts +14 -6
- package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
- package/src/web/templates/DataRowRenderer.ts +93 -20
- package/src/web/templates/IdentityRowRenderer.ts +150 -5
- package/src/web/templates/MessageRowRenderer.ts +23 -7
- package/src/web/templates/RowContainerStyle.ts +2 -0
- package/src/web/templates/RowRendererRegistry.ts +11 -1
- package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
package/lib/module/validation.js
CHANGED
|
@@ -167,10 +167,13 @@ const TEXT_STYLE_KEYS_BY_ROW_TYPE = {
|
|
|
167
167
|
metricCard: ['title', 'value', 'subtitle', 'trend'],
|
|
168
168
|
sectionHeader: ['title', 'subtitle', 'value'],
|
|
169
169
|
action: ['title', 'value'],
|
|
170
|
+
buttonGroup: [],
|
|
170
171
|
system: ['title', 'message', 'actionText']
|
|
171
172
|
};
|
|
172
173
|
const EXTRA_STYLE_KEYS_BY_ROW_TYPE = {
|
|
173
|
-
|
|
174
|
+
identity: ['leadingContentMaxWidthRatio'],
|
|
175
|
+
market: ['titleBadgeLayout', 'contentTrailingGap', 'subtitleTrailingPadding', 'changeWidth', 'changeHeight', 'changeCornerRadius'],
|
|
176
|
+
sectionHeader: ['valuePlacement', 'titleDecoration']
|
|
174
177
|
};
|
|
175
178
|
function assertTextStyle(style, path, additionalKeys = []) {
|
|
176
179
|
if (style === undefined) return;
|
|
@@ -283,6 +286,15 @@ function assertRowStyle(row, path) {
|
|
|
283
286
|
// Market keeps its own richer assertions, driven from assertMarketRow.
|
|
284
287
|
if (row.type === 'market') return;
|
|
285
288
|
assertBoxStyle(style, `${path}.style`);
|
|
289
|
+
if (row.type === 'identity') {
|
|
290
|
+
const ratio = style?.leadingContentMaxWidthRatio;
|
|
291
|
+
if (ratio !== undefined && (!Number.isFinite(ratio) || ratio <= 0 || ratio > 1)) fail(`${path}.style.leadingContentMaxWidthRatio`, 'must be within (0, 1]');
|
|
292
|
+
}
|
|
293
|
+
if (row.type === 'sectionHeader') {
|
|
294
|
+
const sectionStyle = style;
|
|
295
|
+
if (sectionStyle?.valuePlacement !== undefined && sectionStyle.valuePlacement !== 'afterTitle') fail(`${path}.style.valuePlacement`, 'must be afterTitle');
|
|
296
|
+
if (sectionStyle?.titleDecoration !== undefined && !['none', 'dashed'].includes(sectionStyle.titleDecoration)) fail(`${path}.style.titleDecoration`, 'must be none or dashed');
|
|
297
|
+
}
|
|
286
298
|
const slots = style;
|
|
287
299
|
textKeys.forEach(key => assertTextStyle(slots[key], `${path}.style.${key}`));
|
|
288
300
|
}
|
|
@@ -572,15 +584,88 @@ function assertVisualShape(shape, path) {
|
|
|
572
584
|
function assertTrailingAccessories(accessories, path) {
|
|
573
585
|
accessories?.forEach((accessory, index) => {
|
|
574
586
|
const accessoryPath = `${path}[${index}]`;
|
|
587
|
+
if (accessory.kind === 'button') {
|
|
588
|
+
assertUnknownKeys(accessory, ['kind', 'label', 'actionKey', 'disabled', 'testID', 'accessibilityLabel', 'textColor', 'backgroundColor', 'pressedBackgroundColor', 'hoverBackgroundColor', 'focusRingColor'], accessoryPath, 'button key');
|
|
589
|
+
if (typeof accessory.label !== 'string' || !accessory.label.trim()) fail(`${accessoryPath}.label`, 'must be non-blank');
|
|
590
|
+
assertKey(accessory.actionKey, `${accessoryPath}.actionKey`);
|
|
591
|
+
if (accessory.disabled !== undefined && typeof accessory.disabled !== 'boolean') fail(`${accessoryPath}.disabled`, 'must be boolean');
|
|
592
|
+
for (const field of ['textColor', 'backgroundColor', 'pressedBackgroundColor', 'hoverBackgroundColor', 'focusRingColor']) {
|
|
593
|
+
const value = accessory[field];
|
|
594
|
+
if (value !== undefined && (typeof value !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value))) fail(`${accessoryPath}.${field}`, 'must be #RRGGBB or #RRGGBBAA');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (accessory.kind === 'icon' && accessory.buttonStyle !== undefined && accessory.buttonStyle !== 'tertiary') fail(`${accessoryPath}.buttonStyle`, 'must be tertiary');
|
|
575
598
|
if (accessory.kind === 'progress' && (accessory.value < 0 || accessory.value > 1)) {
|
|
576
599
|
fail(`${accessoryPath}.value`, 'must be within 0...1');
|
|
577
600
|
}
|
|
578
601
|
if (accessory.kind === 'valuePair') {
|
|
602
|
+
if (accessory.primaryFontWeight !== undefined && accessory.primaryFontWeight !== 'medium') fail(`${accessoryPath}.primaryFontWeight`, 'must be medium when provided');
|
|
603
|
+
assertTextHelp(accessory.primaryHelp, `${accessoryPath}.primaryHelp`);
|
|
579
604
|
assertTextTone(accessory.primaryTone, `${accessoryPath}.primaryTone`);
|
|
580
605
|
assertTextTone(accessory.secondaryTone, `${accessoryPath}.secondaryTone`);
|
|
606
|
+
for (const field of ['primaryTextSegments', 'secondaryTextSegments']) {
|
|
607
|
+
const segments = accessory[field];
|
|
608
|
+
if (segments === undefined) continue;
|
|
609
|
+
const segmentPath = `${accessoryPath}.${field}`;
|
|
610
|
+
if (!Array.isArray(segments) || segments.length > 64) fail(segmentPath, 'must be an array of at most 64 segments');
|
|
611
|
+
let textLength = 0;
|
|
612
|
+
segments.forEach((segment, segmentIndex) => {
|
|
613
|
+
const path = `${segmentPath}[${segmentIndex}]`;
|
|
614
|
+
const entry = protocolObject(segment, path);
|
|
615
|
+
assertUnknownKeys(entry, ['text', 'style'], path, 'segment key');
|
|
616
|
+
if (typeof entry.text !== 'string' || entry.text.length > MAX_TEXT_LENGTH) fail(`${path}.text`, `must be a string no longer than ${MAX_TEXT_LENGTH} characters`);
|
|
617
|
+
if (entry.style !== undefined && entry.style !== 'subscript') fail(`${path}.style`, 'must be subscript when provided');
|
|
618
|
+
textLength += entry.text.length;
|
|
619
|
+
});
|
|
620
|
+
if (textLength > MAX_TEXT_LENGTH) fail(segmentPath, `total text must be at most ${MAX_TEXT_LENGTH} characters`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
function assertTextHelp(help, path) {
|
|
626
|
+
if (help === undefined) return;
|
|
627
|
+
const object = protocolObject(help, path);
|
|
628
|
+
assertUnknownKeys(object, ['actionKey', 'presentation', 'underline'], path, 'help key');
|
|
629
|
+
assertKey(help.actionKey, `${path}.actionKey`);
|
|
630
|
+
if (!['tooltip', 'popover'].includes(help.presentation)) fail(`${path}.presentation`, 'must be tooltip or popover');
|
|
631
|
+
if (help.underline !== undefined && help.underline !== 'dashed') fail(`${path}.underline`, 'must be dashed');
|
|
632
|
+
}
|
|
633
|
+
function assertButtonGroup(row, path) {
|
|
634
|
+
if (!Array.isArray(row.buttons) || row.buttons.length === 0) fail(`${path}.buttons`, 'must contain at least one button');
|
|
635
|
+
const keys = new Set();
|
|
636
|
+
row.buttons.forEach((button, index) => {
|
|
637
|
+
const itemPath = `${path}.buttons[${index}]`;
|
|
638
|
+
const object = protocolObject(button, itemPath);
|
|
639
|
+
assertUnknownKeys(object, ['key', 'label', 'actionKey', 'disabled', 'loading', 'testID', 'accessibilityLabel'], itemPath, 'button key');
|
|
640
|
+
assertKey(button.key, `${itemPath}.key`);
|
|
641
|
+
if (keys.has(button.key)) fail(`${itemPath}.key`, 'duplicate button key');
|
|
642
|
+
keys.add(button.key);
|
|
643
|
+
if (typeof button.label !== 'string' || !button.label.trim() || button.label.length > MAX_TEXT_LENGTH) fail(`${itemPath}.label`, 'must be non-blank bounded text');
|
|
644
|
+
assertKey(button.actionKey, `${itemPath}.actionKey`);
|
|
645
|
+
for (const field of ['disabled', 'loading']) {
|
|
646
|
+
if (button[field] !== undefined && typeof button[field] !== 'boolean') fail(`${itemPath}.${field}`, 'must be boolean');
|
|
647
|
+
}
|
|
648
|
+
for (const field of ['testID', 'accessibilityLabel']) {
|
|
649
|
+
if (button[field] !== undefined && typeof button[field] !== 'string') fail(`${itemPath}.${field}`, 'must be a string');
|
|
581
650
|
}
|
|
582
651
|
});
|
|
583
652
|
}
|
|
653
|
+
function assertIdentityExtras(row, path) {
|
|
654
|
+
for (const field of ['titleIcon', 'secondaryTitleIcon']) {
|
|
655
|
+
const titleIcon = row[field];
|
|
656
|
+
if (titleIcon === undefined) continue;
|
|
657
|
+
const iconPath = `${path}.${field}`;
|
|
658
|
+
const icon = protocolObject(titleIcon, iconPath);
|
|
659
|
+
assertUnknownKeys(icon, ['name', 'tintColor', 'accessibilityLabel', 'hoverActionKey'], iconPath, 'title icon key');
|
|
660
|
+
if (typeof titleIcon.name !== 'string' || !titleIcon.name.trim()) fail(`${iconPath}.name`, 'must be non-blank');
|
|
661
|
+
if (titleIcon.tintColor !== undefined && (typeof titleIcon.tintColor !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(titleIcon.tintColor))) fail(`${iconPath}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
|
|
662
|
+
if (titleIcon.accessibilityLabel !== undefined && typeof titleIcon.accessibilityLabel !== 'string') fail(`${iconPath}.accessibilityLabel`, 'must be a string');
|
|
663
|
+
if (titleIcon.hoverActionKey !== undefined) assertKey(titleIcon.hoverActionKey, `${iconPath}.hoverActionKey`);
|
|
664
|
+
}
|
|
665
|
+
const ratio = row.style?.leadingContentMaxWidthRatio;
|
|
666
|
+
if (ratio !== undefined && (typeof ratio !== 'number' || !Number.isFinite(ratio) || ratio <= 0 || ratio > 1)) fail(`${path}.style.leadingContentMaxWidthRatio`, 'must be within (0, 1]');
|
|
667
|
+
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
668
|
+
}
|
|
584
669
|
function assertImage(image, path) {
|
|
585
670
|
if (!image) return;
|
|
586
671
|
if (!image.uri.trim()) fail(`${path}.uri`, 'must not be empty');
|
|
@@ -625,6 +710,9 @@ function assertLeadingVisual(visual, path) {
|
|
|
625
710
|
assertText(visual.cornerIcon?.name, `${path}.cornerIcon.name`);
|
|
626
711
|
// OneKey patch: validate optional selector overlays at the JSON boundary.
|
|
627
712
|
assertText(visual.fallbackIcon?.name, `${path}.fallbackIcon.name`);
|
|
713
|
+
if (visual.fallbackTextColor !== undefined && (typeof visual.fallbackTextColor !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(visual.fallbackTextColor))) fail(`${path}.fallbackTextColor`, 'must be a hex color');
|
|
714
|
+
if (visual.fallbackIcon?.size !== undefined && (!Number.isFinite(visual.fallbackIcon.size) || visual.fallbackIcon.size <= 0 || visual.fallbackIcon.size > 40)) fail(`${path}.fallbackIcon.size`, 'must be within (0, 40]');
|
|
715
|
+
if (visual.fallbackIcon?.backgroundColor !== undefined && !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(visual.fallbackIcon.backgroundColor)) fail(`${path}.fallbackIcon.backgroundColor`, 'must be a hex color');
|
|
628
716
|
if ((visual.overlays?.length ?? 0) > 2) fail(`${path}.overlays`, 'supports at most two overlays');
|
|
629
717
|
visual.overlays?.forEach((overlay, index) => {
|
|
630
718
|
if (!['topLeft', 'bottomRight'].includes(overlay.position)) fail(`${path}.overlays[${index}].position`, 'must be topLeft or bottomRight');
|
|
@@ -641,7 +729,11 @@ function assertLeadingVisual(visual, path) {
|
|
|
641
729
|
}
|
|
642
730
|
if (overlay.padding !== undefined && overlay.padding * 2 >= Math.min(overlay.width ?? overlay.size ?? 20, overlay.height ?? overlay.size ?? 20)) fail(`${path}.overlays[${index}].padding`, 'must fit inside the overlay');
|
|
643
731
|
assertImage(overlay.image, `${path}.overlays[${index}].image`);
|
|
732
|
+
if (overlay.fallbackIcon !== undefined && !overlay.image) fail(`${path}.overlays[${index}].fallbackIcon`, 'requires image');
|
|
733
|
+
if (overlay.fallbackIcon !== undefined && (typeof overlay.fallbackIcon.name !== 'string' || !overlay.fallbackIcon.name.trim())) fail(`${path}.overlays[${index}].fallbackIcon.name`, 'must be non-blank');
|
|
734
|
+
if (overlay.fallbackIcon?.tintColor !== undefined && !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(overlay.fallbackIcon.tintColor)) fail(`${path}.overlays[${index}].fallbackIcon.tintColor`, 'must be a hex color');
|
|
644
735
|
assertText(overlay.text, `${path}.overlays[${index}].text`);
|
|
736
|
+
if (overlay.fontWeight !== undefined && !['regular', 'medium', 'semibold', 'bold'].includes(overlay.fontWeight)) fail(`${path}.overlays[${index}].fontWeight`, 'must be regular, medium, semibold, or bold');
|
|
645
737
|
});
|
|
646
738
|
if (visual.kind === 'token') {
|
|
647
739
|
assertImage(visual.networkImage, `${path}.networkImage`);
|
|
@@ -655,6 +747,7 @@ function assertVisual(row, path) {
|
|
|
655
747
|
});
|
|
656
748
|
}
|
|
657
749
|
function assertActivity(row, path) {
|
|
750
|
+
if (row.hideLeading !== undefined && typeof row.hideLeading !== 'boolean') fail(`${path}.hideLeading`, 'must be boolean');
|
|
658
751
|
assertText(row.title, `${path}.title`);
|
|
659
752
|
assertText(row.description, `${path}.description`);
|
|
660
753
|
if (row.presentation !== undefined && !['stacked', 'table'].includes(row.presentation)) fail(`${path}.presentation`, 'must be stacked or table');
|
|
@@ -751,10 +844,26 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
751
844
|
assertWalletGroup(row, path);
|
|
752
845
|
break;
|
|
753
846
|
case 'identity':
|
|
847
|
+
if (row.contentHeight !== undefined && row.contentHeight !== 'auto') {
|
|
848
|
+
fail(`${path}.contentHeight`, 'must be auto');
|
|
849
|
+
}
|
|
850
|
+
if (row.subtitleLines !== undefined && ![0, 1, 2].includes(row.subtitleLines)) {
|
|
851
|
+
fail(`${path}.subtitleLines`, 'must be 0, 1, or 2');
|
|
852
|
+
}
|
|
853
|
+
if (row.longPressActionKey !== undefined) {
|
|
854
|
+
assertKey(row.longPressActionKey, `${path}.longPressActionKey`);
|
|
855
|
+
}
|
|
754
856
|
if (row.presentation !== undefined && row.presentation !== 'walletSidebar' && row.presentation !== 'accountSelector' && row.presentation !== 'networkSelector') {
|
|
755
857
|
fail(`${path}.presentation`, 'must be walletSidebar, accountSelector, or networkSelector when provided');
|
|
756
858
|
}
|
|
757
859
|
assertText(row.title, `${path}.title`);
|
|
860
|
+
if ((row.titleSuffixes?.length ?? 0) > 3) fail(`${path}.titleSuffixes`, 'supports at most 3 suffixes');
|
|
861
|
+
row.titleSuffixes?.forEach((suffix, index) => {
|
|
862
|
+
assertText(suffix.text, `${path}.titleSuffixes[${index}].text`);
|
|
863
|
+
if (!['secondary', 'disabled'].includes(suffix.tone)) fail(`${path}.titleSuffixes[${index}].tone`, 'must be secondary or disabled');
|
|
864
|
+
if (suffix.maxWidth !== undefined && (!Number.isFinite(suffix.maxWidth) || suffix.maxWidth < 0 || suffix.maxWidth > 4096)) fail(`${path}.titleSuffixes[${index}].maxWidth`, 'must be finite within 0...4096');
|
|
865
|
+
});
|
|
866
|
+
assertIdentityExtras(row, path);
|
|
758
867
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
759
868
|
// OneKey patch: selector text segments truncate independently.
|
|
760
869
|
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
@@ -786,10 +895,13 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
786
895
|
assertActivity(row, path);
|
|
787
896
|
break;
|
|
788
897
|
case 'message':
|
|
898
|
+
if (row.presentation !== undefined && row.presentation !== 'inlineWarning') {
|
|
899
|
+
fail(`${path}.presentation`, 'must be inlineWarning');
|
|
900
|
+
}
|
|
789
901
|
assertText(row.title, `${path}.title`);
|
|
790
902
|
assertText(row.body, `${path}.body`);
|
|
791
|
-
if (![1, 2, 3].includes(row.bodyLines ?? 3)) {
|
|
792
|
-
fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
|
|
903
|
+
if (![0, 1, 2, 3].includes(row.bodyLines ?? 3)) {
|
|
904
|
+
fail(`${path}.bodyLines`, 'must be 0, 1, 2, or 3');
|
|
793
905
|
}
|
|
794
906
|
assertImage(row.thumbnail, `${path}.thumbnail`);
|
|
795
907
|
break;
|
|
@@ -800,6 +912,13 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
800
912
|
row.columns.forEach((column, columnIndex) => {
|
|
801
913
|
assertKey(column.key, `${path}.columns[${columnIndex}].key`);
|
|
802
914
|
assertText(column.text, `${path}.columns[${columnIndex}].text`);
|
|
915
|
+
assertIdentityExtras(column, `${path}.columns[${columnIndex}]`);
|
|
916
|
+
for (const field of ['textSegments', 'secondaryTextSegments']) {
|
|
917
|
+
column[field]?.forEach((segment, segmentIndex) => {
|
|
918
|
+
assertText(segment.text, `${path}.columns[${columnIndex}].${field}[${segmentIndex}].text`);
|
|
919
|
+
if (segment.style !== undefined && segment.style !== 'subscript') fail(`${path}.columns[${columnIndex}].${field}[${segmentIndex}].style`, 'must be subscript');
|
|
920
|
+
});
|
|
921
|
+
}
|
|
803
922
|
assertText(column.secondaryLeadingText, `${path}.columns[${columnIndex}].secondaryLeadingText`);
|
|
804
923
|
assertText(column.secondaryText, `${path}.columns[${columnIndex}].secondaryText`);
|
|
805
924
|
assertTextTone(column.secondaryTone, `${path}.columns[${columnIndex}].secondaryTone`);
|
|
@@ -807,6 +926,10 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
807
926
|
if ((row.badges?.length ?? 0) > MAX_BADGES) {
|
|
808
927
|
fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
|
|
809
928
|
}
|
|
929
|
+
if ((row.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
|
|
930
|
+
fail(`${path}.trailing`, `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`);
|
|
931
|
+
}
|
|
932
|
+
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
810
933
|
break;
|
|
811
934
|
case 'market':
|
|
812
935
|
assertMarketRow(row, path);
|
|
@@ -857,6 +980,11 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
857
980
|
assertText(row.title, `${path}.title`);
|
|
858
981
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
859
982
|
assertText(row.value, `${path}.value`);
|
|
983
|
+
if (row.leadingBadge) {
|
|
984
|
+
assertKey(row.leadingBadge.key, `${path}.leadingBadge.key`);
|
|
985
|
+
assertText(row.leadingBadge.text, `${path}.leadingBadge.text`);
|
|
986
|
+
}
|
|
987
|
+
assertTextHelp(row.valueHelp, `${path}.valueHelp`);
|
|
860
988
|
if (row.valueActionKey !== undefined) {
|
|
861
989
|
assertKey(row.valueActionKey, `${path}.valueActionKey`);
|
|
862
990
|
}
|
|
@@ -877,6 +1005,9 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
877
1005
|
}
|
|
878
1006
|
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
879
1007
|
break;
|
|
1008
|
+
case 'buttonGroup':
|
|
1009
|
+
assertButtonGroup(row, path);
|
|
1010
|
+
break;
|
|
880
1011
|
case 'system':
|
|
881
1012
|
const presentation = 'presentation' in row ? row.presentation : undefined;
|
|
882
1013
|
if (presentation !== undefined && presentation !== 'market') {
|
|
@@ -938,9 +1069,22 @@ function assertMediaPreview(value, path) {
|
|
|
938
1069
|
}
|
|
939
1070
|
}
|
|
940
1071
|
function assertSnapshotContract(snapshot) {
|
|
1072
|
+
if (snapshot.disclosure !== undefined) {
|
|
1073
|
+
if (typeof snapshot.disclosure.expanded !== 'boolean') fail('snapshot.disclosure.expanded', 'must be a boolean');
|
|
1074
|
+
const inset = snapshot.disclosure.expandedTrailingInset;
|
|
1075
|
+
if (inset !== undefined && (!Number.isFinite(inset) || inset < 0)) fail('snapshot.disclosure.expandedTrailingInset', 'must be finite and nonnegative');
|
|
1076
|
+
}
|
|
1077
|
+
if (snapshot.structuralTransition) {
|
|
1078
|
+
assertKey(snapshot.structuralTransition.id, 'snapshot.structuralTransition.id');
|
|
1079
|
+
if (snapshot.structuralTransition.anchorKey !== undefined) assertKey(snapshot.structuralTransition.anchorKey, 'snapshot.structuralTransition.anchorKey');
|
|
1080
|
+
const duration = snapshot.structuralTransition.durationMs;
|
|
1081
|
+
if (duration !== undefined && (!Number.isFinite(duration) || duration < 0 || duration > 2000)) fail('snapshot.structuralTransition.durationMs', 'must be finite within 0...2000');
|
|
1082
|
+
}
|
|
941
1083
|
if (snapshot.capabilities?.refreshTriggerDistance !== undefined && (!Number.isFinite(snapshot.capabilities.refreshTriggerDistance) || snapshot.capabilities.refreshTriggerDistance <= 0)) fail('capabilities.refreshTriggerDistance', 'must be a positive finite number');
|
|
942
1084
|
assertPlainSerializable(snapshot, 'snapshot', new Set());
|
|
943
1085
|
assertText(snapshot.theme?.imageFallbackBackground, 'theme.imageFallbackBackground');
|
|
1086
|
+
if (snapshot.theme?.successBackground !== undefined && (typeof snapshot.theme.successBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.successBackground))) fail('theme.successBackground', 'must be a hex color');
|
|
1087
|
+
if (snapshot.theme?.rowHoverBackground !== undefined && (typeof snapshot.theme.rowHoverBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.rowHoverBackground))) fail('theme.rowHoverBackground', 'must be a hex color');
|
|
944
1088
|
if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
|
|
945
1089
|
if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0) {
|
|
946
1090
|
fail('snapshot.generation', 'must be a non-negative safe integer');
|
|
@@ -1018,8 +1162,8 @@ function protocolObject(value, path) {
|
|
|
1018
1162
|
function protocolKey(value, path) {
|
|
1019
1163
|
if (typeof value !== 'string' || value.trim().length === 0) fail(path, 'must be a non-blank string');
|
|
1020
1164
|
}
|
|
1021
|
-
const PROTOCOL_ARRAY_FIELDS = new Set(['columns', 'cells', 'points', 'images', 'overlays', 'badges', 'trailing', 'footerActions', 'amounts', 'metrics', 'children', 'subtitleSegments', 'titleMatch', 'selectedKeys', 'segments', 'hoverSecondary', 'hoverImages', 'secondaryBadges', 'fallbackUris', 'probeOrder', 'priceSegments', 'textSegments', 'secondaryTextSegments', 'primaryTextSegments']);
|
|
1022
|
-
const PROTOCOL_NUMBER_FIELDS = new Set(['height', 'width', 'fontSize', 'lineHeight', 'opacity', 'offsetY', 'offsetX', 'cornerRadius', 'borderWidth', 'horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'trailingGap', 'start', 'end', 'padding', 'offset', 'overscan', 'minWidth', 'flex', 'paddingStart', 'paddingEnd', 'changeWidth', 'changeHeight', 'changeCornerRadius', 'contentTrailingGap', 'subtitleTrailingPadding', 'partGap', 'contentPadding', 'contentPaddingHorizontal', 'contentPaddingTop', 'contentPaddingBottom', 'itemSpacing', 'gridColumns']);
|
|
1165
|
+
const PROTOCOL_ARRAY_FIELDS = new Set(['columns', 'titleSuffixes', 'cells', 'points', 'images', 'overlays', 'badges', 'trailing', 'footerActions', 'buttons', 'amounts', 'metrics', 'children', 'subtitleSegments', 'titleMatch', 'selectedKeys', 'segments', 'hoverSecondary', 'hoverImages', 'secondaryBadges', 'fallbackUris', 'probeOrder', 'priceSegments', 'textSegments', 'secondaryTextSegments', 'primaryTextSegments']);
|
|
1166
|
+
const PROTOCOL_NUMBER_FIELDS = new Set(['leadingContentMaxWidthRatio', 'height', 'width', 'fontSize', 'lineHeight', 'opacity', 'offsetY', 'offsetX', 'cornerRadius', 'borderWidth', 'horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'trailingGap', 'start', 'end', 'padding', 'offset', 'overscan', 'minWidth', 'flex', 'paddingStart', 'paddingEnd', 'changeWidth', 'changeHeight', 'changeCornerRadius', 'contentTrailingGap', 'subtitleTrailingPadding', 'partGap', 'contentPadding', 'contentPaddingHorizontal', 'contentPaddingTop', 'contentPaddingBottom', 'itemSpacing', 'gridColumns']);
|
|
1023
1167
|
|
|
1024
1168
|
/** Validate only shapes that renderers dereference, not display budgets. */
|
|
1025
1169
|
function assertProtocolFields(value, path, parentField = '') {
|
|
@@ -1037,6 +1181,15 @@ function assertProtocolFields(value, path, parentField = '') {
|
|
|
1037
1181
|
return;
|
|
1038
1182
|
}
|
|
1039
1183
|
const object = value;
|
|
1184
|
+
if (parentField === 'fallbackIcon') {
|
|
1185
|
+
protocolKey(object.name, `${path}.name`);
|
|
1186
|
+
if (object.size !== undefined && (typeof object.size !== 'number' || !Number.isFinite(object.size) || object.size <= 0 || object.size > 40)) fail(`${path}.size`, 'must be within (0, 40]');
|
|
1187
|
+
for (const field of ['tintColor', 'backgroundColor']) {
|
|
1188
|
+
const color = object[field];
|
|
1189
|
+
if (color !== undefined && (typeof color !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(color))) fail(`${path}.${field}`, 'must be a hex color');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (parentField === 'overlays' && object.fallbackIcon !== undefined && !object.image) fail(`${path}.fallbackIcon`, 'requires image');
|
|
1040
1193
|
if (!path.includes('.style.') && ['image', 'networkImage', 'thumbnail', 'images', 'hoverImages'].includes(parentField) && typeof object.uri !== 'string') fail(`${path}.uri`, 'must be a string');
|
|
1041
1194
|
Object.entries(value).forEach(([field, item]) => {
|
|
1042
1195
|
if (item === undefined) return;
|
|
@@ -1044,6 +1197,9 @@ function assertProtocolFields(value, path, parentField = '') {
|
|
|
1044
1197
|
if (!path.endsWith('.style') && PROTOCOL_ARRAY_FIELDS.has(field) && !Array.isArray(item)) fail(fieldPath, 'must be an array');
|
|
1045
1198
|
if (PROTOCOL_NUMBER_FIELDS.has(field) && typeof item !== 'number') fail(fieldPath, 'must be a finite number');
|
|
1046
1199
|
if (PROTOCOL_NUMBER_FIELDS.has(field) && typeof item === 'number' && item < 0 && !['offsetX', 'offsetY'].includes(field)) fail(fieldPath, 'must be non-negative');
|
|
1200
|
+
if (parentField === 'overlays' && field === 'fontWeight' && !['regular', 'medium', 'semibold', 'bold'].includes(item)) fail(fieldPath, 'must be regular, medium, semibold, or bold');
|
|
1201
|
+
if (field === 'buttonStyle' && item !== 'tertiary') fail(fieldPath, 'must be tertiary');
|
|
1202
|
+
if (field === 'hideLeading' && typeof item !== 'boolean') fail(fieldPath, 'must be boolean');
|
|
1047
1203
|
if (field === 'key' || field === 'sectionKey' || field === 'actionKey' || field.endsWith('ActionKey')) protocolKey(item, fieldPath);
|
|
1048
1204
|
if ((field === 'uri' || field === 'fallbackUri') && typeof item !== 'string') fail(fieldPath, 'must be a string');
|
|
1049
1205
|
if (field === 'style' && !(item === 'subscript' && /[Ss]egments\[/.test(path)) || field === 'container') protocolObject(item, fieldPath);
|
|
@@ -1069,12 +1225,21 @@ function assertRowProtocol(row, path) {
|
|
|
1069
1225
|
}
|
|
1070
1226
|
if (row.height !== undefined && row.height < 0) fail(`${path}.height`, 'must be non-negative');
|
|
1071
1227
|
if (row.type === 'identity') {
|
|
1228
|
+
assertIdentityExtras(row, path);
|
|
1229
|
+
if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes)) fail(`${path}.titleSuffixes`, 'must be an array');
|
|
1230
|
+
row.titleSuffixes?.forEach((suffix, index) => {
|
|
1231
|
+
protocolObject(suffix, `${path}.titleSuffixes[${index}]`);
|
|
1232
|
+
if (typeof suffix.text !== 'string' || !['secondary', 'disabled'].includes(suffix.tone)) fail(`${path}.titleSuffixes[${index}]`, 'invalid title suffix');
|
|
1233
|
+
if (suffix.maxWidth !== undefined && (!Number.isFinite(suffix.maxWidth) || suffix.maxWidth < 0 || suffix.maxWidth > 4096)) fail(`${path}.titleSuffixes[${index}].maxWidth`, 'must be finite within 0...4096');
|
|
1234
|
+
});
|
|
1072
1235
|
let end = 0;
|
|
1073
1236
|
row.titleMatch?.forEach(match => {
|
|
1074
1237
|
if (!Number.isInteger(match.start) || !Number.isInteger(match.end) || match.start < end || match.end <= match.start || match.end > row.title.length) fail(`${path}.titleMatch`, 'invalid text range');
|
|
1075
1238
|
end = match.end;
|
|
1076
1239
|
});
|
|
1077
1240
|
}
|
|
1241
|
+
if (row.type === 'buttonGroup') assertButtonGroup(row, path);
|
|
1242
|
+
if (row.type === 'sectionHeader') assertTextHelp(row.valueHelp, `${path}.valueHelp`);
|
|
1078
1243
|
if (row.type === 'mediaTile' && row.media) {
|
|
1079
1244
|
protocolObject(row.media.source, `${path}.media.source`);
|
|
1080
1245
|
if (!Array.isArray(row.media.probeOrder)) fail(`${path}.media.probeOrder`, 'must be an array');
|
|
@@ -1091,7 +1256,15 @@ function assertRowProtocol(row, path) {
|
|
|
1091
1256
|
keys.add(member.key);
|
|
1092
1257
|
});
|
|
1093
1258
|
}
|
|
1094
|
-
if (row.type === 'dataRow'
|
|
1259
|
+
if (row.type === 'dataRow') {
|
|
1260
|
+
if (!Array.isArray(row.columns)) fail(`${path}.columns`, 'must be an array');
|
|
1261
|
+
row.columns.forEach((column, index) => {
|
|
1262
|
+
const columnPath = `${path}.columns[${index}]`;
|
|
1263
|
+
protocolObject(column, columnPath);
|
|
1264
|
+
assertIdentityExtras(column, columnPath);
|
|
1265
|
+
});
|
|
1266
|
+
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
1267
|
+
}
|
|
1095
1268
|
if (row.type === 'market') {
|
|
1096
1269
|
protocolObject(row.change, `${path}.change`);
|
|
1097
1270
|
if (row.webTable) {
|
|
@@ -1118,6 +1291,21 @@ function assertRowProtocol(row, path) {
|
|
|
1118
1291
|
}
|
|
1119
1292
|
function assertSnapshotProtocol(snapshot) {
|
|
1120
1293
|
protocolObject(snapshot, 'snapshot');
|
|
1294
|
+
if (snapshot.disclosure !== undefined) {
|
|
1295
|
+
protocolObject(snapshot.disclosure, 'snapshot.disclosure');
|
|
1296
|
+
if (typeof snapshot.disclosure.expanded !== 'boolean') fail('snapshot.disclosure.expanded', 'must be a boolean');
|
|
1297
|
+
const inset = snapshot.disclosure.expandedTrailingInset;
|
|
1298
|
+
if (inset !== undefined && (!Number.isFinite(inset) || inset < 0)) fail('snapshot.disclosure.expandedTrailingInset', 'must be finite and nonnegative');
|
|
1299
|
+
}
|
|
1300
|
+
if (snapshot.structuralTransition) {
|
|
1301
|
+
protocolObject(snapshot.structuralTransition, 'snapshot.structuralTransition');
|
|
1302
|
+
protocolKey(snapshot.structuralTransition.id, 'snapshot.structuralTransition.id');
|
|
1303
|
+
if (snapshot.structuralTransition.anchorKey !== undefined) protocolKey(snapshot.structuralTransition.anchorKey, 'snapshot.structuralTransition.anchorKey');
|
|
1304
|
+
const duration = snapshot.structuralTransition.durationMs;
|
|
1305
|
+
if (duration !== undefined && (!Number.isFinite(duration) || duration < 0 || duration > 2000)) fail('snapshot.structuralTransition.durationMs', 'must be finite within 0...2000');
|
|
1306
|
+
}
|
|
1307
|
+
if (snapshot.theme?.successBackground !== undefined && (typeof snapshot.theme.successBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.successBackground))) fail('theme.successBackground', 'must be a hex color');
|
|
1308
|
+
if (snapshot.theme?.rowHoverBackground !== undefined && (typeof snapshot.theme.rowHoverBackground !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.rowHoverBackground))) fail('theme.rowHoverBackground', 'must be a hex color');
|
|
1121
1309
|
if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
|
|
1122
1310
|
if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0) fail('snapshot.generation', 'must be a non-negative safe integer');
|
|
1123
1311
|
protocolObject(snapshot.layout, 'snapshot.layout');
|
|
@@ -1174,6 +1362,53 @@ export function validateSnapshot(snapshot) {
|
|
|
1174
1362
|
assertSnapshotProtocol(normalized);
|
|
1175
1363
|
return normalizeSnapshotStyles(normalized);
|
|
1176
1364
|
}
|
|
1365
|
+
const ownedNativeSnapshots = new WeakMap();
|
|
1366
|
+
function capturePlainData(value, seen = new Map()) {
|
|
1367
|
+
if (value === null || typeof value !== 'object') return value;
|
|
1368
|
+
const previous = seen.get(value);
|
|
1369
|
+
if (previous) return previous;
|
|
1370
|
+
const copy = Array.isArray(value) ? new Array(value.length) : Object.create(Object.getPrototypeOf(value));
|
|
1371
|
+
seen.set(value, copy);
|
|
1372
|
+
const captureKey = key => {
|
|
1373
|
+
Object.defineProperty(copy, key, {
|
|
1374
|
+
value: capturePlainData(value[key], seen),
|
|
1375
|
+
enumerable: true,
|
|
1376
|
+
writable: true,
|
|
1377
|
+
configurable: true
|
|
1378
|
+
});
|
|
1379
|
+
};
|
|
1380
|
+
if (Array.isArray(value)) {
|
|
1381
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1382
|
+
if (index in value) captureKey(String(index));
|
|
1383
|
+
}
|
|
1384
|
+
Object.keys(value).forEach(key => {
|
|
1385
|
+
const index = Number(key);
|
|
1386
|
+
if (Number.isInteger(index) && index >= 0 && index < value.length && String(index) === key) return;
|
|
1387
|
+
captureKey(key);
|
|
1388
|
+
});
|
|
1389
|
+
} else {
|
|
1390
|
+
Object.keys(value).forEach(captureKey);
|
|
1391
|
+
}
|
|
1392
|
+
return copy;
|
|
1393
|
+
}
|
|
1394
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
1395
|
+
if (value === null || typeof value !== 'object' || seen.has(value)) return value;
|
|
1396
|
+
seen.add(value);
|
|
1397
|
+
Object.values(value).forEach(item => deepFreeze(item, seen));
|
|
1398
|
+
return Object.freeze(value);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/** Capture caller data once before validation, including accessor values. */
|
|
1402
|
+
export function captureNativeListInput(value) {
|
|
1403
|
+
return capturePlainData(value);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** Private native-wrapper ownership; exported validators keep their public identity. */
|
|
1407
|
+
export function ownValidatedNativeSnapshot(snapshot) {
|
|
1408
|
+
const owned = deepFreeze(snapshot);
|
|
1409
|
+
ownedNativeSnapshots.set(owned, new Map(owned.rows.map((row, index) => [row.key, index])));
|
|
1410
|
+
return owned;
|
|
1411
|
+
}
|
|
1177
1412
|
|
|
1178
1413
|
/**
|
|
1179
1414
|
* Resolves typography tokens to numbers. Returns the original snapshot when
|
|
@@ -1218,7 +1453,23 @@ function assertPatchChanges(patch, index) {
|
|
|
1218
1453
|
}
|
|
1219
1454
|
switch (patch.type) {
|
|
1220
1455
|
case 'identity':
|
|
1456
|
+
if (patch.changes.contentHeight !== undefined && patch.changes.contentHeight !== 'auto') {
|
|
1457
|
+
fail(`${path}.contentHeight`, 'must be auto');
|
|
1458
|
+
}
|
|
1459
|
+
if (patch.changes.subtitleLines !== undefined && ![0, 1, 2].includes(patch.changes.subtitleLines)) {
|
|
1460
|
+
fail(`${path}.subtitleLines`, 'must be 0, 1, or 2');
|
|
1461
|
+
}
|
|
1462
|
+
if (patch.changes.longPressActionKey !== undefined) {
|
|
1463
|
+
assertKey(patch.changes.longPressActionKey, `${path}.longPressActionKey`);
|
|
1464
|
+
}
|
|
1221
1465
|
assertText(patch.changes.title, `${path}.title`);
|
|
1466
|
+
if ((patch.changes.titleSuffixes?.length ?? 0) > 3) fail(`${path}.titleSuffixes`, 'supports at most 3 suffixes');
|
|
1467
|
+
patch.changes.titleSuffixes?.forEach((suffix, index) => {
|
|
1468
|
+
assertText(suffix.text, `${path}.titleSuffixes[${index}].text`);
|
|
1469
|
+
if (!['secondary', 'disabled'].includes(suffix.tone)) fail(`${path}.titleSuffixes[${index}].tone`, 'must be secondary or disabled');
|
|
1470
|
+
if (suffix.maxWidth !== undefined && (!Number.isFinite(suffix.maxWidth) || suffix.maxWidth < 0 || suffix.maxWidth > 4096)) fail(`${path}.titleSuffixes[${index}].maxWidth`, 'must be finite within 0...4096');
|
|
1471
|
+
});
|
|
1472
|
+
assertIdentityExtras(patch.changes, path);
|
|
1222
1473
|
assertText(patch.changes.subtitle, `${path}.subtitle`);
|
|
1223
1474
|
assertText(patch.changes.tertiary, `${path}.tertiary`);
|
|
1224
1475
|
if (patch.changes.tertiaryTone !== undefined && !['secondary', 'info'].includes(patch.changes.tertiaryTone)) {
|
|
@@ -1252,11 +1503,14 @@ function assertPatchChanges(patch, index) {
|
|
|
1252
1503
|
}
|
|
1253
1504
|
break;
|
|
1254
1505
|
case 'message':
|
|
1506
|
+
if (patch.changes.presentation !== undefined && patch.changes.presentation !== 'inlineWarning') {
|
|
1507
|
+
fail(`${path}.presentation`, 'must be inlineWarning');
|
|
1508
|
+
}
|
|
1255
1509
|
assertText(patch.changes.title, `${path}.title`);
|
|
1256
1510
|
assertText(patch.changes.body, `${path}.body`);
|
|
1257
1511
|
assertText(patch.changes.time, `${path}.time`);
|
|
1258
|
-
if (patch.changes.bodyLines !== undefined && ![1, 2, 3].includes(patch.changes.bodyLines)) {
|
|
1259
|
-
fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
|
|
1512
|
+
if (patch.changes.bodyLines !== undefined && ![0, 1, 2, 3].includes(patch.changes.bodyLines)) {
|
|
1513
|
+
fail(`${path}.bodyLines`, 'must be 0, 1, 2, or 3');
|
|
1260
1514
|
}
|
|
1261
1515
|
assertImage(patch.changes.thumbnail, `${path}.thumbnail`);
|
|
1262
1516
|
break;
|
|
@@ -1267,12 +1521,23 @@ function assertPatchChanges(patch, index) {
|
|
|
1267
1521
|
patch.changes.columns?.forEach((column, columnIndex) => {
|
|
1268
1522
|
assertKey(column.key, `${path}.columns[${columnIndex}].key`);
|
|
1269
1523
|
assertText(column.text, `${path}.columns[${columnIndex}].text`);
|
|
1524
|
+
assertIdentityExtras(column, `${path}.columns[${columnIndex}]`);
|
|
1525
|
+
for (const field of ['textSegments', 'secondaryTextSegments']) {
|
|
1526
|
+
column[field]?.forEach((segment, segmentIndex) => {
|
|
1527
|
+
assertText(segment.text, `${path}.columns[${columnIndex}].${field}[${segmentIndex}].text`);
|
|
1528
|
+
if (segment.style !== undefined && segment.style !== 'subscript') fail(`${path}.columns[${columnIndex}].${field}[${segmentIndex}].style`, 'must be subscript');
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1270
1531
|
assertText(column.secondaryText, `${path}.columns[${columnIndex}].secondaryText`);
|
|
1271
1532
|
assertTextTone(column.secondaryTone, `${path}.columns[${columnIndex}].secondaryTone`);
|
|
1272
1533
|
});
|
|
1273
1534
|
if ((patch.changes.badges?.length ?? 0) > MAX_BADGES) {
|
|
1274
1535
|
fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
|
|
1275
1536
|
}
|
|
1537
|
+
if ((patch.changes.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
|
|
1538
|
+
fail(`${path}.trailing`, `supports at most ${MAX_TRAILING_ACCESSORIES} accessories`);
|
|
1539
|
+
}
|
|
1540
|
+
assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
|
|
1276
1541
|
break;
|
|
1277
1542
|
case 'market':
|
|
1278
1543
|
assertMarketRow({
|
|
@@ -1331,6 +1596,11 @@ function assertPatchChanges(patch, index) {
|
|
|
1331
1596
|
assertText(patch.changes.title, `${path}.title`);
|
|
1332
1597
|
assertText(patch.changes.subtitle, `${path}.subtitle`);
|
|
1333
1598
|
assertText(patch.changes.value, `${path}.value`);
|
|
1599
|
+
if (patch.changes.leadingBadge) {
|
|
1600
|
+
assertKey(patch.changes.leadingBadge.key, `${path}.leadingBadge.key`);
|
|
1601
|
+
assertText(patch.changes.leadingBadge.text, `${path}.leadingBadge.text`);
|
|
1602
|
+
}
|
|
1603
|
+
assertTextHelp(patch.changes.valueHelp, `${path}.valueHelp`);
|
|
1334
1604
|
if (patch.changes.valueActionKey !== undefined) {
|
|
1335
1605
|
assertKey(patch.changes.valueActionKey, `${path}.valueActionKey`);
|
|
1336
1606
|
}
|
|
@@ -1344,6 +1614,11 @@ function assertPatchChanges(patch, index) {
|
|
|
1344
1614
|
}
|
|
1345
1615
|
assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
|
|
1346
1616
|
break;
|
|
1617
|
+
case 'buttonGroup':
|
|
1618
|
+
if (patch.changes.buttons !== undefined) assertButtonGroup({
|
|
1619
|
+
buttons: patch.changes.buttons
|
|
1620
|
+
}, path);
|
|
1621
|
+
break;
|
|
1347
1622
|
case 'system':
|
|
1348
1623
|
assertText(patch.changes.message, `${path}.message`);
|
|
1349
1624
|
break;
|
|
@@ -1363,6 +1638,7 @@ export function validatePatches(patches) {
|
|
|
1363
1638
|
protocolObject(patch.changes, `patches[${index}].changes`);
|
|
1364
1639
|
assertRowType(patch.type, `patches[${index}]`);
|
|
1365
1640
|
assertProtocolFields(patch.changes, `patches[${index}].changes`);
|
|
1641
|
+
if (patch.type === 'identity') assertIdentityExtras(patch.changes, `patches[${index}].changes`);
|
|
1366
1642
|
if (!isDevelopment()) continue;
|
|
1367
1643
|
assertKey(patch.key, `patches[${index}].key`);
|
|
1368
1644
|
if (Object.keys(patch.changes).length === 0) {
|
|
@@ -1393,6 +1669,96 @@ function normalizePatchStyles(patches) {
|
|
|
1393
1669
|
});
|
|
1394
1670
|
return changed ? next : patches;
|
|
1395
1671
|
}
|
|
1672
|
+
const EMPTY_ROW_PATCHES = Object.freeze([]);
|
|
1673
|
+
function sameJsonValue(left, right) {
|
|
1674
|
+
if (left === right) return true;
|
|
1675
|
+
if (left === null || right === null) return false;
|
|
1676
|
+
if (typeof left !== 'object' || typeof right !== 'object') return false;
|
|
1677
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
1678
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => sameJsonValue(value, right[index]));
|
|
1679
|
+
}
|
|
1680
|
+
const leftObject = left;
|
|
1681
|
+
const rightObject = right;
|
|
1682
|
+
const leftKeys = Object.keys(leftObject).filter(key => leftObject[key] !== undefined);
|
|
1683
|
+
const rightKeys = Object.keys(rightObject).filter(key => rightObject[key] !== undefined);
|
|
1684
|
+
return leftKeys.length === rightKeys.length && leftKeys.every(key => sameJsonValue(leftObject[key], rightObject[key]));
|
|
1685
|
+
}
|
|
1686
|
+
function sameOptionalPresence(left, right) {
|
|
1687
|
+
return left === undefined === (right === undefined);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/** Plan value-only patches; callers own valid immutable snapshots and commit state. */
|
|
1691
|
+
export function planNativeListPatches(previous, next) {
|
|
1692
|
+
if (previous === next) return EMPTY_ROW_PATCHES;
|
|
1693
|
+
const {
|
|
1694
|
+
rows: previousRows,
|
|
1695
|
+
...previousMetadata
|
|
1696
|
+
} = previous;
|
|
1697
|
+
const {
|
|
1698
|
+
rows: nextRows,
|
|
1699
|
+
...nextMetadata
|
|
1700
|
+
} = next;
|
|
1701
|
+
if (previousRows.length !== nextRows.length || !sameJsonValue(previousMetadata, nextMetadata)) return undefined;
|
|
1702
|
+
if (previousRows === nextRows) return EMPTY_ROW_PATCHES;
|
|
1703
|
+
const patches = [];
|
|
1704
|
+
for (let index = 0; index < nextRows.length; index += 1) {
|
|
1705
|
+
const oldRow = previousRows[index];
|
|
1706
|
+
const row = nextRows[index];
|
|
1707
|
+
if (oldRow.key !== row.key || oldRow.type !== row.type) return undefined;
|
|
1708
|
+
if (oldRow === row) continue;
|
|
1709
|
+
if (oldRow.type === 'identity' && row.type === 'identity') {
|
|
1710
|
+
const {
|
|
1711
|
+
subtitle: oldSubtitle,
|
|
1712
|
+
subtitleSegments: oldSubtitleSegments,
|
|
1713
|
+
trailing: oldTrailing,
|
|
1714
|
+
...oldMetadata
|
|
1715
|
+
} = oldRow;
|
|
1716
|
+
const {
|
|
1717
|
+
subtitle,
|
|
1718
|
+
subtitleSegments,
|
|
1719
|
+
trailing,
|
|
1720
|
+
...metadata
|
|
1721
|
+
} = row;
|
|
1722
|
+
if (!sameOptionalPresence(oldSubtitle, subtitle) || !sameOptionalPresence(oldSubtitleSegments, subtitleSegments) || !sameOptionalPresence(oldTrailing, trailing) || !sameJsonValue(oldMetadata, metadata)) return undefined;
|
|
1723
|
+
if (!sameJsonValue(oldSubtitle, subtitle) || !sameJsonValue(oldSubtitleSegments, subtitleSegments) || !sameJsonValue(oldTrailing, trailing)) {
|
|
1724
|
+
patches.push({
|
|
1725
|
+
type: 'identity',
|
|
1726
|
+
key: row.key,
|
|
1727
|
+
changes: {
|
|
1728
|
+
subtitle,
|
|
1729
|
+
subtitleSegments,
|
|
1730
|
+
trailing
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
} else if (oldRow.type === 'dataRow' && row.type === 'dataRow') {
|
|
1735
|
+
const {
|
|
1736
|
+
columns: oldColumns,
|
|
1737
|
+
trailing: oldTrailing,
|
|
1738
|
+
...oldMetadata
|
|
1739
|
+
} = oldRow;
|
|
1740
|
+
const {
|
|
1741
|
+
columns,
|
|
1742
|
+
trailing,
|
|
1743
|
+
...metadata
|
|
1744
|
+
} = row;
|
|
1745
|
+
if (!sameOptionalPresence(oldTrailing, trailing) || !sameJsonValue(oldMetadata, metadata) || !sameJsonValue(oldColumns[0], columns[0])) return undefined;
|
|
1746
|
+
if (!sameJsonValue(oldColumns.slice(1), columns.slice(1)) || !sameJsonValue(oldTrailing, trailing)) {
|
|
1747
|
+
patches.push({
|
|
1748
|
+
type: 'dataRow',
|
|
1749
|
+
key: row.key,
|
|
1750
|
+
changes: {
|
|
1751
|
+
columns,
|
|
1752
|
+
trailing
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
} else if (!sameJsonValue(oldRow, row)) {
|
|
1757
|
+
return undefined;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return patches.length === 0 ? EMPTY_ROW_PATCHES : patches;
|
|
1761
|
+
}
|
|
1396
1762
|
export function applyRowPatches(snapshot, patches) {
|
|
1397
1763
|
const normalizedPatches = validatePatches(patches);
|
|
1398
1764
|
const patchByKey = new Map(normalizedPatches.map(patch => [patch.key, patch]));
|
|
@@ -1419,6 +1785,53 @@ export function applyRowPatches(snapshot, patches) {
|
|
|
1419
1785
|
rows
|
|
1420
1786
|
});
|
|
1421
1787
|
}
|
|
1788
|
+
|
|
1789
|
+
/** Native wrapper only: preserve full public validation for unsupported patches. */
|
|
1790
|
+
export function applyOwnedNativePatches(snapshot, patches) {
|
|
1791
|
+
const acceptedPatches = deepFreeze(validatePatches(capturePlainData(patches)));
|
|
1792
|
+
const rowIndexes = ownedNativeSnapshots.get(snapshot);
|
|
1793
|
+
const canPatchRows = rowIndexes !== undefined && acceptedPatches.every(patch => {
|
|
1794
|
+
const index = rowIndexes.get(patch.key);
|
|
1795
|
+
if (index === undefined || snapshot.rows[index].type !== patch.type) return false;
|
|
1796
|
+
const allowed = patch.type === 'identity' ? ['subtitle', 'subtitleSegments', 'trailing'] : patch.type === 'dataRow' ? ['columns', 'trailing'] : undefined;
|
|
1797
|
+
return allowed !== undefined && Object.entries(patch.changes).every(([key, value]) => allowed.includes(key) && (value !== undefined || snapshot.rows[index][key] === undefined));
|
|
1798
|
+
});
|
|
1799
|
+
if (!canPatchRows) {
|
|
1800
|
+
const baseline = rowIndexes ? snapshot : capturePlainData(snapshot);
|
|
1801
|
+
return {
|
|
1802
|
+
patches: acceptedPatches,
|
|
1803
|
+
snapshot: ownValidatedNativeSnapshot(applyRowPatches(baseline, acceptedPatches)),
|
|
1804
|
+
json: JSON.stringify(acceptedPatches)
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
const nextRows = snapshot.rows.slice();
|
|
1808
|
+
for (const patch of acceptedPatches) {
|
|
1809
|
+
const index = rowIndexes.get(patch.key);
|
|
1810
|
+
const previous = nextRows[index];
|
|
1811
|
+
const merged = {
|
|
1812
|
+
...previous,
|
|
1813
|
+
...patch.changes,
|
|
1814
|
+
key: previous.key,
|
|
1815
|
+
type: previous.type
|
|
1816
|
+
};
|
|
1817
|
+
const path = `rows[${index}]`;
|
|
1818
|
+
assertPlainSerializable(merged, path, new Set());
|
|
1819
|
+
if (isDevelopment()) assertRow(merged, index);
|
|
1820
|
+
const normalized = isDevelopment() ? merged : normalizeEmptyImages(merged);
|
|
1821
|
+
assertRowProtocol(normalized, path);
|
|
1822
|
+
nextRows[index] = deepFreeze(normalizeRowStyles(normalized));
|
|
1823
|
+
}
|
|
1824
|
+
const nextSnapshot = Object.freeze({
|
|
1825
|
+
...snapshot,
|
|
1826
|
+
rows: Object.freeze(nextRows)
|
|
1827
|
+
});
|
|
1828
|
+
ownedNativeSnapshots.set(nextSnapshot, rowIndexes);
|
|
1829
|
+
return {
|
|
1830
|
+
patches: acceptedPatches,
|
|
1831
|
+
snapshot: nextSnapshot,
|
|
1832
|
+
json: JSON.stringify(acceptedPatches)
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1422
1835
|
export function serializeSnapshot(snapshot) {
|
|
1423
1836
|
return JSON.stringify(validateSnapshot(snapshot));
|
|
1424
1837
|
}
|