@onekeyfe/react-native-native-list 3.0.105 → 3.0.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +16 -3
  2. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +6 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +671 -52
  4. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +282 -54
  5. package/ios/HybridNativeList.swift +8 -2
  6. package/ios/NativeListCell.swift +630 -31
  7. package/ios/NativeListDesignAssets.swift +24 -1
  8. package/ios/RNCNativeListView.swift +216 -42
  9. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +15 -0
  10. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +14 -0
  11. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +15 -0
  12. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +12 -0
  13. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +15 -0
  14. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +6 -0
  15. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +15 -0
  16. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +15 -0
  17. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +15 -0
  18. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +3 -0
  19. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +15 -0
  20. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +7 -0
  21. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +15 -0
  22. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +18 -0
  23. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +15 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +7 -0
  25. package/lib/module/NativeList.js +102 -8
  26. package/lib/module/NativeList.web.js +20 -2
  27. package/lib/module/avatarPrefetch.js +174 -0
  28. package/lib/module/validation.js +45 -2
  29. package/lib/module/web/NativeListAvatarWorker.js +542 -0
  30. package/lib/module/web/NativeListWebAvatarCache.js +267 -0
  31. package/lib/module/web/NativeListWebEngine.js +958 -47
  32. package/lib/typescript/src/NativeList.web.d.ts +2 -2
  33. package/lib/typescript/src/avatarPrefetch.d.ts +28 -0
  34. package/lib/typescript/src/models.d.ts +72 -4
  35. package/lib/typescript/src/web/NativeListWebAvatarCache.d.ts +6 -0
  36. package/lib/typescript/src/web/NativeListWebEngine.d.ts +10 -1
  37. package/package.json +2 -2
  38. package/src/NativeList.tsx +149 -13
  39. package/src/NativeList.web.tsx +30 -3
  40. package/src/avatarPrefetch.ts +236 -0
  41. package/src/models.ts +98 -7
  42. package/src/validation.ts +131 -2
  43. package/src/web/NativeListAvatarWorker.js +567 -0
  44. package/src/web/NativeListWebAvatarCache.ts +314 -0
  45. package/src/web/NativeListWebEngine.ts +1409 -60
@@ -37,8 +37,18 @@ import {
37
37
  type NormalizedPositionScroll,
38
38
  } from '../scrolling';
39
39
  import { applyRowPatches, validateSnapshot } from '../validation';
40
-
41
- const SECTION_INDEX_GUTTER = 44;
40
+ import { avatarPrefetchWindow } from '../avatarPrefetch';
41
+ import {
42
+ acquireNativeListAvatar,
43
+ canonicalNativeListAvatarUri,
44
+ type AvatarLease,
45
+ } from './NativeListWebAvatarCache';
46
+
47
+ const SECTION_INDEX_CONTENT_INSET = 16;
48
+ const SECTION_INDEX_RAIL_WIDTH = 32;
49
+ const SECTION_INDEX_EDGE_PADDING = 8;
50
+ const SECTION_INDEX_MIN_LABEL_SPACING = 14;
51
+ const SECTION_INDEX_MIN_HEIGHT = 120;
42
52
  const DEFAULT_VIEWPORT_WIDTH = 320;
43
53
  const DEFAULT_VIEWPORT_HEIGHT = 640;
44
54
  const OVERSCAN_VIEWPORTS = 1;
@@ -354,11 +364,40 @@ export function estimateWebRowHeight(
354
364
  snapshot: NativeListSnapshot,
355
365
  availableWidth: number
356
366
  ): number {
357
- if (row.type === 'system' && row.variant === 'spacer') return row.height;
367
+ // OneKey patch: explicit selector height takes precedence over presets.
368
+ // if (row.type === 'system' && row.variant === 'spacer') return row.height;
369
+ if (row.height !== undefined) return row.height;
370
+ if (row.type === 'system' && row.variant === 'warning') {
371
+ const width = Math.max(1, availableWidth - 24);
372
+ const lines = (text: string) =>
373
+ Math.max(
374
+ 1,
375
+ Math.ceil(
376
+ Array.from(text).reduce(
377
+ (length, char) => length + (char.charCodeAt(0) > 255 ? 14 : 7),
378
+ 0
379
+ ) / width
380
+ )
381
+ );
382
+ return 32 + 20 * (lines(row.title) + lines(row.message));
383
+ }
358
384
  if (row.type === 'walletGroup')
359
- return (row.children.length + 1) * 68 + row.children.length * 12;
385
+ // OneKey patch: wallet badges participate in the outer group height.
386
+ // return (row.children.length + 1) * 68 + row.children.length * 12;
387
+ return (
388
+ [row.parent, ...row.children].reduce(
389
+ (height, member) =>
390
+ height + estimateWebRowHeight(member, snapshot, availableWidth),
391
+ 0
392
+ ) +
393
+ row.children.length * 12 +
394
+ (row.parent.height !== undefined ? 2 : 0)
395
+ );
396
+ // OneKey patch: reserve the source badge line below wallet names.
397
+ // if (row.type === 'identity' && row.presentation === 'walletSidebar')
398
+ // return 68;
360
399
  if (row.type === 'identity' && row.presentation === 'walletSidebar')
361
- return 68;
400
+ return 68 + (row.badges?.length ? 24 : 0);
362
401
  if (row.type === 'identity' && row.presentation === 'networkSelector')
363
402
  return 47;
364
403
  if (row.type === 'identity' && row.presentation === 'accountSelector')
@@ -471,9 +510,13 @@ export function computeWebListLayout(
471
510
  const horizontal = snapshot.layout.orientation === 'horizontal';
472
511
  const spacing = snapshot.layout.itemSpacing ?? 0;
473
512
  const padding = paddingValues(snapshot);
474
- const indexGutter = sectionIndexEnabled(snapshot) ? SECTION_INDEX_GUTTER : 0;
475
513
  const width = Math.max(1, viewportWidth || DEFAULT_VIEWPORT_WIDTH);
476
514
  const height = Math.max(1, viewportHeight || DEFAULT_VIEWPORT_HEIGHT);
515
+ // OneKey patch: the index overlays the content and only keeps a small accessory-safe inset.
516
+ const indexGutter =
517
+ sectionIndexEnabled(snapshot) && height >= SECTION_INDEX_MIN_HEIGHT
518
+ ? SECTION_INDEX_CONTENT_INSET
519
+ : 0;
477
520
  const availableWidth = Math.max(
478
521
  1,
479
522
  width - padding.horizontal * 2 - indexGutter
@@ -699,7 +742,9 @@ export function webRowRenderSignature(row: RowModel): string {
699
742
  return JSON.stringify(rowWithoutSelectionState(row));
700
743
  }
701
744
 
745
+ // OneKey patch: selector names must shrink before the sidebar clips their contents.
702
746
  export const WEB_LIST_CSS = `
747
+ [data-native-list-selector="walletSidebar"] .ok-native-list-title{max-width:100%;min-width:0}
703
748
  .ok-native-list-root{--nl-bg:#f7f7f7;--nl-row:#fff;--nl-selected:#eaf2ff;--nl-pressed:#e8e8e8;--nl-subdued:#f9f9f9;--nl-strong:#0000000f;--nl-primary:#111;--nl-secondary:#6b7280;--nl-disabled:#8d8d8d;--nl-icon:#111;--nl-icon-subdued:#8d8d8d;--nl-separator:#e5e7eb;--nl-accent:#2f6bff;--nl-positive:#15803d;--nl-negative:#dc2626;--nl-critical:#feecec;--nl-inverse:#202020;--nl-inverse-text:#fcfcfc;--nl-info:#0d74ce;position:absolute;inset:0;display:flex;min-width:0;min-height:0;overflow:hidden;background:var(--nl-bg);color:var(--nl-primary);font-family:Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-synthesis:none}
704
749
  .ok-native-list-viewport-frame{position:relative;flex:1;min-width:0;min-height:0;overflow:hidden}
705
750
  .ok-native-list-viewport{position:absolute;inset:0;overflow:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;scrollbar-gutter:stable}
@@ -710,6 +755,7 @@ export const WEB_LIST_CSS = `
710
755
  .ok-native-list-item[data-native-list-selected="true"]>.ok-native-list-row{background:var(--nl-selected)}
711
756
  .ok-native-list-item[data-native-list-disabled="true"]>.ok-native-list-row{opacity:.5;cursor:default}
712
757
  .ok-native-list-item:not([data-native-list-disabled="true"]):hover>.ok-native-list-row{background:var(--nl-pressed)}
758
+ .ok-native-list-item:not([data-native-list-disabled="true"]):active>.ok-native-list-row{background:var(--nl-pressed)}
713
759
  .ok-native-list-item:not([data-native-list-disabled="true"]):not([data-native-list-selected="true"]):hover>.ok-native-list-wallet-row{background:var(--nl-strong)}
714
760
  .ok-native-list-item:not([data-native-list-disabled="true"]):not([data-native-list-selected="true"]):active>.ok-native-list-wallet-row{background:var(--nl-pressed)}
715
761
  .ok-native-list-item[data-native-list-selected="true"]:hover>.ok-native-list-wallet-row{background:var(--nl-selected)}
@@ -752,9 +798,29 @@ export const WEB_LIST_CSS = `
752
798
  .ok-native-list-media{display:block;padding:0 5px;background:transparent;border-radius:16px}.ok-native-list-media-image{display:block;width:100%;aspect-ratio:1;border-radius:10px;background:var(--nl-strong);object-fit:cover}.ok-native-list-media-image[data-state="empty"]{background:transparent}.ok-native-list-media-image[data-state="error"]{display:flex;align-items:center;justify-content:center;color:var(--nl-icon-subdued);font-size:24px}.ok-native-list-media-meta{padding-top:7px}.ok-native-list-media-subtitle-row{display:flex;align-items:center;gap:6px}.ok-native-list-media-subtitle{flex:1;min-width:0;font-size:12px;color:var(--nl-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-media-network{width:14px;height:14px;border-radius:50%}.ok-native-list-media-title{font-size:16px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-media-close{position:absolute;right:9px;top:4px;border:0;background:color-mix(in srgb,var(--nl-inverse) 72%,transparent);color:var(--nl-inverse-text);width:24px;height:24px;border-radius:50%;font:18px/20px inherit;cursor:pointer}
753
799
  .ok-native-list-metric{display:flex;flex-direction:column;align-items:flex-start;padding:12px;border-radius:12px;gap:5px;background:var(--nl-row)}.ok-native-list-metric-value{font-size:22px;line-height:28px;font-weight:700}.ok-native-list-composite{display:flex;flex-direction:column;align-items:stretch;padding:14px;border-radius:12px;gap:12px;background:var(--nl-subdued)}.ok-native-list-composite-heading{font-size:14px;letter-spacing:1px;color:var(--nl-secondary)}.ok-native-list-composite-row{display:flex;gap:12px}.ok-native-list-composite-cell{flex:1;min-width:0}.ok-native-list-composite-cell[data-shaded="true"]{padding:10px;border-radius:10px;background:color-mix(in srgb,var(--nl-primary) 5%,transparent)}.ok-native-list-composite-value{font-size:18px;font-weight:600}.ok-native-list-divider{height:1px;background:var(--nl-separator)}.ok-native-list-progress{height:4px;border-radius:2px;overflow:hidden;background:var(--nl-negative)}.ok-native-list-progress>span{display:block;height:100%;border-radius:2px;background:var(--nl-positive)}
754
800
  .ok-native-list-data{padding:6px 12px}.ok-native-list-index{flex:0 0 28px;color:var(--nl-secondary);font-size:13px}.ok-native-list-favorite{flex:0 0 24px;color:var(--nl-icon-subdued);font-size:22px}.ok-native-list-favorite[data-active="true"]{color:var(--nl-accent)}.ok-native-list-data-cell{display:flex;flex-direction:column;min-width:0}.ok-native-list-data-cell[data-align="center"]{align-items:center}.ok-native-list-data-cell[data-align="end"]{align-items:flex-end}.ok-native-list-data-primary{display:flex;align-items:center;gap:5px;max-width:100%;font-size:16px;font-weight:500;white-space:nowrap}.ok-native-list-unread{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--nl-accent)}.ok-native-list-thumbnail{width:64px;height:64px;border-radius:10px;object-fit:cover}
755
- .ok-native-list-footer{flex:0 0 auto;min-height:0}.ok-native-list-sticky{position:absolute;z-index:4;left:0;right:0;top:0;pointer-events:auto;box-shadow:0 1px 0 var(--nl-separator)}.ok-native-list-index-rail{position:absolute;z-index:6;top:0;right:0;bottom:0;width:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0;touch-action:none}.ok-native-list-index-rail[hidden]{display:none}.ok-native-list-index-button{appearance:none;border:0;background:transparent;display:flex;flex:1;max-height:22px;min-height:12px;width:100%;align-items:center;justify-content:center;padding:0;color:var(--nl-secondary);font:600 11px/1 inherit;cursor:pointer}.ok-native-list-index-button[data-active="true"]{color:var(--nl-accent)}.ok-native-list-index-preview{position:absolute;z-index:8;left:50%;top:50%;display:flex;width:72px;height:72px;align-items:center;justify-content:center;transform:translate(-50%,-50%) scale(.92);border-radius:16px;background:var(--nl-inverse);color:var(--nl-inverse-text);font-size:28px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .15s ease,transform .15s ease}.ok-native-list-index-preview[data-visible="true"]{opacity:1;transform:translate(-50%,-50%) scale(1)}
801
+ .ok-native-list-footer{flex:0 0 auto;min-height:0}.ok-native-list-sticky{position:absolute;z-index:4;left:0;right:0;top:0;pointer-events:auto;box-shadow:0 1px 0 var(--nl-separator)}.ok-native-list-index-rail{position:absolute;z-index:6;top:0;right:0;bottom:0;width:${SECTION_INDEX_RAIL_WIDTH}px;touch-action:none;cursor:pointer}.ok-native-list-index-rail[hidden]{display:none}.ok-native-list-index-button{appearance:none;position:absolute;left:6px;display:flex;width:20px;height:16px;align-items:center;justify-content:center;padding:0;transform:translateY(-50%);border:0;border-radius:8px;background:transparent;color:var(--nl-secondary);font:600 10px/1 inherit;cursor:pointer}.ok-native-list-index-button[data-active="true"]{background:var(--nl-accent);color:var(--nl-inverse-text)}.ok-native-list-index-button:focus-visible{outline:2px solid var(--nl-accent);outline-offset:1px}.ok-native-list-index-preview{position:absolute;z-index:8;right:40px;top:50%;display:flex;width:48px;height:48px;align-items:center;justify-content:center;transform:translateY(-50%) scale(.92);border-radius:14px;background:var(--nl-inverse);color:var(--nl-inverse-text);font-size:22px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .15s ease,transform .15s ease}.ok-native-list-index-preview[data-visible="true"]{opacity:1;transform:translateY(-50%) scale(1)}
756
802
  .ok-native-list-refresh{position:absolute;z-index:7;left:50%;top:8px;display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;background:var(--nl-inverse);color:var(--nl-inverse-text);font-size:12px;opacity:0;transform:translate(-50%,-16px);transition:opacity .15s ease,transform .15s ease;pointer-events:none}.ok-native-list-refresh[data-visible="true"]{opacity:1;transform:translate(-50%,0)}
803
+ .ok-native-list-warning{height:auto;display:flex;flex-direction:column;align-items:stretch;gap:4px;padding:14px 12px;border-top:1px solid;border-bottom:1px solid;box-sizing:border-box;cursor:default}.ok-native-list-warning-title,.ok-native-list-warning-message{font-size:14px;line-height:20px;white-space:normal;overflow-wrap:anywhere}.ok-native-list-warning-title{font-weight:500;color:var(--nl-primary)}.ok-native-list-warning-message{font-weight:400;color:var(--nl-secondary)}
804
+ .ok-native-list-subtitle-segments{display:flex;align-items:center;min-width:0;max-width:100%;height:20px}.ok-native-list-subtitle-segments>.ok-native-list-secondary{flex:0 1 auto;min-width:0}.ok-native-list-subtitle-dot{flex:0 0 4px;width:4px;height:4px;margin:0 6px;border-radius:50%;background:var(--nl-disabled)}.ok-native-list-wallet-row>.ok-native-list-flex{flex:0 1 auto;width:100%;align-items:center}.ok-native-list-wallet-badges{display:flex;gap:4px;justify-content:center;margin-top:4px;height:20px;max-width:100%}.ok-native-list-wallet-badges>.ok-native-list-badge{background:var(--nl-strong);color:var(--nl-secondary);font-size:12px;line-height:16px;height:20px;box-sizing:border-box;padding:2px 4px}.ok-native-list-visual-overlay{position:absolute;display:flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:50%;overflow:hidden;line-height:1;font-size:10px}.ok-native-list-visual-overlay img,.ok-native-list-visual-overlay svg{width:100%;height:100%;object-fit:contain}
757
805
  @media (prefers-reduced-motion:reduce){.ok-native-list-index-preview,.ok-native-list-refresh{transition:none}.ok-native-list-spinner{animation:none}}
806
+ /* OneKey patch: selector controls follow their original semantic colors and geometry. */
807
+ .ok-native-list-checkbox[data-selector="networkSelector"]{padding:0;border-radius:4px;border-color:var(--nl-checkbox-border,var(--nl-separator));background:var(--nl-checkbox-icon,var(--nl-inverse-text))}
808
+ .ok-native-list-checkbox[data-selector="networkSelector"]::after{display:none}
809
+ .ok-native-list-checkbox[data-selector="networkSelector"]>svg{display:none;width:16px;height:16px;color:var(--nl-checkbox-icon,var(--nl-inverse-text));flex-shrink:0}
810
+ .ok-native-list-checkbox[data-selector="networkSelector"][data-state="checked"],.ok-native-list-checkbox[data-selector="networkSelector"][data-state="indeterminate"]{border-color:transparent;background:var(--nl-checkbox-background,var(--nl-primary))}
811
+ .ok-native-list-checkbox[data-selector="networkSelector"][data-state="checked"]>svg[data-state="checked"],.ok-native-list-checkbox[data-selector="networkSelector"][data-state="indeterminate"]>svg[data-state="indeterminate"]{display:block}
812
+ /* OneKey patch: source WalletListItem uses four-point padding on every side. */
813
+ .ok-native-list-wallet-row[data-native-list-selector="walletSidebar"]{border-radius:12px;padding:4px}
814
+ /* OneKey patch: selected group members use the same primary title color as standalone wallets. */
815
+ .ok-native-list-wallet-member[data-native-list-selected="true"]>.ok-native-list-wallet-row[data-native-list-selector="walletSidebar"] .ok-native-list-title{color:var(--nl-primary)}
816
+ .ok-native-list-wallet-row[data-native-list-selector="walletSidebar"] .ok-native-list-wallet-badges{height:18px}
817
+ .ok-native-list-wallet-row[data-native-list-selector="walletSidebar"] .ok-native-list-wallet-badges>.ok-native-list-badge{font-size:11px;line-height:14px;font-weight:400;height:18px;padding:2px 6px;border-radius:4px;background:var(--nl-subdued);color:var(--nl-secondary)}
818
+ .ok-native-list-wallet-row[data-native-list-selector="walletSidebar"] .ok-native-list-wallet-badges>.ok-native-list-badge[data-tone="warning"]{background:var(--nl-caution-background);color:var(--nl-caution)}
819
+ .ok-native-list-account-row[data-native-list-selector="accountSelector"] .ok-native-list-accessories>.ok-native-list-icon-button{box-sizing:border-box;flex:0 0 38px;width:38px;height:38px;margin:-7px;padding:7px}
820
+ /* OneKey patch: AccountSelectorAccountListItem fixes the borderless Plus slot at top18/right20. */
821
+ .ok-native-list-account-row[data-native-list-selector="accountSelector"]>.ok-native-list-accessories[data-native-list-account-control="createAddress"]{position:absolute;top:18px;right:12px}
822
+ .ok-native-list-account-row[data-native-list-selector="accountSelector"] .ok-native-list-accessories>[data-native-list-account-control="createAddress"]{flex-basis:36px;width:36px;height:36px;padding:6px;border-radius:8px}
823
+ .ok-native-list-account-action-row{padding-left:12px;padding-right:12px}.ok-native-list-account-action-row .ok-native-list-action-title{font-size:16px;line-height:24px;font-weight:400}.ok-native-list-account-action-row .ok-native-list-action-title[data-tone="primary"]{color:var(--nl-primary)}
758
824
  `;
759
825
 
760
826
  function createElement(
@@ -788,16 +854,132 @@ function safeImageUri(uri: string): string | undefined {
788
854
  return undefined;
789
855
  }
790
856
 
857
+ // OneKey patch: retries belong to an image binding and must not outlive recycled rows.
858
+ const webImageRetryCleanup = new WeakMap<HTMLImageElement, () => void>();
859
+ const webAvatarCleanup = new WeakMap<HTMLImageElement, () => void>();
860
+ const webAvatarSources = new WeakMap<
861
+ HTMLImageElement,
862
+ { uri: string; source: ImageSource }
863
+ >();
864
+ function disposeWebImageRetries(element: HTMLElement): boolean {
865
+ let disposed = false;
866
+ const images = element.matches('img')
867
+ ? [element as HTMLImageElement]
868
+ : element.querySelectorAll('img');
869
+ images.forEach((image) => {
870
+ const avatarCleanup = webAvatarCleanup.get(image);
871
+ const cleanup = webImageRetryCleanup.get(image);
872
+ if (!cleanup && !avatarCleanup) return;
873
+ avatarCleanup?.();
874
+ webAvatarCleanup.delete(image);
875
+ webAvatarSources.delete(image);
876
+ cleanup?.();
877
+ webImageRetryCleanup.delete(image);
878
+ disposed = true;
879
+ });
880
+ return disposed;
881
+ }
882
+
883
+ function configureWebImageRetry(
884
+ image: HTMLImageElement,
885
+ source: ImageSource,
886
+ initialUri: string
887
+ ) {
888
+ const fallbackUri = source.fallbackUri
889
+ ? safeImageUri(source.fallbackUri)
890
+ : undefined;
891
+ const retryLimit = Number.isFinite(source.retryTimes)
892
+ ? Math.max(0, Math.floor(source.retryTimes ?? 0))
893
+ : 0;
894
+ if (!fallbackUri && retryLimit === 0) return;
895
+ const view = image.ownerDocument.defaultView;
896
+ let currentUri = initialUri;
897
+ let usedFallback = false;
898
+ let retryCount = 0;
899
+ let retryTimer: number | undefined;
900
+ let disposed = false;
901
+ const clearRetry = () => {
902
+ if (retryTimer !== undefined) view?.clearTimeout(retryTimer);
903
+ retryTimer = undefined;
904
+ };
905
+ const handleError = (event: Event) => {
906
+ if (disposed || !image.isConnected || retryTimer !== undefined) {
907
+ event.stopImmediatePropagation();
908
+ return;
909
+ }
910
+ if (fallbackUri && !usedFallback && fallbackUri !== currentUri) {
911
+ event.stopImmediatePropagation();
912
+ usedFallback = true;
913
+ currentUri = fallbackUri;
914
+ image.src = currentUri;
915
+ return;
916
+ }
917
+ if (retryCount >= retryLimit || !view) return;
918
+ event.stopImmediatePropagation();
919
+ retryCount += 1;
920
+ retryTimer = view.setTimeout(() => {
921
+ retryTimer = undefined;
922
+ if (disposed || !image.isConnected) return;
923
+ image.removeAttribute('src');
924
+ image.src = currentUri;
925
+ }, Math.floor(Math.random() * 3) * 1000);
926
+ };
927
+ image.addEventListener('error', handleError);
928
+ image.addEventListener('load', clearRetry);
929
+ webImageRetryCleanup.set(image, () => {
930
+ disposed = true;
931
+ clearRetry();
932
+ image.removeEventListener('error', handleError);
933
+ image.removeEventListener('load', clearRetry);
934
+ });
935
+ }
936
+
937
+ function configureWebAvatar(
938
+ image: HTMLImageElement,
939
+ source: ImageSource,
940
+ uri: string
941
+ ) {
942
+ const dispose = acquireNativeListAvatar(
943
+ image.ownerDocument,
944
+ uri,
945
+ (resolvedUri) => {
946
+ configureWebImageRetry(image, source, resolvedUri);
947
+ image.src = resolvedUri;
948
+ },
949
+ () => {
950
+ const fallbackUri = source.fallbackUri
951
+ ? safeImageUri(source.fallbackUri)
952
+ : undefined;
953
+ if (fallbackUri) {
954
+ configureWebImageRetry(image, source, fallbackUri);
955
+ image.src = fallbackUri;
956
+ return;
957
+ }
958
+ const ImageEvent = image.ownerDocument.defaultView?.Event;
959
+ if (ImageEvent) image.dispatchEvent(new ImageEvent('error'));
960
+ }
961
+ );
962
+ webAvatarSources.set(image, { uri, source });
963
+ webAvatarCleanup.set(image, dispose);
964
+ }
965
+
791
966
  function createImage(
792
967
  context: RenderContext,
793
968
  source: ImageSource,
794
969
  className?: string
795
970
  ): HTMLImageElement | undefined {
796
- const uri = safeImageUri(source.uri);
971
+ const avatarUri = canonicalNativeListAvatarUri(source.uri);
972
+ const uri = avatarUri ?? safeImageUri(source.uri);
797
973
  if (!uri) return undefined;
798
974
  const image = context.document.createElement('img');
799
975
  if (className) image.className = className;
800
- image.src = uri;
976
+ // OneKey patch: consume recoverable errors before the visual's final fallback listener.
977
+ if (avatarUri) {
978
+ configureWebAvatar(image, source, avatarUri);
979
+ } else {
980
+ configureWebImageRetry(image, source, uri);
981
+ image.src = uri;
982
+ }
801
983
  image.alt = '';
802
984
  image.draggable = false;
803
985
  image.loading = 'lazy';
@@ -807,6 +989,324 @@ function createImage(
807
989
  return image;
808
990
  }
809
991
 
992
+ // OneKey patch: React Native Web paints selector images as centered CSS backgrounds.
993
+ // The image keeps its loading/error lifecycle; only its replaced-element pixels are hidden.
994
+ function paintSelectorImageBackground(
995
+ image: HTMLImageElement,
996
+ frame: HTMLElement,
997
+ inset = 0
998
+ ) {
999
+ const paint = createElement(
1000
+ image.ownerDocument,
1001
+ 'span',
1002
+ 'ok-native-list-selector-image-background'
1003
+ );
1004
+ paint.style.cssText =
1005
+ 'position:absolute;pointer-events:none;border-radius:inherit;background-position:center;background-repeat:no-repeat';
1006
+ paint.style.inset = String(inset) + 'px';
1007
+ paint.style.backgroundSize =
1008
+ image.style.objectFit === 'fill'
1009
+ ? '100% 100%'
1010
+ : image.style.objectFit === 'center'
1011
+ ? 'auto'
1012
+ : image.style.objectFit;
1013
+ image.style.opacity = '0';
1014
+ const update = () => {
1015
+ paint.style.backgroundImage =
1016
+ 'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
1017
+ };
1018
+ image.addEventListener('load', update);
1019
+ image.addEventListener('error', () => {
1020
+ paint.style.backgroundImage = 'none';
1021
+ });
1022
+ frame.insertBefore(paint, image);
1023
+ if (image.complete && image.naturalWidth > 0) update();
1024
+ }
1025
+
1026
+ // OneKey patch: use source SVG paths for selector actions and wallet provider marks.
1027
+ const selectorIcons: Readonly<
1028
+ Record<
1029
+ string,
1030
+ Readonly<{
1031
+ viewBox: string;
1032
+ paths: readonly Readonly<{
1033
+ d: string;
1034
+ fill: string;
1035
+ fillRule: string;
1036
+ opacity: number;
1037
+ }>[];
1038
+ }>
1039
+ >
1040
+ > = {
1041
+ GlobusOutline: {
1042
+ viewBox: '0 0 24 24',
1043
+ paths: [
1044
+ {
1045
+ d: 'M12 2c5.185 0 9.448 3.947 9.95 9H22v2h-.05c-.502 5.053-4.765 9-9.95 9s-9.448-3.947-9.95-9H2v-2h.05C2.552 5.947 6.815 2 12 2M9.523 13c.09 1.982.438 3.726.934 5.002.29.746.612 1.282.917 1.614.304.331.517.384.626.384s.322-.053.626-.384c.305-.332.627-.868.917-1.614.496-1.276.845-3.02.934-5.002zm-5.459 0a8 8 0 0 0 4.8 6.36 10 10 0 0 1-.271-.633C7.994 17.187 7.61 15.189 7.52 13zm12.416 0c-.09 2.189-.474 4.187-1.073 5.727a10 10 0 0 1-.271.633 8 8 0 0 0 4.8-6.36zM8.863 4.639A8 8 0 0 0 4.064 11h3.457c.09-2.189.473-4.187 1.072-5.727q.127-.327.27-.634M12 4c-.109 0-.322.053-.626.384-.305.332-.627.868-.917 1.614-.496 1.276-.844 3.02-.934 5.002h4.954c-.09-1.982-.438-3.726-.934-5.002-.29-.746-.612-1.282-.917-1.614C12.322 4.053 12.109 4 12 4m3.136.639q.144.307.271.634c.599 1.54.982 3.538 1.073 5.727h3.456a8 8 0 0 0-4.8-6.361',
1046
+ fill: 'currentColor',
1047
+ fillRule: 'evenodd',
1048
+ opacity: 1.0,
1049
+ },
1050
+ ],
1051
+ },
1052
+ LockSolid: {
1053
+ viewBox: '0 0 24 24',
1054
+ paths: [
1055
+ {
1056
+ d: 'M12 2a5 5 0 0 1 5 5v2h3v13H4V9h3V7a5 5 0 0 1 5-5m-1 11v5h2v-5zm1-9a3 3 0 0 0-3 3v2h6V7a3 3 0 0 0-3-3',
1057
+ fill: 'currentColor',
1058
+ fillRule: 'evenodd',
1059
+ opacity: 1.0,
1060
+ },
1061
+ ],
1062
+ },
1063
+ GoogleIllus: {
1064
+ viewBox: '0 0 24 24',
1065
+ paths: [
1066
+ {
1067
+ d: 'M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09',
1068
+ fill: '#4285F4',
1069
+ fillRule: 'nonzero',
1070
+ opacity: 1.0,
1071
+ },
1072
+ {
1073
+ d: 'M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23',
1074
+ fill: '#34A853',
1075
+ fillRule: 'nonzero',
1076
+ opacity: 1.0,
1077
+ },
1078
+ {
1079
+ d: 'M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22z',
1080
+ fill: '#FBBC05',
1081
+ fillRule: 'nonzero',
1082
+ opacity: 1.0,
1083
+ },
1084
+ {
1085
+ d: 'M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53',
1086
+ fill: '#EA4335',
1087
+ fillRule: 'nonzero',
1088
+ opacity: 1.0,
1089
+ },
1090
+ ],
1091
+ },
1092
+ AppleBrand: {
1093
+ viewBox: '0 0 16 20',
1094
+ paths: [
1095
+ {
1096
+ d: 'M11.67.834c.117 1.074-.315 2.153-.955 2.928-.64.773-1.692 1.378-2.718 1.298-.14-1.054.38-2.151.971-2.836C9.63 1.45 10.746.872 11.67.834M14.994 7.093c-.176.108-1.992 1.224-1.972 3.482.025 2.769 2.428 3.693 2.46 3.705l-.004.015a10.1 10.1 0 0 1-1.264 2.593c-.764 1.116-1.556 2.229-2.806 2.254-.598.011-1-.162-1.416-.343-.437-.19-.891-.386-1.609-.386-.751 0-1.226.203-1.683.398-.397.169-.78.333-1.32.354-1.208.047-2.124-1.207-2.895-2.32C.909 14.57-.294 10.414 1.322 7.612c.803-1.395 2.237-2.275 3.794-2.298.671-.014 1.32.244 1.89.47.434.172.821.326 1.135.326.282 0 .659-.149 1.099-.323.692-.273 1.539-.607 2.41-.518.599.026 2.276.24 3.354 1.818z',
1097
+ fill: 'currentColor',
1098
+ fillRule: 'nonzero',
1099
+ opacity: 1.0,
1100
+ },
1101
+ ],
1102
+ },
1103
+ BotIllus: {
1104
+ viewBox: '0 0 24 24',
1105
+ paths: [
1106
+ {
1107
+ d: 'M11 2a1 1 0 1 1 2 0v1.8l1.6 1.6a1 1 0 1 1-1.4 1.4L12 5.6l-1.2 1.2a1 1 0 0 1-1.4-1.4L11 3.8z',
1108
+ fill: '#8897A5',
1109
+ fillRule: 'nonzero',
1110
+ opacity: 1.0,
1111
+ },
1112
+ {
1113
+ d: 'M8.0 6.0h8.0a5.0 5.0 0 0 1 5.0 5.0v4.0a5.0 5.0 0 0 1 -5.0 5.0h-8.0a5.0 5.0 0 0 1 -5.0 -5.0v-4.0a5.0 5.0 0 0 1 5.0 -5.0z',
1114
+ fill: '#3FA9F5',
1115
+ fillRule: 'nonzero',
1116
+ opacity: 1.0,
1117
+ },
1118
+ {
1119
+ d: 'M3.0 10.0h0.0a1.5 1.5 0 0 1 1.5 1.5v3.0a1.5 1.5 0 0 1 -1.5 1.5h0.0a1.5 1.5 0 0 1 -1.5 -1.5v-3.0a1.5 1.5 0 0 1 1.5 -1.5z',
1120
+ fill: '#8897A5',
1121
+ fillRule: 'nonzero',
1122
+ opacity: 1.0,
1123
+ },
1124
+ {
1125
+ d: 'M21.0 10.0h0.0a1.5 1.5 0 0 1 1.5 1.5v3.0a1.5 1.5 0 0 1 -1.5 1.5h0.0a1.5 1.5 0 0 1 -1.5 -1.5v-3.0a1.5 1.5 0 0 1 1.5 -1.5z',
1126
+ fill: '#8897A5',
1127
+ fillRule: 'nonzero',
1128
+ opacity: 1.0,
1129
+ },
1130
+ {
1131
+ d: 'M7.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0',
1132
+ fill: '#10243E',
1133
+ fillRule: 'nonzero',
1134
+ opacity: 1.0,
1135
+ },
1136
+ {
1137
+ d: 'M13.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0',
1138
+ fill: '#10243E',
1139
+ fillRule: 'nonzero',
1140
+ opacity: 1.0,
1141
+ },
1142
+ {
1143
+ d: 'M8.5 15.4c.9.8 2.08 1.2 3.5 1.2s2.6-.4 3.5-1.2c.24-.2.6-.18.8.06.2.23.17.6-.06.8-1.14.98-2.58 1.46-4.24 1.46s-3.1-.48-4.24-1.46a.58.58 0 0 1-.06-.8c.2-.24.56-.26.8-.06',
1144
+ fill: '#10243E',
1145
+ fillRule: 'nonzero',
1146
+ opacity: 1.0,
1147
+ },
1148
+ ],
1149
+ },
1150
+ AllNetworksSolid: {
1151
+ viewBox: '0 0 24 24',
1152
+ paths: [
1153
+ {
1154
+ d: 'M15.333 13.998a1.335 1.335 0 1 1 0 2.67 1.335 1.335 0 0 1 0-2.67',
1155
+ fill: 'currentColor',
1156
+ fillRule: 'nonzero',
1157
+ opacity: 1.0,
1158
+ },
1159
+ {
1160
+ d: 'M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0M8 12.668A2 2 0 0 0 6 14.666V16c0 1.103.895 1.997 1.998 1.998h1.334A2 2 0 0 0 11.33 16v-1.334a2 2 0 0 0-1.998-1.998zm7.333 0a2.665 2.665 0 1 0 0 5.33 2.665 2.665 0 0 0 0-5.33M7.999 6.001A2 2 0 0 0 6.001 8v1.334c0 1.103.895 1.998 1.998 1.998h1.334a2 2 0 0 0 1.998-1.998V7.999a2 2 0 0 0-1.998-1.998zm6.667 0A2 2 0 0 0 12.668 8v1.334c0 1.103.895 1.998 1.998 1.998H16a2 2 0 0 0 1.998-1.998V7.999A2 2 0 0 0 16 6.001z',
1161
+ fill: 'currentColor',
1162
+ fillRule: 'evenodd',
1163
+ opacity: 1.0,
1164
+ },
1165
+ ],
1166
+ },
1167
+ CrossedSmallSolid: {
1168
+ viewBox: '0 0 24 24',
1169
+ paths: [
1170
+ {
1171
+ d: 'M17.87 8.25 14.12 12l3.75 3.75-2.12 2.121-3.75-3.75-3.75 3.75-2.121-2.121L9.879 12l-3.75-3.75 2.12-2.121L12 9.879l3.75-3.75 2.122 2.121Z',
1172
+ fill: 'currentColor',
1173
+ fillRule: 'nonzero',
1174
+ opacity: 1.0,
1175
+ },
1176
+ ],
1177
+ },
1178
+ AccountErrorCustom: {
1179
+ viewBox: '0 0 18 18',
1180
+ paths: [
1181
+ {
1182
+ d: 'M12.5 12.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5',
1183
+ fill: '#000',
1184
+ fillRule: 'nonzero',
1185
+ opacity: 0.447,
1186
+ },
1187
+ {
1188
+ d: 'M0 3.5A3.5 3.5 0 0 1 3.5 0h8.088A2.41 2.41 0 0 1 14 2.412V5h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H4a4 4 0 0 1-4-4zm2 3.163V14a2 2 0 0 0 2 2h11a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H3.5c-.537 0-1.045-.12-1.5-.337M2 3.5A1.5 1.5 0 0 0 3.5 5H12V2.412A.41.41 0 0 0 11.588 2H3.5A1.5 1.5 0 0 0 2 3.5',
1189
+ fill: '#000',
1190
+ fillRule: 'evenodd',
1191
+ opacity: 0.447,
1192
+ },
1193
+ ],
1194
+ },
1195
+ PlusSmallOutline: {
1196
+ viewBox: '0 0 24 24',
1197
+ paths: [
1198
+ {
1199
+ d: 'M13 11h5v2h-5v5h-2v-5H6v-2h5V6h2z',
1200
+ fill: 'currentColor',
1201
+ fillRule: 'nonzero',
1202
+ opacity: 1.0,
1203
+ },
1204
+ ],
1205
+ },
1206
+ DotHorOutline: {
1207
+ viewBox: '0 0 24 24',
1208
+ paths: [
1209
+ {
1210
+ d: 'M6 14H2v-4h4zm8 0h-4v-4h4zm8 0h-4v-4h4z',
1211
+ fill: 'currentColor',
1212
+ fillRule: 'nonzero',
1213
+ opacity: 1.0,
1214
+ },
1215
+ ],
1216
+ },
1217
+ ChevronRightSmallOutline: {
1218
+ viewBox: '0 0 24 24',
1219
+ paths: [
1220
+ {
1221
+ d: 'M15.414 12 10 17.414 8.586 16l4-4-4-4L10 6.586z',
1222
+ fill: 'currentColor',
1223
+ fillRule: 'nonzero',
1224
+ opacity: 1.0,
1225
+ },
1226
+ ],
1227
+ },
1228
+ DragOutline: {
1229
+ viewBox: '0 0 24 24',
1230
+ paths: [
1231
+ {
1232
+ d: 'M11 21H7v-4h4zm6 0h-4v-4h4zm-6-7H7v-4h4zm6 0h-4v-4h4zm-6-7H7V3h4zm6 0h-4V3h4z',
1233
+ fill: 'currentColor',
1234
+ fillRule: 'nonzero',
1235
+ opacity: 1.0,
1236
+ },
1237
+ ],
1238
+ },
1239
+ PencilOutline: {
1240
+ viewBox: '0 0 24 24',
1241
+ paths: [
1242
+ {
1243
+ d: 'M22.414 7.5 7.914 22H2v-5.914l14.5-14.5zM4 16.914V20h3.086l9.5-9.5L13.5 7.414zM14.914 6 18 9.086 19.586 7.5 16.5 4.414z',
1244
+ fill: 'currentColor',
1245
+ fillRule: 'evenodd',
1246
+ opacity: 1.0,
1247
+ },
1248
+ ],
1249
+ },
1250
+ CheckboxCheckedCustom: {
1251
+ viewBox: '0 0 16 16',
1252
+ paths: [
1253
+ {
1254
+ d: 'M12.204 5.043a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414 0l-2-2a1 1 0 1 1 1.414-1.414l1.293 1.293 3.793-3.793a1 1 0 0 1 1.414 0',
1255
+ fill: 'currentColor',
1256
+ fillRule: 'evenodd',
1257
+ opacity: 1.0,
1258
+ },
1259
+ ],
1260
+ },
1261
+ CheckboxIndeterminateCustom: {
1262
+ viewBox: '0 0 16 16',
1263
+ paths: [
1264
+ {
1265
+ d: 'M4 8a1 1 0 0 1 1-1h6a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1',
1266
+ fill: 'currentColor',
1267
+ fillRule: 'evenodd',
1268
+ opacity: 1.0,
1269
+ },
1270
+ ],
1271
+ },
1272
+ Circle: {
1273
+ viewBox: '0 0 24 24',
1274
+ paths: [
1275
+ {
1276
+ d: 'M0 12a12 12 0 1 0 24 0a12 12 0 1 0 -24 0',
1277
+ fill: 'currentColor',
1278
+ fillRule: 'nonzero',
1279
+ opacity: 1,
1280
+ },
1281
+ ],
1282
+ },
1283
+ };
1284
+ function applySelectorIcon(element: HTMLElement, name: string) {
1285
+ const icon = selectorIcons[name];
1286
+ if (!icon) return;
1287
+ element.textContent = '';
1288
+ const svg = element.ownerDocument.createElementNS(
1289
+ 'http://www.w3.org/2000/svg',
1290
+ 'svg'
1291
+ );
1292
+ svg.setAttribute('viewBox', icon.viewBox);
1293
+ svg.setAttribute('width', '24');
1294
+ svg.setAttribute('height', '24');
1295
+ svg.setAttribute('aria-hidden', 'true');
1296
+ icon.paths.forEach((path) => {
1297
+ const child = element.ownerDocument.createElementNS(
1298
+ 'http://www.w3.org/2000/svg',
1299
+ 'path'
1300
+ );
1301
+ child.setAttribute('d', path.d);
1302
+ child.setAttribute('fill', path.fill);
1303
+ child.setAttribute('fill-rule', path.fillRule);
1304
+ child.setAttribute('fill-opacity', String(path.opacity));
1305
+ svg.appendChild(child);
1306
+ });
1307
+ element.appendChild(svg);
1308
+ }
1309
+
810
1310
  function iconGlyph(name: string): string {
811
1311
  const normalized = name.toLocaleLowerCase();
812
1312
  if (normalized.includes('chevron')) {
@@ -841,7 +1341,8 @@ function visualFromRow(row: RowModel): LeadingVisual | undefined {
841
1341
 
842
1342
  function createVisual(
843
1343
  context: RenderContext,
844
- visual: LeadingVisual | undefined
1344
+ visual: LeadingVisual | undefined,
1345
+ selectorPresentation?: string
845
1346
  ): HTMLElement | undefined {
846
1347
  if (!visual) return undefined;
847
1348
  if (visual.kind === 'stackedImages') {
@@ -873,6 +1374,7 @@ function createVisual(
873
1374
  'ok-native-list-visual-fallback',
874
1375
  iconGlyph(visual.name)
875
1376
  );
1377
+ applySelectorIcon(fallback, visual.name);
876
1378
  if (visual.tintColor) fallback.style.color = visual.tintColor;
877
1379
  frame.appendChild(fallback);
878
1380
  return frame;
@@ -885,6 +1387,7 @@ function createVisual(
885
1387
  if (image) {
886
1388
  image.className = 'ok-native-list-visual-main';
887
1389
  frame.appendChild(image);
1390
+ if (selectorPresentation) paintSelectorImageBackground(image, frame);
888
1391
  } else {
889
1392
  frame.appendChild(
890
1393
  createElement(
@@ -913,8 +1416,98 @@ function createVisual(
913
1416
  corner.style.color = visual.cornerIcon.tintColor;
914
1417
  if (visual.cornerIcon.backgroundColor)
915
1418
  corner.style.background = visual.cornerIcon.backgroundColor;
1419
+ applySelectorIcon(corner, visual.cornerIcon.name);
916
1420
  frame.appendChild(corner);
917
1421
  }
1422
+ // OneKey patch: image failure uses the same source-derived fallback as v1.
1423
+ if ('fallbackIcon' in visual && visual.fallbackIcon) {
1424
+ const icon = visual.fallbackIcon;
1425
+ const showFallback = () => {
1426
+ if (image) {
1427
+ disposeWebImageRetries(image);
1428
+ image.remove();
1429
+ }
1430
+ frame
1431
+ .querySelector(
1432
+ '.ok-native-list-visual-fallback:not(.ok-native-list-visual-corner)'
1433
+ )
1434
+ ?.remove();
1435
+ const fallback = createElement(
1436
+ context.document,
1437
+ 'span',
1438
+ 'ok-native-list-visual-fallback'
1439
+ );
1440
+ applySelectorIcon(fallback, icon.name);
1441
+ if (icon.tintColor) fallback.style.color = icon.tintColor;
1442
+ frame.prepend(fallback);
1443
+ };
1444
+ if (image) image.addEventListener('error', showFallback, { once: true });
1445
+ else showFallback();
1446
+ }
1447
+ if ('borderStyle' in visual && visual.borderStyle === 'dashed') {
1448
+ frame.style.border =
1449
+ '2px dashed ' + (visual.borderColor ?? 'var(--nl-disabled)');
1450
+ frame.style.boxSizing = 'border-box';
1451
+ }
1452
+ if ('overlays' in visual)
1453
+ visual.overlays?.forEach((overlay) => {
1454
+ const corner = createElement(
1455
+ context.document,
1456
+ 'span',
1457
+ 'ok-native-list-visual-overlay',
1458
+ overlay.text
1459
+ );
1460
+ const size = overlay.size ?? 20;
1461
+ const isWalletText =
1462
+ selectorPresentation === 'walletSidebar' &&
1463
+ !!overlay.text &&
1464
+ !overlay.image &&
1465
+ !overlay.name;
1466
+ corner.style.width =
1467
+ isWalletText && overlay.width === undefined
1468
+ ? 'auto'
1469
+ : String(overlay.width ?? size) + 'px';
1470
+ corner.style.height =
1471
+ String(overlay.height ?? (isWalletText ? 16 : size)) + 'px';
1472
+ corner.style.padding = isWalletText
1473
+ ? '0 2px'
1474
+ : String(overlay.padding ?? 0) + 'px';
1475
+ if (isWalletText) {
1476
+ corner.style.fontSize = '12px';
1477
+ corner.style.lineHeight = '16px';
1478
+ corner.style.fontWeight = '400';
1479
+ }
1480
+ if (
1481
+ isWalletText ||
1482
+ overlay.width !== undefined ||
1483
+ overlay.height !== undefined
1484
+ )
1485
+ corner.style.borderRadius = '9999px';
1486
+ const offsetX = String(-(overlay.offsetX ?? overlay.offset ?? 2)) + 'px';
1487
+ const offsetY = String(-(overlay.offsetY ?? overlay.offset ?? 2)) + 'px';
1488
+ corner.style.background = overlay.backgroundColor ?? 'transparent';
1489
+ corner.style.color = overlay.tintColor ?? 'var(--nl-secondary)';
1490
+ if (overlay.position === 'topLeft') {
1491
+ corner.style.left = offsetX;
1492
+ corner.style.top = offsetY;
1493
+ } else {
1494
+ corner.style.right = offsetX;
1495
+ corner.style.bottom = offsetY;
1496
+ }
1497
+ if (overlay.image) {
1498
+ const overlayImage = createImage(context, overlay.image);
1499
+ if (overlayImage) {
1500
+ corner.appendChild(overlayImage);
1501
+ if (selectorPresentation)
1502
+ paintSelectorImageBackground(
1503
+ overlayImage,
1504
+ corner,
1505
+ overlay.padding ?? 0
1506
+ );
1507
+ }
1508
+ } else if (overlay.name) applySelectorIcon(corner, overlay.name);
1509
+ frame.appendChild(corner);
1510
+ });
918
1511
  return frame;
919
1512
  }
920
1513
 
@@ -1022,6 +1615,22 @@ function createCheckbox(
1022
1615
  setData(element, 'checkboxFallback', accessory.state);
1023
1616
  setData(element, 'nativeListAction', accessory.actionKey ?? 'selection');
1024
1617
  setData(element, 'selectionScope', accessory.target?.scope ?? 'row');
1618
+ const row = context.snapshot.rows[context.itemIndex];
1619
+ if (row && 'presentation' in row && row.presentation === 'networkSelector') {
1620
+ setData(element, 'selector', 'networkSelector');
1621
+ for (const [state, name] of [
1622
+ ['checked', 'CheckboxCheckedCustom'],
1623
+ ['indeterminate', 'CheckboxIndeterminateCustom'],
1624
+ ]) {
1625
+ const holder = createElement(context.document, 'span');
1626
+ applySelectorIcon(holder, name);
1627
+ const svg = holder.firstElementChild;
1628
+ if (svg) {
1629
+ svg.setAttribute('data-state', state);
1630
+ element.appendChild(svg);
1631
+ }
1632
+ }
1633
+ }
1025
1634
  if (accessory.target?.scope === 'section')
1026
1635
  setData(element, 'selectionKey', accessory.target.sectionKey);
1027
1636
  else if (accessory.target?.scope === 'row')
@@ -1046,6 +1655,7 @@ function createIconAction(
1046
1655
  element.setAttribute('type', 'button');
1047
1656
  element.toggleAttribute('disabled', Boolean(disabled));
1048
1657
  }
1658
+ applySelectorIcon(element, name);
1049
1659
  if (actionKey) setData(element, 'nativeListAction', actionKey);
1050
1660
  if (tintColor) element.style.color = tintColor;
1051
1661
  return element;
@@ -1060,6 +1670,36 @@ function markActionAnchorSource(
1060
1670
  if (slot !== undefined) setData(element, 'nativeListAnchorSlot', slot);
1061
1671
  }
1062
1672
 
1673
+ // OneKey patch: the compact zero-count digits share the amount baseline.
1674
+ function applyValueSegments(
1675
+ element: HTMLElement,
1676
+ segments:
1677
+ | readonly Readonly<{ text: string; style?: 'subscript' }>[]
1678
+ | undefined,
1679
+ fontSize = 16,
1680
+ lineHeight = 24,
1681
+ weight = 500
1682
+ ) {
1683
+ if (!segments?.length) return;
1684
+ element.textContent = '';
1685
+ element.style.fontSize = String(fontSize) + 'px';
1686
+ element.style.lineHeight = String(lineHeight) + 'px';
1687
+ element.style.fontWeight = String(weight);
1688
+ segments.forEach((segment) => {
1689
+ const span = createElement(
1690
+ element.ownerDocument,
1691
+ 'span',
1692
+ undefined,
1693
+ segment.text
1694
+ );
1695
+ if (segment.style === 'subscript') {
1696
+ span.style.fontSize = String(Math.ceil(fontSize * 0.6)) + 'px';
1697
+ span.style.lineHeight = String(fontSize) + 'px';
1698
+ }
1699
+ element.appendChild(span);
1700
+ });
1701
+ }
1702
+
1063
1703
  function createAccessory(
1064
1704
  context: RenderContext,
1065
1705
  rowKey: string,
@@ -1080,6 +1720,25 @@ function createAccessory(
1080
1720
  accessory.tintColor
1081
1721
  );
1082
1722
  markActionAnchorSource(element, 'trailingAccessory', slot);
1723
+ setData(element, 'testid', accessory.testID);
1724
+ if (accessory.hoverActionKey)
1725
+ setData(element, 'nativeListHoverAction', accessory.hoverActionKey);
1726
+ if (accessory.accessibilityLabel)
1727
+ element.setAttribute('aria-label', accessory.accessibilityLabel);
1728
+ const row = context.snapshot.rows[context.itemIndex];
1729
+ if (
1730
+ row &&
1731
+ 'presentation' in row &&
1732
+ row.presentation === 'accountSelector'
1733
+ ) {
1734
+ setData(element, 'nativeListAnchorInset', 7);
1735
+ // OneKey patch: the create-address button omits IconButton's one-point border.
1736
+ if (row.height !== undefined && accessory.name === 'PlusSmallOutline') {
1737
+ setData(element, 'nativeListAccountControl', 'createAddress');
1738
+ if (!accessory.tintColor)
1739
+ element.style.color = 'var(--nl-icon-subdued)';
1740
+ }
1741
+ }
1083
1742
  return element;
1084
1743
  }
1085
1744
  if (accessory.kind === 'spinner') {
@@ -1099,6 +1758,7 @@ function createAccessory(
1099
1758
  switch (accessory.kind) {
1100
1759
  case 'value':
1101
1760
  element.textContent = accessory.text;
1761
+ applyValueSegments(element, accessory.textSegments);
1102
1762
  if (accessory.secondary)
1103
1763
  element.classList.add('ok-native-list-accessory-secondary');
1104
1764
  break;
@@ -1157,6 +1817,30 @@ function appendAccessories(
1157
1817
  'span',
1158
1818
  'ok-native-list-accessories'
1159
1819
  );
1820
+ const row = context.snapshot.rows[context.itemIndex];
1821
+ if (
1822
+ row &&
1823
+ row.height !== undefined &&
1824
+ 'presentation' in row &&
1825
+ row.presentation === 'networkSelector'
1826
+ ) {
1827
+ container.style.gap = accessories.some(
1828
+ (accessory) => accessory.kind === 'checkbox'
1829
+ )
1830
+ ? '12px'
1831
+ : '20px';
1832
+ }
1833
+ if (
1834
+ row &&
1835
+ row.height !== undefined &&
1836
+ 'presentation' in row &&
1837
+ row.presentation === 'accountSelector' &&
1838
+ accessories.length === 1 &&
1839
+ accessories[0]?.kind === 'icon' &&
1840
+ accessories[0].name === 'PlusSmallOutline'
1841
+ ) {
1842
+ setData(container, 'nativeListAccountControl', 'createAddress');
1843
+ }
1160
1844
  accessories.forEach((accessory, slot) =>
1161
1845
  container.appendChild(createAccessory(context, rowKey, accessory, slot))
1162
1846
  );
@@ -1234,7 +1918,88 @@ function createSectionHeader(
1234
1918
  markActionAnchorSource(titleIcon, 'leadingAction');
1235
1919
  body.appendChild(titleIcon);
1236
1920
  }
1237
- body.appendChild(createTextColumn(context, row.title, row.subtitle));
1921
+ // OneKey patch: section title help has its own measurable action target.
1922
+ // body.appendChild(createTextColumn(context, row.title, row.subtitle));
1923
+ const column = createTextColumn(context, row.title, row.subtitle);
1924
+ const title = column.firstElementChild as HTMLElement;
1925
+ title.classList.add('ok-native-list-section-title');
1926
+ if (row.titleActionKey) {
1927
+ setData(title, 'nativeListAction', row.titleActionKey);
1928
+ markActionAnchorSource(title, 'leadingAction');
1929
+ title.setAttribute('role', 'button');
1930
+ title.tabIndex = 0;
1931
+ title.style.alignSelf = 'flex-start';
1932
+ title.style.maxWidth = '100%';
1933
+ // OneKey patch: explicit network headers reserve a separate 3-point underline area.
1934
+ if (row.presentation !== 'networkSelector' || row.height === undefined) {
1935
+ title.style.textDecoration = 'underline dotted';
1936
+ title.style.textUnderlineOffset = '6px';
1937
+ }
1938
+ if (row.titleActionOnHover) setData(title, 'nativeListHoverAction', true);
1939
+ }
1940
+ if (row.presentation === 'networkSelector' && row.height !== undefined) {
1941
+ body.style.padding =
1942
+ row.variant === 'summary' ? '24px 12px 20px' : '0 12px';
1943
+ body.style.backgroundColor = 'var(--nl-bg)';
1944
+ body.style.gap = row.checkbox ? '12px' : '8px';
1945
+ title.style.fontSize = row.variant === 'summary' ? '16px' : '14px';
1946
+ title.style.lineHeight = row.variant === 'summary' ? '24px' : '20px';
1947
+ title.style.fontWeight =
1948
+ row.variant === 'summary' || (row.titleActionKey && !row.checkbox)
1949
+ ? '500'
1950
+ : '600';
1951
+ if (row.titleActionKey) {
1952
+ const text = createElement(
1953
+ context.document,
1954
+ 'span',
1955
+ 'ok-native-list-section-title-text',
1956
+ row.title
1957
+ );
1958
+ text.style.overflow = 'hidden';
1959
+ text.style.textOverflow = 'ellipsis';
1960
+ text.style.maxWidth = '100%';
1961
+ const dotted = context.document.createElementNS(
1962
+ 'http://www.w3.org/2000/svg',
1963
+ 'svg'
1964
+ );
1965
+ dotted.setAttribute('height', '2');
1966
+ dotted.style.cssText =
1967
+ 'display:block;position:absolute;left:0;bottom:0;width:100%;height:2px;color:var(--nl-secondary)';
1968
+ const line = context.document.createElementNS(
1969
+ 'http://www.w3.org/2000/svg',
1970
+ 'line'
1971
+ );
1972
+ for (const [key, value] of Object.entries({
1973
+ x1: '1',
1974
+ y1: '1',
1975
+ x2: '100%',
1976
+ y2: '1',
1977
+ stroke: 'currentColor',
1978
+ 'stroke-width': '1.5',
1979
+ 'stroke-dasharray': '0,4',
1980
+ 'stroke-linecap': 'round',
1981
+ }))
1982
+ line.setAttribute(key, value);
1983
+ // OneKey patch: keep both round caps inside the original full-width viewport.
1984
+ const lineViewport = context.document.createElementNS(
1985
+ 'http://www.w3.org/2000/svg',
1986
+ 'svg'
1987
+ );
1988
+ lineViewport.setAttribute('width', 'calc(100% - 1px)');
1989
+ lineViewport.setAttribute('height', '2');
1990
+ lineViewport.setAttribute('overflow', 'visible');
1991
+ lineViewport.appendChild(line);
1992
+ dotted.appendChild(lineViewport);
1993
+ // OneKey patch: SVG intrinsic width must not expand the title action beyond its text.
1994
+ title.style.display = 'block';
1995
+ title.style.position = 'relative';
1996
+ title.style.width = 'fit-content';
1997
+ title.style.paddingBottom = '3px';
1998
+ text.style.display = 'block';
1999
+ title.replaceChildren(text, dotted);
2000
+ }
2001
+ }
2002
+ body.appendChild(column);
1238
2003
  if (row.value) {
1239
2004
  const value = createElement(
1240
2005
  context.document,
@@ -1244,6 +2009,19 @@ function createSectionHeader(
1244
2009
  : 'ok-native-list-value ok-native-list-section-value',
1245
2010
  row.value
1246
2011
  );
2012
+ applyValueSegments(value, row.valueSegments);
2013
+ if (row.presentation === 'networkSelector' && row.height !== undefined) {
2014
+ value.style.fontFamily = 'inherit';
2015
+ value.style.fontSize = '16px';
2016
+ value.style.lineHeight = '24px';
2017
+ value.style.fontWeight = '500';
2018
+ if (row.valueActionKey) {
2019
+ value.style.color = 'var(--nl-secondary)';
2020
+ value.style.padding = '0';
2021
+ value.style.flexShrink = '0';
2022
+ }
2023
+ }
2024
+ if (row.valueActionTestID) setData(value, 'testid', row.valueActionTestID);
1247
2025
  if (row.valueActionKey) {
1248
2026
  value.setAttribute('type', 'button');
1249
2027
  setData(value, 'nativeListAction', row.valueActionKey);
@@ -1292,6 +2070,8 @@ function createActionRow(
1292
2070
  row.title
1293
2071
  );
1294
2072
  setData(title, 'tone', row.tone);
2073
+ if (row.presentation === 'accountSelector' && row.icon)
2074
+ title.style.fontWeight = '500';
1295
2075
  body.appendChild(title);
1296
2076
  if (row.checkbox)
1297
2077
  body.appendChild(createCheckbox(context, row.key, row.checkbox));
@@ -1309,6 +2089,27 @@ function createSystemRow(
1309
2089
  'ok-native-list-row ok-native-list-system'
1310
2090
  );
1311
2091
  setData(body, 'variant', row.variant);
2092
+ if (row.variant === 'warning') {
2093
+ body.classList.add('ok-native-list-warning');
2094
+ body.style.borderColor = row.borderColor ?? 'var(--nl-separator)';
2095
+ body.appendChild(
2096
+ createElement(
2097
+ context.document,
2098
+ 'span',
2099
+ 'ok-native-list-warning-title',
2100
+ row.title
2101
+ )
2102
+ );
2103
+ body.appendChild(
2104
+ createElement(
2105
+ context.document,
2106
+ 'span',
2107
+ 'ok-native-list-warning-message',
2108
+ row.message
2109
+ )
2110
+ );
2111
+ return body;
2112
+ }
1312
2113
  if (row.variant === 'loading')
1313
2114
  body.appendChild(
1314
2115
  createElement(context.document, 'span', 'ok-native-list-spinner')
@@ -1703,6 +2504,15 @@ function createIdentityActivityOrMessageRow(
1703
2504
  .filter(Boolean)
1704
2505
  .join(' ')
1705
2506
  );
2507
+ setData(
2508
+ body,
2509
+ 'nativeListSelector',
2510
+ row.height !== undefined ? presentation : undefined
2511
+ );
2512
+ if (row.type === 'identity' && row.titleActionKey && row.titleActionOnHover) {
2513
+ setData(body, 'nativeListHoverAction', row.titleActionKey);
2514
+ markActionAnchorSource(body, 'leadingAction');
2515
+ }
1706
2516
  if (row.type === 'identity' && row.leadingAction) {
1707
2517
  const action = createIconAction(
1708
2518
  context,
@@ -1714,7 +2524,44 @@ function createIdentityActivityOrMessageRow(
1714
2524
  markActionAnchorSource(action, 'leadingAction');
1715
2525
  body.appendChild(action);
1716
2526
  }
1717
- const visual = createVisual(context, visualFromRow(row));
2527
+ const visual = createVisual(
2528
+ context,
2529
+ visualFromRow(row),
2530
+ row.height !== undefined ? presentation : undefined
2531
+ );
2532
+ if (
2533
+ visual &&
2534
+ row.type === 'identity' &&
2535
+ row.height !== undefined &&
2536
+ row.presentation === 'walletSidebar' &&
2537
+ 'fallbackIcon' in row.leading &&
2538
+ row.leading.fallbackIcon?.name === 'LockSolid'
2539
+ ) {
2540
+ // OneKey patch: hidden-wallet locks use WalletAvatar's full 40-point icon.
2541
+ const icon = visual.querySelector<SVGElement>(
2542
+ '.ok-native-list-visual-fallback svg'
2543
+ );
2544
+ if (icon) {
2545
+ icon.style.width = '40px';
2546
+ icon.style.height = '40px';
2547
+ }
2548
+ const fallback = visual.querySelector<HTMLElement>(
2549
+ '.ok-native-list-visual-fallback'
2550
+ );
2551
+ if (fallback) {
2552
+ fallback.style.borderRadius = '0';
2553
+ fallback.style.overflow = 'visible';
2554
+ }
2555
+ }
2556
+ if (
2557
+ visual &&
2558
+ row.type === 'identity' &&
2559
+ row.height !== undefined &&
2560
+ row.presentation === 'walletSidebar' &&
2561
+ 'borderStyle' in row.leading &&
2562
+ row.leading.borderStyle === 'dashed'
2563
+ )
2564
+ visual.style.borderWidth = '1px';
1718
2565
  if (visual) body.appendChild(visual);
1719
2566
  if (row.type === 'activity' && row.secondaryLeading) {
1720
2567
  const secondVisual = createVisual(context, row.secondaryLeading);
@@ -1737,8 +2584,82 @@ function createIdentityActivityOrMessageRow(
1737
2584
  subtitle,
1738
2585
  row.type === 'identity' ? row.tertiary : undefined,
1739
2586
  row.type === 'identity' ? row.tertiaryTone : undefined,
1740
- row.type === 'identity' ? row.badges : undefined
2587
+ row.type === 'identity' && presentation !== 'walletSidebar'
2588
+ ? row.badges
2589
+ : undefined
1741
2590
  );
2591
+ // OneKey patch: match existing search, subtitle fragments, and sidebar badges.
2592
+ if (row.type === 'identity') {
2593
+ const titleElement = column.firstElementChild as HTMLElement;
2594
+ if (row.titleMatch?.length) {
2595
+ const firstText = titleElement.firstChild;
2596
+ if (firstText) firstText.remove();
2597
+ const fragment = context.document.createDocumentFragment();
2598
+ let offset = 0;
2599
+ row.titleMatch.forEach(({ start, end }) => {
2600
+ fragment.appendChild(
2601
+ context.document.createTextNode(row.title.slice(offset, start))
2602
+ );
2603
+ const match = createElement(
2604
+ context.document,
2605
+ 'span',
2606
+ 'ok-native-list-info',
2607
+ row.title.slice(start, end)
2608
+ );
2609
+ fragment.appendChild(match);
2610
+ offset = end;
2611
+ });
2612
+ fragment.appendChild(
2613
+ context.document.createTextNode(row.title.slice(offset))
2614
+ );
2615
+ titleElement.prepend(fragment);
2616
+ }
2617
+ if (row.subtitleSegments?.length) {
2618
+ column.querySelector('.ok-native-list-secondary')?.remove();
2619
+ const segments = createElement(
2620
+ context.document,
2621
+ 'span',
2622
+ 'ok-native-list-subtitle-segments'
2623
+ );
2624
+ row.subtitleSegments.forEach((segment) => {
2625
+ if (segment.separatorBefore)
2626
+ segments.appendChild(
2627
+ createElement(
2628
+ context.document,
2629
+ 'span',
2630
+ 'ok-native-list-subtitle-dot'
2631
+ )
2632
+ );
2633
+ const text = createElement(
2634
+ context.document,
2635
+ 'span',
2636
+ 'ok-native-list-secondary',
2637
+ segment.text
2638
+ );
2639
+ applyValueSegments(text, segment.textSegments, 14, 20, 400);
2640
+ setData(text, 'tone', segment.tone);
2641
+ text.style.color =
2642
+ segment.tone === 'disabled'
2643
+ ? 'var(--nl-disabled)'
2644
+ : segment.tone === 'caution'
2645
+ ? 'var(--nl-caution)'
2646
+ : toneColor(segment.tone, 'secondary');
2647
+ segments.appendChild(text);
2648
+ });
2649
+ column.insertBefore(segments, titleElement.nextSibling);
2650
+ }
2651
+ if (presentation === 'walletSidebar' && row.badges?.length) {
2652
+ const badges = createElement(
2653
+ context.document,
2654
+ 'span',
2655
+ 'ok-native-list-wallet-badges'
2656
+ );
2657
+ row.badges.forEach((badge) =>
2658
+ badges.appendChild(createBadge(context, badge))
2659
+ );
2660
+ column.appendChild(badges);
2661
+ }
2662
+ }
1742
2663
  if (row.type === 'activity' && row.status)
1743
2664
  column.appendChild(
1744
2665
  createElement(
@@ -1814,6 +2735,21 @@ function createIdentityActivityOrMessageRow(
1814
2735
  return body;
1815
2736
  }
1816
2737
 
2738
+ // OneKey patch: SizableText enables tabular digits without replacing its font family.
2739
+ function applySelectorTabularNumbers(body: HTMLElement, row: RowModel) {
2740
+ if (
2741
+ !('presentation' in row) ||
2742
+ !['accountSelector', 'networkSelector', 'walletSidebar'].includes(
2743
+ row.presentation ?? ''
2744
+ )
2745
+ )
2746
+ return;
2747
+ body.style.fontVariantNumeric = 'tabular-nums';
2748
+ body.querySelectorAll<HTMLElement>('span,button').forEach((text) => {
2749
+ text.style.fontVariantNumeric = 'tabular-nums';
2750
+ });
2751
+ }
2752
+
1817
2753
  function createWalletGroupRow(
1818
2754
  context: RenderContext,
1819
2755
  row: Extract<RowModel, { type: 'walletGroup' }>
@@ -1830,11 +2766,19 @@ function createWalletGroupRow(
1830
2766
  'ok-native-list-wallet-member'
1831
2767
  );
1832
2768
  setData(memberElement, 'nativeListGroupMemberKey', member.key);
2769
+ setData(memberElement, 'testid', member.testID);
1833
2770
  setData(memberElement, 'nativeListGroupParent', memberIndex === 0);
1834
2771
  setData(memberElement, 'nativeListSelected', member.selected);
1835
- memberElement.appendChild(
1836
- createIdentityActivityOrMessageRow(context, member)
1837
- );
2772
+ // OneKey patch: grouped members have the same selector typography as standalone wallets.
2773
+ // memberElement.appendChild(createIdentityActivityOrMessageRow(context, member));
2774
+ const memberBody = createIdentityActivityOrMessageRow(context, member);
2775
+ applySelectorTabularNumbers(memberBody, member);
2776
+ memberElement.appendChild(memberBody);
2777
+ // OneKey patch: group children use their own measured badge height.
2778
+ memberElement.style.flexBasis =
2779
+ String(member.height ?? 68 + (member.badges?.length ? 24 : 0)) + 'px';
2780
+ memberElement.style.height = memberElement.style.flexBasis;
2781
+ memberElement.style.opacity = String(member.opacity ?? 1);
1838
2782
  body.appendChild(memberElement);
1839
2783
  });
1840
2784
  return body;
@@ -1894,9 +2838,18 @@ export class NativeListWebEngine {
1894
2838
  private resizeObserver: ResizeObserver | undefined;
1895
2839
  private pendingScroll: PendingScroll | undefined;
1896
2840
  private lastVisibleSignature: string | undefined;
2841
+ // OneKey patch: URI leases outlive DOM overscan only inside this bounded window.
2842
+ private readonly avatarLeases = new Map<string, AvatarLease>();
2843
+ private avatarOffset = 0;
2844
+ private avatarDirection = 1;
1897
2845
  private reachedGeneration: number | undefined;
1898
2846
  private stickyKey: string | undefined;
1899
2847
  private previewTimer: number | undefined;
2848
+ private sectionIndexEntries: readonly Readonly<{
2849
+ key: string;
2850
+ title: string;
2851
+ position: number;
2852
+ }>[] = [];
1900
2853
  private pointerReorder: PointerReorderState | undefined;
1901
2854
  private keyboardReorder: KeyboardReorderState | undefined;
1902
2855
  private reorderMoveFrame: number | undefined;
@@ -1919,6 +2872,8 @@ export class NativeListWebEngine {
1919
2872
  private lastViewportWidth = -1;
1920
2873
  private lastViewportHeight = -1;
1921
2874
  private destroyed = false;
2875
+ // OneKey patch: warning banners are measured after normal browser text wrapping.
2876
+ private measuredWarningHeights = new Map<string, number>();
1922
2877
 
1923
2878
  constructor(
1924
2879
  host: HTMLElement,
@@ -2004,6 +2959,9 @@ export class NativeListWebEngine {
2004
2959
  passive: true,
2005
2960
  });
2006
2961
  this.root.addEventListener('click', this.handleClick);
2962
+ // OneKey patch: preserve web tooltip hover for section titles.
2963
+ this.root.addEventListener('pointerover', this.handleTitlePointerOver);
2964
+ this.root.addEventListener('pointerout', this.handleTitlePointerOut);
2007
2965
  this.root.addEventListener('keydown', this.handleKeyDown);
2008
2966
  this.viewport.addEventListener(
2009
2967
  'pointerdown',
@@ -2160,7 +3118,10 @@ export class NativeListWebEngine {
2160
3118
  const index = resolveLocationIndex(this.snapshot.rows, params);
2161
3119
  if (index === undefined) {
2162
3120
  const sectionCount = this.snapshot.rows.filter(
2163
- (row) => row.type === 'sectionHeader' && row.variant !== 'summary'
3121
+ (row) =>
3122
+ row.type === 'sectionHeader' &&
3123
+ row.sticky !== false &&
3124
+ row.variant !== 'summary'
2164
3125
  ).length;
2165
3126
  this.emitScrollFailure(
2166
3127
  params.itemIndex,
@@ -2219,6 +3180,8 @@ export class NativeListWebEngine {
2219
3180
  );
2220
3181
  this.viewport.removeEventListener('scroll', this.handleScroll);
2221
3182
  this.root.removeEventListener('click', this.handleClick);
3183
+ this.root.removeEventListener('pointerover', this.handleTitlePointerOver);
3184
+ this.root.removeEventListener('pointerout', this.handleTitlePointerOut);
2222
3185
  this.root.removeEventListener('keydown', this.handleKeyDown);
2223
3186
  this.cancelPointerReorder(true);
2224
3187
  this.viewport.removeEventListener(
@@ -2251,25 +3214,29 @@ export class NativeListWebEngine {
2251
3214
  this.viewport.removeEventListener('pointerup', this.handlePullEnd);
2252
3215
  this.viewport.removeEventListener('pointercancel', this.handlePullEnd);
2253
3216
  const host = this.root.parentElement;
3217
+ disposeWebImageRetries(this.root);
3218
+ this.pool.forEach(disposeWebImageRetries);
2254
3219
  this.root.remove();
2255
3220
  this.hideReorderPreview();
2256
3221
  this.reorderPreview.remove();
2257
3222
  if (host) host.style.position = this.previousHostPosition;
2258
3223
  this.mounted.clear();
2259
3224
  this.pool.length = 0;
3225
+ this.avatarLeases.forEach((release) => release());
3226
+ this.avatarLeases.clear();
2260
3227
  }
2261
3228
 
2262
3229
  private setSnapshot(
2263
3230
  snapshot: NativeListSnapshot,
2264
3231
  selectedKeys?: ReadonlySet<string>
2265
3232
  ) {
3233
+ this.measuredWarningHeights.clear();
2266
3234
  this.snapshot = snapshot;
2267
3235
  this.rows = effectiveRows(snapshot);
2268
3236
  this.selectedKeys =
2269
3237
  selectedKeys ?? selectionStateFromSnapshot(snapshot).selectedKeys;
2270
3238
  if (this.reachedGeneration !== snapshot.generation)
2271
3239
  this.reachedGeneration = undefined;
2272
- this.renderSectionIndex();
2273
3240
  this.applyTheme();
2274
3241
  this.recomputeLayout();
2275
3242
  this.renderFooter();
@@ -2288,6 +3255,11 @@ export class NativeListWebEngine {
2288
3255
  '--nl-primary': theme.primaryText,
2289
3256
  '--nl-secondary': theme.secondaryText,
2290
3257
  '--nl-disabled': theme.disabledText,
3258
+ '--nl-caution': theme.caution ?? '#AB6400',
3259
+ '--nl-caution-background': theme.cautionBackground,
3260
+ '--nl-checkbox-background': theme.checkboxBackground,
3261
+ '--nl-checkbox-border': theme.checkboxBorder,
3262
+ '--nl-checkbox-icon': theme.checkboxIcon,
2291
3263
  '--nl-icon': theme.icon,
2292
3264
  '--nl-icon-subdued': theme.iconSubdued,
2293
3265
  '--nl-separator': theme.separator,
@@ -2316,18 +3288,31 @@ export class NativeListWebEngine {
2316
3288
  viewportHeight !== this.lastViewportHeight)
2317
3289
  ) {
2318
3290
  this.invalidateActionAnchor('layout');
3291
+ this.measuredWarningHeights.clear();
2319
3292
  }
2320
3293
  this.lastViewportWidth = viewportWidth;
2321
3294
  this.lastViewportHeight = viewportHeight;
2322
3295
  const previousHorizontal = this.layout.horizontal;
3296
+ const measuredSnapshot: NativeListSnapshot = {
3297
+ ...this.snapshot,
3298
+ rows: this.snapshot.rows.map((row) =>
3299
+ row.type === 'system' &&
3300
+ row.variant === 'warning' &&
3301
+ row.height === undefined &&
3302
+ this.measuredWarningHeights.has(row.key)
3303
+ ? { ...row, height: this.measuredWarningHeights.get(row.key) }
3304
+ : row
3305
+ ),
3306
+ };
2323
3307
  this.layout = computeWebListLayout(
2324
- this.snapshot,
3308
+ measuredSnapshot,
2325
3309
  viewportWidth,
2326
3310
  viewportHeight,
2327
3311
  this.reorderCompactKey
2328
3312
  );
2329
3313
  this.content.style.width = String(this.layout.contentWidth) + 'px';
2330
3314
  this.content.style.height = String(this.layout.contentHeight) + 'px';
3315
+ this.renderSectionIndex(viewportHeight || DEFAULT_VIEWPORT_HEIGHT);
2331
3316
  if (previousHorizontal !== this.layout.horizontal) {
2332
3317
  this.viewport.scrollLeft = 0;
2333
3318
  this.viewport.scrollTop = 0;
@@ -2336,7 +3321,62 @@ export class NativeListWebEngine {
2336
3321
  this.performPendingScroll();
2337
3322
  };
2338
3323
 
3324
+ private updateAvatarWindow() {
3325
+ const offset = this.currentOffset();
3326
+ if (offset !== this.avatarOffset)
3327
+ this.avatarDirection = Math.sign(offset - this.avatarOffset);
3328
+ this.avatarOffset = offset;
3329
+ const visible = visibleWebLayoutItems(
3330
+ this.layout,
3331
+ offset,
3332
+ this.viewportLength(),
3333
+ 0
3334
+ );
3335
+ const first = visible[0]?.index ?? -1;
3336
+ const last = visible[visible.length - 1]?.index ?? -1;
3337
+ const candidates = avatarPrefetchWindow(
3338
+ this.rows,
3339
+ first,
3340
+ last,
3341
+ this.avatarDirection
3342
+ );
3343
+ const desired = new Set(
3344
+ candidates
3345
+ .map(({ source }) => canonicalNativeListAvatarUri(source.uri))
3346
+ .filter((uri) => uri !== undefined)
3347
+ );
3348
+ this.avatarLeases.forEach((release, uri) => {
3349
+ if (!desired.has(uri)) {
3350
+ release();
3351
+ this.avatarLeases.delete(uri);
3352
+ }
3353
+ });
3354
+ candidates.forEach(({ source, priority }) => {
3355
+ const uri = canonicalNativeListAvatarUri(source.uri);
3356
+ if (!uri) return;
3357
+ const existing = this.avatarLeases.get(uri);
3358
+ if (existing) existing.setPriority(priority);
3359
+ else {
3360
+ let lease: AvatarLease | undefined;
3361
+ lease = acquireNativeListAvatar(
3362
+ this.document,
3363
+ uri,
3364
+ () => {},
3365
+ () => {
3366
+ if (this.avatarLeases.get(uri) === lease) {
3367
+ lease?.();
3368
+ this.avatarLeases.delete(uri);
3369
+ }
3370
+ },
3371
+ priority
3372
+ );
3373
+ this.avatarLeases.set(uri, lease);
3374
+ }
3375
+ });
3376
+ }
3377
+
2339
3378
  private renderWindow() {
3379
+ this.updateAvatarWindow();
2340
3380
  const viewportLength = this.viewportLength();
2341
3381
  const visible = webLayoutItemsForMount(
2342
3382
  this.layout,
@@ -2350,6 +3390,8 @@ export class NativeListWebEngine {
2350
3390
  if (!desired.has(index)) {
2351
3391
  this.invalidateActionAnchorForElement(element);
2352
3392
  this.mounted.delete(index);
3393
+ if (disposeWebImageRetries(element))
3394
+ element.removeAttribute('data-render-signature');
2353
3395
  element.remove();
2354
3396
  this.pool.push(element);
2355
3397
  }
@@ -2377,6 +3419,27 @@ export class NativeListWebEngine {
2377
3419
  element.dataset.renderSignature = signature;
2378
3420
  }
2379
3421
  });
3422
+ let measuredWarningChanged = false;
3423
+ this.mounted.forEach((element, index) => {
3424
+ const row = this.rows[index];
3425
+ if (
3426
+ row?.type !== 'system' ||
3427
+ row.variant !== 'warning' ||
3428
+ row.height !== undefined
3429
+ )
3430
+ return;
3431
+ const height =
3432
+ element.querySelector<HTMLElement>('.ok-native-list-warning')
3433
+ ?.offsetHeight ?? 0;
3434
+ if (height > 0 && height !== this.measuredWarningHeights.get(row.key)) {
3435
+ this.measuredWarningHeights.set(row.key, height);
3436
+ measuredWarningChanged = true;
3437
+ }
3438
+ });
3439
+ if (measuredWarningChanged) {
3440
+ this.recomputeLayout();
3441
+ return;
3442
+ }
2380
3443
  this.updateVisibleSelection();
2381
3444
  this.updateVisibleState();
2382
3445
  }
@@ -2395,6 +3458,7 @@ export class NativeListWebEngine {
2395
3458
  overlay = false
2396
3459
  ) {
2397
3460
  this.invalidateActionAnchorForElement(element);
3461
+ disposeWebImageRetries(element);
2398
3462
  const bindingEpoch = String(++this.bindingEpochCounter);
2399
3463
  element.className = overlay
2400
3464
  ? 'ok-native-list-item ok-native-list-sticky'
@@ -2403,6 +3467,9 @@ export class NativeListWebEngine {
2403
3467
  setData(element, 'nativeListBindingEpoch', bindingEpoch);
2404
3468
  setData(element, 'nativeListRowIndex', index);
2405
3469
  setData(element, 'nativeListDisabled', Boolean(row.disabled));
3470
+ setData(element, 'testid', row.testID);
3471
+ // OneKey patch: deprecation dims a row without disabling its actions.
3472
+ element.style.opacity = String(row.opacity ?? 1);
2406
3473
  setData(element, 'nativeListReorderable', this.isReorderable(row));
2407
3474
  setData(
2408
3475
  element,
@@ -2435,12 +3502,90 @@ export class NativeListWebEngine {
2435
3502
  selectedKeys: this.selectedKeys,
2436
3503
  itemIndex: index,
2437
3504
  };
2438
- element.replaceChildren(createRowBody(context, row));
3505
+ const body = createRowBody(context, row);
3506
+ applySelectorTabularNumbers(body, row);
3507
+ // OneKey patch: explicit selector fields preserve original page geometry.
3508
+ element.style.contain = row.backgroundFullWidth ? 'layout style' : '';
3509
+ if (row.backgroundColor) body.style.backgroundColor = row.backgroundColor;
3510
+ if (row.backgroundFullWidth && row.backgroundColor) {
3511
+ const bleed = paddingValues(this.snapshot).horizontal;
3512
+ body.style.position = 'relative';
3513
+ body.style.overflow = 'visible';
3514
+ body.style.boxShadow =
3515
+ String(-bleed) +
3516
+ 'px 0 ' +
3517
+ row.backgroundColor +
3518
+ ',' +
3519
+ String(bleed) +
3520
+ 'px 0 ' +
3521
+ row.backgroundColor;
3522
+ }
3523
+ if (row.type === 'identity' && row.height !== undefined) {
3524
+ const title = body.querySelector<HTMLElement>('.ok-native-list-title');
3525
+ if (row.presentation === 'accountSelector') {
3526
+ body.style.gap = '12px';
3527
+ body.style.borderRadius = '12px';
3528
+ if ('shape' in row.leading && row.leading.shape === 'rounded') {
3529
+ const visual = body.querySelector<HTMLElement>(
3530
+ '.ok-native-list-visual'
3531
+ );
3532
+ if (visual) visual.style.borderRadius = '8px';
3533
+ }
3534
+ if (title) title.style.lineHeight = '24px';
3535
+ }
3536
+ if (row.presentation === 'networkSelector') {
3537
+ body.style.borderRadius = '12px';
3538
+ const visual = body.querySelector<HTMLElement>(
3539
+ '.ok-native-list-visual'
3540
+ );
3541
+ if (visual) {
3542
+ visual.style.width = '32px';
3543
+ visual.style.height = '32px';
3544
+ visual.style.flexBasis = '32px';
3545
+ }
3546
+ if (
3547
+ row.leading.kind === 'network' &&
3548
+ !row.leading.image &&
3549
+ !row.leading.fallbackIcon &&
3550
+ row.leading.fallbackText
3551
+ ) {
3552
+ const fallback = visual?.querySelector<HTMLElement>(
3553
+ '.ok-native-list-visual-fallback'
3554
+ );
3555
+ if (fallback) {
3556
+ fallback.style.fontSize = '19px';
3557
+ fallback.style.lineHeight = '27px';
3558
+ fallback.style.fontWeight = '600';
3559
+ fallback.style.color = 'var(--nl-inverse-text)';
3560
+ }
3561
+ }
3562
+ visual
3563
+ ?.querySelectorAll<HTMLElement>('.ok-native-list-visual-main')
3564
+ .forEach((image) => {
3565
+ image.style.width = '32px';
3566
+ image.style.height = '32px';
3567
+ });
3568
+ if (title) {
3569
+ title.style.fontSize = '16px';
3570
+ title.style.lineHeight = '24px';
3571
+ title.style.fontWeight = '500';
3572
+ }
3573
+ body
3574
+ .querySelectorAll<HTMLElement>('.ok-native-list-accessory')
3575
+ .forEach((value) => {
3576
+ value.style.fontSize = '16px';
3577
+ value.style.lineHeight = '24px';
3578
+ value.style.fontWeight = '500';
3579
+ });
3580
+ }
3581
+ }
3582
+ element.replaceChildren(body);
2439
3583
  }
2440
3584
 
2441
3585
  private renderFooter() {
2442
3586
  const row = this.snapshot.fixedFooter;
2443
3587
  this.invalidateActionAnchorForElement(this.footer);
3588
+ disposeWebImageRetries(this.footer);
2444
3589
  this.footer.replaceChildren();
2445
3590
  if (!row) return;
2446
3591
  const element = createElement(this.document, 'div', 'ok-native-list-item');
@@ -2459,25 +3604,79 @@ export class NativeListWebEngine {
2459
3604
  this.footer.appendChild(element);
2460
3605
  }
2461
3606
 
2462
- private renderSectionIndex() {
3607
+ private sectionIndexVisibleEntryIndices(
3608
+ viewportHeight: number
3609
+ ): readonly number[] {
3610
+ const entryCount = this.sectionIndexEntries.length;
3611
+ if (entryCount <= 1) return entryCount ? [0] : [];
3612
+ const availableHeight = Math.max(
3613
+ 0,
3614
+ viewportHeight - SECTION_INDEX_EDGE_PADDING * 2
3615
+ );
3616
+ const maxVisible = Math.max(
3617
+ 2,
3618
+ Math.floor(availableHeight / SECTION_INDEX_MIN_LABEL_SPACING) + 1
3619
+ );
3620
+ if (entryCount <= maxVisible) {
3621
+ return Array.from({ length: entryCount }, (_, index) => index);
3622
+ }
3623
+ const result = new Set<number>();
3624
+ for (let slot = 0; slot < maxVisible; slot += 1) {
3625
+ result.add(Math.round((slot * (entryCount - 1)) / (maxVisible - 1)));
3626
+ }
3627
+ return [...result].sort((left, right) => left - right);
3628
+ }
3629
+
3630
+ private renderSectionIndex(viewportHeight: number) {
2463
3631
  this.indexRail.replaceChildren();
2464
3632
  if (!sectionIndexEnabled(this.snapshot)) {
3633
+ this.sectionIndexEntries = [];
2465
3634
  this.indexRail.hidden = true;
2466
3635
  return;
2467
3636
  }
3637
+ this.sectionIndexEntries = this.snapshot.rows.flatMap((row, position) =>
3638
+ row.type === 'sectionHeader' && row.indexTitle
3639
+ ? [{ key: row.key, title: row.indexTitle, position }]
3640
+ : []
3641
+ );
3642
+ if (
3643
+ this.sectionIndexEntries.length === 0 ||
3644
+ viewportHeight < SECTION_INDEX_MIN_HEIGHT
3645
+ ) {
3646
+ this.indexRail.hidden = true;
3647
+ return;
3648
+ }
3649
+ const visibleEntryIndices =
3650
+ this.sectionIndexVisibleEntryIndices(viewportHeight);
3651
+ setData(
3652
+ this.indexRail,
3653
+ 'compact',
3654
+ visibleEntryIndices.length < this.sectionIndexEntries.length
3655
+ );
2468
3656
  const fragment = this.document.createDocumentFragment();
2469
- this.snapshot.rows.forEach((row, index) => {
2470
- if (row.type !== 'sectionHeader' || !row.indexTitle) return;
3657
+ visibleEntryIndices.forEach((entryIndex) => {
3658
+ const entry = this.sectionIndexEntries[entryIndex];
3659
+ if (!entry) return;
2471
3660
  const button = createElement(
2472
3661
  this.document,
2473
3662
  'button',
2474
3663
  'ok-native-list-index-button',
2475
- row.indexTitle
3664
+ entry.title
2476
3665
  );
2477
3666
  button.setAttribute('type', 'button');
2478
- button.setAttribute('aria-label', 'Jump to ' + row.indexTitle);
2479
- setData(button, 'sectionPosition', index);
2480
- setData(button, 'sectionKey', row.key);
3667
+ button.setAttribute('aria-label', 'Jump to ' + entry.title);
3668
+ setData(button, 'sectionEntryIndex', entryIndex);
3669
+ setData(button, 'sectionPosition', entry.position);
3670
+ setData(button, 'sectionKey', entry.key);
3671
+ const progress =
3672
+ this.sectionIndexEntries.length === 1
3673
+ ? 0.5
3674
+ : entryIndex / (this.sectionIndexEntries.length - 1);
3675
+ button.style.top =
3676
+ String(
3677
+ SECTION_INDEX_EDGE_PADDING +
3678
+ progress * (viewportHeight - SECTION_INDEX_EDGE_PADDING * 2)
3679
+ ) + 'px';
2481
3680
  fragment.appendChild(button);
2482
3681
  });
2483
3682
  this.indexRail.appendChild(fragment);
@@ -2487,7 +3686,9 @@ export class NativeListWebEngine {
2487
3686
  private updateVisibleSelection() {
2488
3687
  const update = (element: HTMLElement, row: RowModel | undefined) => {
2489
3688
  if (!row) return;
2490
- const selected = this.selectedKeys.has(row.key);
3689
+ // OneKey patch: selector adapters mark active rows independently of checkbox selection.
3690
+ // const selected = this.selectedKeys.has(row.key);
3691
+ const selected = row.selected === true || this.selectedKeys.has(row.key);
2491
3692
  setData(element, 'nativeListSelected', selected);
2492
3693
  element.setAttribute('aria-selected', String(selected));
2493
3694
  element
@@ -2548,7 +3749,9 @@ export class NativeListWebEngine {
2548
3749
  }
2549
3750
  this.checkEndReached(last?.index ?? -1);
2550
3751
  this.updateStickyHeader(first?.index ?? -1);
2551
- this.updateSectionIndex(first?.index ?? -1);
3752
+ // OneKey patch: index highlighting follows header positions at scroll boundaries.
3753
+ // this.updateSectionIndex(first?.index ?? -1);
3754
+ this.updateSectionIndex();
2552
3755
  }
2553
3756
 
2554
3757
  private updateStickyHeader(firstVisibleIndex: number) {
@@ -2564,7 +3767,11 @@ export class NativeListWebEngine {
2564
3767
  let index = -1;
2565
3768
  for (let cursor = firstVisibleIndex; cursor >= 0; cursor -= 1) {
2566
3769
  const row = this.rows[cursor];
2567
- if (row?.type === 'sectionHeader' && row.variant !== 'summary') {
3770
+ if (
3771
+ row?.type === 'sectionHeader' &&
3772
+ row.sticky !== false &&
3773
+ row.variant !== 'summary'
3774
+ ) {
2568
3775
  index = cursor;
2569
3776
  break;
2570
3777
  }
@@ -2595,6 +3802,7 @@ export class NativeListWebEngine {
2595
3802
  const candidate = this.rows[cursor];
2596
3803
  if (
2597
3804
  candidate?.type === 'sectionHeader' &&
3805
+ candidate.sticky !== false &&
2598
3806
  candidate.variant !== 'summary'
2599
3807
  ) {
2600
3808
  nextIndex = cursor;
@@ -2610,12 +3818,17 @@ export class NativeListWebEngine {
2610
3818
  this.updateVisibleSelection();
2611
3819
  }
2612
3820
 
2613
- private updateSectionIndex(firstVisibleIndex: number) {
3821
+ // private updateSectionIndex(firstVisibleIndex: number) {
3822
+ private updateSectionIndex() {
2614
3823
  let activeKey: string | undefined;
2615
3824
  this.snapshot.rows.forEach((row, index) => {
2616
3825
  if (
2617
- index <= firstVisibleIndex &&
3826
+ // OneKey patch: a spacer ending exactly at the viewport is not the active section.
3827
+ // index <= firstVisibleIndex &&
3828
+ itemStart(this.layout.items[index], this.layout.horizontal) <=
3829
+ this.currentOffset() &&
2618
3830
  row.type === 'sectionHeader' &&
3831
+ row.sticky !== false &&
2619
3832
  row.indexTitle
2620
3833
  )
2621
3834
  activeKey = row.key;
@@ -2793,7 +4006,14 @@ export class NativeListWebEngine {
2793
4006
  if (!source || !bindingEpoch || !rowElement.contains(actionElement))
2794
4007
  return undefined;
2795
4008
  this.invalidateActionAnchor('rebind');
2796
- const rect = actionElement.getBoundingClientRect();
4009
+ const actualRect = actionElement.getBoundingClientRect();
4010
+ const inset = Number(actionElement.dataset.nativeListAnchorInset ?? 0);
4011
+ const rect = {
4012
+ left: actualRect.left + inset,
4013
+ top: actualRect.top + inset,
4014
+ width: actualRect.width - inset * 2,
4015
+ height: actualRect.height - inset * 2,
4016
+ };
2797
4017
  const token = [
2798
4018
  this.actionAnchorInstanceId,
2799
4019
  this.snapshot.generation,
@@ -2866,7 +4086,9 @@ export class NativeListWebEngine {
2866
4086
  rowElement?: HTMLElement,
2867
4087
  sourceElement = rowElement
2868
4088
  ) {
2869
- if (row.disabled) return;
4089
+ // OneKey patch: missing-address rows keep their create-address accessory interactive.
4090
+ // if (row.disabled) return;
4091
+ if (row.disabled || row.pressDisabled) return;
2870
4092
  if (
2871
4093
  this.snapshot.selection?.rowPressToggles &&
2872
4094
  this.snapshot.selection.mode !== 'none' &&
@@ -2894,6 +4116,56 @@ export class NativeListWebEngine {
2894
4116
  }
2895
4117
  }
2896
4118
 
4119
+ // OneKey patch: hover opens the same anchored help action as native taps.
4120
+ private handleTitlePointerOver = (event: PointerEvent) => {
4121
+ const target = event.target;
4122
+ if (!(target instanceof Element)) return;
4123
+ const action = target.closest<HTMLElement>(
4124
+ '[data-native-list-hover-action]'
4125
+ );
4126
+ if (
4127
+ !action ||
4128
+ (event.relatedTarget instanceof Node &&
4129
+ action.contains(event.relatedTarget))
4130
+ )
4131
+ return;
4132
+ const rowElement = action.closest<HTMLElement>(
4133
+ '[data-native-list-row-key]'
4134
+ );
4135
+ const row = this.rowAtElement(rowElement);
4136
+ const memberKey = action.closest<HTMLElement>(
4137
+ '[data-native-list-group-member-key]'
4138
+ )?.dataset.nativeListGroupMemberKey;
4139
+ const sourceRow =
4140
+ row?.type === 'walletGroup'
4141
+ ? [row.parent, ...row.children].find(
4142
+ (member) => member.key === memberKey
4143
+ ) ?? row
4144
+ : row;
4145
+ const actionKey =
4146
+ action.dataset.nativeListHoverAction === 'true'
4147
+ ? action.dataset.nativeListAction
4148
+ : action.dataset.nativeListHoverAction;
4149
+ if (sourceRow && !sourceRow.disabled && actionKey)
4150
+ this.emitRowAction(sourceRow, actionKey, action, rowElement ?? undefined);
4151
+ };
4152
+
4153
+ private handleTitlePointerOut = (event: PointerEvent) => {
4154
+ const target = event.target;
4155
+ if (!(target instanceof Element)) return;
4156
+ const action = target.closest<HTMLElement>(
4157
+ '[data-native-list-hover-action]'
4158
+ );
4159
+ if (
4160
+ !action ||
4161
+ (event.relatedTarget instanceof Node &&
4162
+ action.contains(event.relatedTarget))
4163
+ )
4164
+ return;
4165
+ if (this.actionAnchor?.actionElement === action)
4166
+ this.invalidateActionAnchor('pointerLeave');
4167
+ };
4168
+
2897
4169
  private handleClick = (event: Event) => {
2898
4170
  if (Date.now() < this.suppressClickUntil) {
2899
4171
  event.preventDefault();
@@ -2944,6 +4216,16 @@ export class NativeListWebEngine {
2944
4216
  };
2945
4217
 
2946
4218
  private handleKeyDown = (event: KeyboardEvent) => {
4219
+ // OneKey patch: non-button title help supports keyboard activation.
4220
+ if (
4221
+ (event.key === 'Enter' || event.key === ' ') &&
4222
+ event.target instanceof HTMLElement &&
4223
+ event.target.matches('[role="button"][data-native-list-action]')
4224
+ ) {
4225
+ event.preventDefault();
4226
+ event.target.click();
4227
+ return;
4228
+ }
2947
4229
  if (event.key === 'Escape') {
2948
4230
  if (this.pointerReorder?.active) {
2949
4231
  event.preventDefault();
@@ -3073,7 +4355,10 @@ export class NativeListWebEngine {
3073
4355
  this.frameHandle = this.requestFrame(() => {
3074
4356
  this.frameHandle = undefined;
3075
4357
  if (this.virtualizationEnabled) this.renderWindow();
3076
- else this.updateVisibleState();
4358
+ else {
4359
+ this.updateAvatarWindow();
4360
+ this.updateVisibleState();
4361
+ }
3077
4362
  });
3078
4363
  }
3079
4364
 
@@ -3090,13 +4375,27 @@ export class NativeListWebEngine {
3090
4375
  else view?.clearTimeout(handle);
3091
4376
  }
3092
4377
 
3093
- private selectIndexPosition(position: number, title: string) {
4378
+ private selectIndexPosition(
4379
+ position: number,
4380
+ title: string,
4381
+ previewClientY?: number
4382
+ ) {
3094
4383
  this.scrollToIndex(position, {
3095
4384
  animated: false,
3096
4385
  alignment: 'start',
3097
4386
  viewPosition: 0,
3098
4387
  viewOffset: 0,
3099
4388
  });
4389
+ const frame = this.viewportFrame.getBoundingClientRect();
4390
+ if (previewClientY !== undefined && frame.height > 0) {
4391
+ const previewY = Math.min(
4392
+ frame.height - 24,
4393
+ Math.max(24, previewClientY - frame.top)
4394
+ );
4395
+ this.indexPreview.style.top = String(previewY) + 'px';
4396
+ } else {
4397
+ this.indexPreview.style.top = '50%';
4398
+ }
3100
4399
  this.indexPreview.textContent = title;
3101
4400
  setData(this.indexPreview, 'visible', true);
3102
4401
  if (this.previewTimer !== undefined)
@@ -3106,37 +4405,69 @@ export class NativeListWebEngine {
3106
4405
  }, 180);
3107
4406
  }
3108
4407
 
3109
- private indexButtonAtEvent(event: PointerEvent): HTMLElement | undefined {
3110
- const direct = (event.target as Element | null)?.closest<HTMLElement>(
3111
- '[data-section-position]'
4408
+ private sectionIndexEntryAtEvent(event: PointerEvent):
4409
+ | Readonly<{
4410
+ entry: NativeListWebEngine['sectionIndexEntries'][number];
4411
+ previewClientY: number;
4412
+ }>
4413
+ | undefined {
4414
+ const rail = this.indexRail.getBoundingClientRect();
4415
+ if (this.sectionIndexEntries.length === 0 || rail.height <= 0) {
4416
+ return undefined;
4417
+ }
4418
+ const availableHeight = Math.max(
4419
+ 1,
4420
+ rail.height - SECTION_INDEX_EDGE_PADDING * 2
3112
4421
  );
3113
- if (direct) return direct;
3114
- return (
3115
- this.document
3116
- .elementFromPoint(event.clientX, event.clientY)
3117
- ?.closest<HTMLElement>('[data-section-position]') ?? undefined
4422
+ const progress = Math.min(
4423
+ 1,
4424
+ Math.max(
4425
+ 0,
4426
+ (event.clientY - rail.top - SECTION_INDEX_EDGE_PADDING) /
4427
+ availableHeight
4428
+ )
3118
4429
  );
4430
+ const entryIndex = Math.round(
4431
+ progress * (this.sectionIndexEntries.length - 1)
4432
+ );
4433
+ const entry = this.sectionIndexEntries[entryIndex];
4434
+ return entry ? { entry, previewClientY: event.clientY } : undefined;
3119
4435
  }
3120
4436
 
3121
4437
  private handleIndexPointer = (event: PointerEvent) => {
3122
4438
  if (event.type === 'pointermove' && event.buttons === 0) return;
3123
- const button = this.indexButtonAtEvent(event);
3124
- if (!button) return;
4439
+ const selection = this.sectionIndexEntryAtEvent(event);
4440
+ if (!selection) return;
3125
4441
  event.preventDefault();
4442
+ if (event.type === 'pointerdown') {
4443
+ this.indexRail.setPointerCapture?.(event.pointerId);
4444
+ }
3126
4445
  this.selectIndexPosition(
3127
- Number(button.dataset.sectionPosition),
3128
- button.textContent ?? ''
4446
+ selection.entry.position,
4447
+ selection.entry.title,
4448
+ selection.previewClientY
3129
4449
  );
3130
4450
  };
3131
4451
 
3132
4452
  private handleIndexClick = (event: Event) => {
4453
+ if (
4454
+ 'detail' in event &&
4455
+ typeof event.detail === 'number' &&
4456
+ event.detail > 0
4457
+ )
4458
+ return;
3133
4459
  const target = event.target;
3134
4460
  if (!(target instanceof Element)) return;
3135
- const button = target.closest<HTMLElement>('[data-section-position]');
4461
+ const button = target.closest<HTMLElement>('[data-section-entry-index]');
3136
4462
  if (!button) return;
4463
+ const entry =
4464
+ this.sectionIndexEntries[Number(button.dataset.sectionEntryIndex)];
4465
+ if (!entry) return;
4466
+ const rect = button.getBoundingClientRect();
3137
4467
  this.selectIndexPosition(
3138
- Number(button.dataset.sectionPosition),
3139
- button.textContent ?? ''
4468
+ entry.position,
4469
+ entry.title,
4470
+ rect.top + rect.height / 2
3140
4471
  );
3141
4472
  };
3142
4473
 
@@ -3203,12 +4534,13 @@ export class NativeListWebEngine {
3203
4534
  const index = Number(rowElement?.dataset.nativeListRowIndex);
3204
4535
  const row = this.rows[index];
3205
4536
  if (!row || !this.isReorderable(row)) return;
3206
- if (
3207
- row.type === 'walletGroup' &&
3208
- target.closest<HTMLElement>('[data-native-list-group-parent]')?.dataset
3209
- .nativeListGroupParent !== 'true'
3210
- )
3211
- return;
4537
+ // OneKey patch: a child drag reorders its parent wallet group as one item.
4538
+ // if (
4539
+ // row.type === 'walletGroup' &&
4540
+ // target.closest<HTMLElement>('[data-native-list-group-parent]')?.dataset
4541
+ // .nativeListGroupParent !== 'true'
4542
+ // )
4543
+ // return;
3212
4544
 
3213
4545
  const view = this.document.defaultView;
3214
4546
  const state: PointerReorderState = {
@@ -3225,9 +4557,8 @@ export class NativeListWebEngine {
3225
4557
  active: false,
3226
4558
  };
3227
4559
  this.pointerReorder = state;
3228
- if (state.pointerType === 'mouse') {
3229
- this.captureReorderPointer(state);
3230
- } else {
4560
+ // OneKey patch: normal wallet taps retain their target until a drag is activated.
4561
+ if (state.pointerType !== 'mouse') {
3231
4562
  state.longPressTimer = view?.setTimeout(
3232
4563
  () => this.activatePointerReorder(state),
3233
4564
  REORDER_TOUCH_LONG_PRESS_MS
@@ -3363,6 +4694,23 @@ export class NativeListWebEngine {
3363
4694
  Math.max(0, state.startY - rect.top)
3364
4695
  );
3365
4696
  this.reorderPreview.replaceChildren(previewRow.cloneNode(true));
4697
+ // Cloned previews need their own lease when a source row is recycled during dragging.
4698
+ const originals = previewRow.querySelectorAll('img');
4699
+ this.reorderPreview.querySelectorAll('img').forEach((image, index) => {
4700
+ const original = originals.item(index);
4701
+ const avatar = original ? webAvatarSources.get(original) : undefined;
4702
+ if (!avatar) return;
4703
+ const paint = image.previousElementSibling;
4704
+ if (
4705
+ paint?.classList.contains('ok-native-list-selector-image-background')
4706
+ ) {
4707
+ image.addEventListener('load', () => {
4708
+ (paint as HTMLElement).style.backgroundImage =
4709
+ 'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
4710
+ });
4711
+ }
4712
+ configureWebAvatar(image, avatar.source, avatar.uri);
4713
+ });
3366
4714
  const sourceRow = state.workingRows[state.currentIndex];
3367
4715
  const badgeText = sourceRow
3368
4716
  ? webWalletGroupReorderBadge(sourceRow)
@@ -3430,6 +4778,7 @@ export class NativeListWebEngine {
3430
4778
 
3431
4779
  private clearReorderPreviewVisual() {
3432
4780
  this.reorderPreview.hidden = true;
4781
+ disposeWebImageRetries(this.reorderPreview);
3433
4782
  this.reorderPreview.replaceChildren();
3434
4783
  this.reorderPreview.style.removeProperty('transform');
3435
4784
  this.reorderPreview.style.removeProperty('transition');